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