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