טיימר מפקח (watchdog) של רכב

אפשר להשתמש ב-watchdog של המכונית כדי לנפות באגים ב-VHAL. הטיימר המפקח (watchdog) של המכונית עוקב אחרי תהליכים לא תקינים ומפסיק אותם. כדי שהתהליך יהיה במעקב על ידי הטיימר המפקח (watchdog) של הרכב, צריך לרשום את התהליך גם במפקח (watchdog) של הרכב. כשה-car watchdog מפסיק תהליכים לא תקינים, הוא רושם את הסטטוס של התהליכים ב-data/anr, כמו בדפים אחרים של נתונים שנוצרים כשהאפליקציה לא מגיבה (ANR). כך קל יותר לנפות באגים.

המאמר הזה מתאר איך בקשות HAL ושירותים של ספקים יכולים לרשום תהליך באמצעות הטיימר המפקח (watchdog) של הרכב.

HAL של הספק

בדרך כלל, HAL של הספק משתמש במאגר חוטים ל-hwbinder. עם זאת, הלקוח של הטיימר המפקח (watchdog) של הרכב מתקשר עם הדימון של הטיימר המפקח (watchdog) של הרכב דרך binder, ששונה מ-hwbinder. לכן נעשה שימוש במאגר אחר של שרשורים ב-binder.

ציון הכלי עזר המפקח (watchdog) של מכונית ב-makefile

  1. כוללים את carwatchdog_aidl_interface-ndk_platform ב-shared_libs:

    Android.bp:

    cc_defaults {
        name: "vhal_v2_0_defaults",
        shared_libs: [
            "libbinder_ndk",
            "libhidlbase",
            "liblog",
            "libutils",
            "android.hardware.automotive.vehicle@2.0",
            "carwatchdog_aidl_interface-ndk_platform",
        ],
        cflags: [
            "-Wall",
            "-Wextra",
            "-Werror",
        ],
    }
    

הוספת מדיניות SELinux

  1. מאשרים ל-system_server להרוג את ה-HAL. אם אין לכם system_server.te, צריך ליצור אותו. מומלץ מאוד להוסיף מדיניות SELinux לכל מכשיר.
  2. מתן הרשאה ל-HAL של הספק להשתמש ב-binder (מאקרו binder_use) ולהוסיף את ה-HAL של הספק לדומיין הלקוח carwatchdog (פקודת מאקרו carwatchdog_client_domain). ראו את הקוד הבא עבור systemserver.te ו-vehicle_default.te:

    system_server.te

    # Allow system_server to kill vehicle HAL
    allow system_server hal_vehicle_server:process sigkill;
    

    hal_vehicle_default.te

    # Configuration for register VHAL to car watchdog
    carwatchdog_client_domain(hal_vehicle_default)
    binder_use(hal_vehicle_default)
    

הטמעת סוג לקוח באמצעות ירושה מ-BnCarWatchdogClient

  1. ב-checkIfAlive, מבצעים בדיקת תקינות. לדוגמה, פרסום ב-handler של לולאת השרשור. אם הוא תקין, צריך להתקשר למספר ICarWatchdog::tellClientAlive. הקוד שלמטה מיועד ל-WatchogClient.h ול-WatchogClient.cpp:

    WatchogClient.h

    class WatchdogClient : public aidl::android::automotive::watchdog::BnCarWatchdogClient {
      public:
        explicit WatchdogClient(const ::android::sp<::android::Looper>& handlerLooper, VehicleHalManager* vhalManager);
    
    ndk::ScopedAStatus checkIfAlive(int32_t sessionId, aidl::android::automotive::watchdog::TimeoutLength timeout) override; ndk::ScopedAStatus prepareProcessTermination() override; };

    WatchogClient.cpp

    ndk::ScopedAStatus WatchdogClient::checkIfAlive(int32_t sessionId, TimeoutLength /*timeout*/) {
        // Implement or call your health check logic here
        return ndk::ScopedAStatus::ok();
    }
    

