Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Check for firmware updates #45

Merged
merged 20 commits into from
May 21, 2023
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ jobs:
- name: Install Dependencies
run: |
apt update
apt install -y libaccountsservice-dev libdbus-1-dev libgranite-dev libgeoclue-2-dev meson valac
apt install -y libaccountsservice-dev libdbus-1-dev libgranite-dev libgeoclue-2-dev libfwupd-dev meson valac
- name: Build
env:
DESTDIR: out
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@ You'll need the following dependencies:
* gobject-2.0
* libaccountsservice-dev
* libdbus-1-dev
* libgranite-dev
* libfwupd-dev
* libgeoclue-2-dev
* libgranite-dev
* meson
* valac

Expand Down
1 change: 1 addition & 0 deletions data/autostart.desktop
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ Type=Application
Name=Elementary Settings Daemon
Comment=Elementary Settings Daemon
Exec=io.elementary.settings-daemon
Icon=preferences-desktop
Terminal=false
Categories=System;Core;
NoDisplay=true
Expand Down
8 changes: 8 additions & 0 deletions data/io.elementary.settings-daemon.gschema.xml
Original file line number Diff line number Diff line change
Expand Up @@ -65,4 +65,12 @@
<description>The day for calendars to use as the first day of the week</description>
</key>
</schema>

<schema path="/io/elementary/settings-daemon/firmware/" id="io.elementary.settings-daemon.firmware">
<key type="x" name="last-refresh-time">
<default>0</default>
<summary>Unix UTC time of last cache refresh</summary>
<description>Used to determine when settings daemon last refreshed its caches and checked for firmware updates</description>
</key>
</schema>
</schemalist>
7 changes: 7 additions & 0 deletions meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,17 @@ project('io.elementary.settings-daemon',
license: 'GPL3',
)

fwupd = dependency('fwupd')
gio_dep = dependency ('gio-2.0')
glib_dep = dependency('glib-2.0')
granite_dep = dependency('granite', version: '>= 5.3.0')
i18n = import('i18n')
gettext_name = meson.project_name()

add_project_arguments(
'-DGETTEXT_PACKAGE="@0@"'.format(gettext_name),
language:'c'
)

