1 /* 2 * Copyright (C) 2015 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 package com.android.car; 17 18 import static com.android.car.internal.ExcludeFromCodeCoverageGeneratedReport.DUMP_INFO; 19 20 import android.car.Car; 21 import android.car.builtin.util.Slogf; 22 import android.car.test.ICarTest; 23 import android.content.Context; 24 import android.os.IBinder; 25 import android.os.Looper; 26 import android.os.MessageQueue.OnFileDescriptorEventListener; 27 import android.os.ParcelFileDescriptor; 28 import android.os.RemoteException; 29 import android.os.ServiceSpecificException; 30 import android.util.proto.ProtoOutputStream; 31 32 import com.android.car.internal.ExcludeFromCodeCoverageGeneratedReport; 33 import com.android.car.internal.util.IndentingPrintWriter; 34 import com.android.internal.annotations.GuardedBy; 35 36 import java.io.ByteArrayOutputStream; 37 import java.io.FileDescriptor; 38 import java.io.FileInputStream; 39 import java.io.IOException; 40 import java.util.Arrays; 41 import java.util.HashMap; 42 import java.util.List; 43 import java.util.Map; 44 45 /** 46 * Service to allow testing / mocking vehicle HAL. 47 * This service uses Vehicle HAL APIs directly (one exception) as vehicle HAL mocking anyway 48 * requires accessing that level directly. 49 */ 50 class CarTestService extends ICarTest.Stub implements CarServiceBase { 51 52 private static final String TAG = CarLog.tagFor(CarTestService.class); 53 54 private final Context mContext; 55 private final ICarImpl mICarImpl; 56 57 private final Object mLock = new Object(); 58 59 @GuardedBy("mLock") 60 private final Map<IBinder, TokenDeathRecipient> mTokens = new HashMap<>(); 61 CarTestService(Context context, ICarImpl carImpl)62 CarTestService(Context context, ICarImpl carImpl) { 63 mContext = context; 64 mICarImpl = carImpl; 65 } 66 67 @Override init()68 public void init() { 69 // nothing to do. 70 // This service should not reset anything for init / release to maintain mocking. 71 } 72 73 @Override release()74 public void release() { 75 // nothing to do 76 // This service should not reset anything for init / release to maintain mocking. 77 } 78 79 @Override 80 @ExcludeFromCodeCoverageGeneratedReport(reason = DUMP_INFO) dump(IndentingPrintWriter writer)81 public void dump(IndentingPrintWriter writer) { 82 writer.println("*CarTestService*"); 83 synchronized (mLock) { 84 writer.println(" mTokens:" + Arrays.toString(mTokens.entrySet().toArray())); 85 } 86 } 87 88 @Override 89 @ExcludeFromCodeCoverageGeneratedReport(reason = DUMP_INFO) dumpProto(ProtoOutputStream proto)90 public void dumpProto(ProtoOutputStream proto) {} 91 92 @Override stopCarService(IBinder token)93 public void stopCarService(IBinder token) throws RemoteException { 94 Slogf.d(TAG, "stopCarService, token: " + token); 95 CarServiceUtils.assertPermission(mContext, Car.PERMISSION_CAR_TEST_SERVICE); 96 97 synchronized (mLock) { 98 if (mTokens.containsKey(token)) { 99 Slogf.w(TAG, "Calling stopCarService twice with the same token."); 100 return; 101 } 102 103 TokenDeathRecipient deathRecipient = new TokenDeathRecipient(token); 104 mTokens.put(token, deathRecipient); 105 token.linkToDeath(deathRecipient, 0); 106 107 if (mTokens.size() == 1) { 108 CarServiceUtils.runOnMainSync(mICarImpl::release); 109 } 110 } 111 } 112 113 @Override startCarService(IBinder token)114 public void startCarService(IBinder token) throws RemoteException { 115 Slogf.d(TAG, "startCarService, token: " + token); 116 CarServiceUtils.assertPermission(mContext, Car.PERMISSION_CAR_TEST_SERVICE); 117 releaseToken(token); 118 } 119 120 @Override dumpVhal(List<String> options, long waitTimeoutMs)121 public String dumpVhal(List<String> options, long waitTimeoutMs) throws RemoteException { 122 CarServiceUtils.assertPermission(mContext, Car.PERMISSION_CAR_TEST_SERVICE); 123 try (NativePipe pipe = NativePipe.newPipe()) { 124 mICarImpl.dumpVhal(pipe.getFileDescriptor(), options); 125 return pipe.getOutput(waitTimeoutMs); 126 } catch (IOException | InterruptedException e) { 127 throw new ServiceSpecificException(0, 128 "Error: fail to create or access pipe used for dumping VHAL, options: " 129 + options + ", error: " + e); 130 } 131 } 132 133 @Override hasAidlVhal()134 public boolean hasAidlVhal() throws RemoteException { 135 CarServiceUtils.assertPermission(mContext, Car.PERMISSION_CAR_TEST_SERVICE); 136 return mICarImpl.hasAidlVhal(); 137 } 138 139 @Override getOemServiceName()140 public String getOemServiceName() { 141 return mICarImpl.getOemServiceName(); 142 } 143 144 private static class FdEventListener implements OnFileDescriptorEventListener { 145 private static final int BUFFER_SIZE = 1024; 146 private byte[] mBuffer = new byte[BUFFER_SIZE]; 147 private ByteArrayOutputStream mOutputStream = new ByteArrayOutputStream(); 148 private Looper mLooper; 149 private IOException mException = null; 150 FdEventListener(Looper looper)151 FdEventListener(Looper looper) { 152 mLooper = looper; 153 } 154 155 @Override onFileDescriptorEvents(FileDescriptor fd, int events)156 public int onFileDescriptorEvents(FileDescriptor fd, int events) { 157 if ((events & EVENT_INPUT) != 0) { 158 try { 159 FileInputStream inputStream = new FileInputStream(fd); 160 while (inputStream.available() != 0) { 161 int size = inputStream.read(mBuffer); 162 mOutputStream.write(mBuffer, /* off= */ 0, size); 163 } 164 } catch (IOException e) { 165 mException = e; 166 return 0; 167 } 168 } 169 if ((events & EVENT_ERROR) != 0) { 170 // The remote end closes the connection. 171 mLooper.quit(); 172 return 0; 173 } 174 return EVENT_INPUT | EVENT_ERROR; 175 } 176 getOutput()177 public String getOutput() throws IOException { 178 if (mException != null) { 179 throw mException; 180 } 181 return mOutputStream.toString(); 182 } 183 } 184 185 // A helper class to create a native pipe used in debug functions. 186 /* package */ static class NativePipe implements AutoCloseable { 187 private final ParcelFileDescriptor mWriter; 188 private final ParcelFileDescriptor mReader; 189 private Thread mThread; 190 private Looper mLooper; 191 private FdEventListener mEventListener; 192 NativePipe(ParcelFileDescriptor writer, ParcelFileDescriptor reader)193 private NativePipe(ParcelFileDescriptor writer, ParcelFileDescriptor reader) { 194 mWriter = writer; 195 mReader = reader; 196 197 // Start a new thread to read from pipe to prevent the writer blocking on write. 198 mThread = new Thread(() -> { 199 Looper.prepare(); 200 mLooper = Looper.myLooper(); 201 mEventListener = new FdEventListener(mLooper); 202 Looper.myQueue().addOnFileDescriptorEventListener(mReader.getFileDescriptor(), 203 OnFileDescriptorEventListener.EVENT_INPUT 204 | OnFileDescriptorEventListener.EVENT_ERROR, mEventListener); 205 Looper.loop(); 206 }, "nativePipe_readThread"); 207 mThread.start(); 208 } 209 newPipe()210 public static NativePipe newPipe() throws IOException { 211 ParcelFileDescriptor[] pipe = ParcelFileDescriptor.createPipe(); 212 ParcelFileDescriptor reader = new ParcelFileDescriptor(pipe[0]); 213 ParcelFileDescriptor writer = new ParcelFileDescriptor(pipe[1]); 214 return new NativePipe(writer, reader); 215 } 216 getFileDescriptor()217 public ParcelFileDescriptor getFileDescriptor() { 218 return mWriter; 219 } 220 221 /** 222 * Reads all the output data received from the pipe. This function should only be called 223 * once for one pipe. 224 */ getOutput(long waitTimeoutMs)225 public String getOutput(long waitTimeoutMs) throws IOException, InterruptedException { 226 // Close our side for the writer. 227 mWriter.close(); 228 // Wait until we read all the data from the pipe. 229 try { 230 mThread.join(waitTimeoutMs); 231 if (!mThread.isAlive()) { 232 return mEventListener.getOutput(); 233 } 234 } catch (InterruptedException e) { 235 mLooper.quit(); 236 throw e; 237 } 238 // If the other side don't close the writer FD within timeout, we would forcefully 239 // quit the looper, causing the thread to end. 240 mLooper.quit(); 241 throw new ServiceSpecificException(0, 242 "timeout while waiting for VHAL to close writer FD"); 243 } 244 245 @Override close()246 public void close() throws IOException { 247 mReader.close(); 248 // No need to close mOutputStream because close for ByteArrayOutputStream is no-op. 249 } 250 } 251 releaseToken(IBinder token)252 private void releaseToken(IBinder token) { 253 Slogf.d(TAG, "releaseToken, token: " + token); 254 synchronized (mLock) { 255 DeathRecipient deathRecipient = mTokens.remove(token); 256 if (deathRecipient != null) { 257 token.unlinkToDeath(deathRecipient, 0); 258 } 259 260 if (mTokens.size() == 0) { 261 CarServiceUtils.runOnMainSync(() -> { 262 mICarImpl.priorityInit(); 263 mICarImpl.init(); 264 }); 265 } 266 } 267 } 268 269 private class TokenDeathRecipient implements DeathRecipient { 270 private final IBinder mToken; 271 TokenDeathRecipient(IBinder token)272 TokenDeathRecipient(IBinder token) throws RemoteException { 273 mToken = token; 274 } 275 276 @Override binderDied()277 public void binderDied() { 278 releaseToken(mToken); 279 } 280 } 281 } 282