התחלת השרשור של ה-binder ורישום הלקוח

  1. יצירת מאגר של חוטים לתקשורת של Binder. אם HAL של הספק משתמש ב-hwbinder למטרות משלו, צריך ליצור מאגר משימות נוסף לתקשורת של car watchdog binder).
  2. מחפשים את הדימון לפי השם שלו ומפעילים את ICarWatchdog::registerClient. שם הממשק של הדימון של טיימר המפקח (watchdog) של המכונית הוא android.automotive.watchdog.ICarWatchdog/default.
  3. בהתאם למהירות התגובה של השירות, בוחרים באחד משלושת סוגי הזמן הקצוב לתפוגה הבאים שנתמכים על ידי ה-watchdog ברכב, ולאחר מכן מעבירים את הזמן הקצוב לתפוגה בקריאה ל-ICarWatchdog::registerClient:
    • critical(3s)
    • בינוני(5 שניות)
    • normal(10s)
    ראו את הקוד הבא עבור VehicleService.cpp ו-WatchogClient.cpp:

    VehicleService.cpp

    int main(int /* argc */, char* /* argv */ []) {
        // Set up thread pool for hwbinder
        configureRpcThreadpool(4, false /* callerWillJoin */);
    
        ALOGI("Registering as service...");
        status_t status = service->registerAsService();
    
        if (status != OK) {
            ALOGE("Unable to register vehicle service (%d)", status);
            return 1;
        }
    
        // Setup a binder thread pool to be a car watchdog client.
        ABinderProcess_setThreadPoolMaxThreadCount(1);
        ABinderProcess_startThreadPool();
        sp<Looper> looper(Looper::prepare(0 /* opts */));
        std::shared_ptr<WatchdogClient> watchdogClient =
                ndk::SharedRefBase::make<WatchdogClient>(looper, service.get());
        // The current health check is done in the main thread, so it falls short of capturing the real
        // situation. Checking through HAL binder thread should be considered.
        if (!watchdogClient->initialize()) {
            ALOGE("Failed to initialize car watchdog client");
            return 1;
        }
        ALOGI("Ready");
        while (true) {
            looper->pollAll(-1 /* timeoutMillis */);
        }
    
        return 1;
    }
    

    WatchogClient.cpp

    bool WatchdogClient::initialize() {
        ndk::SpAIBinder binder(AServiceManager_getService("android.automotive.watchdog.ICarWatchdog/default"));
        if (binder.get() == nullptr) {
            ALOGE("Failed to get carwatchdog daemon");
            return false;
        }
        std::shared_ptr<ICarWatchdog> server = ICarWatchdog::fromBinder(binder);
        if (server == nullptr) {
            ALOGE("Failed to connect to carwatchdog daemon");
            return false;
        }
        mWatchdogServer = server;
    
        binder = this->asBinder();
        if (binder.get() == nullptr) {
            ALOGE("Failed to get car watchdog client binder object");
            return false;
        }
        std::shared_ptr<ICarWatchdogClient> client = ICarWatchdogClient::fromBinder(binder);
        if (client == nullptr) {
            ALOGE("Failed to get ICarWatchdogClient from binder");
            return false;
        }
        mTestClient = client;
        mWatchdogServer->registerClient(client, TimeoutLength::TIMEOUT_NORMAL);
        ALOGI("Successfully registered the client to car watchdog server");
        return true;
    }
    

שירותי ספק (מקורי)

ציון קובץ ה-makefile של aidl של הטיימר המפקח (watchdog) של המכונית

  1. כוללים את carwatchdog_aidl_interface-ndk_platform ב-shared_libs.

    Android.bp

    cc_binary {
        name: "sample_native_client",
        srcs: [
            "src/*.cpp"
        ],
        shared_libs: [
            "carwatchdog_aidl_interface-ndk_platform",
            "libbinder_ndk",
        ],
        vendor: true,
    }
    

הוספת מדיניות SELinux

  1. כדי להוסיף מדיניות SELinux, צריך לאפשר לדומיין השירות של הספק להשתמש ב-binder (מאקרו binder_use) ולהוסיף את דומיין השירות של הספק לדומיין הלקוח carwatchdog (במאקרו carwatchdog_client_domain). הקוד שלמטה מיועד ל-sample_client.te ול-file_contexts:

    sample_client.te

    type sample_client, domain;
    type sample_client_exec, exec_type, file_type, vendor_file_type;
    
    carwatchdog_client_domain(sample_client)
    
    init_daemon_domain(sample_client)
    binder_use(sample_client)
    

    file_contexts

    /vendor/bin/sample_native_client  u:object_r:sample_client_exec:s0
    

הטמעת סוג לקוח באמצעות ירושה מ-BnCarWatchdogClient

  1. ב-checkIfAlive, מבצעים בדיקת תקינות. אפשרות אחת היא לפרסם את האירוע בטיפול בלולאת השרשור. אם הוא תקין, צריך להתקשר למספר ICarWatchdog::tellClientAlive. הקוד שלמטה מיועד ל-SampleNativeClient.h ול-SampleNativeClient.cpp:

    SampleNativeClient.h

    class SampleNativeClient : public BnCarWatchdogClient {
    public:
        ndk::ScopedAStatus checkIfAlive(int32_t sessionId, TimeoutLength
            timeout) override;
        ndk::ScopedAStatus prepareProcessTermination() override;
        void initialize();
    
    private:
        void respondToDaemon();
    private:
        ::android::sp<::android::Looper> mHandlerLooper;
        std::shared_ptr<ICarWatchdog> mWatchdogServer;
        std::shared_ptr<ICarWatchdogClient> mClient;
        int32_t mSessionId;
    };
    

    SampleNativeClient.cpp

    ndk::ScopedAStatus WatchdogClient::checkIfAlive(int32_t sessionId, TimeoutLength timeout) {
        mHandlerLooper->removeMessages(mMessageHandler,
            WHAT_CHECK_ALIVE);
        mSessionId = sessionId;
        mHandlerLooper->sendMessage(mMessageHandler,
            Message(WHAT_CHECK_ALIVE));
        return ndk::ScopedAStatus::ok();
    }
    // WHAT_CHECK_ALIVE triggers respondToDaemon from thread handler
    void WatchdogClient::respondToDaemon() {
      // your health checking method here
      ndk::ScopedAStatus status = mWatchdogServer->tellClientAlive(mClient,
            mSessionId);
    }
    