cc = meson.get_compiler('c')
m_dep = cc.find_library('m', required : false)
Expand Down
1 change: 1 addition & 0 deletions po/POTFILES
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
data/settings-daemon.appdata.xml.in
src/Backends/Firmware.vala
18 changes: 18 additions & 0 deletions src/Application.vala
Original file line number Diff line number Diff line change
Expand Up @@ -42,12 +42,30 @@ public class SettingsDaemon.Application : GLib.Application {

private Backends.Housekeeping housekeeping;

private Backends.Firmware firmware;

construct {
application_id = Build.PROJECT_NAME;

add_main_option_entries (OPTIONS);

housekeeping = new Backends.Housekeeping ();

firmware = new Backends.Firmware (this);

var show_firmware_updates_action = new SimpleAction ("show-firmware-updates", null);
show_firmware_updates_action.activate.connect (() => {
var uri = "settings://about/firmware";
AppInfo.launch_default_for_uri_async.begin (uri, null, null, (obj, res) => {
try {
AppInfo.launch_default_for_uri_async.end (res);
} catch (Error e) {
critical (e.message);
}
});
meisenzahl marked this conversation as resolved.
Show resolved Hide resolved
});

add_action (show_firmware_updates_action);
}

public override int handle_local_options (VariantDict options) {
Expand Down
55 changes: 55 additions & 0 deletions src/AsyncMutex.vala
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/*
* Copyright 2019 elementary, Inc. (https://elementary.io)
*
* 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 3 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 Street, Fifth Floor,
* Boston, MA 02110-1301 USA
*
*/

public class AsyncMutex {
private class Callback {
public SourceFunc callback;

public Callback (owned SourceFunc cb) {
callback = (owned)cb;
}
}

private Gee.ArrayQueue<Callback> callbacks;
private bool locked;

public AsyncMutex () {
locked = false;
callbacks = new Gee.ArrayQueue<Callback> ();
}

public async void lock () {
while (locked) {
SourceFunc cb = lock.callback;
callbacks.offer_head (new Callback ((owned)cb));
yield;
}

locked = true;
}

public void unlock () {
locked = false;
var callback = callbacks.poll_head ();
if (callback != null) {
Idle.add ((owned)callback.callback);
}
}
}
112 changes: 112 additions & 0 deletions src/Backends/Firmware.vala
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
/*
* Copyright 2022 elementary, Inc. (https://elementary.io)
*
* 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 3 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 Street, Fifth Floor,
* Boston, MA 02110-1301 USA
*
*/

public class SettingsDaemon.Backends.Firmware : GLib.Object {
private GLib.Settings firmware_settings;

private GLib.DateTime last_firmware_check = null;

private const int SECONDS_BETWEEN_REFRESHES = 60 * 60 * 24;

public uint firmware_updates_number { get; private set; default = 0U; }

private AsyncMutex update_notification_mutex = new AsyncMutex ();
private AsyncMutex firmware_update_notification_mutex = new AsyncMutex ();

public GLib.Application application { get; construct; }

public Firmware (GLib.Application application) {
Object (
application: application
);
}

construct {
firmware_settings = new GLib.Settings ("io.elementary.settings-daemon.firmware");

last_firmware_check = new DateTime.from_unix_utc (firmware_settings.get_int64 ("last-refresh-time"));

check_seconds_between_refreshes ();

Timeout.add_seconds (SECONDS_BETWEEN_REFRESHES, check_seconds_between_refreshes);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we be using SystemD timers for this?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you have an example project where we use systemd timer?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You basically need an helper executable to be run at intervals.
The ArchLinux documentation is quite good on it: https://wiki.archlinux.org/title/Systemd/Timers

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the last commit I created a helper executable, a service and a timer. What is unclear to me is how I can trigger the action in the notification so that the firmware page can be opened in the settings, since the helper executable only runs for a short time. Is it possible to trigger an action from another application via a notification?

}

private bool check_seconds_between_refreshes () {
/* One cache update a day, keeps the doctor away! */
var seconds_since_last_refresh = new DateTime.now_utc ().difference (last_firmware_check) / GLib.TimeSpan.SECOND;
if (seconds_since_last_refresh >= SECONDS_BETWEEN_REFRESHES) {
last_firmware_check = new DateTime.now_utc ();
firmware_settings.set_int64 ("last-refresh-time", last_firmware_check.to_unix ());

refresh_firmware_updates.begin ();
}

return Source.CONTINUE;
}

private async void refresh_firmware_updates () {
yield firmware_update_notification_mutex.lock ();
meisenzahl marked this conversation as resolved.
Show resolved Hide resolved

bool was_empty = firmware_updates_number == 0U;

var fwupd_client = new Fwupd.Client ();
var num_updates = 0;
try {
var devices = yield FirmwareClient.get_devices (fwupd_client);
for (int i = 0; i < devices.length; i++) {
var device = devices[i];
if (device.has_flag (Fwupd.DEVICE_FLAG_UPDATABLE)) {
Fwupd.Release? release = null;
try {
var upgrades = yield FirmwareClient.get_upgrades (fwupd_client, device.get_id ());

if (upgrades != null) {
release = upgrades[0];
}
} catch (Error e) {
warning (e.message);
}

if (release != null && device.get_version () != release.get_version ()) {
num_updates++;
}
}
}
} catch (Error e) {
warning (e.message);
}

if (was_empty && num_updates != 0U) {
string title = ngettext ("Firmware Update Available", "Firmware Updates Available", num_updates);
string body = ngettext ("%u update is available for your hardware", "%u updates are available for your hardware", num_updates).printf (num_updates);

var notification = new Notification (title);
notification.set_body (body);
notification.set_icon (new ThemedIcon ("application-x-firmware"));
notification.set_default_action ("app.show-firmware-updates");

application.send_notification ("io.elementary.settings-daemon.firmware.updates", notification);
} else {
application.withdraw_notification ("io.elementary.settings-daemon.firmware.updates");
}

update_notification_mutex.unlock ();
}
}
71 changes: 71 additions & 0 deletions src/FirmwareClient.vala
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/*
* Copyright 2021 elementary, Inc. (https://elementary.io)
*
* 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 3 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 Street, Fifth Floor,
* Boston, MA 02110-1301 USA
*/

/*
* This class only exists to provide async methods that aren't present in older
* versions of LibFwupd. It should be removed when LibFwupd is updated.
*/

public class FirmwareClient {
public static async GLib.GenericArray<weak Fwupd.Device> get_devices (Fwupd.Client client) throws GLib.Error {
SourceFunc callback = get_devices.callback;
GLib.Error error = null;

var devices = new GLib.GenericArray<weak Fwupd.Device> ();
new Thread<void> ("get_devices", () => {
try {
devices = client.get_devices ();
} catch (Error e) {
error = e;
}
Idle.add ((owned) callback);
});

yield;

if (error != null) {
throw error;
}

return devices;
}

public static async GLib.GenericArray<weak Fwupd.Release> get_upgrades (Fwupd.Client client, string device_id) throws GLib.Error {
SourceFunc callback = get_upgrades.callback;
GLib.Error error = null;

var releases = new GLib.GenericArray<weak Fwupd.Release> ();
new Thread<void> ("get_upgrades", () => {
try {
releases = client.get_upgrades (device_id);
} catch (Error e) {
error = e;
}
Idle.add ((owned) callback);
});

yield;

if (error != null) {
throw error;
}

return releases;
}
}
4 changes: 4 additions & 0 deletions src/meson.build
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
sources = files(
'AccountsService.vala',
'Application.vala',
'AsyncMutex.vala',
'FirmwareClient.vala',
'SessionManager.vala',
'Backends/Firmware.vala',
'Backends/Housekeeping.vala',
'Backends/KeyboardSettings.vala',
'Backends/MouseSettings.vala',
Expand All @@ -14,6 +17,7 @@ executable(
sources,
dependencies: [
config_dep,
fwupd,
gio_dep,
glib_dep,
granite_dep,
Expand Down