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