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