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