/** * @file cpu-keepalive.c * cpu-keepalive module -- this implements late suspend blocking for MCE *

* Copyright (C) 2013-2019 Jolla Ltd. *

* @author Simo Piiroinen * * mce is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License * version 2.1 as published by the Free Software Foundation. * * mce is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with mce. If not, see . */ #include "../mce-log.h" #include "../mce-lib.h" #include "../mce-dbus.h" #ifdef ENABLE_WAKELOCKS # include "../libwakelock.h" #endif #include #include #include #include /* ========================================================================= * * CONSTANTS * ========================================================================= */ /** The name of this module */ static const char module_name[] = "cpu-keepalive"; #ifdef ENABLE_WAKELOCKS /** RTC wakeup wakelock - acquired by dsme and released by mce */ static const char rtc_wakelock[] = "mce_rtc_wakeup"; /* CPU keepalive wakelock - held by mce while there are active clients */ static const char cpu_wakelock[] = "mce_cpu_keepalive"; #endif /** Fallback session_id to use when clients make query keepalive period */ #define SESSION_ID_INITIAL "initial" /** Fallback session_id to use when clients start/stop keepalive period */ #define SESSION_ID_DEFAULT "undefined" /** Suggested delay between MCE_CPU_KEEPALIVE_START_REQ method calls [s] */ #ifdef ENABLE_WAKELOCKS # define MCE_CPU_KEEPALIVE_SUGGESTED_PERIOD_S 60 // 1 minute #else # define MCE_CPU_KEEPALIVE_SUGGESTED_PERIOD_S (24*60*60) // 1 day #endif /** Maximum delay between MCE_CPU_KEEPALIVE_START_REQ method calls [s] */ # define MCE_CPU_KEEPALIVE_MAXIMUM_PERIOD_S \ (MCE_CPU_KEEPALIVE_SUGGESTED_PERIOD_S + 15) /** Auto blocking after MCE_CPU_KEEPALIVE_PERIOD_REQ method calls [s] */ # define MCE_CPU_KEEPALIVE_QUERY_PERIOD_S 2 /** Maximum delay between rtc wakeup and the 1st keep alive request * * FIXME: The rtc wakeup timeouts need to be tuned once timed and * alarm-ui are modified to use iphb wakeups + cpu-keepalive. * * For now we need to delay going back to suspend just in case the wakeup * is needed for showing an alarm and there are hiccups with starting * alarm-ui. */ #define MCE_RTC_WAKEUP_1ST_TIMEOUT_S 5 /** Extend rtc wakeup timeout if at least one keep alive request is received */ #define MCE_RTC_WAKEUP_2ND_TIMEOUT_S 5 /** Warning limit for: individual session lasts too long */ #define KEEPALIVE_SESSION_WARN_LIMIT_MS (3 * 60 * 1000) // 3 minutes /** Warning limit for: keepalive state is kept active too long */ #define KEEPALIVE_STATE_WARN_LIMIT_MS (5 * 60 * 1000) // 5 minutes /* ========================================================================= * * TYPEDEFS * ========================================================================= */ /** Millisecond resolution time value used for cpu keepalive tracking */ typedef int64_t tick_t; typedef struct cka_session_t cka_session_t; typedef struct cka_client_t cka_client_t; /* ========================================================================= * * FUNCTION_PROTOTYPES * ========================================================================= */ /* ------------------------------------------------------------------------- * * GENERIC_UTILITIES * ------------------------------------------------------------------------- */ static tick_t cka_tick_get_current (void); static tick_t cka_tick_get_timeout (tick_t base_ms, int add_seconds); /* ------------------------------------------------------------------------- * * DBUS_UTILITIES * ------------------------------------------------------------------------- */ static gboolean cka_dbusutil_reply_bool (DBusMessage *const msg, gboolean value); static gboolean cka_dbusutil_reply_int (DBusMessage *const msg, gint value); static DBusMessage *cka_dbusutil_create_GetNameOwner_req(const char *name); static gchar *cka_dbusutil_parse_GetNameOwner_rsp (DBusMessage *rsp); /* ------------------------------------------------------------------------- * * SESSION_TRACKING * ------------------------------------------------------------------------- */ /** Book keeping information for client sessions we are tracking */ struct cka_session_t { /** Link to containing client object */ cka_client_t *ses_client; /** Session identifier provided by client via D-Bus API */ char *ses_session; /** Internal unique identifier */ unsigned ses_unique; /** When the session was started */ tick_t ses_started; /** When the session should end */ tick_t ses_timeout; /** Number of times timeout has been renewed */ unsigned ses_renewed; /** Has "too long session" already been reported */ bool ses_flagged; /** Has the session been finished */ bool ses_finished; }; static cka_session_t *cka_session_create (cka_client_t *client, const char *session); static void cka_session_renew (cka_session_t *self, tick_t timeout); static void cka_session_finish (cka_session_t *self, tick_t now); static void cka_session_delete (cka_session_t *self); static void cka_session_delete_cb(void *self); /* ------------------------------------------------------------------------- * * CLIENT_TRACKING * ------------------------------------------------------------------------- */ /** Book keeping information for clients we are tracking */ struct cka_client_t { /** The (private/sender) name of the dbus client */ gchar *cli_dbus_name; /** NameOwnerChanged signal match used for tracking death of client */ char *cli_match_rule; /** Upper bound for reneval of cpu keepalive for this client */ tick_t cli_timeout; /** One client can have several keepalive objects */ GHashTable *cli_sessions; // [string] -> cka_session_t * }; /** Format string for constructing name owner lost match rules */ static const char cka_client_match_fmt[] = "type='signal'" ",sender='"DBUS_SERVICE_DBUS"'" ",interface='"DBUS_INTERFACE_DBUS"'" ",member='NameOwnerChanged'" ",path='"DBUS_PATH_DBUS"'" ",arg0='%s'" ",arg2=''" ; static cka_session_t *cka_client_get_session (cka_client_t *self, const char *session_id); static cka_session_t *cka_client_add_session (cka_client_t *self, const char *session_id); static void cka_client_scan_timeout (cka_client_t *self); static void cka_client_remove_timeout(cka_client_t *self, const char *session_id); static void cka_client_update_timeout(cka_client_t *self, const char *session_id, tick_t when); static cka_client_t *cka_client_create (const char *dbus_name); static const char *cka_client_identify (cka_client_t *self); static void cka_client_delete (cka_client_t *self); static void cka_client_delete_cb (void *self); /* ------------------------------------------------------------------------- * * KEEPALIVE_STATE * ------------------------------------------------------------------------- */ /** Timer for releasing cpu-keepalive wakelock */ static guint cka_state_timer_id = 0; static void cka_state_set (bool active); static gboolean cka_state_timer_cb (gpointer data); static void cka_state_reset (void); static void cka_state_rethink (void); /* ------------------------------------------------------------------------- * * CLIENT_MANAGEMENT * ------------------------------------------------------------------------- */ /** Clients we are tracking over D-Bus */ static GHashTable *cka_clients_lut = 0; /** Timestamp of wakeup from dsme */ static tick_t cka_clients_wakeup_started = 0; /** Timeout for "clients should have issued keep alive requests" */ static tick_t cka_clients_wakeup_timeout = 0; static void cka_clients_verify_name_cb (DBusPendingCall *pending, void *user_data); static gboolean cka_clients_verify_name (const char *name); static void cka_clients_remove_client (const gchar *dbus_name); static cka_client_t *cka_clients_get_client (const gchar *dbus_name); static cka_client_t *cka_clients_add_client (const gchar *dbus_name); static void cka_clients_add_session (const gchar *dbus_name, const char *session_id); static void cka_clients_start_session (const gchar *dbus_name, const gchar *session_id); static void cka_clients_stop_session (const gchar *dbus_name, const char *session_id); static void cka_clients_handle_wakeup (const gchar *dbus_name); static void cka_clients_init (void); static void cka_clients_quit (void); /* ------------------------------------------------------------------------- * * DBUS_HANDLERS * ------------------------------------------------------------------------- */ /** D-Bus system bus connection */ static DBusConnection *cka_dbus_systembus = 0; static gboolean cka_dbus_handle_period_cb (DBusMessage *const msg); static gboolean cka_dbus_handle_start_cb (DBusMessage *const msg); static gboolean cka_dbus_handle_stop_cb (DBusMessage *const msg); static gboolean cka_dbus_handle_wakeup_cb (DBusMessage *const msg); static DBusHandlerResult cka_dbus_filter_message_cb (DBusConnection *con, DBusMessage *msg, void *user_data); static gboolean cka_dbus_init (void); static void cka_dbus_quit (void); /* ------------------------------------------------------------------------- * * MODULE_INIT_QUIT * ------------------------------------------------------------------------- */ G_MODULE_EXPORT const gchar *g_module_check_init(GModule *module); G_MODULE_EXPORT void g_module_unload (GModule *module); /* ========================================================================= * * * GENERIC_UTILITIES * * ========================================================================= */ /** Get monotonic timestamp not affected by system time / timezone changes * * @return milliseconds since some reference point in time */ static tick_t cka_tick_get_current(void) { return mce_lib_get_boot_tick(); } /** Helper for calculating timeout values from ms base + seconds offset * * @param base_ms Base time in milliseconds, or -1 to use current time * @param add_Seconds Offset time in seconds * * @return timeout in millisecond resolution */ static tick_t cka_tick_get_timeout(tick_t base_ms, int add_seconds) { if( base_ms < 0 ) base_ms = cka_tick_get_current(); return base_ms + add_seconds * 1000; } /* ========================================================================= * * * DBUS_UTILITIES * * ========================================================================= */ /** Helper for sending boolean replies to dbus method calls * * Reply will not be sent if no_reply attribute is set * in the method call message. * * @param msg method call message to reply * @param value TRUE/FALSE to send * * @return TRUE on success, or FALSE if reply could not be sent */ static gboolean cka_dbusutil_reply_bool(DBusMessage *const msg, gboolean value) { gboolean success = TRUE; if( !dbus_message_get_no_reply(msg) ) { dbus_bool_t data = value; DBusMessage *reply = dbus_new_method_reply(msg); dbus_message_append_args(reply, DBUS_TYPE_BOOLEAN, &data, DBUS_TYPE_INVALID); /* dbus_send_message() unrefs the message */ success = dbus_send_message(reply), reply = 0; if( !success ) { mce_log(LL_WARN, "failed to send reply to %s", dbus_message_get_member(msg)); } } return success; } /** Helper for sending boolean replies to dbus method calls * * Reply will not be sent if no_reply attribute is set * in the method call message. * * @param msg method call message to reply * @param value integer number to send * * @return TRUE on success, or FALSE if reply could not be sent */ static gboolean cka_dbusutil_reply_int(DBusMessage *const msg, gint value) { gboolean success = TRUE; if( !dbus_message_get_no_reply(msg) ) { dbus_int32_t data = value; DBusMessage *reply = dbus_new_method_reply(msg); dbus_message_append_args(reply, DBUS_TYPE_INT32, &data, DBUS_TYPE_INVALID); /* dbus_send_message() unrefs the message */ success = dbus_send_message(reply), reply = 0; if( !success ) { mce_log(LL_WARN, "failed to send reply to %s", dbus_message_get_member(msg)); } } return success; } /** Create a GetNameOwner method call message * * @param name the dbus name to query * * @return DBusMessage pointer */ static DBusMessage * cka_dbusutil_create_GetNameOwner_req(const char *name) { DBusMessage *req = 0; req = dbus_message_new_method_call(DBUS_SERVICE_DBUS, DBUS_PATH_DBUS, DBUS_INTERFACE_DBUS, "GetNameOwner"); dbus_message_append_args(req, DBUS_TYPE_STRING, &name, DBUS_TYPE_INVALID); return req; } /** Parse a reply message to GetNameOwner method call * * @param rsp method call reply message * * @return dbus name of the name owner, or NULL in case of errors */ static gchar * cka_dbusutil_parse_GetNameOwner_rsp(DBusMessage *rsp) { char *res = 0; DBusError err = DBUS_ERROR_INIT; char *dta = NULL; if( dbus_set_error_from_message(&err, rsp) || !dbus_message_get_args(rsp, &err, DBUS_TYPE_STRING, &dta, DBUS_TYPE_INVALID) ) { if( strcmp(err.name, DBUS_ERROR_NAME_HAS_NO_OWNER) ) { mce_log(LL_WARN, "%s: %s", err.name, err.message); } goto EXIT; } res = g_strdup(dta); EXIT: dbus_error_free(&err); return res; } /* ------------------------------------------------------------------------- * * SESSION_TRACKING * ------------------------------------------------------------------------- */ /** Create bookkeeping information for a keepalive session * * @param client client object * @param session_id assumed unique id provided by the client * * @return pointer to cka_session_t structure */ static cka_session_t * cka_session_create(cka_client_t *client, const char *session_id) { static unsigned id = 0; cka_session_t *self = g_malloc0(sizeof *self); self->ses_client = client; self->ses_session = g_strdup(session_id); self->ses_unique = ++id; self->ses_timeout = 0; self->ses_started = cka_tick_get_current(); self->ses_renewed = 0; self->ses_flagged = false; self->ses_finished = false; mce_log(LL_DEVEL, "session created; id=%u/%s %s", self->ses_unique, self->ses_session, cka_client_identify(self->ses_client)); return self; } /** Renew timeout for a keepalive session * * @param self session object * @param timeout end of session timeout to use */ static void cka_session_renew(cka_session_t *self, tick_t timeout) { self->ses_timeout = timeout; self->ses_renewed += 1; tick_t now = cka_tick_get_current(); tick_t dur = now - self->ses_started; if( !self->ses_flagged && dur > KEEPALIVE_SESSION_WARN_LIMIT_MS ) { self->ses_flagged = true; mce_log(LL_CRIT, "long session active after %"PRId64" ms; " "id=%u/%s %s", dur, self->ses_unique, self->ses_session, cka_client_identify(self->ses_client)); } else { mce_log(LL_DEBUG, "session T%+"PRId64"; id=%u/%s %s", now - self->ses_timeout, self->ses_unique, self->ses_session, cka_client_identify(self->ses_client)); } } /** Finish a keepalive session * * @param self session object * @param now current time */ static void cka_session_finish(cka_session_t *self, tick_t now) { tick_t dur = now - self->ses_started; if( dur > KEEPALIVE_SESSION_WARN_LIMIT_MS ) { mce_log(LL_CRIT, "long session lasted %"PRId64" ms; id=%u/%s %s", dur, self->ses_unique, self->ses_session, cka_client_identify(self->ses_client)); } else { mce_log(LL_DEVEL, "session lasted %"PRId64" ms; id=%u/%s %s", dur, self->ses_unique, self->ses_session, cka_client_identify(self->ses_client)); } self->ses_finished = true; } /** Delete bookkeeping information for a keepalive session * * @param self session object, or NULL */ static void cka_session_delete(cka_session_t *self) { if( !self ) { goto EXIT; } mce_log(LL_DEBUG, "session deleted; id=%u/%s %s", self->ses_unique, self->ses_session, cka_client_identify(self->ses_client)); g_free(self->ses_session); g_free(self); EXIT: return; } /** Typeless helper function for use as destroy callback * * @param self session object, or NULL */ static void cka_session_delete_cb(void *self) { cka_session_delete(self); } /* ========================================================================= * * * CLIENT_TRACKING * * ========================================================================= */ /** Lookup client session object * * @param self pointer to cka_client_t structure * @param session_id client provided session id * * @return session object, or NULL */ static cka_session_t * cka_client_get_session(cka_client_t *self, const char *session_id) { cka_session_t *session = g_hash_table_lookup(self->cli_sessions, session_id); return session; } /** Lookup existing / create new client session object * * @param self pointer to cka_client_t structure * @param session_id client provided session id * * @return session object */ static cka_session_t * cka_client_add_session(cka_client_t *self, const char *session_id) { cka_session_t *session = g_hash_table_lookup(self->cli_sessions, session_id); if( !session ) { session = cka_session_create(self, session_id); g_hash_table_replace(self->cli_sessions, g_strdup(session_id), session); } return session; } /** Update client timeout to be maximum of context timeouts * * @param self pointer to cka_client_t structure */ static void cka_client_scan_timeout(cka_client_t *self) { tick_t now = cka_tick_get_current(); self->cli_timeout = 0; /* Expire sessions / update client timeout */ GHashTableIter iter; gpointer val; g_hash_table_iter_init(&iter, self->cli_sessions); while( g_hash_table_iter_next(&iter, 0, &val) ) { cka_session_t *session = val; if( session->ses_timeout <= now ) { /* Expire session */ cka_session_finish(session, now); g_hash_table_iter_remove(&iter); } else if( self->cli_timeout < session->ses_timeout ) { /* Update client timeout */ self->cli_timeout = session->ses_timeout; } } if( self->cli_timeout > now ) { mce_log(LL_DEBUG, "client T%+"PRId64"; %s", now - self->cli_timeout, cka_client_identify(self)); } } /** Clear client cpu-keepalive timeout * * @param self pointer to cka_client_t structure * @param session_id client provided session id */ static void cka_client_remove_timeout(cka_client_t *self, const char *session_id) { cka_session_t *session = cka_client_get_session(self, session_id); if( session ) { tick_t now = cka_tick_get_current(); cka_session_finish(session, now); g_hash_table_remove(self->cli_sessions, session_id); } } /** Update client cpu-keepalive timeout * * @param self pointer to cka_client_t structure * @param session_id client provided session id * @param when end of client cpu-keepalive period */ static void cka_client_update_timeout(cka_client_t *self, const char *session_id, tick_t when) { cka_session_t *session = cka_client_add_session(self, session_id); if( session ) { cka_session_renew(session, when); } } /** Get client identification information * * @param self pointer to cka_client_t structure * * @return human readable string identifying the client process */ static const char * cka_client_identify(cka_client_t *self) { return mce_dbus_get_name_owner_ident(self->cli_dbus_name); } /** Create bookkeeping information for a dbus client * * Note: Will also add signal matching rule so that we get notified * when the client loses dbus connection * * @param dbus_name name of the dbus client to track * * @return pointer to cka_client_t structure */ static cka_client_t * cka_client_create(const char *dbus_name) { cka_client_t *self = g_malloc0(sizeof *self); self->cli_dbus_name = g_strdup(dbus_name); self->cli_match_rule = g_strdup_printf(cka_client_match_fmt, self->cli_dbus_name); self->cli_timeout = 0; self->cli_sessions = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, cka_session_delete_cb); mce_log(LL_DEBUG, "client created; %s", cka_client_identify(self)); /* NULL error -> match will be added asynchronously */ dbus_bus_add_match(cka_dbus_systembus, self->cli_match_rule, 0); return self; } /** Destroy bookkeeping information about a dbus client * * Note: Will also remove the signal matching rule used for detecting * when the client loses dbus connection * * @param self pointer to cka_client_t structure */ static void cka_client_delete(cka_client_t *self) { if( self != 0 ) { mce_log(LL_DEBUG, "client deleted; %s", cka_client_identify(self)); tick_t now = cka_tick_get_current(); /* Finish all sessions */ GHashTableIter iter; gpointer val; g_hash_table_iter_init(&iter, self->cli_sessions); while( g_hash_table_iter_next(&iter, 0, &val) ) { cka_session_t *session = val; cka_session_finish(session, now); } /* NULL error -> match will be removed asynchronously */ dbus_bus_remove_match(cka_dbus_systembus, self->cli_match_rule, 0); /* Cleanup */ g_hash_table_unref(self->cli_sessions); g_free(self->cli_dbus_name); g_free(self->cli_match_rule); g_free(self); } } /** Typeless helper function for use as destroy callback * * @param self pointer to cka_client_t structure */ static void cka_client_delete_cb(void *self) { cka_client_delete(self); } /* ========================================================================= * * * KEEPALIVE_STATE * * ========================================================================= */ /** Set keepalive state * * @param active true to start keepalive, false to end keepalive */ static void cka_state_set(bool active) { static bool keepalive_is_active = false; static bool flagged = false; static tick_t started = 0; if( keepalive_is_active != active ) { tick_t now = cka_tick_get_current(); if( (keepalive_is_active = active) ) { #ifdef ENABLE_WAKELOCKS wakelock_lock(cpu_wakelock, -1); #endif started = now; mce_log(LL_DEVEL, "keepalive started"); } else { tick_t dur = now - started; if( dur > KEEPALIVE_STATE_WARN_LIMIT_MS ) { mce_log(LL_CRIT, "long keepalive stopped after %"PRId64" ms", dur); } else { mce_log(LL_DEVEL, "keepalive stopped after %"PRId64" ms", dur); } flagged = false; #ifdef ENABLE_WAKELOCKS wakelock_unlock(cpu_wakelock); #endif } } else if( keepalive_is_active && !flagged ) { tick_t now = cka_tick_get_current(); tick_t dur = now - started; if( dur > KEEPALIVE_STATE_WARN_LIMIT_MS ) { flagged = true; mce_log(LL_CRIT, "long keepalive active after %"PRId64" ms", dur); } } } /** Handle triggering of cpu-keepalive timer * * Releases cpu keepalive wakelock and thus allows the system to * enter late suspend according to other policies. * * @param data (not used) * * @return FALSE (to stop then timer from repeating) */ static gboolean cka_state_timer_cb(gpointer data) { (void)data; if( cka_state_timer_id != 0 ) { mce_log(LL_DEBUG, "cpu-keepalive timeout triggered"); cka_state_timer_id = 0; /* Do full rethink to expire client sessions */ cka_state_rethink(); } return FALSE; } /** Cancel end of cpu-keepalive timer */ static void cka_state_reset(void) { if( cka_state_timer_id != 0 ) { mce_log(LL_DEBUG, "cpu-keepalive timeout canceled"); g_source_remove(cka_state_timer_id), cka_state_timer_id = 0; } cka_state_set(false); } /** Re-evaluate the end of cpu-keepalive period * * Calculates maximum of wakeup period and per client renew periods * and uses it to reprogram the end of cpu-keepalive period */ static void cka_state_rethink(void) { tick_t now = cka_tick_get_current(); /* Find furthest away client renew timeout */ tick_t maxtime = cka_clients_wakeup_timeout; GHashTableIter iter; gpointer val; g_hash_table_iter_init(&iter, cka_clients_lut); while( g_hash_table_iter_next(&iter, 0, &val) ) { cka_client_t *client = val; cka_client_scan_timeout(client); if( maxtime < client->cli_timeout ) { maxtime = client->cli_timeout; } } /* Remove existing timer */ if( cka_state_timer_id != 0 ) { g_source_remove(cka_state_timer_id), cka_state_timer_id = 0; } /* If needed, program timer */ static tick_t oldtime = 0; if( now < maxtime ) { if( maxtime != oldtime ) { mce_log(LL_DEBUG, "cpu-keepalive timeout at T%+"PRId64"", now - maxtime); } cka_state_timer_id = g_timeout_add(maxtime - now, cka_state_timer_cb, 0); } oldtime = maxtime; cka_state_set(cka_state_timer_id != 0); } /* ========================================================================= * * * CLIENT_MANAGEMENT * * ========================================================================= */ /** Remove bookkeeping data for a client and re-evaluate cpu keepalive status * * @param dbus_name dbus name of the client */ static void cka_clients_remove_client(const gchar *dbus_name) { if( g_hash_table_remove(cka_clients_lut, dbus_name) ) { cka_state_rethink(); } } /** Obtain bookkeeping data for a client * * @param dbus_name dbus name of the client * * @return client data, or NULL if dbus_name is not tracked */ static cka_client_t * cka_clients_get_client(const gchar *dbus_name) { cka_client_t *client = g_hash_table_lookup(cka_clients_lut, dbus_name); return client; } /** Call back for handling asynchronous client verification via GetNameOwner * * @param pending control structure for asynchronous d-bus methdod call * @param user_data dbus_name of the client as void poiter */ static void cka_clients_verify_name_cb(DBusPendingCall *pending, void *user_data) { const gchar *name = user_data; gchar *owner = 0; DBusMessage *rsp = 0; cka_client_t *client = 0; if( !(rsp = dbus_pending_call_steal_reply(pending)) ) { goto EXIT; } if( !(client = cka_clients_get_client(name)) ) { mce_log(LL_WARN, "untracked client %s", name); } if( !(owner = cka_dbusutil_parse_GetNameOwner_rsp(rsp)) || !*owner ) { mce_log(LL_WARN, "dead client %s", name); cka_clients_remove_client(name), client = 0; } else { mce_log(LL_DEBUG, "live client %s, owner %s", name, owner); } EXIT: g_free(owner); if( rsp ) dbus_message_unref(rsp); } /** Verify that a client exists via an asynchronous GetNameOwner method call * * @param name the dbus name who's owner we wish to know * * @return TRUE if the method call was initiated, or FALSE in case of errors */ static gboolean cka_clients_verify_name(const char *name) { gboolean res = FALSE; DBusMessage *req = 0; DBusPendingCall *pc = 0; gchar *key = 0; if( !(req = cka_dbusutil_create_GetNameOwner_req(name)) ) { goto EXIT; } if( !dbus_connection_send_with_reply(cka_dbus_systembus, req, &pc, -1) ) { goto EXIT; } if( !pc ) { goto EXIT; } mce_dbus_pending_call_blocks_suspend(pc); key = g_strdup(name); if( !dbus_pending_call_set_notify(pc, cka_clients_verify_name_cb, key, g_free) ) { goto EXIT; } // key string is owned by pending call key = 0; // success res = TRUE; EXIT: g_free(key); if( pc ) dbus_pending_call_unref(pc); if( req ) dbus_message_unref(req); return res; } /** Find existing / create new client data by dbus name * * @param dbus_name dbus name of the client * * @return client data */ static cka_client_t * cka_clients_add_client(const gchar *dbus_name) { cka_client_t *client = g_hash_table_lookup(cka_clients_lut, dbus_name); if( !client ) { /* Make a dummy peer identification request, so we have it already * cached in case we actually need it later on */ mce_dbus_get_name_owner_ident(dbus_name); /* The cka_client_create() adds NameOwnerChanged signal match * so that we know when/if the client exits, crashes or * otherwise loses dbus connection. */ client = cka_client_create(dbus_name); g_hash_table_insert(cka_clients_lut, g_strdup(dbus_name), client); /* Then make an explicit GetNameOwner request to verify that * the client is still running when we have the signal * listening in the place. */ cka_clients_verify_name(dbus_name); } return client; } /** Register client and start minor keep-alive period * * If needed will add the dbus_name to list of tracked clients. * * @param dbus_name name of the tracked client */ static void cka_clients_add_session(const gchar *dbus_name, const char *session_id) { cka_client_t *client = cka_clients_add_client(dbus_name); tick_t when = cka_tick_get_timeout(-1, MCE_CPU_KEEPALIVE_QUERY_PERIOD_S); cka_client_update_timeout(client, session_id, when); cka_state_rethink(); } /** Adjust the cpu-keepalive timeout for dbus client * * If needed will add the dbus_name to list of tracked clients. * * @param dbus_name name of the tracked client */ static void cka_clients_start_session(const gchar *dbus_name, const gchar *session_id) { cka_client_t *client = cka_clients_add_client(dbus_name); tick_t when = cka_tick_get_timeout(-1, MCE_CPU_KEEPALIVE_MAXIMUM_PERIOD_S); cka_client_remove_timeout(client, SESSION_ID_INITIAL); cka_client_update_timeout(client, session_id, when); /* We got at least one keep alive request, extend the minimum * alive time a bit to give other clients time to get scheduled */ cka_clients_wakeup_timeout = cka_tick_get_timeout(cka_clients_wakeup_started, MCE_RTC_WAKEUP_2ND_TIMEOUT_S); cka_state_rethink(); } /** Remove the cpu-keepalive timeout for dbus client * * @param dbus_name name of the tracked client */ static void cka_clients_stop_session(const gchar *dbus_name, const char *session_id) { cka_client_t *client = cka_clients_get_client(dbus_name); if( client ) { cka_client_remove_timeout(client, SESSION_ID_INITIAL); cka_client_remove_timeout(client, session_id); cka_state_rethink(); } else { mce_log(LL_WARN, "untracked client %s", dbus_name); } } /** Transfer resume-due-to-rtc-input wakelock from dsme to mce * * @param dbus_name name of the client issuing the request */ static void cka_clients_handle_wakeup(const gchar *dbus_name) { // FIXME: we should check that the dbus_name == DSME (void)dbus_name; /* Time of wakeup received */ cka_clients_wakeup_started = cka_tick_get_current(); /* Timeout for the 1st keepalive message to come through */ cka_clients_wakeup_timeout = cka_tick_get_timeout(cka_clients_wakeup_started, MCE_RTC_WAKEUP_1ST_TIMEOUT_S); cka_state_rethink(); mce_log(LL_NOTICE, "rtc wakeup finished"); #ifdef ENABLE_WAKELOCKS wakelock_unlock(rtc_wakelock); #endif } /** Initialize client tracking */ static void cka_clients_init(void) { if( !cka_clients_lut ) { cka_clients_lut = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, cka_client_delete_cb); } } /** Cleanup client tracking */ static void cka_clients_quit(void) { if( cka_clients_lut ) { g_hash_table_unref(cka_clients_lut), cka_clients_lut = 0; } } /* ========================================================================= * * * DBUS_HANDLERS * * ========================================================================= */ /** D-Bus callback for the MCE_CPU_KEEPALIVE_PERIOD_REQ method call * * @param msg The D-Bus message * * @return TRUE on success, FALSE on failure */ static gboolean cka_dbus_handle_period_cb(DBusMessage *const msg) { gboolean success = FALSE; DBusError err = DBUS_ERROR_INIT; const char *sender = 0; const char *session_id = 0; if( !(sender = dbus_message_get_sender(msg)) ) { goto EXIT; } mce_log(LL_NOTICE, "got keepalive period query from %s", mce_dbus_get_name_owner_ident(sender)); if( !dbus_message_get_args(msg, &err, DBUS_TYPE_STRING, &session_id, DBUS_TYPE_INVALID) ) { // initial dbus interface did not include session_id string if( strcmp(err.name, DBUS_ERROR_INVALID_ARGS) ) { mce_log(LL_WARN, "%s: %s", err.name, err.message); goto EXIT; } session_id = SESSION_ID_INITIAL; mce_log(LL_DEBUG, "sender did not supply session_id string; using '%s'", session_id); } cka_clients_add_session(sender, session_id); success = cka_dbusutil_reply_int(msg, MCE_CPU_KEEPALIVE_SUGGESTED_PERIOD_S); EXIT: dbus_error_free(&err); return success; } /** D-Bus callback for the MCE_CPU_KEEPALIVE_START_REQ method call * * @param msg The D-Bus message * * @return TRUE on success, FALSE on failure */ static gboolean cka_dbus_handle_start_cb(DBusMessage *const msg) { gboolean success = FALSE; DBusError err = DBUS_ERROR_INIT; const char *sender = 0; const char *session_id = 0; if( !(sender = dbus_message_get_sender(msg)) ) { goto EXIT; } mce_log(LL_NOTICE, "got keepalive start from %s", mce_dbus_get_name_owner_ident(sender)); if( !dbus_message_get_args(msg, &err, DBUS_TYPE_STRING, &session_id, DBUS_TYPE_INVALID) ) { // initial dbus interface did not include session_id string if( strcmp(err.name, DBUS_ERROR_INVALID_ARGS) ) { mce_log(LL_WARN, "%s: %s", err.name, err.message); goto EXIT; } session_id = SESSION_ID_DEFAULT; mce_log(LL_DEBUG, "sender did not supply session_id string; using '%s'", session_id); } cka_clients_start_session(sender, session_id); success = TRUE; EXIT: cka_dbusutil_reply_bool(msg, success); dbus_error_free(&err); return success; } /** D-Bus callback for the MCE_CPU_KEEPALIVE_STOP_REQ method call * * @param msg The D-Bus message * * @return TRUE on success, FALSE on failure */ static gboolean cka_dbus_handle_stop_cb(DBusMessage *const msg) { gboolean success = FALSE; DBusError err = DBUS_ERROR_INIT; const char *sender = 0; const char *session_id = 0; if( !(sender = dbus_message_get_sender(msg)) ) { goto EXIT; } mce_log(LL_NOTICE, "got keepalive stop from %s", mce_dbus_get_name_owner_ident(sender)); if( !dbus_message_get_args(msg, &err, DBUS_TYPE_STRING, &session_id, DBUS_TYPE_INVALID) ) { // initial dbus interface did not include session_id string if( strcmp(err.name, DBUS_ERROR_INVALID_ARGS) ) { mce_log(LL_WARN, "%s: %s", err.name, err.message); goto EXIT; } session_id = SESSION_ID_DEFAULT; mce_log(LL_DEBUG, "sender did not supply session_id string; using '%s'", session_id); } cka_clients_stop_session(sender, session_id); success = TRUE; EXIT: cka_dbusutil_reply_bool(msg, success); dbus_error_free(&err); return success; } /** D-Bus callback for the MCE_CPU_KEEPALIVE_WAKEUP_REQ method call * * @param msg The D-Bus message * * @return TRUE on success, FALSE on failure */ static gboolean cka_dbus_handle_wakeup_cb(DBusMessage *const msg) { gboolean success = FALSE; const char *sender = 0; if( !(sender = dbus_message_get_sender(msg)) ) { goto EXIT; } mce_log(LL_NOTICE, "got keepalive wakeup from %s", mce_dbus_get_name_owner_ident(sender)); cka_clients_handle_wakeup(sender); success = TRUE; EXIT: cka_dbusutil_reply_bool(msg, success); return success; } /** D-Bus message filter for handling NameOwnerChanged signals * * @param con dbus connection * @param msg message to be acted upon * @param user_data (not used) * * @return DBUS_HANDLER_RESULT_NOT_YET_HANDLED (other filters see the msg too) */ static DBusHandlerResult cka_dbus_filter_message_cb(DBusConnection *con, DBusMessage *msg, void *user_data) { (void)user_data; DBusHandlerResult res = DBUS_HANDLER_RESULT_NOT_YET_HANDLED; const char *sender = 0; const char *object = 0; const char *name = 0; const char *prev = 0; const char *curr = 0; DBusError err = DBUS_ERROR_INIT; if( con != cka_dbus_systembus ) { goto EXIT; } if( !dbus_message_is_signal(msg, DBUS_INTERFACE_DBUS, "NameOwnerChanged") ) { goto EXIT; } sender = dbus_message_get_sender(msg); if( !sender || strcmp(sender, DBUS_SERVICE_DBUS) ) { goto EXIT; } object = dbus_message_get_path(msg); if( !object || strcmp(object, DBUS_PATH_DBUS) ) { goto EXIT; } if( !dbus_message_get_args(msg, &err, DBUS_TYPE_STRING, &name, DBUS_TYPE_STRING, &prev, DBUS_TYPE_STRING, &curr, DBUS_TYPE_INVALID) ) { mce_log(LL_WARN, "%s: %s", err.name, err.message); goto EXIT; } if( !*curr ) { mce_log(LL_DEBUG, "name lost owner: %s", name); cka_clients_remove_client(name); } EXIT: dbus_error_free(&err); return res; } /** Array of dbus message handlers */ static mce_dbus_handler_t cka_dbus_handlers[] = { /* method calls */ { .interface = MCE_REQUEST_IF, .name = MCE_CPU_KEEPALIVE_PERIOD_REQ, .type = DBUS_MESSAGE_TYPE_METHOD_CALL, .callback = cka_dbus_handle_period_cb, .args = " \n" " \n" }, { .interface = MCE_REQUEST_IF, .name = MCE_CPU_KEEPALIVE_START_REQ, .type = DBUS_MESSAGE_TYPE_METHOD_CALL, .callback = cka_dbus_handle_start_cb, .args = " \n" " \n" }, { .interface = MCE_REQUEST_IF, .name = MCE_CPU_KEEPALIVE_STOP_REQ, .type = DBUS_MESSAGE_TYPE_METHOD_CALL, .callback = cka_dbus_handle_stop_cb, .args = " \n" " \n" }, { .interface = MCE_REQUEST_IF, .name = MCE_CPU_KEEPALIVE_WAKEUP_REQ, .type = DBUS_MESSAGE_TYPE_METHOD_CALL, .callback = cka_dbus_handle_wakeup_cb, .privileged = true, .args = " \n" }, /* sentinel */ { .interface = 0 } }; /** Install signal and method call message handlers * * @return TRUE on success, or FALSE on failure */ static gboolean cka_dbus_init(void) { gboolean success = FALSE; if( !(cka_dbus_systembus = dbus_connection_get()) ) { goto EXIT; } /* Register signal handling filter */ dbus_connection_add_filter(cka_dbus_systembus, cka_dbus_filter_message_cb, 0, 0); /* Register dbus method call handlers */ mce_dbus_handler_register_array(cka_dbus_handlers); success = TRUE; EXIT: return success; } /** Remove signal and method call message handlers */ static void cka_dbus_quit(void) { if( !cka_dbus_systembus ) { goto EXIT; } /* Remove signal handling filter */ dbus_connection_remove_filter(cka_dbus_systembus, cka_dbus_filter_message_cb, 0); /* Remove dbus method call handlers that we have registered */ mce_dbus_handler_unregister_array(cka_dbus_handlers); dbus_connection_unref(cka_dbus_systembus), cka_dbus_systembus = 0; EXIT: return; } /* ========================================================================= * * * MODULE_INIT_QUIT * * ========================================================================= */ /** Init function for the cpu-keepalive module * * @param module (not used) * * @return NULL on success, a string with an error message on failure */ const gchar *g_module_check_init(GModule *module) { (void)module; const gchar *status = NULL; if( !cka_dbus_init() ) { status = "initializing dbus connection failed"; goto EXIT; } cka_clients_init(); EXIT: mce_log(LL_DEBUG, "loaded %s, status: %s", module_name, status ?: "ok"); return status; } /** Exit function for the cpu-keepalive module * * @param module (not used) */ void g_module_unload(GModule *module) { (void)module; /* If we have active clients, removal expects a valid dbus * connection -> purge clients first */ cka_clients_quit(); cka_dbus_quit(); /* Make sure the wakelock is released */ cka_state_reset(); mce_log(LL_DEBUG, "unloaded %s", module_name); return; }