blob: b63822fe6b59c479e8666bad21c3b74258adcf08 [file] [log] [blame]
Selim Gurunc8c82f62019-04-17 14:07:30 -07001/*
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
17package com.android.car;
18
Zhomart Mukhamejanovad23f272019-06-04 20:27:24 -070019import static android.car.CarBugreportManager.CarBugreportManagerCallback.CAR_BUGREPORT_DUMPSTATE_CONNECTION_FAILED;
20import static android.car.CarBugreportManager.CarBugreportManagerCallback.CAR_BUGREPORT_DUMPSTATE_FAILED;
21
22import android.annotation.NonNull;
23import android.annotation.Nullable;
Selim Gurunc8c82f62019-04-17 14:07:30 -070024import android.annotation.RequiresPermission;
25import android.car.CarBugreportManager.CarBugreportManagerCallback;
26import android.car.ICarBugreportCallback;
27import android.car.ICarBugreportService;
28import android.content.Context;
Selim Gurun606b2ec2019-05-24 22:47:47 -070029import android.content.pm.PackageManager;
Selim Gurunc8c82f62019-04-17 14:07:30 -070030import android.net.LocalSocket;
31import android.net.LocalSocketAddress;
Selim Gurun606b2ec2019-05-24 22:47:47 -070032import android.os.Binder;
Selim Gurunc8c82f62019-04-17 14:07:30 -070033import android.os.Handler;
34import android.os.HandlerThread;
35import android.os.ParcelFileDescriptor;
Selim Gurun606b2ec2019-05-24 22:47:47 -070036import android.os.Process;
Selim Gurunc8c82f62019-04-17 14:07:30 -070037import android.os.RemoteException;
38import android.os.SystemClock;
39import android.os.SystemProperties;
40import android.util.Log;
41import android.util.Slog;
42
43import com.android.internal.annotations.GuardedBy;
44
Zhomart Mukhamejanovad23f272019-06-04 20:27:24 -070045import java.io.BufferedReader;
Selim Gurunc8c82f62019-04-17 14:07:30 -070046import java.io.DataInputStream;
47import java.io.DataOutputStream;
48import java.io.IOException;
49import java.io.InputStream;
Zhomart Mukhamejanovad23f272019-06-04 20:27:24 -070050import java.io.InputStreamReader;
Selim Gurunc8c82f62019-04-17 14:07:30 -070051import java.io.OutputStream;
52import java.io.PrintWriter;
53
Selim Gurunc8c82f62019-04-17 14:07:30 -070054/**
Selim Guruna85b7e72019-06-07 11:01:42 -070055 * Bugreport service for cars.
Selim Gurunc8c82f62019-04-17 14:07:30 -070056 */
57public class CarBugreportManagerService extends ICarBugreportService.Stub implements
58 CarServiceBase {
59
60 private static final String TAG = "CarBugreportMgrService";
Selim Gurunc8c82f62019-04-17 14:07:30 -070061
Zhomart Mukhamejanovad23f272019-06-04 20:27:24 -070062 /**
63 * {@code dumpstate} progress prefixes.
64 *
65 * <p>The protocol is described in {@code frameworks/native/cmds/bugreportz/readme.md}.
66 */
67 private static final String BEGIN_PREFIX = "BEGIN:";
68 private static final String PROGRESS_PREFIX = "PROGRESS:";
69 private static final String OK_PREFIX = "OK:";
70 private static final String FAIL_PREFIX = "FAIL:";
Selim Gurunc8c82f62019-04-17 14:07:30 -070071
Zhomart Mukhamejanovad23f272019-06-04 20:27:24 -070072 private static final String BUGREPORTD_SERVICE = "car-bugreportd";
Selim Gurunc8c82f62019-04-17 14:07:30 -070073
Selim Gurunc4d7c012019-05-13 20:27:22 -070074 // The socket definitions must match the actual socket names defined in car_bugreportd service
75 // definition.
76 private static final String BUGREPORT_PROGRESS_SOCKET = "car_br_progress_socket";
77 private static final String BUGREPORT_OUTPUT_SOCKET = "car_br_output_socket";
Selim Guruna85b7e72019-06-07 11:01:42 -070078 private static final String BUGREPORT_EXTRA_OUTPUT_SOCKET = "car_br_extra_output_socket";
Selim Gurunc4d7c012019-05-13 20:27:22 -070079
Selim Gurunc8c82f62019-04-17 14:07:30 -070080 private static final int SOCKET_CONNECTION_MAX_RETRY = 10;
Selim Guruna85b7e72019-06-07 11:01:42 -070081 private static final int SOCKET_CONNECTION_RETRY_DELAY_IN_MS = 5000;
Selim Gurunc8c82f62019-04-17 14:07:30 -070082
Zhomart Mukhamejanovad23f272019-06-04 20:27:24 -070083 private final Context mContext;
84 private final Object mLock = new Object();
85
86 private HandlerThread mHandlerThread;
87 private Handler mHandler;
88 private boolean mIsServiceRunning;
89
Selim Gurunc8c82f62019-04-17 14:07:30 -070090 /**
91 * Create a CarBugreportManagerService instance.
92 *
93 * @param context the context
94 */
95 public CarBugreportManagerService(Context context) {
96 mContext = context;
97 }
98
99 @Override
100 public void init() {
Selim Gurun606b2ec2019-05-24 22:47:47 -0700101 mHandlerThread = new HandlerThread(TAG);
102 mHandlerThread.start();
103 mHandler = new Handler(mHandlerThread.getLooper());
Selim Gurunc8c82f62019-04-17 14:07:30 -0700104 }
105
106 @Override
107 public void release() {
Selim Gurun606b2ec2019-05-24 22:47:47 -0700108 mHandlerThread.quitSafely();
Selim Gurunc8c82f62019-04-17 14:07:30 -0700109 }
110
111 @Override
112 @RequiresPermission(android.Manifest.permission.DUMP)
Selim Guruna85b7e72019-06-07 11:01:42 -0700113 public void requestBugreport(ParcelFileDescriptor output, ParcelFileDescriptor extraOutput,
114 ICarBugreportCallback callback) {
Selim Gurunc4d7c012019-05-13 20:27:22 -0700115
Selim Gurun606b2ec2019-05-24 22:47:47 -0700116 // Check the caller has proper permission
Selim Gurunc4d7c012019-05-13 20:27:22 -0700117 mContext.enforceCallingOrSelfPermission(android.Manifest.permission.DUMP,
Selim Gurun606b2ec2019-05-24 22:47:47 -0700118 "requestZippedBugreport");
119 // Check the caller is signed with platform keys
120 PackageManager pm = mContext.getPackageManager();
121 int callingUid = Binder.getCallingUid();
122 if (pm.checkSignatures(Process.myUid(), callingUid) != PackageManager.SIGNATURE_MATCH) {
123 throw new SecurityException("Caller " + pm.getNameForUid(callingUid)
124 + " does not have the right signature");
125 }
Zhomart Mukhamejanov30862412019-06-10 16:59:15 -0700126 // Check if the caller is the designated bugreport app
127 String defaultAppPkgName = mContext.getString(R.string.config_car_bugreport_application);
Selim Gurun606b2ec2019-05-24 22:47:47 -0700128 String[] packageNamesForCallerUid = pm.getPackagesForUid(callingUid);
129 boolean found = false;
130 if (packageNamesForCallerUid != null) {
131 for (String packageName : packageNamesForCallerUid) {
132 if (defaultAppPkgName.equals(packageName)) {
133 found = true;
134 break;
135 }
136 }
137 }
138 if (!found) {
139 throw new SecurityException("Caller " + pm.getNameForUid(callingUid)
140 + " is not a designated bugreport app");
141 }
Selim Gurunc4d7c012019-05-13 20:27:22 -0700142
143 synchronized (mLock) {
Selim Guruna85b7e72019-06-07 11:01:42 -0700144 requestBugReportLocked(output, extraOutput, callback);
Selim Gurunc4d7c012019-05-13 20:27:22 -0700145 }
146 }
147
148 @GuardedBy("mLock")
Selim Guruna85b7e72019-06-07 11:01:42 -0700149 private void requestBugReportLocked(ParcelFileDescriptor output,
150 ParcelFileDescriptor extraOutput, ICarBugreportCallback callback) {
Selim Gurunc4d7c012019-05-13 20:27:22 -0700151 if (mIsServiceRunning) {
152 Slog.w(TAG, "Bugreport Service already running");
153 reportError(callback, CarBugreportManagerCallback.CAR_BUGREPORT_IN_PROGRESS);
154 return;
155 }
156 mIsServiceRunning = true;
Selim Guruna85b7e72019-06-07 11:01:42 -0700157 mHandler.post(() -> startBugreportd(output, extraOutput, callback));
Selim Gurunc4d7c012019-05-13 20:27:22 -0700158 }
159
Selim Guruna85b7e72019-06-07 11:01:42 -0700160 private void startBugreportd(ParcelFileDescriptor output, ParcelFileDescriptor extraOutput,
161 ICarBugreportCallback callback) {
Zhomart Mukhamejanovad23f272019-06-04 20:27:24 -0700162 Slog.i(TAG, "Starting " + BUGREPORTD_SERVICE);
163 try {
164 SystemProperties.set("ctl.start", BUGREPORTD_SERVICE);
165 } catch (RuntimeException e) {
166 Slog.e(TAG, "Failed to start " + BUGREPORTD_SERVICE, e);
167 reportError(callback, CAR_BUGREPORT_DUMPSTATE_FAILED);
168 return;
169 }
Selim Guruna85b7e72019-06-07 11:01:42 -0700170 processBugreportSockets(output, extraOutput, callback);
Selim Gurunc4d7c012019-05-13 20:27:22 -0700171 synchronized (mLock) {
172 mIsServiceRunning = false;
173 }
174 }
175
Zhomart Mukhamejanovad23f272019-06-04 20:27:24 -0700176 private void handleProgress(String line, ICarBugreportCallback callback) {
177 String progressOverTotal = line.substring(PROGRESS_PREFIX.length());
178 String[] parts = progressOverTotal.split("/");
179 if (parts.length != 2) {
180 Slog.w(TAG, "Invalid progress line from bugreportz: " + line);
181 return;
182 }
183 float progress;
184 float total;
Selim Gurunc4d7c012019-05-13 20:27:22 -0700185 try {
Zhomart Mukhamejanovad23f272019-06-04 20:27:24 -0700186 progress = Float.parseFloat(parts[0]);
187 total = Float.parseFloat(parts[1]);
188 } catch (NumberFormatException e) {
189 Slog.w(TAG, "Invalid progress value: " + line, e);
Selim Gurunc4d7c012019-05-13 20:27:22 -0700190 return;
191 }
Zhomart Mukhamejanovad23f272019-06-04 20:27:24 -0700192 if (total == 0) {
193 Slog.w(TAG, "Invalid progress total value: " + line);
Selim Gurunc4d7c012019-05-13 20:27:22 -0700194 return;
195 }
Selim Gurunc4d7c012019-05-13 20:27:22 -0700196 try {
Zhomart Mukhamejanovad23f272019-06-04 20:27:24 -0700197 callback.onProgress(100f * progress / total);
Selim Gurunc4d7c012019-05-13 20:27:22 -0700198 } catch (RemoteException e) {
Zhomart Mukhamejanovad23f272019-06-04 20:27:24 -0700199 Slog.e(TAG, "Failed to call onProgress callback", e);
Selim Gurunc4d7c012019-05-13 20:27:22 -0700200 }
201 }
202
Selim Guruna85b7e72019-06-07 11:01:42 -0700203 private void handleFinished(ParcelFileDescriptor output, ParcelFileDescriptor extraOutput,
204 ICarBugreportCallback callback) {
Zhomart Mukhamejanovad23f272019-06-04 20:27:24 -0700205 Slog.i(TAG, "Finished reading bugreport");
Selim Guruna85b7e72019-06-07 11:01:42 -0700206 // copysockettopfd calls callback.onError on error
207 if (!copySocketToPfd(output, BUGREPORT_OUTPUT_SOCKET, callback)) {
208 return;
209 }
210 if (!copySocketToPfd(extraOutput, BUGREPORT_EXTRA_OUTPUT_SOCKET, callback)) {
Zhomart Mukhamejanovad23f272019-06-04 20:27:24 -0700211 return;
212 }
Selim Gurunc4d7c012019-05-13 20:27:22 -0700213 try {
Zhomart Mukhamejanovad23f272019-06-04 20:27:24 -0700214 callback.onFinished();
215 } catch (RemoteException e) {
216 Slog.e(TAG, "Failed to call onFinished callback", e);
217 }
218 }
219
220 /**
221 * Reads from dumpstate progress and output sockets and invokes appropriate callbacks.
222 *
223 * <p>dumpstate prints {@code BEGIN:} right away, then prints {@code PROGRESS:} as it
224 * progresses. When it finishes or fails it prints {@code OK:pathToTheZipFile} or
225 * {@code FAIL:message} accordingly.
226 */
227 private void processBugreportSockets(
Selim Guruna85b7e72019-06-07 11:01:42 -0700228 ParcelFileDescriptor output, ParcelFileDescriptor extraOutput,
229 ICarBugreportCallback callback) {
Zhomart Mukhamejanovad23f272019-06-04 20:27:24 -0700230 LocalSocket localSocket = connectSocket(BUGREPORT_PROGRESS_SOCKET);
231 if (localSocket == null) {
232 reportError(callback, CAR_BUGREPORT_DUMPSTATE_CONNECTION_FAILED);
233 return;
234 }
235
236 try (BufferedReader reader =
237 new BufferedReader(new InputStreamReader(localSocket.getInputStream()))) {
238 String line;
239 while ((line = reader.readLine()) != null) {
240 if (line.startsWith(PROGRESS_PREFIX)) {
241 handleProgress(line, callback);
242 } else if (line.startsWith(FAIL_PREFIX)) {
243 String errorMessage = line.substring(FAIL_PREFIX.length());
244 Slog.e(TAG, "Failed to dumpstate: " + errorMessage);
245 reportError(callback, CAR_BUGREPORT_DUMPSTATE_FAILED);
246 return;
247 } else if (line.startsWith(OK_PREFIX)) {
Selim Guruna85b7e72019-06-07 11:01:42 -0700248 handleFinished(output, extraOutput, callback);
Zhomart Mukhamejanovad23f272019-06-04 20:27:24 -0700249 return;
250 } else if (!line.startsWith(BEGIN_PREFIX)) {
251 Slog.w(TAG, "Received unknown progress line from dumpstate: " + line);
252 }
253 }
254 Slog.e(TAG, "dumpstate progress unexpectedly ended");
255 reportError(callback, CAR_BUGREPORT_DUMPSTATE_FAILED);
256 } catch (IOException | RuntimeException e) {
257 Slog.i(TAG, "Failed to read from progress socket", e);
258 reportError(callback, CAR_BUGREPORT_DUMPSTATE_CONNECTION_FAILED);
259 }
260 }
261
262 private boolean copySocketToPfd(
Selim Guruna85b7e72019-06-07 11:01:42 -0700263 ParcelFileDescriptor pfd, String remoteSocket, ICarBugreportCallback callback) {
264 LocalSocket localSocket = connectSocket(remoteSocket);
Zhomart Mukhamejanovad23f272019-06-04 20:27:24 -0700265 if (localSocket == null) {
266 reportError(callback, CAR_BUGREPORT_DUMPSTATE_CONNECTION_FAILED);
Selim Gurunc4d7c012019-05-13 20:27:22 -0700267 return false;
268 }
269
270 try (
271 DataInputStream in = new DataInputStream(localSocket.getInputStream());
272 DataOutputStream out =
Zhomart Mukhamejanovad23f272019-06-04 20:27:24 -0700273 new DataOutputStream(new ParcelFileDescriptor.AutoCloseOutputStream(pfd))
Selim Gurunc4d7c012019-05-13 20:27:22 -0700274 ) {
275 rawCopyStream(out, in);
276 } catch (IOException | RuntimeException e) {
Zhomart Mukhamejanovad23f272019-06-04 20:27:24 -0700277 Slog.e(TAG, "Failed to grab dump state from " + BUGREPORT_OUTPUT_SOCKET, e);
278 reportError(callback, CAR_BUGREPORT_DUMPSTATE_FAILED);
Selim Gurunc4d7c012019-05-13 20:27:22 -0700279 return false;
280 }
281 return true;
282 }
283
Selim Gurunc8c82f62019-04-17 14:07:30 -0700284 private void reportError(ICarBugreportCallback callback, int errorCode) {
285 try {
286 callback.onError(errorCode);
287 } catch (RemoteException e) {
Zhomart Mukhamejanova77417b2019-04-30 13:04:03 -0700288 Slog.e(TAG, "onError() failed: " + e.getMessage());
Selim Gurunc8c82f62019-04-17 14:07:30 -0700289 }
290 }
291
Selim Gurunc4d7c012019-05-13 20:27:22 -0700292 @Override
293 public void dump(PrintWriter writer) {
294 // TODO(sgurun) implement
295 }
296
Zhomart Mukhamejanovad23f272019-06-04 20:27:24 -0700297 @Nullable
298 private LocalSocket connectSocket(@NonNull String socketName) {
Selim Gurunc8c82f62019-04-17 14:07:30 -0700299 LocalSocket socket = new LocalSocket();
300 // The dumpstate socket will be created by init upon receiving the
301 // service request. It may not be ready by this point. So we will
302 // keep retrying until success or reaching timeout.
303 int retryCount = 0;
304 while (true) {
Selim Guruna85b7e72019-06-07 11:01:42 -0700305 // There are a few factors impacting the socket delay:
306 // 1. potential system slowness
307 // 2. car-bugreportd takes the screenshots early (before starting dumpstate). This
308 // should be taken into account as the socket opens after screenshots are
309 // captured.
310 // Therefore we are generous in setting the timeout. Most cases should not even
311 // come close to the timeouts, but since bugreports are taken when there is a
312 // system issue, it is hard to guess.
313 SystemClock.sleep(SOCKET_CONNECTION_RETRY_DELAY_IN_MS);
Selim Gurunc8c82f62019-04-17 14:07:30 -0700314 try {
Selim Gurunc4d7c012019-05-13 20:27:22 -0700315 socket.connect(new LocalSocketAddress(socketName,
Selim Gurunc8c82f62019-04-17 14:07:30 -0700316 LocalSocketAddress.Namespace.RESERVED));
317 return socket;
318 } catch (IOException e) {
Selim Gurunc8c82f62019-04-17 14:07:30 -0700319 if (++retryCount >= SOCKET_CONNECTION_MAX_RETRY) {
Zhomart Mukhamejanovad23f272019-06-04 20:27:24 -0700320 Slog.i(TAG, "Failed to connect to dumpstate socket " + socketName
321 + " after " + retryCount + " retries", e);
322 return null;
Selim Gurunc8c82f62019-04-17 14:07:30 -0700323 }
Selim Guruna85b7e72019-06-07 11:01:42 -0700324 Log.i(TAG, "Failed to connect to " + socketName + ". Will try again "
Selim Gurunc4d7c012019-05-13 20:27:22 -0700325 + e.getMessage());
Selim Gurunc8c82f62019-04-17 14:07:30 -0700326 }
327 }
Selim Gurunc8c82f62019-04-17 14:07:30 -0700328 }
329
330 // does not close the reader or writer.
331 private static void rawCopyStream(OutputStream writer, InputStream reader) throws IOException {
332 int read;
333 byte[] buf = new byte[8192];
334 while ((read = reader.read(buf, 0, buf.length)) > 0) {
335 writer.write(buf, 0, read);
336 }
337 }
338}