התחלת שרשור ב-binder ורישום הלקוח

שם הממשק של הטיימר המפקח (watchdog) של המכונית הוא android.automotive.watchdog.ICarWatchdog/default.

  1. מחפשים את הדימון בעל השם ומתקשרים אל ICarWatchdog::registerClient. הקוד שלמטה מיועד ל-main.cpp ול-SampleNativeClient.cpp:

    main.cpp

    int main(int argc, char** argv) {
        sp<Looper> looper(Looper::prepare(/*opts=*/0));
    
        ABinderProcess_setThreadPoolMaxThreadCount(1);
        ABinderProcess_startThreadPool();
        std::shared_ptr<SampleNativeClient> client =
            ndk::SharedRefBase::make<SampleNatvieClient>(looper);
    
        // The client is registered in initialize()
        client->initialize();
        ...
    }
    

    SampleNativeClient.cpp

    void SampleNativeClient::initialize() {
        ndk::SpAIBinder binder(AServiceManager_getService(
            "android.automotive.watchdog.ICarWatchdog/default"));
        std::shared_ptr<ICarWatchdog> server =
            ICarWatchdog::fromBinder(binder);
        mWatchdogServer = server;
        ndk::SpAIBinder binder = this->asBinder();
        std::shared_ptr<ICarWatchdogClient> client =
            ICarWatchdogClient::fromBinder(binder)
        mClient = client;
        server->registerClient(client, TimeoutLength::TIMEOUT_NORMAL);
    }
    

שירותי ספקים (Android)

הטמעת לקוח באמצעות ירושה מ-CarWatchdogClientCallback

  1. עורכים את הקובץ החדש באופן הבא:
    private final CarWatchdogClientCallback mClientCallback = new CarWatchdogClientCallback() {
        @Override
        public boolean onCheckHealthStatus(int sessionId, int timeout) {
            // Your health check logic here
            // Returning true implies the client is healthy
            // If false is returned, the client should call
            // CarWatchdogManager.tellClientAlive after health check is
            // completed
        }
    
        @Override
        public void onPrepareProcessTermination() {}
    };
    

רישום הלקוח

  1. התקשרות אל CarWatchdogManager.registerClient():
    private void startClient() {
        CarWatchdogManager manager =
            (CarWatchdogManager) car.getCarManager(
            Car.CAR_WATCHDOG_SERVICE);
        // Choose a proper executor according to your health check method
        ExecutorService executor = Executors.newFixedThreadPool(1);
        manager.registerClient(executor, mClientCallback,
            CarWatchdogManager.TIMEOUT_NORMAL);
    }
    

ביטול הרישום של הלקוח

  1. קוראים לפונקציה CarWatchdogManager.unregisterClient() כשהשירות יסתיים:
    private void finishClient() {
        CarWatchdogManager manager =
            (CarWatchdogManager) car.getCarManager(
            Car.CAR_WATCHDOG_SERVICE);
        manager.unregisterClient(mClientCallback);
    }
    

זיהוי תהליכים שהסתיימו על ידי הטיימר המפקח (watchdog) של הרכב

Car watchdog מאחסן או משמיד תהליכים (HAL של ספק, שירותים מקומיים של ספק, שירותי Android של ספק) שמתועדים ב-Car watchdog כשהם תקועים ולא מגיבים. כדי לזהות דיווחים כאלה, בודקים את ה-logcats. הטיימר המפקח (watchdog) של הרכב יוצר יומן carwatchdog killed process_name (pid:process_id) כשתהליך בעייתי מבוטל או מבוטל. לכן:

$ adb logcat -s CarServiceHelper | fgrep "carwatchdog killed"

היומנים הרלוונטיים מתועדים. לדוגמה, אם אפליקציית KitchenSink (לקוח של Watchdog לרכב) נתקעת, שורה כמו זו שבהמשך תירשם ביומן:

05-01 09:50:19.683   578  5777 W CarServiceHelper: carwatchdog killed com.google.android.car.kitchensink (pid: 5574)

כדי לקבוע למה או איפה האפליקציה KitchenSink נתקעה, משתמשים ב-dump של התהליך שנשמר ב-/data/anr בדיוק כמו שמשתמשים במקרים של ANR ב-Activity.

$ adb root
$ adb shell grep -Hn "pid process_pid" /data/anr/*

הפלט לדוגמה הבא הוא ספציפי לאפליקציית KitchenSink:

$ adb shell su root grep -Hn "pid 5574" /data/anr/*.
/data/anr/anr_2020-05-01-09-50-18-290:3:----- pid 5574 at 2020-05-01 09:50:18 -----
/data/anr/anr_2020-05-01-09-50-18-290:285:----- Waiting Channels: pid 5574 at 2020-05-01 09:50:18 -----

מחפשים את קובץ הדמפ (לדוגמה, /data/anr/anr_2020-05-01-09-50-18-290 בדוגמה שלמעלה) ומתחילים את הניתוח.