1 /* 2 * Copyright (C) 2019 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.car; 18 19 import static android.car.CarBugreportManager.CarBugreportManagerCallback.CAR_BUGREPORT_DUMPSTATE_CONNECTION_FAILED; 20 import static android.car.CarBugreportManager.CarBugreportManagerCallback.CAR_BUGREPORT_DUMPSTATE_FAILED; 21 22 import android.annotation.NonNull; 23 import android.annotation.Nullable; 24 import android.annotation.RequiresPermission; 25 import android.car.CarBugreportManager.CarBugreportManagerCallback; 26 import android.car.ICarBugreportCallback; 27 import android.car.ICarBugreportService; 28 import android.content.Context; 29 import android.content.pm.PackageManager; 30 import android.net.LocalSocket; 31 import android.net.LocalSocketAddress; 32 import android.os.Binder; 33 import android.os.Handler; 34 import android.os.HandlerThread; 35 import android.os.ParcelFileDescriptor; 36 import android.os.Process; 37 import android.os.RemoteException; 38 import android.os.SystemClock; 39 import android.os.SystemProperties; 40 import android.util.Log; 41 import android.util.Slog; 42 43 import com.android.internal.annotations.GuardedBy; 44 45 import java.io.BufferedReader; 46 import java.io.DataInputStream; 47 import java.io.DataOutputStream; 48 import java.io.IOException; 49 import java.io.InputStream; 50 import java.io.InputStreamReader; 51 import java.io.OutputStream; 52 import java.io.PrintWriter; 53 54 /** 55 * Bugreport service for cars. 56 */ 57 public class CarBugreportManagerService extends ICarBugreportService.Stub implements 58 CarServiceBase { 59 60 private static final String TAG = "CarBugreportMgrService"; 61 62 /** 63 * {@code dumpstate} progress prefixes. 64 * 65 * <p>The protocol is described in {@code frameworks/native/cmds/bugreportz/readme.md}. 66 */ 67 private static final String BEGIN_PREFIX = "BEGIN:"; 68 private static final String PROGRESS_PREFIX = "PROGRESS:"; 69 private static final String OK_PREFIX = "OK:"; 70 private static final String FAIL_PREFIX = "FAIL:"; 71 72 private static final String BUGREPORTD_SERVICE = "car-bugreportd"; 73 74 // The socket definitions must match the actual socket names defined in car_bugreportd service 75 // definition. 76 private static final String BUGREPORT_PROGRESS_SOCKET = "car_br_progress_socket"; 77 private static final String BUGREPORT_OUTPUT_SOCKET = "car_br_output_socket"; 78 private static final String BUGREPORT_EXTRA_OUTPUT_SOCKET = "car_br_extra_output_socket"; 79 80 private static final int SOCKET_CONNECTION_MAX_RETRY = 10; 81 private static final int SOCKET_CONNECTION_RETRY_DELAY_IN_MS = 5000; 82 83 private final Context mContext; 84 private final Object mLock = new Object(); 85 86 private HandlerThread mHandlerThread; 87 private Handler mHandler; 88 private boolean mIsServiceRunning; 89 90 /** 91 * Create a CarBugreportManagerService instance. 92 * 93 * @param context the context 94 */ CarBugreportManagerService(Context context)95 public CarBugreportManagerService(Context context) { 96 mContext = context; 97 } 98 99 @Override init()100 public void init() { 101 mHandlerThread = new HandlerThread(TAG); 102 mHandlerThread.start(); 103 mHandler = new Handler(mHandlerThread.getLooper()); 104 } 105 106 @Override release()107 public void release() { 108 mHandlerThread.quitSafely(); 109 } 110 111 @Override 112 @RequiresPermission(android.Manifest.permission.DUMP) requestBugreport(ParcelFileDescriptor output, ParcelFileDescriptor extraOutput, ICarBugreportCallback callback)113 public void requestBugreport(ParcelFileDescriptor output, ParcelFileDescriptor extraOutput, 114 ICarBugreportCallback callback) { 115 116 // Check the caller has proper permission 117 mContext.enforceCallingOrSelfPermission(android.Manifest.permission.DUMP, 118 "requestZippedBugreport"); 119 // Check the caller is signed with platform keys 120 PackageManager pm = mContext.getPackageManager(); 121 int callingUid = Binder.getCallingUid(); 122 if (pm.checkSignatures(Process.myUid(), callingUid) != PackageManager.SIGNATURE_MATCH) { 123 throw new SecurityException("Caller " + pm.getNameForUid(callingUid) 124 + " does not have the right signature"); 125 } 126 // Check if the caller is the designated bugreport app 127 String defaultAppPkgName = mContext.getString(R.string.config_car_bugreport_application); 128 String[] packageNamesForCallerUid = pm.getPackagesForUid(callingUid); 129 boolean found = false; 130 if (packageNamesForCallerUid != null) { 131 for (String packageName : packageNamesForCallerUid) { 132 if (defaultAppPkgName.equals(packageName)) { 133 found = true; 134 break; 135 } 136 } 137 } 138 if (!found) { 139 throw new SecurityException("Caller " + pm.getNameForUid(callingUid) 140 + " is not a designated bugreport app"); 141 } 142 143 synchronized (mLock) { 144 requestBugReportLocked(output, extraOutput, callback); 145 } 146 } 147 148 @GuardedBy("mLock") requestBugReportLocked(ParcelFileDescriptor output, ParcelFileDescriptor extraOutput, ICarBugreportCallback callback)149 private void requestBugReportLocked(ParcelFileDescriptor output, 150 ParcelFileDescriptor extraOutput, ICarBugreportCallback callback) { 151 if (mIsServiceRunning) { 152 Slog.w(TAG, "Bugreport Service already running"); 153 reportError(callback, CarBugreportManagerCallback.CAR_BUGREPORT_IN_PROGRESS); 154 return; 155 } 156 mIsServiceRunning = true; 157 mHandler.post(() -> startBugreportd(output, extraOutput, callback)); 158 } 159 startBugreportd(ParcelFileDescriptor output, ParcelFileDescriptor extraOutput, ICarBugreportCallback callback)160 private void startBugreportd(ParcelFileDescriptor output, ParcelFileDescriptor extraOutput, 161 ICarBugreportCallback callback) { 162 Slog.i(TAG, "Starting " + BUGREPORTD_SERVICE); 163 try { 164 SystemProperties.set("ctl.start", BUGREPORTD_SERVICE); 165 } catch (RuntimeException e) { 166 Slog.e(TAG, "Failed to start " + BUGREPORTD_SERVICE, e); 167 reportError(callback, CAR_BUGREPORT_DUMPSTATE_FAILED); 168 return; 169 } 170 processBugreportSockets(output, extraOutput, callback); 171 synchronized (mLock) { 172 mIsServiceRunning = false; 173 } 174 } 175 handleProgress(String line, ICarBugreportCallback callback)176 private void handleProgress(String line, ICarBugreportCallback callback) { 177 String progressOverTotal = line.substring(PROGRESS_PREFIX.length()); 178 String[] parts = progressOverTotal.split("/"); 179 if (parts.length != 2) { 180 Slog.w(TAG, "Invalid progress line from bugreportz: " + line); 181 return; 182 } 183 float progress; 184 float total; 185 try { 186 progress = Float.parseFloat(parts[0]); 187 total = Float.parseFloat(parts[1]); 188 } catch (NumberFormatException e) { 189 Slog.w(TAG, "Invalid progress value: " + line, e); 190 return; 191 } 192 if (total == 0) { 193 Slog.w(TAG, "Invalid progress total value: " + line); 194 return; 195 } 196 try { 197 callback.onProgress(100f * progress / total); 198 } catch (RemoteException e) { 199 Slog.e(TAG, "Failed to call onProgress callback", e); 200 } 201 } 202 handleFinished(ParcelFileDescriptor output, ParcelFileDescriptor extraOutput, ICarBugreportCallback callback)203 private void handleFinished(ParcelFileDescriptor output, ParcelFileDescriptor extraOutput, 204 ICarBugreportCallback callback) { 205 Slog.i(TAG, "Finished reading bugreport"); 206 // copysockettopfd calls callback.onError on error 207 if (!copySocketToPfd(output, BUGREPORT_OUTPUT_SOCKET, callback)) { 208 return; 209 } 210 if (!copySocketToPfd(extraOutput, BUGREPORT_EXTRA_OUTPUT_SOCKET, callback)) { 211 return; 212 } 213 try { 214 callback.onFinished(); 215 } catch (RemoteException e) { 216 Slog.e(TAG, "Failed to call onFinished callback", e); 217 } 218 } 219 220 /** 221 * Reads from dumpstate progress and output sockets and invokes appropriate callbacks. 222 * 223 * <p>dumpstate prints {@code BEGIN:} right away, then prints {@code PROGRESS:} as it 224 * progresses. When it finishes or fails it prints {@code OK:pathToTheZipFile} or 225 * {@code FAIL:message} accordingly. 226 */ processBugreportSockets( ParcelFileDescriptor output, ParcelFileDescriptor extraOutput, ICarBugreportCallback callback)227 private void processBugreportSockets( 228 ParcelFileDescriptor output, ParcelFileDescriptor extraOutput, 229 ICarBugreportCallback callback) { 230 LocalSocket localSocket = connectSocket(BUGREPORT_PROGRESS_SOCKET); 231 if (localSocket == null) { 232 reportError(callback, CAR_BUGREPORT_DUMPSTATE_CONNECTION_FAILED); 233 return; 234 } 235 236 try (BufferedReader reader = 237 new BufferedReader(new InputStreamReader(localSocket.getInputStream()))) { 238 String line; 239 while ((line = reader.readLine()) != null) { 240 if (line.startsWith(PROGRESS_PREFIX)) { 241 handleProgress(line, callback); 242 } else if (line.startsWith(FAIL_PREFIX)) { 243 String errorMessage = line.substring(FAIL_PREFIX.length()); 244 Slog.e(TAG, "Failed to dumpstate: " + errorMessage); 245 reportError(callback, CAR_BUGREPORT_DUMPSTATE_FAILED); 246 return; 247 } else if (line.startsWith(OK_PREFIX)) { 248 handleFinished(output, extraOutput, callback); 249 return; 250 } else if (!line.startsWith(BEGIN_PREFIX)) { 251 Slog.w(TAG, "Received unknown progress line from dumpstate: " + line); 252 } 253 } 254 Slog.e(TAG, "dumpstate progress unexpectedly ended"); 255 reportError(callback, CAR_BUGREPORT_DUMPSTATE_FAILED); 256 } catch (IOException | RuntimeException e) { 257 Slog.i(TAG, "Failed to read from progress socket", e); 258 reportError(callback, CAR_BUGREPORT_DUMPSTATE_CONNECTION_FAILED); 259 } 260 } 261 copySocketToPfd( ParcelFileDescriptor pfd, String remoteSocket, ICarBugreportCallback callback)262 private boolean copySocketToPfd( 263 ParcelFileDescriptor pfd, String remoteSocket, ICarBugreportCallback callback) { 264 LocalSocket localSocket = connectSocket(remoteSocket); 265 if (localSocket == null) { 266 reportError(callback, CAR_BUGREPORT_DUMPSTATE_CONNECTION_FAILED); 267 return false; 268 } 269 270 try ( 271 DataInputStream in = new DataInputStream(localSocket.getInputStream()); 272 DataOutputStream out = 273 new DataOutputStream(new ParcelFileDescriptor.AutoCloseOutputStream(pfd)) 274 ) { 275 rawCopyStream(out, in); 276 } catch (IOException | RuntimeException e) { 277 Slog.e(TAG, "Failed to grab dump state from " + BUGREPORT_OUTPUT_SOCKET, e); 278 reportError(callback, CAR_BUGREPORT_DUMPSTATE_FAILED); 279 return false; 280 } 281 return true; 282 } 283 reportError(ICarBugreportCallback callback, int errorCode)284 private void reportError(ICarBugreportCallback callback, int errorCode) { 285 try { 286 callback.onError(errorCode); 287 } catch (RemoteException e) { 288 Slog.e(TAG, "onError() failed: " + e.getMessage()); 289 } 290 } 291 292 @Override dump(PrintWriter writer)293 public void dump(PrintWriter writer) { 294 // TODO(sgurun) implement 295 } 296 297 @Nullable connectSocket(@onNull String socketName)298 private LocalSocket connectSocket(@NonNull String socketName) { 299 LocalSocket socket = new LocalSocket(); 300 // The dumpstate socket will be created by init upon receiving the 301 // service request. It may not be ready by this point. So we will 302 // keep retrying until success or reaching timeout. 303 int retryCount = 0; 304 while (true) { 305 // There are a few factors impacting the socket delay: 306 // 1. potential system slowness 307 // 2. car-bugreportd takes the screenshots early (before starting dumpstate). This 308 // should be taken into account as the socket opens after screenshots are 309 // captured. 310 // Therefore we are generous in setting the timeout. Most cases should not even 311 // come close to the timeouts, but since bugreports are taken when there is a 312 // system issue, it is hard to guess. 313 SystemClock.sleep(SOCKET_CONNECTION_RETRY_DELAY_IN_MS); 314 try { 315 socket.connect(new LocalSocketAddress(socketName, 316 LocalSocketAddress.Namespace.RESERVED)); 317 return socket; 318 } catch (IOException e) { 319 if (++retryCount >= SOCKET_CONNECTION_MAX_RETRY) { 320 Slog.i(TAG, "Failed to connect to dumpstate socket " + socketName 321 + " after " + retryCount + " retries", e); 322 return null; 323 } 324 Log.i(TAG, "Failed to connect to " + socketName + ". Will try again " 325 + e.getMessage()); 326 } 327 } 328 } 329 330 // does not close the reader or writer. rawCopyStream(OutputStream writer, InputStream reader)331 private static void rawCopyStream(OutputStream writer, InputStream reader) throws IOException { 332 int read; 333 byte[] buf = new byte[8192]; 334 while ((read = reader.read(buf, 0, buf.length)) > 0) { 335 writer.write(buf, 0, read); 336 } 337 } 338 } 339