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 } 109 try { 110 inactiveMainAfterInSec = Integer.parseInt(tokens[2]); 111 } catch (NumberFormatException e) { 112 throw new IllegalArgumentException("time for \"inactive main after\" is not number"); 113 } 114 boolean verbose = false; 115 if (paramCount == 4) { 116 switch (tokens[3]) { 117 case "true": 118 verbose = true; 119 break; 120 case "false": 121 verbose = false; 122 break; 123 default: 124 throw new IllegalArgumentException("invalid verbose value: " + tokens[3]); 125 } 126 } 127 Log.i(TAG, "CarWatchdogClient command: timeout = " + tokens[0] + ", notRespondingAfter = " 128 + notRespondAfterInSec + ", inactiveMainAfter = " + inactiveMainAfterInSec 129 + ", verbose = " + verbose); 130 return new ClientConfig(timeout, inactiveMainAfterInSec, notRespondAfterInSec, verbose); 131 } 132 CarWatchdogClient(Car car, ClientConfig config)133 private CarWatchdogClient(Car car, ClientConfig config) { 134 mClientConfig = config; 135 mCarWatchdogManager = (CarWatchdogManager) car.getCarManager(Car.CAR_WATCHDOG_SERVICE); 136 int clientHealthcheckIntervalSecs = SystemProperties.getInt( 137 PROPERTY_RO_CLIENT_HEALTHCHECK_INTERVAL, MISSING_INT_PROPERTY_VALUE); 138 if (clientHealthcheckIntervalSecs != MISSING_INT_PROPERTY_VALUE) { 139 // Client must be inactive for at least twice the duration of the client health check 140 // window. 141 mOverriddenClientHealthCheckWindowMs = clientHealthcheckIntervalSecs * 2005L; 142 mOverriddenClientHealthCheckWindowMs = Math.max(mOverriddenClientHealthCheckWindowMs, 143 getTimeForInactiveMain(CarWatchdogManager.TIMEOUT_NORMAL)); 144 } 145 } 146 registerAndGo()147 private void registerAndGo() { 148 mClientStartTime = SystemClock.uptimeMillis(); 149 mCarWatchdogManager.registerClient(mCallbackExecutor, mClientCallback, 150 mClientConfig.timeout); 151 // Post a runnable which takes long time to finish to the main thread if inactive_main_after 152 // is no less than 0 153 if (mClientConfig.inactiveMainAfterInMs >= 0) { 154 Handler handler = new Handler(Looper.getMainLooper()); 155 handler.postDelayed(() -> { 156 try { 157 if (mClientConfig.verbose) { 158 Log.i(TAG, "Main thread gets inactive"); 159 } 160 Thread.sleep(getTimeForInactiveMain(mClientConfig.timeout)); 161 } catch (InterruptedException e) { 162 // Ignore 163 } 164 }, mClientConfig.inactiveMainAfterInMs); 165 } 166 } 167 168 // The waiting time = (timeout * 2) + 50 milliseconds. getTimeForInactiveMain(int timeout)169 private long getTimeForInactiveMain(int timeout) { 170 if (mOverriddenClientHealthCheckWindowMs != MISSING_INT_PROPERTY_VALUE) { 171 return mOverriddenClientHealthCheckWindowMs; 172 } 173 switch (timeout) { 174 case CarWatchdogManager.TIMEOUT_CRITICAL: 175 return 6050L; 176 case CarWatchdogManager.TIMEOUT_MODERATE: 177 return 10050L; 178 case CarWatchdogManager.TIMEOUT_NORMAL: 179 return 20050L; 180 default: 181 Log.w(TAG, "Invalid timeout"); 182 return 20050L; 183 } 184 } 185 186 private static final class ClientConfig { 187 public int timeout; 188 public long inactiveMainAfterInMs; 189 public long notRespondAfterInMs; 190 public boolean verbose; 191 ClientConfig(int timeout, int inactiveMainAfterInSec, int notRespondAfterInSec, boolean verbose)192 ClientConfig(int timeout, int inactiveMainAfterInSec, int notRespondAfterInSec, 193 boolean verbose) { 194 this.timeout = timeout; 195 inactiveMainAfterInMs = inactiveMainAfterInSec * 1000L; 196 notRespondAfterInMs = notRespondAfterInSec * 1000L; 197 this.verbose = verbose; 198 } 199 } 200 } 201