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 static com.android.car.internal.ExcludeFromCodeCoverageGeneratedReport.DUMP_INFO; 23 24 import android.annotation.NonNull; 25 import android.annotation.Nullable; 26 import android.annotation.RequiresPermission; 27 import android.car.CarBugreportManager.CarBugreportManagerCallback; 28 import android.car.ICarBugreportCallback; 29 import android.car.ICarBugreportService; 30 import android.car.builtin.os.BuildHelper; 31 import android.car.builtin.os.SystemPropertiesHelper; 32 import android.car.builtin.util.Slogf; 33 import android.content.Context; 34 import android.content.pm.PackageManager; 35 import android.net.LocalSocket; 36 import android.net.LocalSocketAddress; 37 import android.os.Binder; 38 import android.os.Handler; 39 import android.os.HandlerThread; 40 import android.os.ParcelFileDescriptor; 41 import android.os.Process; 42 import android.os.RemoteException; 43 import android.os.SystemClock; 44 import android.util.proto.ProtoOutputStream; 45 46 import com.android.car.internal.ExcludeFromCodeCoverageGeneratedReport; 47 import com.android.car.internal.util.IndentingPrintWriter; 48 import com.android.internal.annotations.GuardedBy; 49 import com.android.internal.annotations.VisibleForTesting; 50 51 import java.io.BufferedReader; 52 import java.io.DataInputStream; 53 import java.io.DataOutputStream; 54 import java.io.IOException; 55 import java.io.InputStream; 56 import java.io.InputStreamReader; 57 import java.io.OutputStream; 58 import java.util.concurrent.atomic.AtomicBoolean; 59 60 /** 61 * Bugreport service for cars. 62 */ 63 public class CarBugreportManagerService extends ICarBugreportService.Stub implements 64 CarServiceBase { 65 66 private static final String TAG = CarLog.tagFor(CarBugreportManagerService.class); 67 68 /** 69 * {@code dumpstate} progress prefixes. 70 * 71 * <p>The protocol is described in {@code frameworks/native/cmds/bugreportz/readme.md}. 72 */ 73 private static final String BEGIN_PREFIX = "BEGIN:"; 74 private static final String PROGRESS_PREFIX = "PROGRESS:"; 75 private static final String OK_PREFIX = "OK:"; 76 private static final String FAIL_PREFIX = "FAIL:"; 77 78 /** 79 * The services are defined in {@code packages/services/Car/cpp/bugreport/carbugreportd.rc}. 80 */ 81 @VisibleForTesting 82 static final String BUGREPORTD_SERVICE = "carbugreportd"; 83 @VisibleForTesting 84 static final String DUMPSTATEZ_SERVICE = "cardumpstatez"; 85 86 // The socket definitions must match the actual socket names defined in car_bugreportd service 87 // definition. 88 private static final String BUGREPORT_PROGRESS_SOCKET = "car_br_progress_socket"; 89 private static final String BUGREPORT_OUTPUT_SOCKET = "car_br_output_socket"; 90 private static final String BUGREPORT_EXTRA_OUTPUT_SOCKET = "car_br_extra_output_socket"; 91 92 private static final int SOCKET_CONNECTION_MAX_RETRY = 10; 93 private static final int SOCKET_CONNECTION_RETRY_DELAY_IN_MS = 5000; 94 95 private final Context mContext; 96 private final boolean mIsUserBuild; 97 private final Object mLock = new Object(); 98 99 private final HandlerThread mHandlerThread = CarServiceUtils.getHandlerThread( 100 getClass().getSimpleName()); 101 private final Handler mHandler = new Handler(mHandlerThread.getLooper()); 102 @VisibleForTesting 103 final AtomicBoolean mIsServiceRunning = new AtomicBoolean(false); 104 private boolean mIsDumpstateDryRun = false; 105 106 /** 107 * Create a CarBugreportManagerService instance. 108 * 109 * @param context the context 110 */ CarBugreportManagerService(Context context)111 public CarBugreportManagerService(Context context) { 112 // Per https://source.android.com/setup/develop/new-device, user builds are debuggable=0 113 this(context, !BuildHelper.isDebuggableBuild()); 114 } 115 116 @VisibleForTesting CarBugreportManagerService(Context context, boolean isUserBuild)117 CarBugreportManagerService(Context context, boolean isUserBuild) { 118 mContext = context; 119 mIsUserBuild = isUserBuild; 120 } 121 122 @Override init()123 public void init() { 124 // nothing to do 125 } 126 127 @Override release()128 public void release() { 129 // To stop any pending tasks in HandlerThread 130 mIsServiceRunning.set(false); 131 } 132 133 @Override 134 @RequiresPermission(android.Manifest.permission.DUMP) requestBugreport(ParcelFileDescriptor output, ParcelFileDescriptor extraOutput, ICarBugreportCallback callback, boolean dumpstateDryRun)135 public void requestBugreport(ParcelFileDescriptor output, ParcelFileDescriptor extraOutput, 136 ICarBugreportCallback callback, boolean dumpstateDryRun) { 137 mContext.enforceCallingOrSelfPermission( 138 android.Manifest.permission.DUMP, "requestBugreport"); 139 ensureTheCallerIsSignedWithPlatformKeys(); 140 ensureTheCallerIsDesignatedBugReportApp(); 141 synchronized (mLock) { 142 if (mIsServiceRunning.getAndSet(true)) { 143 Slogf.w(TAG, "Bugreport Service already running"); 144 reportError(callback, CarBugreportManagerCallback.CAR_BUGREPORT_IN_PROGRESS); 145 return; 146 } 147 requestBugReportLocked(output, extraOutput, callback, dumpstateDryRun); 148 } 149 } 150 151 @Override 152 @RequiresPermission(android.Manifest.permission.DUMP) cancelBugreport()153 public void cancelBugreport() { 154 mContext.enforceCallingOrSelfPermission( 155 android.Manifest.permission.DUMP, "cancelBugreport"); 156 ensureTheCallerIsSignedWithPlatformKeys(); 157 ensureTheCallerIsDesignatedBugReportApp(); 158 synchronized (mLock) { 159 if (!mIsServiceRunning.getAndSet(false)) { 160 Slogf.i(TAG, "Ignoring cancelBugreport. Service is not running."); 161 return; 162 } 163 Slogf.i(TAG, "Cancelling the running bugreport"); 164 mHandler.removeCallbacksAndMessages(/* token= */ null); 165 // This tells init to cancel the services. Note that this is achieved through 166 // setting a system property which is not thread-safe. So the lock here offers 167 // thread-safety only among callers of the API. 168 try { 169 SystemPropertiesHelper.set("ctl.stop", BUGREPORTD_SERVICE); 170 } catch (RuntimeException e) { 171 Slogf.e(TAG, "Failed to stop " + BUGREPORTD_SERVICE, e); 172 } 173 try { 174 // Stop DUMPSTATEZ_SERVICE service too, because stopping BUGREPORTD_SERVICE doesn't 175 // guarantee stopping DUMPSTATEZ_SERVICE. 176 SystemPropertiesHelper.set("ctl.stop", DUMPSTATEZ_SERVICE); 177 } catch (RuntimeException e) { 178 Slogf.e(TAG, "Failed to stop " + DUMPSTATEZ_SERVICE, e); 179 } 180 if (mIsDumpstateDryRun) { 181 setDumpstateDryRun(false); 182 } 183 } 184 } 185 186 /** See {@code dumpstate} docs to learn about dry_run. */ setDumpstateDryRun(boolean dryRun)187 private void setDumpstateDryRun(boolean dryRun) { 188 try { 189 SystemPropertiesHelper.set("dumpstate.dry_run", dryRun ? "true" : null); 190 } catch (RuntimeException e) { 191 Slogf.e(TAG, "Failed to set dumpstate.dry_run", e); 192 } 193 } 194 ensureTheCallerIsSignedWithPlatformKeys()195 private void ensureTheCallerIsSignedWithPlatformKeys() { 196 PackageManager pm = mContext.getPackageManager(); 197 int callingUid = Binder.getCallingUid(); 198 if (pm.checkSignatures(Process.myUid(), callingUid) != PackageManager.SIGNATURE_MATCH) { 199 throw new SecurityException("Caller " + pm.getNameForUid(callingUid) 200 + " does not have the right signature"); 201 } 202 } 203 204 /** Checks only on user builds. */ ensureTheCallerIsDesignatedBugReportApp()205 private void ensureTheCallerIsDesignatedBugReportApp() { 206 if (!mIsUserBuild) { 207 return; 208 } 209 String defaultAppPkgName = mContext.getString(R.string.config_car_bugreport_application); 210 int callingUid = Binder.getCallingUid(); 211 PackageManager pm = mContext.getPackageManager(); 212 String[] packageNamesForCallerUid = pm.getPackagesForUid(callingUid); 213 if (packageNamesForCallerUid != null) { 214 for (String packageName : packageNamesForCallerUid) { 215 if (defaultAppPkgName.equals(packageName)) { 216 return; 217 } 218 } 219 } 220 throw new SecurityException("Caller " + pm.getNameForUid(callingUid) 221 + " is not a designated bugreport app"); 222 } 223 224 @GuardedBy("mLock") requestBugReportLocked( ParcelFileDescriptor output, ParcelFileDescriptor extraOutput, ICarBugreportCallback callback, boolean dumpstateDryRun)225 private void requestBugReportLocked( 226 ParcelFileDescriptor output, 227 ParcelFileDescriptor extraOutput, 228 ICarBugreportCallback callback, 229 boolean dumpstateDryRun) { 230 Slogf.i(TAG, "Starting " + BUGREPORTD_SERVICE); 231 mIsDumpstateDryRun = dumpstateDryRun; 232 if (mIsDumpstateDryRun) { 233 setDumpstateDryRun(true); 234 } 235 try { 236 // This tells init to start the service. Note that this is achieved through 237 // setting a system property which is not thread-safe. So the lock here offers 238 // thread-safety only among callers of the API. 239 SystemPropertiesHelper.set("ctl.start", BUGREPORTD_SERVICE); 240 } catch (RuntimeException e) { 241 mIsServiceRunning.set(false); 242 Slogf.e(TAG, "Failed to start " + BUGREPORTD_SERVICE, e); 243 reportError(callback, CAR_BUGREPORT_DUMPSTATE_FAILED); 244 return; 245 } 246 mHandler.post(() -> { 247 try { 248 processBugreportSockets(output, extraOutput, callback); 249 } finally { 250 if (mIsDumpstateDryRun) { 251 setDumpstateDryRun(false); 252 } 253 mIsServiceRunning.set(false); 254 } 255 }); 256 } 257 handleProgress(String line, ICarBugreportCallback callback)258 private void handleProgress(String line, ICarBugreportCallback callback) { 259 String progressOverTotal = line.substring(PROGRESS_PREFIX.length()); 260 String[] parts = progressOverTotal.split("/"); 261 if (parts.length != 2) { 262 Slogf.w(TAG, "Invalid progress line from bugreportz: " + line); 263 return; 264 } 265 float progress; 266 float total; 267 try { 268 progress = Float.parseFloat(parts[0]); 269 total = Float.parseFloat(parts[1]); 270 } catch (NumberFormatException e) { 271 Slogf.w(TAG, "Invalid progress value: " + line, e); 272 return; 273 } 274 if (total == 0) { 275 Slogf.w(TAG, "Invalid progress total value: " + line); 276 return; 277 } 278 try { 279 callback.onProgress(100f * progress / total); 280 } catch (RemoteException e) { 281 Slogf.e(TAG, "Failed to call onProgress callback", e); 282 } 283 } 284 handleFinished(ParcelFileDescriptor output, ParcelFileDescriptor extraOutput, ICarBugreportCallback callback)285 private void handleFinished(ParcelFileDescriptor output, ParcelFileDescriptor extraOutput, 286 ICarBugreportCallback callback) { 287 Slogf.i(TAG, "Finished reading bugreport"); 288 // copysockettopfd calls callback.onError on error 289 if (!copySocketToPfd(output, BUGREPORT_OUTPUT_SOCKET, callback)) { 290 return; 291 } 292 if (!copySocketToPfd(extraOutput, BUGREPORT_EXTRA_OUTPUT_SOCKET, callback)) { 293 return; 294 } 295 try { 296 callback.onFinished(); 297 } catch (RemoteException e) { 298 Slogf.e(TAG, "Failed to call onFinished callback", e); 299 } 300 } 301 302 /** 303 * Reads from dumpstate progress and output sockets and invokes appropriate callbacks. 304 * 305 * <p>dumpstate prints {@code BEGIN:} right away, then prints {@code PROGRESS:} as it 306 * progresses. When it finishes or fails it prints {@code OK:pathToTheZipFile} or 307 * {@code FAIL:message} accordingly. 308 */ processBugreportSockets( ParcelFileDescriptor output, ParcelFileDescriptor extraOutput, ICarBugreportCallback callback)309 private void processBugreportSockets( 310 ParcelFileDescriptor output, ParcelFileDescriptor extraOutput, 311 ICarBugreportCallback callback) { 312 LocalSocket localSocket = connectSocket(BUGREPORT_PROGRESS_SOCKET); 313 if (localSocket == null) { 314 reportError(callback, CAR_BUGREPORT_DUMPSTATE_CONNECTION_FAILED); 315 return; 316 } 317 try (BufferedReader reader = 318 new BufferedReader(new InputStreamReader(localSocket.getInputStream()))) { 319 String line; 320 while (mIsServiceRunning.get() && (line = reader.readLine()) != null) { 321 if (line.startsWith(PROGRESS_PREFIX)) { 322 handleProgress(line, callback); 323 } else if (line.startsWith(FAIL_PREFIX)) { 324 String errorMessage = line.substring(FAIL_PREFIX.length()); 325 Slogf.e(TAG, "Failed to dumpstate: " + errorMessage); 326 reportError(callback, CAR_BUGREPORT_DUMPSTATE_FAILED); 327 return; 328 } else if (line.startsWith(OK_PREFIX)) { 329 handleFinished(output, extraOutput, callback); 330 return; 331 } else if (!line.startsWith(BEGIN_PREFIX)) { 332 Slogf.w(TAG, "Received unknown progress line from dumpstate: " + line); 333 } 334 } 335 Slogf.e(TAG, "dumpstate progress unexpectedly ended"); 336 reportError(callback, CAR_BUGREPORT_DUMPSTATE_FAILED); 337 } catch (IOException | RuntimeException e) { 338 Slogf.i(TAG, "Failed to read from progress socket", e); 339 reportError(callback, CAR_BUGREPORT_DUMPSTATE_CONNECTION_FAILED); 340 } 341 } 342 copySocketToPfd( ParcelFileDescriptor pfd, String remoteSocket, ICarBugreportCallback callback)343 private boolean copySocketToPfd( 344 ParcelFileDescriptor pfd, String remoteSocket, ICarBugreportCallback callback) { 345 LocalSocket localSocket = connectSocket(remoteSocket); 346 if (localSocket == null) { 347 reportError(callback, CAR_BUGREPORT_DUMPSTATE_CONNECTION_FAILED); 348 return false; 349 } 350 351 try ( 352 DataInputStream in = new DataInputStream(localSocket.getInputStream()); 353 DataOutputStream out = 354 new DataOutputStream(new ParcelFileDescriptor.AutoCloseOutputStream(pfd)) 355 ) { 356 rawCopyStream(out, in); 357 } catch (IOException | RuntimeException e) { 358 Slogf.e(TAG, "Failed to grab dump state from " + BUGREPORT_OUTPUT_SOCKET, e); 359 reportError(callback, CAR_BUGREPORT_DUMPSTATE_FAILED); 360 return false; 361 } 362 return true; 363 } 364 reportError(ICarBugreportCallback callback, int errorCode)365 private void reportError(ICarBugreportCallback callback, int errorCode) { 366 try { 367 callback.onError(errorCode); 368 } catch (RemoteException e) { 369 Slogf.e(TAG, "onError() failed", e); 370 } 371 } 372 373 @Override 374 @ExcludeFromCodeCoverageGeneratedReport(reason = DUMP_INFO) dump(IndentingPrintWriter writer)375 public void dump(IndentingPrintWriter writer) { 376 // TODO(sgurun) implement 377 } 378 379 @Override 380 @ExcludeFromCodeCoverageGeneratedReport(reason = DUMP_INFO) dumpProto(ProtoOutputStream proto)381 public void dumpProto(ProtoOutputStream proto) {} 382 383 @Nullable connectSocket(@onNull String socketName)384 private LocalSocket connectSocket(@NonNull String socketName) { 385 LocalSocket socket = new LocalSocket(); 386 // The dumpstate socket will be created by init upon receiving the 387 // service request. It may not be ready by this point. So we will 388 // keep retrying until success or reaching timeout. 389 int retryCount = 0; 390 while (true) { 391 // There are a few factors impacting the socket delay: 392 // 1. potential system slowness 393 // 2. carbugreportd takes the screenshots early (before starting dumpstate). This 394 // should be taken into account as the socket opens after screenshots are 395 // captured. 396 // Therefore we are generous in setting the timeout. Most cases should not even 397 // come close to the timeouts, but since bugreports are taken when there is a 398 // system issue, it is hard to guess. 399 // The following lines waits for SOCKET_CONNECTION_RETRY_DELAY_IN_MS or until 400 // mIsServiceRunning becomes false. 401 for (int i = 0; i < SOCKET_CONNECTION_RETRY_DELAY_IN_MS / 50; i++) { 402 if (!mIsServiceRunning.get()) { 403 Slogf.i(TAG, "Failed to connect to socket " + socketName 404 + ". The service is prematurely cancelled."); 405 return null; 406 } 407 SystemClock.sleep(50); // Millis. 408 } 409 410 try { 411 socket.connect(new LocalSocketAddress(socketName, 412 LocalSocketAddress.Namespace.RESERVED)); 413 return socket; 414 } catch (IOException e) { 415 if (++retryCount >= SOCKET_CONNECTION_MAX_RETRY) { 416 Slogf.i(TAG, "Failed to connect to dumpstate socket " + socketName 417 + " after " + retryCount + " retries", e); 418 return null; 419 } 420 Slogf.i(TAG, "Failed to connect to " + socketName + ". Will try again. " 421 + e.getMessage()); 422 } 423 } 424 } 425 426 // does not close the reader or writer. rawCopyStream(OutputStream writer, InputStream reader)427 private static void rawCopyStream(OutputStream writer, InputStream reader) throws IOException { 428 int read; 429 byte[] buf = new byte[8192]; 430 while ((read = reader.read(buf, 0, buf.length)) > 0) { 431 writer.write(buf, 0, read); 432 } 433 } 434 } 435