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