• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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