upower/src/linux/up-device-supply.c
Kate Hsuan c5df437cc1 linux: up-device-supply: fix the resource leak
1. free the udev parent node automatically.
2. free new_model_name variable in the same scope.
2025-11-28 15:00:22 +08:00

743 lines
21 KiB
C

/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
*
* Copyright (C) 2008 David Zeuthen <davidz@redhat.com>
* Copyright (C) 2008 Richard Hughes <richard@hughsie.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
*
*/
#include "config.h"
#include <string.h>
#include <math.h>
#include <glib.h>
#include <glib/gstdio.h>
#include <glib/gprintf.h>
#include <glib/gi18n-lib.h>
#include <glib-object.h>
#include <gudev/gudev.h>
#include "up-config.h"
#include "up-types.h"
#include "up-constants.h"
#include "up-device-supply.h"
#include "up-common.h"
struct UpDeviceSupplyPrivate
{
gboolean has_coldplug_values;
gboolean shown_invalid_voltage_warning;
};
G_DEFINE_TYPE_WITH_PRIVATE (UpDeviceSupply, up_device_supply, UP_TYPE_DEVICE)
static gboolean up_device_supply_refresh (UpDevice *device,
UpRefreshReason reason);
static UpDeviceKind up_device_supply_guess_type (GUdevDevice *native,
const char *native_path);
static gboolean
up_device_supply_refresh_line_power (UpDeviceSupply *supply,
UpRefreshReason reason)
{
UpDevice *device = UP_DEVICE (supply);
GUdevDevice *native;
gboolean online_old, online_new;
/* get new AC value */
native = G_UDEV_DEVICE (up_device_get_native (device));
g_object_get (device,
"online", &online_old,
NULL);
online_new = g_udev_device_get_sysfs_attr_as_int_uncached (native, "online");
/* Avoid notification if the value did not change. */
if (online_old != online_new)
g_object_set (device,
"online", online_new,
NULL);
return TRUE;
}
/**
* up_device_supply_reset_values:
**/
static void
up_device_supply_reset_values (UpDeviceSupply *supply)
{
supply->priv->has_coldplug_values = FALSE;
/* reset to default */
g_object_set (supply,
"vendor", NULL,
"model", NULL,
"serial", NULL,
"update-time", (guint64) 0,
"online", FALSE,
"energy", (gdouble) 0.0,
"is-present", FALSE,
"is-rechargeable", FALSE,
"has-history", FALSE,
"has-statistics", FALSE,
"state", UP_DEVICE_STATE_UNKNOWN,
"capacity", (gdouble) 0.0,
"energy-empty", (gdouble) 0.0,
"energy-full", (gdouble) 0.0,
"energy-full-design", (gdouble) 0.0,
"energy-rate", (gdouble) 0.0,
"voltage", (gdouble) 0.0,
"time-to-empty", (gint64) 0,
"time-to-full", (gint64) 0,
"percentage", (gdouble) 0.0,
"temperature", (gdouble) 0.0,
"technology", UP_DEVICE_TECHNOLOGY_UNKNOWN,
"charge-cycles", -1,
"charge-start-threshold", 0,
"charge-end-threshold", 100,
"charge-threshold-enabled", FALSE,
"charge-threshold-supported", FALSE,
NULL);
}
/**
* up_device_supply_get_online:
**/
static gboolean
up_device_supply_get_online (UpDevice *device, gboolean *online)
{
UpDeviceSupply *supply = UP_DEVICE_SUPPLY (device);
UpDeviceKind type;
gboolean online_tmp;
g_return_val_if_fail (UP_IS_DEVICE_SUPPLY (supply), FALSE);
g_return_val_if_fail (online != NULL, FALSE);
g_object_get (device,
"type", &type,
"online", &online_tmp,
NULL);
if (type != UP_DEVICE_KIND_LINE_POWER)
return FALSE;
*online = online_tmp;
return TRUE;
}
/**
* up_device_supply_get_string:
**/
static gchar *
up_device_supply_get_string (GUdevDevice *native, const gchar *key)
{
gchar *value;
/* get value, and strip to remove spaces */
value = g_strdup (g_udev_device_get_sysfs_attr_uncached (native, key));
if (value)
g_strstrip (value);
/* no value */
if (value == NULL)
goto out;
/* empty value */
if (value[0] == '\0') {
g_free (value);
value = NULL;
goto out;
}
out:
return value;
}
UpDeviceState
up_device_supply_get_state (GUdevDevice *native)
{
UpDeviceState state;
gchar *status;
status = up_device_supply_get_string (native, "status");
if (status == NULL ||
g_ascii_strcasecmp (status, "unknown") == 0 ||
*status == '\0') {
state = UP_DEVICE_STATE_UNKNOWN;
} else if (g_ascii_strcasecmp (status, "charging") == 0)
state = UP_DEVICE_STATE_CHARGING;
else if (g_ascii_strcasecmp (status, "discharging") == 0)
state = UP_DEVICE_STATE_DISCHARGING;
else if (g_ascii_strcasecmp (status, "full") == 0)
state = UP_DEVICE_STATE_FULLY_CHARGED;
else if (g_ascii_strcasecmp (status, "empty") == 0)
state = UP_DEVICE_STATE_EMPTY;
else if (g_ascii_strcasecmp (status, "not charging") == 0)
state = UP_DEVICE_STATE_PENDING_CHARGE;
else {
g_warning ("unknown status string: %s", status);
state = UP_DEVICE_STATE_UNKNOWN;
}
g_free (status);
return state;
}
static gdouble
sysfs_get_capacity_level (GUdevDevice *native,
UpDeviceLevel *level)
{
char *str;
gdouble ret = -1.0;
guint i;
struct {
const char *str;
gdouble percentage;
UpDeviceLevel level;
} levels[] = {
/* In order of most likely to least likely,
* Keep in sync with up_daemon_compute_warning_level() */
{ "Normal", 55.0, UP_DEVICE_LEVEL_NORMAL },
{ "High", 70.0, UP_DEVICE_LEVEL_HIGH },
{ "Low", 10.0, UP_DEVICE_LEVEL_LOW },
{ "Critical", 5.0, UP_DEVICE_LEVEL_CRITICAL },
{ "Full", 100.0, UP_DEVICE_LEVEL_FULL },
{ "Unknown", 50.0, UP_DEVICE_LEVEL_UNKNOWN }
};
g_return_val_if_fail (level != NULL, -1.0);
if (!g_udev_device_has_sysfs_attr_uncached (native, "capacity_level")) {
g_debug ("capacity_level doesn't exist, skipping");
*level = UP_DEVICE_LEVEL_NONE;
return -1.0;
}
*level = UP_DEVICE_LEVEL_UNKNOWN;
str = g_strchomp (g_strdup (g_udev_device_get_sysfs_attr_uncached (native, "capacity_level")));
if (!str) {
g_debug ("Failed to read capacity_level!");
return ret;
}
for (i = 0; i < G_N_ELEMENTS(levels); i++) {
if (strcmp (levels[i].str, str) == 0) {
ret = levels[i].percentage;
*level = levels[i].level;
break;
}
}
if (ret < 0.0)
g_debug ("Could not find a percentage for capacity level '%s'", str);
g_free (str);
return ret;
}
static gboolean
up_device_supply_refresh_device (UpDeviceSupply *supply,
UpRefreshReason reason)
{
UpDeviceState state;
UpDevice *device = UP_DEVICE (supply);
GUdevDevice *native;
gdouble percentage = 0.0f;
UpDeviceLevel level = UP_DEVICE_LEVEL_NONE;
gboolean is_present = TRUE;
native = G_UDEV_DEVICE (up_device_get_native (device));
/* initial values */
if (!supply->priv->has_coldplug_values) {
gchar *model_name;
gchar *serial_number;
/* get values which may be blank */
model_name = up_device_supply_get_string (native, "model_name");
serial_number = up_device_supply_get_string (native, "serial_number");
/* some vendors fill this with binary garbage */
up_make_safe_string (model_name);
up_make_safe_string (serial_number);
g_object_set (device,
"model", model_name,
"serial", serial_number,
"is-rechargeable", TRUE,
"has-history", TRUE,
"has-statistics", TRUE,
NULL);
/* we only coldplug once, as these values will never change */
supply->priv->has_coldplug_values = TRUE;
g_free (model_name);
g_free (serial_number);
}
/* Some devices change whether they're present or not */
if (g_udev_device_has_sysfs_attr_uncached (native, "present"))
is_present = g_udev_device_get_sysfs_attr_as_boolean_uncached (native, "present");
/* get a precise percentage */
percentage = g_udev_device_get_sysfs_attr_as_double_uncached (native, "capacity");
if (percentage == 0.0f)
percentage = sysfs_get_capacity_level (native, &level);
if (percentage < 0.0) {
/* Probably talking to the device over Bluetooth */
state = UP_DEVICE_STATE_UNKNOWN;
g_object_set (device,
"state", state,
"is-present", is_present,
NULL);
return FALSE;
}
state = up_device_supply_get_state (native);
/* Override whatever the device might have told us
* because a number of them are always discharging */
if (percentage == 100.0)
state = UP_DEVICE_STATE_FULLY_CHARGED;
g_object_set (device,
"percentage", percentage,
"battery-level", level,
"state", state,
"is-present", is_present,
NULL);
return TRUE;
}
static void
up_device_supply_sibling_discovered_guess_type (UpDevice *device,
GObject *sibling)
{
GUdevDevice *input, *native_device;
g_autoptr (GUdevDevice) parent_device = NULL;
g_autoptr (GUdevDevice) parent_sibling = NULL;
UpDeviceKind cur_type, new_type;
gboolean is_same_parent = FALSE;
char *new_model_name;
char *model_name;
char *serial_number;
int i;
struct {
const char *prop;
UpDeviceKind type;
} types[] = {
/* In order of type priority (*within* one input node). */
{ "SOUND_INITIALIZED", UP_DEVICE_KIND_OTHER_AUDIO },
{ "ID_INPUT_TABLET", UP_DEVICE_KIND_TABLET },
{ "ID_INPUT_TOUCHPAD", UP_DEVICE_KIND_TOUCHPAD },
{ "ID_INPUT_MOUSE", UP_DEVICE_KIND_MOUSE },
{ "ID_INPUT_JOYSTICK", UP_DEVICE_KIND_GAMING_INPUT },
{ "ID_INPUT_KEYBOARD", UP_DEVICE_KIND_KEYBOARD },
};
/* The type priority if we have multiple siblings,
* i.e. we select the first of the current type of the found type.
* Give a new priority for device type since the GAMING_INPUT may include
* a keyboard, a touchpad, and... etc, for example Sony DualShock4 joystick.
* A mouse and a touchpad may include a mouse and a keyboard.
* Therefore, the priority is:
* 1. Gaming_input
* 2. Audio
* 3. Keyboard
* 4. Tablet
* 5. Touchpad
* 6. Mouse
*/
UpDeviceKind priority[] = {
UP_DEVICE_KIND_GAMING_INPUT,
UP_DEVICE_KIND_OTHER_AUDIO,
UP_DEVICE_KIND_KEYBOARD,
UP_DEVICE_KIND_TABLET,
UP_DEVICE_KIND_TOUCHPAD,
UP_DEVICE_KIND_MOUSE
};
/* Form-factors set in rules.d/78-sound-card.rules in systemd */
struct {
const char *form_factor;
UpDeviceKind kind;
} sound_types[] = {
{ "webcam", UP_DEVICE_KIND_VIDEO },
{ "speaker", UP_DEVICE_KIND_SPEAKERS },
{ "headphone", UP_DEVICE_KIND_HEADPHONES },
{ "headset", UP_DEVICE_KIND_HEADSET },
/* unhandled:
* - handset
* - microphone */
};
input = G_UDEV_DEVICE (sibling);
/* Do not process if we already have a "good" guess for the device type. */
g_object_get (device, "type", &cur_type, NULL);
if (cur_type == UP_DEVICE_KIND_LINE_POWER)
return;
if (g_strcmp0 (g_udev_device_get_subsystem (input), "input") != 0 &&
g_strcmp0 (g_udev_device_get_subsystem (input), "sound") != 0)
return;
/* Only process "card" devices, as those are tagged with form-factor */
if (g_str_equal (g_udev_device_get_subsystem (input), "sound") &&
!g_str_has_prefix (g_udev_device_get_name (input), "card"))
return;
g_object_get (device,
"model", &model_name,
"serial", &serial_number,
NULL);
if (model_name == NULL) {
model_name = up_device_supply_get_string (input, "name");
up_make_safe_string (model_name);
g_object_set (device,
"model", model_name,
NULL);
g_free (model_name);
}
if (serial_number == NULL) {
serial_number = up_device_supply_get_string (input, "uniq");
up_make_safe_string (serial_number);
g_object_set (device,
"serial", serial_number,
NULL);
g_free (serial_number);
}
new_type = UP_DEVICE_KIND_UNKNOWN;
for (i = 0; i < G_N_ELEMENTS (types); i++) {
if (g_udev_device_get_property_as_boolean (input, types[i].prop)) {
new_type = types[i].type;
break;
}
}
for (i = 0; i < G_N_ELEMENTS (priority); i++) {
if (priority[i] == cur_type || priority[i] == new_type) {
new_type = priority[i];
break;
}
}
/* Match audio sub-type */
if (new_type == UP_DEVICE_KIND_OTHER_AUDIO) {
const char *form_factor = g_udev_device_get_property (input, "SOUND_FORM_FACTOR");
g_debug ("Guessing audio sub-type from SOUND_FORM_FACTOR='%s'", form_factor);
for (i = 0; form_factor != NULL && i < G_N_ELEMENTS (sound_types); i++) {
if (g_strcmp0 (form_factor, sound_types[i].form_factor) == 0) {
new_type = sound_types[i].kind;
break;
}
}
}
/* TODO: Add a heuristic here (and during initial discovery) that uses
* the model name.
*/
/* Fall back to "keyboard" if we didn't find anything. */
if (new_type == UP_DEVICE_KIND_UNKNOWN) {
if (cur_type != UP_DEVICE_KIND_UNKNOWN) {
g_debug ("Not overwriting existing type '%s'",
up_device_kind_to_string(cur_type));
return;
}
new_type = UP_DEVICE_KIND_KEYBOARD;
}
if (cur_type != new_type) {
native_device = G_UDEV_DEVICE (up_device_get_native (device));
parent_device = g_udev_device_get_parent (native_device);
parent_sibling = g_udev_device_get_parent (input);
g_debug ("Type changed from %s to %s",
up_device_kind_to_string (cur_type),
up_device_kind_to_string (new_type));
/* Check if the device and the sibling have the same parent. */
if (!g_strcmp0 (g_udev_device_get_sysfs_path (parent_device),
g_udev_device_get_sysfs_path (parent_sibling)))
is_same_parent = TRUE;
new_model_name = up_device_supply_get_string (input, "name");
/* The model name of a device component may be different. For example, DualSense
* joystick owns "Sony Interactive Entertainment DualSense Wireless Controller"
* for the joystick and "Sony Interactive Entertainment DualSense Wireless Controller
* Motion Sensors" for the accelerometer. If the type is change, the corresponding
* model name have to be changed too. */
if (new_model_name != NULL && is_same_parent) {
up_make_safe_string (new_model_name);
g_object_set (device,
"type", new_type,
"model", new_model_name,
NULL);
} else {
g_object_set (device, "type", new_type, NULL);
}
g_free (new_model_name);
}
}
static void
up_device_supply_sibling_discovered_handle_wireless_status (UpDevice *device,
GObject *obj)
{
const char *status;
GUdevDevice *sibling = G_UDEV_DEVICE (obj);
status = g_udev_device_get_sysfs_attr_uncached (sibling, "wireless_status");
if (!status)
return;
if (!g_str_equal (status, "connected") &&
!g_str_equal (status, "disconnected")) {
g_warning ("Unhandled wireless_status value '%s' on %s",
status, g_udev_device_get_sysfs_path (sibling));
return;
}
g_debug ("Detected wireless_status '%s' on %s",
status, g_udev_device_get_sysfs_path (sibling));
g_object_set (G_OBJECT (device),
"disconnected", g_str_equal (status, "disconnected"),
NULL);
}
static void
up_device_supply_sibling_discovered (UpDevice *device,
GObject *sibling)
{
GUdevDevice *native;
if (!G_UDEV_IS_DEVICE (sibling))
return;
native = G_UDEV_DEVICE (up_device_get_native (device));
g_debug ("up_device_supply_sibling_discovered (device: %s, sibling: %s)",
g_udev_device_get_sysfs_path (native),
g_udev_device_get_sysfs_path (G_UDEV_DEVICE (sibling)));
up_device_supply_sibling_discovered_guess_type (device, sibling);
up_device_supply_sibling_discovered_handle_wireless_status (device, sibling);
}
static UpDeviceKind
up_device_supply_guess_type (GUdevDevice *native,
const char *native_path)
{
gchar *device_type;
UpDeviceKind type = UP_DEVICE_KIND_UNKNOWN;
device_type = up_device_supply_get_string (native, "type");
if (device_type == NULL)
return type;
if (g_ascii_strcasecmp (device_type, "mains") == 0) {
type = UP_DEVICE_KIND_LINE_POWER;
goto out;
}
if (g_ascii_strcasecmp (device_type, "battery") == 0) {
type = UP_DEVICE_KIND_BATTERY;
} else if (g_ascii_strcasecmp (device_type, "USB") == 0) {
/* USB supplies should have a usb_type attribute which we would
* ideally decode further.
*
* For historic reasons, we have a heuristic for wacom tablets
* that can be dropped in the future.
* As of May 2022, it is expected to be fixed in kernel 5.19.
* https://patchwork.kernel.org/project/linux-input/patch/20220407115406.115112-1-hadess@hadess.net/
*/
if (g_udev_device_has_sysfs_attr (native, "usb_type") &&
g_udev_device_has_sysfs_attr (native, "online"))
type = UP_DEVICE_KIND_LINE_POWER;
else if (g_strstr_len (native_path, -1, "wacom_") != NULL)
type = UP_DEVICE_KIND_TABLET;
else
g_warning ("USB power supply %s without usb_type property, please report",
native_path);
} else {
g_warning ("did not recognize type %s, please report", device_type);
}
out:
g_free (device_type);
return type;
}
/**
* up_device_supply_coldplug:
*
* Return %TRUE on success, %FALSE if we failed to get data and should be removed
**/
static gboolean
up_device_supply_coldplug (UpDevice *device)
{
UpDeviceSupply *supply = UP_DEVICE_SUPPLY (device);
GUdevDevice *native;
const gchar *native_path;
const gchar *scope;
UpDeviceKind type;
gboolean is_power_supply;
up_device_supply_reset_values (supply);
/* detect what kind of device we are */
native = G_UDEV_DEVICE (up_device_get_native (device));
native_path = g_udev_device_get_sysfs_path (native);
if (native_path == NULL) {
g_warning ("could not get native path for %p", device);
return FALSE;
}
/* try to work out if the device is powering the system */
scope = g_udev_device_get_sysfs_attr (native, "scope");
if (scope != NULL && g_ascii_strcasecmp (scope, "device") == 0) {
is_power_supply = FALSE;
} else if (scope != NULL && g_ascii_strcasecmp (scope, "system") == 0) {
is_power_supply = TRUE;
} else {
g_debug ("taking a guess for power supply scope");
is_power_supply = TRUE;
}
/* we don't use separate ACs for devices */
if (is_power_supply == FALSE &&
!g_udev_device_has_sysfs_attr_uncached (native, "capacity") &&
!g_udev_device_has_sysfs_attr_uncached (native, "capacity_level")) {
g_debug ("Ignoring device AC, we'll monitor the device battery");
return FALSE;
}
/* try to detect using the device type */
type = up_device_supply_guess_type (native, native_path);
/* if reading the device type did not work, use heuristic */
if (type == UP_DEVICE_KIND_UNKNOWN) {
if (g_udev_device_has_sysfs_attr_uncached (native, "online")) {
g_debug ("'online' attribute was found. "
"Assume it is a line power supply.");
type = UP_DEVICE_KIND_LINE_POWER;
} else {
/*
* This might be a battery or a UPS, but it might also
* be something else that we really don't know how to
* handle (e.g. BMS, exposed by Android-centric vendor
* kernels in parallel to actual battery).
*
* As such, we have no choice but to assume that we
* can't handle this device, and ignore it.
*/
return FALSE;
}
}
/* set the value */
g_object_set (device,
"type", type,
"power-supply", is_power_supply,
NULL);
/* Handled by separate battery class */
if (is_power_supply)
g_assert (type == UP_DEVICE_KIND_LINE_POWER);
if (type != UP_DEVICE_KIND_LINE_POWER)
g_object_set (device, "poll-timeout", UP_DAEMON_SHORT_TIMEOUT, NULL);
return TRUE;
}
static gboolean
up_device_supply_refresh (UpDevice *device, UpRefreshReason reason)
{
gboolean updated;
UpDeviceSupply *supply = UP_DEVICE_SUPPLY (device);
UpDeviceKind type;
g_object_get (device,
"type", &type,
NULL);
if (type == UP_DEVICE_KIND_LINE_POWER) {
updated = up_device_supply_refresh_line_power (supply, reason);
} else {
updated = up_device_supply_refresh_device (supply, reason);
}
/* reset time if we got new data */
if (updated)
g_object_set (device, "update-time", (guint64) g_get_real_time () / G_USEC_PER_SEC, NULL);
return updated;
}
/**
* up_device_supply_init:
**/
static void
up_device_supply_init (UpDeviceSupply *supply)
{
supply->priv = up_device_supply_get_instance_private (supply);
supply->priv->shown_invalid_voltage_warning = FALSE;
}
/**
* up_device_supply_finalize:
**/
static void
up_device_supply_finalize (GObject *object)
{
UpDeviceSupply *supply;
g_return_if_fail (object != NULL);
g_return_if_fail (UP_IS_DEVICE_SUPPLY (object));
supply = UP_DEVICE_SUPPLY (object);
g_return_if_fail (supply->priv != NULL);
G_OBJECT_CLASS (up_device_supply_parent_class)->finalize (object);
}
/**
* up_device_supply_class_init:
**/
static void
up_device_supply_class_init (UpDeviceSupplyClass *klass)
{
GObjectClass *object_class = G_OBJECT_CLASS (klass);
UpDeviceClass *device_class = UP_DEVICE_CLASS (klass);
object_class->finalize = up_device_supply_finalize;
device_class->get_online = up_device_supply_get_online;
device_class->coldplug = up_device_supply_coldplug;
device_class->sibling_discovered = up_device_supply_sibling_discovered;
device_class->refresh = up_device_supply_refresh;
}