1 /*
2  * Copyright (C) 2020 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 
17 package com.google.android.car.kitchensink;
18 
19 import android.annotation.NonNull;
20 import android.car.Car;
21 import android.car.watchdog.CarWatchdogManager;
22 import android.car.watchdog.CarWatchdogManager.CarWatchdogClientCallback;
23 import android.os.Handler;
24 import android.os.Looper;
25 import android.os.SystemClock;
26 import android.os.SystemProperties;
27 import android.util.Log;
28 
29 import java.util.concurrent.ExecutorService;
30 import java.util.concurrent.Executors;
31 
32 public final class CarWatchdogClient {
33     private static final String TAG = CarWatchdogClient.class.getSimpleName();
34     private static final String TIMEOUT_CRITICAL = "critical";
35     private static final String TIMEOUT_MODERATE = "moderate";
36     private static final String TIMEOUT_NORMAL = "normal";
37     private static final String PROPERTY_RO_CLIENT_HEALTHCHECK_INTERVAL =
38             "ro.carwatchdog.client_healthcheck.interval";
39     private static final int MISSING_INT_PROPERTY_VALUE = -1;
40 
41     private static CarWatchdogClient sCarWatchdogClient;
42 
43     private final CarWatchdogManager mCarWatchdogManager;
44     private final CarWatchdogClientCallback mClientCallback = new CarWatchdogClientCallback() {
45         @Override
46         public boolean onCheckHealthStatus(int sessionId, int timeout) {
47             if (mClientConfig.verbose) {
48                 Log.i(TAG, "onCheckHealthStatus: session id =  " + sessionId);
49             }
50             long currentUptime = SystemClock.uptimeMillis();
51             return mClientConfig.notRespondAfterInMs < 0
52                     || mClientConfig.notRespondAfterInMs > currentUptime - mClientStartTime;
53         }
54 
55         @Override
56         public void onPrepareProcessTermination() {
57             Log.w(TAG, "This process is being terminated by car watchdog");
58         }
59     };
60     private final ExecutorService mCallbackExecutor = Executors.newFixedThreadPool(1);
61     private ClientConfig mClientConfig;
62     private long mClientStartTime;
63     private long mOverriddenClientHealthCheckWindowMs = MISSING_INT_PROPERTY_VALUE;
64 
65     // This method is not intended for multi-threaded calls.
start(Car car, @NonNull String command)66     public static void start(Car car, @NonNull String command) {
67         if (sCarWatchdogClient != null) {
68             Log.w(TAG, "Car watchdog client already started");
69             return;
70         }
71         ClientConfig config;
72         try {
73             config = parseCommand(command);
74         } catch (IllegalArgumentException e) {
75             Log.w(TAG, "Watchdog command error: " + e);
76             return;
77         }
78         sCarWatchdogClient = new CarWatchdogClient(car, config);
79         sCarWatchdogClient.registerAndGo();
80     }
81 
parseCommand(String command)82     private static ClientConfig parseCommand(String command) {
83         String[] tokens = command.split("[ ]+");
84         int paramCount = tokens.length;
85         if (paramCount != 3 && paramCount != 4) {
86             throw new IllegalArgumentException("invalid command syntax");
87         }
88         int timeout;
89         int inactiveMainAfterInSec;
90         int notRespondAfterInSec;
91         switch (tokens[0]) {
92             case TIMEOUT_CRITICAL:
93                 timeout = CarWatchdogManager.TIMEOUT_CRITICAL;
94                 break;
95             case TIMEOUT_MODERATE:
96                 timeout = CarWatchdogManager.TIMEOUT_MODERATE;
97                 break;
98             case TIMEOUT_NORMAL:
99                 timeout = CarWatchdogManager.TIMEOUT_NORMAL;
100                 break;
101             default:
102                 throw new IllegalArgumentException("invalid timeout value");
103         }
104         try {
105             notRespondAfterInSec = Integer.parseInt(tokens[1]);
106         } catch (NumberFormatException e) {
107             throw new IllegalArgumentException("time for \"not responding after\" is not number",
108                     e);
109         }
110         try {
111             inactiveMainAfterInSec = Integer.parseInt(tokens[2]);
112         } catch (NumberFormatException e) {
113             throw new IllegalArgumentException("time for \"inactive main after\" is not number", e);
114         }
115         boolean verbose = false;
116         if (paramCount == 4) {
117             switch (tokens[3]) {
118                 case "true":
119                     verbose = true;
120                     break;
121                 case "false":
122                     verbose = false;
123                     break;
124                 default:
125                     throw new IllegalArgumentException("invalid verbose value: " + tokens[3]);
126             }
127         }
128         Log.i(TAG, "CarWatchdogClient command: timeout = " + tokens[0] + ", notRespondingAfter = "
129                 + notRespondAfterInSec + ", inactiveMainAfter = " + inactiveMainAfterInSec
130                 + ", verbose = " + verbose);
131         return new ClientConfig(timeout, inactiveMainAfterInSec, notRespondAfterInSec, verbose);
132     }
133 
CarWatchdogClient(Car car, ClientConfig config)134     private CarWatchdogClient(Car car, ClientConfig config) {
135         mClientConfig = config;
136         mCarWatchdogManager = (CarWatchdogManager) car.getCarManager(Car.CAR_WATCHDOG_SERVICE);
137         int clientHealthcheckIntervalSecs = SystemProperties.getInt(
138                 PROPERTY_RO_CLIENT_HEALTHCHECK_INTERVAL, MISSING_INT_PROPERTY_VALUE);
139         if (clientHealthcheckIntervalSecs != MISSING_INT_PROPERTY_VALUE) {
140             // Client must be inactive for at least twice the duration of the client health check
141             // window.
142             mOverriddenClientHealthCheckWindowMs = clientHealthcheckIntervalSecs * 2005L;
143             mOverriddenClientHealthCheckWindowMs = Math.max(mOverriddenClientHealthCheckWindowMs,
144                     getTimeForInactiveMain(CarWatchdogManager.TIMEOUT_NORMAL));
145         }
146     }
147 
registerAndGo()148     private void registerAndGo() {
149         mClientStartTime = SystemClock.uptimeMillis();
150         mCarWatchdogManager.registerClient(mCallbackExecutor, mClientCallback,
151                 mClientConfig.timeout);
152         // Post a runnable which takes long time to finish to the main thread if inactive_main_after
153         // is no less than 0
154         if (mClientConfig.inactiveMainAfterInMs >= 0) {
155             Handler handler = new Handler(Looper.getMainLooper());
156             handler.postDelayed(() -> {
157                 try {
158                     if (mClientConfig.verbose) {
159                         Log.i(TAG, "Main thread gets inactive");
160                     }
161                     Thread.sleep(getTimeForInactiveMain(mClientConfig.timeout));
162                 } catch (InterruptedException e) {
163                     // Ignore
164                 }
165             }, mClientConfig.inactiveMainAfterInMs);
166         }
167     }
168 
169     // The waiting time = (timeout * 2) + 50 milliseconds.
getTimeForInactiveMain(int timeout)170     private long getTimeForInactiveMain(int timeout) {
171         if (mOverriddenClientHealthCheckWindowMs != MISSING_INT_PROPERTY_VALUE) {
172             return mOverriddenClientHealthCheckWindowMs;
173         }
174         switch (timeout) {
175             case CarWatchdogManager.TIMEOUT_CRITICAL:
176                 return 6050L;
177             case CarWatchdogManager.TIMEOUT_MODERATE:
178                 return 10050L;
179             case CarWatchdogManager.TIMEOUT_NORMAL:
180                 return 20050L;
181             default:
182                 Log.w(TAG, "Invalid timeout");
183                 return 20050L;
184         }
185     }
186 
187     private static final class ClientConfig {
188         public int timeout;
189         public long inactiveMainAfterInMs;
190         public long notRespondAfterInMs;
191         public boolean verbose;
192 
ClientConfig(int timeout, int inactiveMainAfterInSec, int notRespondAfterInSec, boolean verbose)193         ClientConfig(int timeout, int inactiveMainAfterInSec, int notRespondAfterInSec,
194                 boolean verbose) {
195             this.timeout = timeout;
196             inactiveMainAfterInMs = inactiveMainAfterInSec * 1000L;
197             notRespondAfterInMs = notRespondAfterInSec * 1000L;
198             this.verbose = verbose;
199         }
200     }
201 }
202