├── rust-toolchain ├── debian ├── source │ └── format ├── libpop-system-updater-gtk-dev.install ├── libpop-system-updater-gtk.install ├── pop-system-updater.links ├── changelog ├── pop-system-updater.install ├── pop-system-updater.postinst ├── rules └── control ├── gtk ├── i18n.toml ├── i18n │ ├── ja │ │ └── pop_system_updater_gtk.ftl │ ├── en │ │ └── pop_system_updater_gtk.ftl │ ├── tr │ │ └── pop_system_updater_gtk.ftl │ ├── da │ │ └── pop_system_updater_gtk.ftl │ ├── sr │ │ └── pop_system_updater_gtk.ftl │ ├── de │ │ └── pop_system_updater_gtk.ftl │ ├── cs │ │ └── pop_system_updater_gtk.ftl │ ├── pt-BR │ │ └── pop_system_updater_gtk.ftl │ ├── fr │ │ └── pop_system_updater_gtk.ftl │ └── es │ │ └── pop_system_updater_gtk.ftl ├── src │ ├── lib.rs │ ├── localize.rs │ ├── utils.rs │ ├── main.rs │ ├── proxy.rs │ ├── bsb.rs │ ├── dialog.rs │ └── widget.rs └── Cargo.toml ├── Cargo.toml ├── daemon ├── src │ ├── service │ │ ├── mod.rs │ │ ├── session.rs │ │ └── system.rs │ ├── lib.rs │ ├── dbus │ │ ├── local_client.rs │ │ ├── client.rs │ │ ├── mod.rs │ │ ├── local_server.rs │ │ └── server.rs │ ├── package_managers │ │ ├── nix.rs │ │ ├── mod.rs │ │ ├── apt_pop.rs │ │ ├── flatpak.rs │ │ └── apt.rs │ ├── notify.rs │ ├── main.rs │ ├── accounts.rs │ ├── signal_handler.rs │ ├── appcenter.rs │ ├── utils.rs │ └── config.rs ├── examples │ └── appcenter_updates.rs └── Cargo.toml ├── .vscode └── settings.json ├── gtk-ffi ├── pop_system_updater_gtk.pc.in ├── pop_system_updater_gtk.h ├── Cargo.toml ├── src │ └── lib.rs └── build.rs ├── .gitignore ├── shop-zbus ├── Cargo.toml └── src │ └── lib.rs ├── data ├── com.system76.SystemUpdater.Local.service ├── com.system76.SystemUpdater.service └── com.system76.SystemUpdater.conf ├── scheduler ├── Cargo.toml ├── src │ ├── job.rs │ ├── lib.rs │ └── scheduler.rs ├── README.md ├── examples │ └── simple.rs └── LICENSE ├── TESTING.md ├── README.md ├── justfile └── LICENSE /rust-toolchain: -------------------------------------------------------------------------------- 1 | 1.65.0 -------------------------------------------------------------------------------- /debian/source/format: -------------------------------------------------------------------------------- 1 | 3.0 (native) -------------------------------------------------------------------------------- /debian/libpop-system-updater-gtk-dev.install: -------------------------------------------------------------------------------- 1 | /usr/include/pop_system_updater_gtk.h -------------------------------------------------------------------------------- /gtk/i18n.toml: -------------------------------------------------------------------------------- 1 | fallback_language = "en" 2 | 3 | [fluent] 4 | assets_dir = "i18n" 5 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = ["daemon", "gtk", "gtk-ffi", "scheduler", "shop-zbus"] -------------------------------------------------------------------------------- /debian/libpop-system-updater-gtk.install: -------------------------------------------------------------------------------- 1 | /usr/lib/libpop_system_updater_gtk.so 2 | /usr/lib/pkgconfig/pop_system_updater_gtk.pc -------------------------------------------------------------------------------- /daemon/src/service/mod.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2021-2022 System76 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | pub mod session; 5 | pub mod system; 6 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "rust-analyzer.checkOnSave.command": "clippy", 3 | "rust-analyzer.checkOnSave.extraArgs": ["--", "-W", "clippy::pedantic"], 4 | } 5 | -------------------------------------------------------------------------------- /debian/pop-system-updater.links: -------------------------------------------------------------------------------- 1 | /lib/systemd/user/com.system76.SystemUpdater.Local.service /lib/systemd/user/default.target.wants/com.system76.SystemUpdater.Local.service -------------------------------------------------------------------------------- /gtk-ffi/pop_system_updater_gtk.pc.in: -------------------------------------------------------------------------------- 1 | 2 | Name: {name} 3 | Description: {description} 4 | Version: {version} 5 | Cflags: -I${{includedir}} 6 | Libs: -L${{libdir}} -l{name} -------------------------------------------------------------------------------- /debian/changelog: -------------------------------------------------------------------------------- 1 | pop-system-updater (0.1.0) hirsute; urgency=medium 2 | 3 | * Initial release. 4 | 5 | -- Michael Aaron Murphy Thu, 12 Oct 2021 12:06:31 +0200 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | debian/* 2 | !debian/*install 3 | !debian/*postinst 4 | !debian/changelog 5 | !debian/control 6 | !debian/*.links 7 | !debian/rules 8 | !debian/source 9 | .cargo 10 | target 11 | vendor 12 | vendor.tar 13 | .cargo -------------------------------------------------------------------------------- /daemon/examples/appcenter_updates.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2022 System76 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | #[tokio::main] 5 | async fn main() { 6 | pop_system_updater::appcenter::show_updates().await; 7 | } 8 | -------------------------------------------------------------------------------- /debian/pop-system-updater.install: -------------------------------------------------------------------------------- 1 | /lib/systemd/system/com.system76.SystemUpdater.service 2 | /lib/systemd/user/com.system76.SystemUpdater.Local.service 3 | /usr/bin/pop-system-updater 4 | /usr/share/dbus-1/system.d/com.system76.SystemUpdater.conf -------------------------------------------------------------------------------- /daemon/src/lib.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2021-2022 System76 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | #[macro_use] 5 | extern crate zbus; 6 | #[macro_use] 7 | extern crate tracing; 8 | 9 | pub mod appcenter; 10 | pub mod config; 11 | pub mod dbus; 12 | -------------------------------------------------------------------------------- /gtk-ffi/pop_system_updater_gtk.h: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | /** Creates and attaches the system-updater widget */ 4 | pop_system_updater_attach (GtkContainer *container); 5 | 6 | /** Used before attach, localizes the system-updater widget */ 7 | pop_system_updater_localize (); -------------------------------------------------------------------------------- /shop-zbus/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "pop-shop-zbus" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | zbus = { version = "3.4.0", default-features = false } 10 | -------------------------------------------------------------------------------- /debian/pop-system-updater.postinst: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | SERVICE="com.system76.SystemUpdater" 4 | 5 | systemctl daemon-reload 6 | 7 | systemctl --root=/ enable "${SERVICE}" 8 | 9 | if systemctl is-active "${SERVICE}"; then 10 | systemctl restart "${SERVICE}" 11 | fi 12 | 13 | #DEBHELPER# 14 | 15 | exit 0 -------------------------------------------------------------------------------- /data/com.system76.SystemUpdater.Local.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Local session service for the Pop System Updater 3 | After=network-online.target 4 | DefaultDependencies=no 5 | StartLimitIntervalSec=1d 6 | 7 | [Service] 8 | Type=dbus 9 | BusName=com.system76.SystemUpdater.Local 10 | Restart=on-failure 11 | ExecStartPre=/bin/sleep 10 12 | ExecStart=/usr/bin/pop-system-updater 13 | StartLimitBurst=6 14 | 15 | [Install] 16 | Alias=pop-system-updater-local.service -------------------------------------------------------------------------------- /data/com.system76.SystemUpdater.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | After=network-online.target 3 | Description=Distribution updater 4 | DefaultDependencies=no 5 | StartLimitIntervalSec=4h 6 | 7 | [Service] 8 | Type=dbus 9 | BusName=com.system76.SystemUpdater 10 | ExecStart=/usr/bin/pop-system-updater 11 | Restart=on-failure 12 | KillMode=process 13 | SendSIGKILL=no 14 | StartLimitBurst=3 15 | 16 | [Install] 17 | Alias=pop-system-updater.service 18 | WantedBy=multi-user.target -------------------------------------------------------------------------------- /gtk-ffi/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "gtk-ffi" 3 | version = "0.1.0" 4 | edition = "2021" 5 | description = "Dynamically link the pop-system-updater-gtk widget" 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [build-dependencies] 10 | cdylib-link-lines = "0.1.4" 11 | 12 | [lib] 13 | name = "pop_system_updater_gtk" 14 | crate-type = [ "cdylib" ] 15 | 16 | [dependencies] 17 | glib = "0.16.2" 18 | gtk = "0.16.0" 19 | gtk-sys = "0.16.0" 20 | pop-system-updater-gtk = { path = "../gtk" } 21 | -------------------------------------------------------------------------------- /gtk/i18n/ja/pop_system_updater_gtk.ftl: -------------------------------------------------------------------------------- 1 | automatic-updates-label = 自動アップデート 2 | automatically-install-label = 自動インストール 3 | off = オフ 4 | schedule-daily = 日次 5 | schedule-dialog-title = 自動インストール 6 | schedule-label = 自動アップデートを予約 7 | schedule-monthly = 月次 8 | schedule-weekly = 週次 9 | time-am = AM 10 | time-friday = 金曜日 11 | time-monday = 月曜日 12 | time-pm = PM 13 | time-saturday = 土曜日 14 | time-sunday = 日曜日 15 | time-thursday = 木曜日 16 | time-tuesday = 火曜日 17 | time-wednesday = 水曜日 18 | time-weekdays = 平日 19 | update-notifications-label = アップデートの通知を表示 20 | update-when-available = 随時更新 21 | update-when-available-label = 随時更新 -------------------------------------------------------------------------------- /daemon/src/dbus/local_client.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2021-2022 System76 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | use crate::config::Frequency; 5 | 6 | #[dbus_proxy( 7 | default_service = "com.system76.SystemUpdater.Local", 8 | interface = "com.system76.SystemUpdater.Local", 9 | default_path = "/com/system76/SystemUpdater/Local" 10 | )] 11 | pub trait LocalClient { 12 | fn notifications_enabled(&mut self, enabled: bool) -> zbus::Result<()>; 13 | fn notification_frequency(&mut self) -> zbus::Result; 14 | fn set_notification_frequency(&mut self, frequency: Frequency) -> zbus::Result<()>; 15 | } 16 | -------------------------------------------------------------------------------- /daemon/src/package_managers/nix.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2021-2022 System76 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | use crate::utils; 5 | 6 | pub async fn update(conn: &zbus::Connection) { 7 | const SOURCE: &str = "nix"; 8 | 9 | const COMMANDS: &[&[&str]] = &[ 10 | &["nix-channel", "--update"], 11 | &["nix-env", "--upgrade"], 12 | &["nix-collect-garbage", "-d"], 13 | ]; 14 | 15 | for command in COMMANDS { 16 | if !utils::command_exists(command[0]) { 17 | return; 18 | } 19 | } 20 | 21 | if let Err(why) = utils::async_commands(COMMANDS).await { 22 | utils::error_handler(conn, SOURCE, why).await; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /gtk/src/lib.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2021-2022 System76 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | #[macro_use] 5 | extern crate cascade; 6 | 7 | pub(crate) mod bsb; 8 | pub(crate) mod dialog; 9 | pub mod localize; 10 | pub(crate) mod proxy; 11 | pub(crate) mod utils; 12 | mod widget; 13 | 14 | pub use self::widget::SettingsWidget; 15 | 16 | pub fn localize() { 17 | let localizer = localize::localizer(); 18 | let requested_languages = i18n_embed::DesktopLanguageRequester::requested_languages(); 19 | 20 | if let Err(error) = localizer.select(&requested_languages) { 21 | eprintln!( 22 | "Error while loading language for system-updater-gtk {}", 23 | error 24 | ); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /gtk/i18n/en/pop_system_updater_gtk.ftl: -------------------------------------------------------------------------------- 1 | automatic-updates-label = Automatic Updates 2 | automatically-install-label = Automatically Install 3 | off = Off 4 | schedule-daily = Daily 5 | schedule-dialog-title = Automatically Install 6 | schedule-label = Schedule Automatic Updates 7 | schedule-monthly = Monthly 8 | schedule-weekly = Weekly 9 | time-am = AM 10 | time-friday = Friday 11 | time-monday = Monday 12 | time-pm = PM 13 | time-saturday = Saturday 14 | time-sunday = Sunday 15 | time-thursday = Thursday 16 | time-tuesday = Tuesday 17 | time-wednesday = Wednesday 18 | time-weekdays = Weekdays 19 | update-notifications-label = Show Update Notifications 20 | update-when-available = Update when Available 21 | update-when-available-label = Update when Available -------------------------------------------------------------------------------- /scheduler/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "async-cron-scheduler" 3 | description = "Simple lightweight efficient runtime-agnostic async task scheduler with cron expression support" 4 | repository = "https://github.com/pop-os/system-updater" 5 | version = "1.0.0" 6 | license = "MPL-2.0" 7 | authors = ["Michael Aaron Murphy "] 8 | categories = [ "asynchronous", "date-and-time" ] 9 | keywords = ["cron", "job", "scheduler", "tokio", "smol"] 10 | edition = "2021" 11 | 12 | [dependencies] 13 | chrono = "0.4.22" 14 | cron = "0.12.0" 15 | flume = "0.10.14" 16 | slotmap = "1.0.6" 17 | futures = "0.3.25" 18 | tracing = { version = "0.1.37", optional = true } 19 | 20 | [features] 21 | logging = ["tracing"] 22 | 23 | [dev-dependencies] 24 | smol = "1.2.5" 25 | -------------------------------------------------------------------------------- /shop-zbus/src/lib.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2021-2022 System76 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | use zbus::dbus_proxy; 5 | 6 | #[dbus_proxy( 7 | interface = "io.elementary.appcenter", 8 | default_path = "/io/elementary/appcenter" 9 | )] 10 | trait ElementaryAppcenter { 11 | fn get_component_from_desktop_id(&self, desktop_id: &str) -> zbus::Result; 12 | 13 | fn install(&self, component_id: &str) -> zbus::Result<()>; 14 | 15 | fn search_components(&self, query: &str) -> zbus::Result>; 16 | 17 | fn uninstall(&self, component_id: &str) -> zbus::Result<()>; 18 | 19 | fn update(&self, component_id: &str) -> zbus::Result<()>; 20 | 21 | fn show_updates(&self) -> zbus::Result<()>; 22 | } 23 | -------------------------------------------------------------------------------- /gtk/i18n/tr/pop_system_updater_gtk.ftl: -------------------------------------------------------------------------------- 1 | automatic-updates-label = Otomatik Güncellemeler 2 | automatically-install-label = Otomatik Yükle 3 | off = kapalı 4 | schedule-daily = Günlük 5 | schedule-dialog-title = Otomatik Yükle 6 | schedule-label = Otomatik Güncellemeleri Planla 7 | schedule-monthly = Aylık 8 | schedule-weekly = Haftalık 9 | time-am = ÖÖ 10 | time-friday = Cuma 11 | time-monday = Pazartesi 12 | time-pm = ÖS 13 | time-saturday = Cumartesi 14 | time-sunday = Pazar 15 | time-thursday = Perşembe 16 | time-tuesday = Salı 17 | time-wednesday = Çarşamba 18 | time-weekdays = Hafta içi 19 | update-notifications-label = Güncelleme Bildirimlerini Göster 20 | update-when-available = Müsait olduğunda Güncelle 21 | update-when-available-label = Müsait olduğunda Güncelle 22 | -------------------------------------------------------------------------------- /gtk/i18n/da/pop_system_updater_gtk.ftl: -------------------------------------------------------------------------------- 1 | automatic-updates-label = Automatiske Opdateringer 2 | automatically-install-label = Installer Automatisk 3 | off = Fra 4 | schedule-daily = Dagligt 5 | schedule-dialog-title = Installer Automatisk 6 | schedule-label = Planlæg Automatiske Opdateringer 7 | schedule-monthly = Månedlig 8 | schedule-weekly = Ugentlig 9 | time-am = AM 10 | time-friday = Fradag 11 | time-monday = Mandag 12 | time-pm = PM 13 | time-saturday = Lørdag 14 | time-sunday = Søndag 15 | time-thursday = Torsdag 16 | time-tuesday = Tirsdag 17 | time-wednesday = Onsdag 18 | time-weekdays = Hverdage 19 | update-notifications-label = Vis Opdateringsnotifikationer 20 | update-when-available = Opdater når tilgængelig 21 | update-when-available-label = Opdater når tilgængelig 22 | -------------------------------------------------------------------------------- /gtk/i18n/sr/pop_system_updater_gtk.ftl: -------------------------------------------------------------------------------- 1 | automatic-updates-label = Automatska ažuriranja 2 | automatically-install-label = Automatski instaliraj 3 | off = Ugašeno 4 | schedule-daily = Dnevno 5 | schedule-dialog-title = Automatski instaliraj 6 | schedule-label = Schedule Automatska ažuriranja 7 | schedule-monthly = Mesečno 8 | schedule-weekly = Nedeljno 9 | time-am = AM 10 | time-friday = Petak 11 | time-monday = Ponedeljak 12 | time-pm = PM 13 | time-saturday = Subota 14 | time-sunday = Nedelja 15 | time-thursday = Četvrtak 16 | time-tuesday = Utorak 17 | time-wednesday = Sreda 18 | time-weekdays = Radni dani 19 | update-notifications-label = Pokazati notifikaciju ažuriranja 20 | update-when-available = Ažurirati kad je moguće 21 | update-when-available-label = Ažurirati kad je moguće -------------------------------------------------------------------------------- /gtk/i18n/de/pop_system_updater_gtk.ftl: -------------------------------------------------------------------------------- 1 | automatic-updates-label = Automatische Updates 2 | automatically-install-label = Automatisch installieren 3 | off = Aus 4 | schedule-daily = Täglich 5 | schedule-dialog-title = Automatisch installieren 6 | schedule-label = Automatische Updates planen 7 | schedule-monthly = Monatlich 8 | schedule-weekly = Wöchentlich 9 | time-am = AM 10 | time-friday = Freitag 11 | time-monday = Montag 12 | time-pm = PM 13 | time-saturday = Samstag 14 | time-sunday = Sonntag 15 | time-thursday = Donnerstag 16 | time-tuesday = Dienstag 17 | time-wednesday = Mittwoch 18 | time-weekdays = Wochentage 19 | update-notifications-label = Update-Benachrichtigungen anzeigen 20 | update-when-available = Update, falls verfügbar 21 | update-when-available-label = Update, falls verfügbar -------------------------------------------------------------------------------- /debian/rules: -------------------------------------------------------------------------------- 1 | #!/usr/bin/make -f 2 | 3 | export CARGO_TARGET_DIR=target 4 | DESTDIR=debian/tmp 5 | CLEAN ?= 1 6 | DEBUG ?= 0 7 | VENDOR ?= 1 8 | 9 | %: 10 | dh $@ 11 | 12 | override_dh_auto_clean: 13 | if test "${CLEAN}" = "1"; then \ 14 | cargo clean; \ 15 | fi 16 | 17 | if ! ischroot && test "${VENDOR}" = "1"; then \ 18 | mkdir -p .cargo; \ 19 | cargo vendor --sync gtk/Cargo.toml | head -n -1 > .cargo/config; \ 20 | echo 'directory = "vendor"' >> .cargo/config; \ 21 | tar pcf vendor.tar vendor; \ 22 | rm -rf vendor; \ 23 | fi 24 | 25 | override_dh_auto_build: 26 | just rootdir=$(DESTDIR) debug=$(DEBUG) vendor=$(VENDOR) package_build 27 | 28 | override_dh_auto_install: 29 | just rootdir=$(DESTDIR) install 30 | 31 | override_dh_systemd_enable: 32 | 33 | override_dh_systemd_start: -------------------------------------------------------------------------------- /gtk/i18n/cs/pop_system_updater_gtk.ftl: -------------------------------------------------------------------------------- 1 | automatic-updates-label = Automatické Aktualizace 2 | automatically-install-label = Automaticky Instalovat 3 | off = Vypnuto 4 | schedule-daily = Denně 5 | schedule-dialog-title = Automaticky Instalovat 6 | schedule-label = Naplánovat Automatické Aktualizace 7 | schedule-monthly = Měsíčně 8 | schedule-weekly = Týdně 9 | time-am = AM 10 | time-friday = Pátek 11 | time-monday = Pondělí 12 | time-pm = PM 13 | time-saturday = Sobota 14 | time-sunday = Neděle 15 | time-thursday = Čtvrtek 16 | time-tuesday = Úterý 17 | time-wednesday = Středa 18 | time-weekdays = Kromě víkendu 19 | update-notifications-label = Zobrazovat Oznámení o Aktualizacích 20 | update-when-available = Aktualizovat pokud k dipozici 21 | update-when-available-label = Aktualizovat pokud k dizpozici 22 | 23 | -------------------------------------------------------------------------------- /gtk/i18n/pt-BR/pop_system_updater_gtk.ftl: -------------------------------------------------------------------------------- 1 | automatic-updates-label = Atualizações Automáticas 2 | automatically-install-label = Instalação Automática 3 | off = Desligado 4 | schedule-daily = Diário 5 | schedule-dialog-title = Instalação Automática 6 | schedule-label = Agendamento das Atualizações Automáticas 7 | schedule-monthly = Mensal 8 | schedule-weekly = Semanal 9 | time-am = AM 10 | time-friday = Sexta 11 | time-monday = Segunda 12 | time-pm = PM 13 | time-saturday = Sábado 14 | time-sunday = Domingo 15 | time-thursday = Quinta 16 | time-tuesday = Terça 17 | time-wednesday = Quarta 18 | time-weekdays = Diáriamente 19 | update-notifications-label = Mostrar Notificação de Atualização 20 | update-when-available = Atualizar quando Disponível 21 | update-when-available-label = Atualizar quando Disponível 22 | -------------------------------------------------------------------------------- /gtk/i18n/fr/pop_system_updater_gtk.ftl: -------------------------------------------------------------------------------- 1 | automatic-updates-label = Mises à jour automatique 2 | automatically-install-label = Installation automatique 3 | off = Désactivé 4 | schedule-daily = Quotidien 5 | schedule-dialog-title = Installation automatique 6 | schedule-label = Installation automatique programmée 7 | schedule-monthly = Mensuel 8 | schedule-weekly = Hebdomadaire 9 | time-am = AM 10 | time-friday = Vendredi 11 | time-monday = Lundi 12 | time-pm = PM 13 | time-saturday = Samedi 14 | time-sunday = Dimanche 15 | time-thursday = Jeudi 16 | time-tuesday = Mardi 17 | time-wednesday = Mercredi 18 | time-weekdays = Jours 19 | update-notifications-label = Affichage des notifications de mises à jour 20 | update-when-available = Mettre à jour quand Disponible 21 | update-when-available-label = Mettre à jour quand Disponible 22 | -------------------------------------------------------------------------------- /data/com.system76.SystemUpdater.conf: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /gtk/i18n/es/pop_system_updater_gtk.ftl: -------------------------------------------------------------------------------- 1 | automatic-updates-label = Actualizaciones Automáticas 2 | automatically-install-label = Instalar Automáticamente 3 | off = Apagado 4 | schedule-daily = Diariamente 5 | schedule-dialog-title = Instalar Automáticamente 6 | schedule-label = Programar Actualizaciones Automáticas 7 | schedule-monthly = Mensualmente 8 | schedule-weekly = Semanalmente 9 | time-am = AM 10 | time-friday = Viernes 11 | time-monday = Lunes 12 | time-pm = PM 13 | time-saturday = Sábado 14 | time-sunday = Domingo 15 | time-thursday = Jueves 16 | time-tuesday = Martes 17 | time-wednesday = Miércoles 18 | time-weekdays = Días de la semana 19 | update-notifications-label = Mostrar Notificaciones de Actualizaciones 20 | update-when-available = Actualizar cuando esté disponible 21 | update-when-available-label = Actualizar cuando esté disponible 22 | -------------------------------------------------------------------------------- /gtk-ffi/src/lib.rs: -------------------------------------------------------------------------------- 1 | use glib::translate::FromGlibPtrNone; 2 | use gtk::prelude::ContainerExt; 3 | use pop_system_updater_gtk::{localize, SettingsWidget}; 4 | 5 | /// Localizes the system-updater widget strings. 6 | /// 7 | /// # Warning 8 | /// 9 | /// This must be called before attaching the widget. 10 | #[no_mangle] 11 | pub extern "C" fn pop_system_updater_localize() { 12 | localize(); 13 | } 14 | 15 | /// Creates and attaches the system-updater widget to the container. 16 | /// 17 | /// # Safety 18 | /// 19 | /// The container pointer must be valid. 20 | #[no_mangle] 21 | pub unsafe extern "C" fn pop_system_updater_attach(container: *mut gtk_sys::GtkContainer) { 22 | if container.is_null() { 23 | eprintln!("cannot attach system updater widget to null container"); 24 | return; 25 | } 26 | 27 | gtk::set_initialized(); 28 | 29 | gtk::Container::from_glib_none(container).add(&SettingsWidget::new().inner); 30 | } 31 | -------------------------------------------------------------------------------- /gtk/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "pop-system-updater-gtk" 3 | version = "0.1.0" 4 | authors = ["Michael Aaron Murphy "] 5 | edition = "2018" 6 | license = "MPL-2.0" 7 | publish = false 8 | 9 | [dependencies] 10 | cascade = "1.0.1" 11 | chrono = { version = "0.4.22", features = ["serde"] } 12 | flume = "0.10.14" 13 | futures = "0.3.25" 14 | gio = "0.16.2" 15 | glib = "0.16.2" 16 | gtk = "0.16.0" 17 | i18n-embed = { version = "0.13.4", features = ["fluent-system", "desktop-requester"] } 18 | i18n-embed-fl = "0.6.4" 19 | once_cell = "1.16.0" 20 | pop-system-updater = { path = "../daemon" } 21 | postage = "0.5.0" 22 | ron = "0.8.0" 23 | rust-embed = "6.4.2" 24 | tracing = "0.1.37" 25 | tracing-subscriber = { version = "0.3.16", features = ["env-filter"] } 26 | zbus = { version = "3.4.0", default-features = false, features = ["tokio"] } 27 | 28 | [dependencies.tokio] 29 | version = "1.21.2" 30 | features = ["rt", "macros"] 31 | 32 | [features] 33 | -------------------------------------------------------------------------------- /TESTING.md: -------------------------------------------------------------------------------- 1 | # System Updater Testing 2 | 3 | Testing requires having another staging repo ready that can be added/removed to simulate updates becoming available. 4 | 5 | Watch the following logs while testing: 6 | 7 | ``` 8 | sudo journalctl -u com.system76.SystemUpdater.service -f 9 | journalctl --user -u com.system76.SystemUpdater.Local.service -f 10 | ``` 11 | 12 | ## Update Installation 13 | 14 | - Set `Automatic Updates` to enabled. 15 | - Make updates available. 16 | - [ ] Set `Schedule Automatic Updates` to one minute after the current time; wait and verify that updates run. 17 | 18 | ## Update Notification 19 | 20 | - Make updates available. 21 | - [ ] Set `Schedule Automatic Updates` to one minute after the current time, then turn `Automatic Updates` off; confirm updates do not run. 22 | - [ ] Edit or remove `~/.cache/pop-system-updater/cache.ron`, restart the `--user` service, and confirm a notification is displayed. 23 | - [ ] Click the notification and confirm that the Pop!\_Shop opens to the Installed tab. 24 | -------------------------------------------------------------------------------- /gtk-ffi/build.rs: -------------------------------------------------------------------------------- 1 | use std::{env, fs::File, io::Write, path::PathBuf, os::unix::prelude::OsStrExt}; 2 | 3 | fn main() { 4 | cdylib_link_lines::metabuild(); 5 | 6 | let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap()); 7 | let mut target_dir = out_dir.as_path(); 8 | 9 | while let Some(parent) = target_dir.parent() { 10 | if target_dir.as_os_str().as_bytes().ends_with(b"target") { 11 | break 12 | } 13 | target_dir = parent; 14 | } 15 | 16 | let pkg_config = format!( 17 | include_str!("pop_system_updater_gtk.pc.in"), 18 | name = "pop_system_updater_gtk", 19 | description = env::var("CARGO_PKG_DESCRIPTION").unwrap(), 20 | version = env::var("CARGO_PKG_VERSION").unwrap() 21 | ); 22 | 23 | std::fs::create_dir_all("../target").unwrap(); 24 | 25 | File::create("../target/pop_system_updater_gtk.pc.stub") 26 | .expect("failed to create pc.stub") 27 | .write_all(pkg_config.as_bytes()) 28 | .expect("failed to write pc.stub"); 29 | } 30 | -------------------------------------------------------------------------------- /daemon/src/dbus/client.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2021-2022 System76 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | use crate::config::Schedule; 5 | 6 | #[dbus_proxy( 7 | interface = "com.system76.SystemUpdater", 8 | default_service = "com.system76.SystemUpdater", 9 | default_path = "/com/system76/SystemUpdater" 10 | )] 11 | pub trait Client { 12 | fn auto_update_set(&mut self, enable: bool) -> zbus::Result<()>; 13 | 14 | fn check_for_updates(&mut self) -> zbus::Result<()>; 15 | 16 | fn is_updating(&mut self) -> zbus::Result; 17 | 18 | fn repair(&mut self) -> zbus::Result<()>; 19 | 20 | fn update_scheduling_disable(&mut self) -> zbus::Result<()>; 21 | 22 | fn update_scheduling_set(&mut self, schedule: Schedule) -> zbus::Result<()>; 23 | 24 | fn update_system(&mut self) -> zbus::Result<()>; 25 | 26 | #[dbus_proxy(signal)] 27 | fn error(&self, why: &str) -> zbus::Result<()>; 28 | 29 | #[dbus_proxy(signal)] 30 | fn progress(&self, source: &str, percent: u8) -> zbus::Result<()>; 31 | } 32 | -------------------------------------------------------------------------------- /debian/control: -------------------------------------------------------------------------------- 1 | Source: pop-system-updater 2 | Section: admin 3 | Priority: optional 4 | Maintainer: Michael Aaron Murphy 5 | Build-Depends: 6 | cargo, 7 | just, 8 | rustc, 9 | libdbus-1-dev, 10 | libgtk-3-dev, 11 | libssl-dev, 12 | pkg-config, 13 | debhelper-compat (= 10), 14 | Standards-Version: 4.1.1 15 | Homepage: https://github.com/pop-os/system-updater 16 | 17 | Package: pop-system-updater 18 | Architecture: amd64 arm64 19 | Depends: 20 | libcurl4, 21 | libssl3 | libssl1.1, 22 | ${misc:Depends}, 23 | ${shlibs:Depends} 24 | Description: Automatic System Updates for Pop!_OS 25 | 26 | Package: libpop-system-updater-gtk 27 | Architecture: amd64 arm64 28 | Depends: 29 | libgtk-3-0 30 | ${misc:Depends}, 31 | ${shlibs:Depends} 32 | Description: GTK widget library for dynamic linking 33 | 34 | Package: libpop-system-updater-gtk-dev 35 | Architecture: amd64 arm64 36 | Depends: 37 | libpop-system-updater-gtk, 38 | ${misc:Depends}, 39 | ${shlibs:Depends} 40 | Description: Development libraries for libpop-system-updater-gtk -------------------------------------------------------------------------------- /gtk/src/localize.rs: -------------------------------------------------------------------------------- 1 | use i18n_embed::{ 2 | fluent::{fluent_language_loader, FluentLanguageLoader}, 3 | DefaultLocalizer, LanguageLoader, Localizer, 4 | }; 5 | use once_cell::sync::Lazy; 6 | use rust_embed::RustEmbed; 7 | 8 | #[derive(RustEmbed)] 9 | #[folder = "i18n/"] 10 | struct Localizations; 11 | 12 | pub static LANGUAGE_LOADER: Lazy = Lazy::new(|| { 13 | let loader: FluentLanguageLoader = fluent_language_loader!(); 14 | 15 | loader 16 | .load_fallback_language(&Localizations) 17 | .expect("Error while loading fallback language"); 18 | 19 | loader 20 | }); 21 | 22 | #[macro_export] 23 | macro_rules! fl { 24 | ($message_id:literal) => {{ 25 | i18n_embed_fl::fl!($crate::localize::LANGUAGE_LOADER, $message_id) 26 | }}; 27 | 28 | ($message_id:literal, $($args:expr),*) => {{ 29 | i18n_embed_fl::fl!($crate::localize::LANGUAGE_LOADER, $message_id, $($args), *) 30 | }}; 31 | } 32 | 33 | // Get the `Localizer` to be used for localizing this library. 34 | #[must_use] 35 | pub fn localizer() -> Box { 36 | Box::from(DefaultLocalizer::new(&*LANGUAGE_LOADER, &Localizations)) 37 | } 38 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Pop System Updater 2 | 3 | DBus services to enable Linux distributions to schedule system updates across a variety of package managers. 4 | 5 | ## Translators 6 | 7 | Go [here](gtk/i18n) for localizations. The `en` directory contains the English localization. Copy and rename that directory to your language codename, and translate each of the keys into your language. Translations may be submitted as a PR, as an issue, by email, or any other means necessary. 8 | 9 | ## Build 10 | 11 | This project uses [just](https://github.com/casey/just) as a command runner. 12 | 13 | ```sh 14 | # Vendor dependencies 15 | just vendor 16 | 17 | # Build with vendored dependencies 18 | just vendor=1 19 | 20 | # Run GTK test 21 | just debug=1 vendor=1 gtk-test 22 | 23 | # install to custom root path 24 | just rootdir=chroot prefix=/usr vendor=1 install 25 | 26 | # List Recipes 27 | just -l 28 | ``` 29 | 30 | ## License 31 | 32 | Licensed under the [Mozilla Public License 2.0](https://choosealicense.com/licenses/mpl-2.0/). 33 | 34 | ### Contribution 35 | 36 | Any contribution intentionally submitted for inclusion in the work by you shall be licensed under the Mozilla Public License 2.0 (MPL-2.0). 37 | -------------------------------------------------------------------------------- /daemon/src/dbus/mod.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2021-2022 System76 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | pub mod client; 5 | pub mod local_client; 6 | pub mod local_server; 7 | pub mod server; 8 | 9 | use crate::config::{Frequency, Local, Schedule}; 10 | 11 | // Where this service's interface is being served at. 12 | pub const IFACE: &str = "/com/system76/SystemUpdater"; 13 | pub const IFACE_LOCAL: &str = "/com/system76/SystemUpdater/Local"; 14 | 15 | #[derive(Debug)] 16 | pub enum Event { 17 | CheckForUpdates, 18 | Exit, 19 | Repair, 20 | ScheduleWhenAvailable, 21 | SetSchedule(Option), 22 | SetAutoUpdate(bool), 23 | Update, 24 | UpdateComplete, 25 | } 26 | 27 | #[derive(Debug)] 28 | pub enum LocalEvent { 29 | CheckUpdates, 30 | UpdateConfig(Local), 31 | } 32 | 33 | pub struct PopService { 34 | pub sender: flume::Sender, 35 | } 36 | 37 | impl PopService { 38 | async fn send(&mut self, event: E) -> zbus::fdo::Result<()> { 39 | if let Err(why) = self.sender.send_async(event).await { 40 | Err(zbus::fdo::Error::Failed(format!("{}", why))) 41 | } else { 42 | Ok(()) 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /daemon/src/notify.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2021-2022 System76 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | use std::time::Duration; 5 | 6 | use notify_rust::{Hint, Notification, Urgency}; 7 | 8 | pub fn notify(summary: &str, body: &str, func: F) { 9 | let show_notification = || { 10 | Notification::new() 11 | .icon("distributor-logo") 12 | .summary(summary) 13 | .body(body) 14 | .action("default", "default") 15 | .hint(Hint::Urgency(Urgency::Critical)) 16 | .show() 17 | }; 18 | 19 | let mut notification = show_notification(); 20 | 21 | while notification.is_err() { 22 | std::thread::sleep(Duration::from_secs(1)); 23 | 24 | notification = show_notification(); 25 | } 26 | 27 | notification 28 | .expect("failed to show desktop notification") 29 | .wait_for_action(|action| { 30 | if "default" == action { 31 | func(); 32 | } 33 | }); 34 | } 35 | 36 | pub fn updates_available() { 37 | notify( 38 | "System updates are available to install", 39 | "Click here to view available updates", 40 | || { 41 | tokio::spawn(pop_system_updater::appcenter::show_updates()); 42 | }, 43 | ); 44 | } 45 | -------------------------------------------------------------------------------- /scheduler/src/job.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2021-2022 System76 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | use crate::TimeZoneExt; 5 | use chrono::DateTime; 6 | use std::str::FromStr; 7 | 8 | /// The ID of a scheduled job. 9 | #[allow(clippy::module_name_repetitions)] 10 | #[derive(Copy, Clone, Debug, PartialEq, Eq)] 11 | pub struct JobId(pub slotmap::DefaultKey); 12 | 13 | /// Contains scheduling information for a job at a given timezone. 14 | pub struct Job { 15 | pub(crate) iterator: cron::OwnedScheduleIterator, 16 | pub(crate) next: DateTime, 17 | } 18 | 19 | impl Job { 20 | /// Creates a job from a cron expression string. 21 | /// 22 | /// # Errors 23 | /// 24 | /// Errors if the cron expression is invalid. 25 | pub fn cron(expression: &str) -> Result { 26 | cron::Schedule::from_str(expression).map(Job::cron_schedule) 27 | } 28 | 29 | /// Creates a job from a pre-generated cron schedule. 30 | /// 31 | /// # Panics 32 | /// 33 | /// Panics at the end of time. 34 | #[must_use] 35 | #[allow(clippy::needless_pass_by_value)] 36 | pub fn cron_schedule(schedule: cron::Schedule) -> Self { 37 | let mut iterator = schedule.upcoming_owned(Tz::timescale()); 38 | let next = iterator.next().unwrap(); 39 | Job { iterator, next } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /daemon/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "pop-system-updater" 3 | version = "0.1.0" 4 | authors = ["Michael Aaron Murphy "] 5 | edition = "2018" 6 | license = "MPL-2.0" 7 | publish = false 8 | 9 | [dependencies] 10 | anyhow = "1.0.66" 11 | apt-cmd = { git = "https://github.com/pop-os/apt-cmd" } 12 | as-result = "0.2.1" 13 | async-cron-scheduler = { path = "../scheduler", features = ["logging"] } 14 | async-fetcher = { version = "0.10.0", features = ["reqwest"] } 15 | async-stream = "0.3.3" 16 | better-panic = "0.3.0" 17 | bytesize = "1.1.0" 18 | chrono = "0.4.22" 19 | enumflags2 = "0.7.5" 20 | flume = "0.10.14" 21 | fork = "0.1.20" 22 | futures = "0.3.25" 23 | libc = "0.2.137" 24 | notify-rust = { version = "4.5.10", default-features = false, features = ["dbus"] } 25 | os-release = "0.1.0" 26 | pop-shop-zbus = { path = "../shop-zbus" } 27 | reqwest = { version = "0.11.12", default-features = false, features = ["rustls-tls", "trust-dns"] } 28 | ron = "0.8.0" 29 | serde = { version = "1.0.147", features = ["derive"] } 30 | serde_repr = "0.1.9" 31 | tracing = "0.1.37" 32 | tracing-subscriber = { version = "0.3.16", features = ["env-filter"] } 33 | users = "0.11.0" 34 | whitespace-conf = "1.0.0" 35 | zbus = { version = "3.4.0", default-features = false, features = ["tokio"] } 36 | zvariant = { version = "3.7.1", features = ["enumflags2"] } 37 | 38 | [dependencies.tokio] 39 | version = "1.21.2" 40 | features = ["full"] 41 | 42 | [features] 43 | -------------------------------------------------------------------------------- /daemon/src/package_managers/mod.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2021-2022 System76 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | pub mod apt; 5 | pub mod apt_pop; 6 | pub mod flatpak; 7 | pub mod nix; 8 | 9 | pub mod fwupd { 10 | pub async fn update(conn: zbus::Connection) { 11 | use crate::utils; 12 | 13 | const SOURCE: &str = "fwupdmgr"; 14 | 15 | if !utils::command_exists(SOURCE) { 16 | return; 17 | } 18 | 19 | if let Err(why) = utils::async_command(&[SOURCE, "refresh", "--force"]).await { 20 | utils::error_handler(&conn, SOURCE, why).await; 21 | } 22 | } 23 | } 24 | 25 | pub mod snap { 26 | pub async fn update(conn: zbus::Connection) { 27 | use crate::utils; 28 | 29 | const SOURCE: &str = "snap"; 30 | 31 | info!("{}: updating software for system", SOURCE); 32 | 33 | if !utils::command_exists(SOURCE) { 34 | return; 35 | } 36 | 37 | if let Err(why) = utils::async_command(&[SOURCE, "refresh"]).await { 38 | utils::error_handler(&conn, SOURCE, why).await; 39 | } 40 | 41 | info!("{}: updated software for system", SOURCE); 42 | } 43 | } 44 | 45 | pub async fn updates_are_available() -> bool { 46 | // TODO: Flatpak 47 | if let Ok(packages) = apt::packages_to_fetch().await { 48 | return !packages.is_empty(); 49 | } 50 | 51 | false 52 | } 53 | -------------------------------------------------------------------------------- /scheduler/README.md: -------------------------------------------------------------------------------- 1 | # async-cron-scheduler 2 | 3 | Simple lightweight efficient runtime-agnostic async task scheduler with cron expression support 4 | 5 | ## Features 6 | 7 | - **Simple**: The most important feature of all, integrate easily in any codebase. 8 | - **Lightweight**: Minimal dependencies with a small amount of code implementing it. 9 | - **Efficient**: Tickless design with no reference counters and light structs. 10 | - **Runtime-Agnostic**: Bring your own runtime. No runtime dependencies. 11 | - **Async**: A single future drives the entire scheduler service. 12 | - **Task Scheduling**: Schedule multiple jobs with varying timeframes between them. 13 | - **Cron Expressions**: Standardized format for scheduling syntax. 14 | 15 | ## Tips 16 | 17 | Scheduled jobs block the executor when they are executing, so it's best to keep their execution short. It's recommended practice to either spawn tasks onto an executor, or send messages from a channel. The good news is that each job being executed has a unique ID associated with it, which you can use for tracking specific tasks. 18 | 19 | ## Demo 20 | 21 | [Example here](./examples/simple.rs) 22 | 23 | ## License 24 | 25 | Licensed under the [Mozilla Public License 2.0](https://choosealicense.com/licenses/mpl-2.0/). 26 | 27 | ### Contribution 28 | 29 | Any contribution intentionally submitted for inclusion in the work by you shall be licensed under the Mozilla Public License 2.0 (MPL-2.0). 30 | -------------------------------------------------------------------------------- /daemon/src/dbus/local_server.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2021-2022 System76 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | use super::PopService; 5 | use super::{Frequency, LocalEvent}; 6 | use crate::config; 7 | 8 | pub struct LocalServer { 9 | pub config: config::Local, 10 | pub service: PopService, 11 | } 12 | 13 | #[rustfmt::skip] 14 | #[dbus_interface(name = "com.system76.SystemUpdater.Local")] 15 | impl LocalServer { 16 | /// Enable or disable notifications 17 | async fn notifications_enabled(&mut self, enabled: bool) { 18 | self.config.enabled = enabled; 19 | crate::config::write_session(&self.config).await; 20 | let _res = self.service.send(LocalEvent::UpdateConfig(self.config.clone())).await; 21 | } 22 | 23 | /// Get the frequency that the notification prompt will show. 24 | async fn notification_frequency(&mut self) -> Frequency { 25 | self.config.notification_frequency 26 | } 27 | 28 | /// Change the frequency that the notification prompt is shown. 29 | async fn set_notification_frequency( 30 | &mut self, 31 | frequency: Frequency, 32 | ) -> zbus::fdo::Result<()> { 33 | self.config.notification_frequency = frequency; 34 | 35 | crate::config::write_session(&self.config).await; 36 | let _res = self.service.send(LocalEvent::UpdateConfig(self.config.clone())).await; 37 | 38 | Ok(()) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /gtk/src/utils.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2021-2022 System76 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | use gtk::prelude::*; 5 | use postage::sink::Sink; 6 | use std::future::Future; 7 | 8 | pub fn glib_spawn + 'static>(future: F) { 9 | glib::MainContext::default().spawn_local(future); 10 | } 11 | 12 | pub fn glib_send + Unpin + 'static>(mut sink: S, event: E) { 13 | glib_spawn(async move { 14 | let _res = sink.send(event).await; 15 | }); 16 | } 17 | 18 | pub fn option_container() -> gtk::Grid { 19 | gtk::Grid::builder() 20 | .margin_start(20) 21 | .margin_end(20) 22 | .margin_top(8) 23 | .margin_bottom(8) 24 | .column_spacing(24) 25 | .row_spacing(4) 26 | .width_request(-1) 27 | .height_request(32) 28 | .build() 29 | } 30 | 31 | pub fn option_frame(widget: >k::Widget) -> gtk::Frame { 32 | cascade! { 33 | gtk::Frame::new(None); 34 | ..set_margin_bottom(12); 35 | ..add(widget); 36 | ..show_all(); 37 | } 38 | } 39 | 40 | pub fn separator_header(current: >k::ListBoxRow, _before: Option<>k::ListBoxRow>) { 41 | current.set_header(Some(>k::Separator::new(gtk::Orientation::Horizontal))); 42 | } 43 | 44 | pub fn as_12(hour: u8) -> (u8, bool) { 45 | if hour == 0 { 46 | (12, false) 47 | } else if hour < 12 { 48 | (hour, false) 49 | } else if hour == 12 { 50 | (12, true) 51 | } else { 52 | (hour - 12, true) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /daemon/src/main.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2021-2022 System76 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | #[macro_use] 5 | extern crate tracing; 6 | 7 | mod accounts; 8 | mod notify; 9 | mod package_managers; 10 | mod service; 11 | mod signal_handler; 12 | mod utils; 13 | 14 | use anyhow::Context; 15 | 16 | #[tokio::main] 17 | async fn main() -> anyhow::Result<()> { 18 | std::env::set_var("LANG", "C"); 19 | if std::env::var_os("RUST_LOG").is_none() { 20 | std::env::set_var("RUST_LOG", "info"); 21 | } 22 | 23 | // Use `env RUST_LOG=debug` for debug logging. 24 | tracing_subscriber::fmt() 25 | .with_ansi(false) 26 | .without_time() 27 | .with_writer(std::io::stderr) 28 | .with_env_filter(tracing_subscriber::EnvFilter::from_default_env()) 29 | .init(); 30 | 31 | // Colorful and useful error messages in unlikely event that the service crashes. 32 | better_panic::install(); 33 | 34 | // If root then system service, else local session service. 35 | let main_future = async move { 36 | let effective_uid = users::get_effective_uid(); 37 | if effective_uid == 0 { 38 | crate::service::system::run().await 39 | } else if accounts::is_desktop_account(effective_uid) { 40 | crate::service::session::run().await 41 | } else { 42 | Ok(()) 43 | } 44 | }; 45 | 46 | // Spawns main future on tokio runtime for best async performance. 47 | tokio::spawn(main_future) 48 | .await 49 | .context("failed to spawn on tokio runtime")? 50 | } 51 | -------------------------------------------------------------------------------- /daemon/src/accounts.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2021-2022 System76 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | use anyhow::Context; 5 | use std::fs; 6 | 7 | pub fn is_desktop_account(euid: u32) -> bool { 8 | match uid_min_max() { 9 | Ok((min, max)) => min <= euid && max >= euid, 10 | Err(_) => false, 11 | } 12 | } 13 | 14 | /// Users which are defined as being desktop accounts. 15 | /// 16 | /// On Linux, this means the user ID is between `UID_MIN` and `UID_MAX` in `/etc/login.defs`. 17 | pub fn user_names() -> Box + Send> { 18 | let (uid_min, uid_max) = match uid_min_max() { 19 | Ok(v) => v, 20 | Err(_) => return Box::new(std::iter::empty()), 21 | }; 22 | 23 | Box::new((unsafe { users::all_users() }).filter_map(move |user| { 24 | if user.uid() >= uid_min && user.uid() <= uid_max { 25 | let name = user.name(); 26 | if let Some(name) = name.to_str() { 27 | return Some(name.to_owned()); 28 | } 29 | } 30 | 31 | None 32 | })) 33 | } 34 | 35 | /// The `UID_MIN` and `UID_MAX` values from `/etc/login.defs`. 36 | pub fn uid_min_max() -> anyhow::Result<(u32, u32)> { 37 | let login_defs = 38 | fs::read_to_string("/etc/login.defs").context("could not read /etc/login.defs")?; 39 | 40 | let defs = whitespace_conf::parse(&login_defs); 41 | 42 | defs.get("UID_MIN") 43 | .zip(defs.get("UID_MAX")) 44 | .context("/etc/login.defs does not contain UID_MIN + UID_MAX") 45 | .and_then(|(min, max)| { 46 | let min = min.parse::().context("UID_MIN is not a u32 value")?; 47 | let max = max.parse::().context("UID_MAX is not a u32 value")?; 48 | Ok((min, max)) 49 | }) 50 | } 51 | -------------------------------------------------------------------------------- /gtk/src/main.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2021-2022 System76 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | #[macro_use] 5 | extern crate cascade; 6 | 7 | use gio::{prelude::*, ApplicationFlags}; 8 | use gtk::{prelude::*, Application}; 9 | use pop_system_updater_gtk::{localize, SettingsWidget}; 10 | 11 | pub const APP_ID: &str = "com.system76.UpgradeManager"; 12 | 13 | fn main() { 14 | tracing_subscriber::fmt() 15 | .with_writer(std::io::stderr) 16 | .with_env_filter(tracing_subscriber::EnvFilter::from_default_env()) 17 | .init(); 18 | 19 | localize(); 20 | 21 | glib::set_program_name(APP_ID.into()); 22 | 23 | let application = Application::new(Some(APP_ID), ApplicationFlags::empty()); 24 | 25 | application.connect_activate(|app| { 26 | if let Some(window) = app.window_by_id(0) { 27 | window.present(); 28 | } 29 | }); 30 | 31 | application.connect_startup(|app| { 32 | let widget = SettingsWidget::new(); 33 | 34 | let headerbar = cascade! { 35 | gtk::HeaderBar::new(); 36 | ..set_title(Some("Pop! System Update Scheduler")); 37 | ..set_show_close_button(true); 38 | ..show(); 39 | }; 40 | 41 | let _window = cascade! { 42 | gtk::ApplicationWindow::new(app); 43 | ..set_titlebar(Some(&headerbar)); 44 | ..set_icon_name(Some("firmware-manager")); 45 | ..set_keep_above(true); 46 | ..set_window_position(gtk::WindowPosition::Center); 47 | ..add(cascade! { 48 | &widget.inner; 49 | // ..set_border_width(12); 50 | ..set_margin_top(24); 51 | ..set_halign(gtk::Align::Center); 52 | }); 53 | ..show(); 54 | }; 55 | }); 56 | 57 | application.run(); 58 | } 59 | -------------------------------------------------------------------------------- /daemon/src/signal_handler.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2021-2022 System76 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | use std::{ 5 | fmt::{self, Display, Formatter}, 6 | sync::atomic::{AtomicU8, Ordering}, 7 | }; 8 | 9 | static PENDING: AtomicU8 = AtomicU8::new(0); 10 | 11 | #[repr(u8)] 12 | pub enum Signal { 13 | Interrupt = 1, 14 | Hangup = 2, 15 | Terminate = 3, 16 | TermStop = 4, 17 | } 18 | 19 | pub fn status() -> Option { 20 | match PENDING.swap(0, Ordering::SeqCst) { 21 | 1 => Some(Signal::Interrupt), 22 | 2 => Some(Signal::Hangup), 23 | 3 => Some(Signal::Terminate), 24 | 4 => Some(Signal::TermStop), 25 | _ => None, 26 | } 27 | } 28 | 29 | impl Display for Signal { 30 | fn fmt(&self, fmt: &mut Formatter) -> fmt::Result { 31 | let string = match *self { 32 | Signal::Interrupt => "interrupt", 33 | Signal::Hangup => "hangup", 34 | Signal::Terminate => "terminate", 35 | Signal::TermStop => "term stop", 36 | }; 37 | 38 | fmt.write_str(string) 39 | } 40 | } 41 | 42 | pub fn init() { 43 | extern "C" fn handler(signal: i32) { 44 | let signal = match signal { 45 | libc::SIGINT => Signal::Interrupt, 46 | libc::SIGHUP => Signal::Hangup, 47 | libc::SIGTERM => Signal::Terminate, 48 | libc::SIGTSTP => Signal::TermStop, 49 | _ => unreachable!(), 50 | }; 51 | 52 | PENDING.store(signal as u8, Ordering::SeqCst); 53 | } 54 | 55 | let handler = handler as libc::sighandler_t; 56 | 57 | unsafe { 58 | let _ = libc::signal(libc::SIGHUP, handler); 59 | let _ = libc::signal(libc::SIGTSTP, handler); 60 | let _ = libc::signal(libc::SIGINT, handler); 61 | let _ = libc::signal(libc::SIGTERM, handler); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /daemon/src/appcenter.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2022 System76 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | pub async fn show_updates() { 5 | let connection = match zbus::Connection::session().await { 6 | Ok(conn) => conn, 7 | Err(why) => { 8 | eprintln!("could not get connection to dbus session: {}", why); 9 | return; 10 | } 11 | }; 12 | 13 | let proxy = match pop_shop_zbus::ElementaryAppcenterProxy::new(&connection).await { 14 | Ok(proxy) => proxy, 15 | Err(why) => { 16 | eprintln!("could not connect to io.elementary.appcenter: {}", why); 17 | return; 18 | } 19 | }; 20 | 21 | let mut spawned = false; 22 | 23 | loop { 24 | match proxy.show_updates().await { 25 | Ok(()) => return, 26 | Err(why) => { 27 | if spawned { 28 | eprintln!( 29 | "io.elementary.appcenter show-updates dbus method failed: {:?}", 30 | why 31 | ); 32 | 33 | let _res = tokio::process::Command::new("io.elementary.appcenter") 34 | .arg("--show-updates") 35 | .spawn(); 36 | 37 | return; 38 | } 39 | 40 | // Kill the process if it's running. 41 | let _res = tokio::process::Command::new("sh") 42 | .args(&["-c", "kill $(pidof io.elementary.appcenter)"]) 43 | .status() 44 | .await; 45 | 46 | // Start a new service process. 47 | let _res = tokio::process::Command::new("io.elementary.appcenter") 48 | .arg("--silent") 49 | .spawn(); 50 | 51 | spawned = true; 52 | 53 | tokio::time::sleep(std::time::Duration::from_secs(3)).await; 54 | } 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /daemon/src/package_managers/apt_pop.rs: -------------------------------------------------------------------------------- 1 | use tokio::fs; 2 | 3 | const SOURCES_LIST: &str = "/etc/apt/sources.list"; 4 | const SYSTEM_SOURCES: &str = "/etc/apt/sources.list.d/system.sources"; 5 | const PROPRIETARY_SOURCES: &str = "/etc/apt/sources.list.d/pop-os-apps.sources"; 6 | const RELEASE_SOURCES: &str = "/etc/apt/sources.list.d/pop-os-release.sources"; 7 | 8 | const SOURCES_LIST_PLACEHOLDER: &str = r#"## This file is deprecated in Pop!_OS. 9 | ## See `man deb822` and /etc/apt/sources.list.d/system.sources. 10 | "#; 11 | 12 | pub async fn regenerate(release: &str) -> anyhow::Result<()> { 13 | if release == "impish" { 14 | futures::try_join!( 15 | fs::write(SOURCES_LIST, SOURCES_LIST_PLACEHOLDER), 16 | fs::write(SYSTEM_SOURCES, system_sources(release)), 17 | fs::write(PROPRIETARY_SOURCES, proprietary_sources(release)), 18 | fs::write(RELEASE_SOURCES, release_sources(release)), 19 | )?; 20 | } 21 | 22 | Ok(()) 23 | } 24 | 25 | fn system_sources(release: &str) -> String { 26 | format!( 27 | r#"X-Repolib-Name: Pop_OS System Sources 28 | Enabled: yes 29 | Types: deb deb-src 30 | URIs: http://us.archive.ubuntu.com/ubuntu/ 31 | Suites: {0} {0}-security {0}-updates {0}-backports 32 | Components: main restricted universe multiverse 33 | X-Repolib-Default-Mirror: http://us.archive.ubuntu.com/ubuntu/ 34 | "#, 35 | release 36 | ) 37 | } 38 | 39 | fn proprietary_sources(release: &str) -> String { 40 | format!( 41 | r#"X-Repolib-Name: Pop_OS Apps 42 | Enabled: yes 43 | Types: deb 44 | URIs: http://apt.pop-os.org/proprietary 45 | Suites: {0} 46 | Components: main 47 | "#, 48 | release 49 | ) 50 | } 51 | 52 | fn release_sources(release: &str) -> String { 53 | format!( 54 | r#"X-Repolib-Name: Pop_OS Release Sources 55 | Enabled: yes 56 | Types: deb deb-src 57 | URIs: http://apt.pop-os.org/release 58 | Suites: {0} 59 | Components: main 60 | "#, 61 | release 62 | ) 63 | } 64 | -------------------------------------------------------------------------------- /scheduler/examples/simple.rs: -------------------------------------------------------------------------------- 1 | use async_cron_scheduler::{Job, Scheduler}; 2 | use chrono::offset::Local; 3 | use smol::Timer; 4 | use std::time::Duration; 5 | 6 | #[allow(clippy::similar_names)] 7 | fn main() { 8 | smol::block_on(async move { 9 | // Creates a scheduler based on the Local timezone. Note that the `sched_service` 10 | // contains the background job as a future for the caller to decide how to await 11 | // it. When the scheduler is dropped, the scheduler service will exit as well. 12 | let (mut scheduler, sched_service) = Scheduler::::launch(Timer::after); 13 | 14 | // Creates a job which executes every 1 seconds. 15 | let job = Job::cron("1/1 * * * * *").unwrap(); 16 | let fizz_id = scheduler.insert(job, |_id| println!("Fizz")); 17 | 18 | // Creates a job which executes every 3 seconds. 19 | let job = Job::cron("1/3 * * * * *").unwrap(); 20 | let buzz_id = scheduler.insert(job, |_id| println!("Buzz")); 21 | 22 | // Creates a job which executes every 5 seconds. 23 | let job = Job::cron("1/5 * * * * *").unwrap(); 24 | let bazz_id = scheduler.insert(job, |_id| println!("Bazz")); 25 | 26 | // A future which gradually drops jobs from the scheduler. 27 | let dropper = async move { 28 | Timer::after(Duration::from_secs(7)).await; 29 | scheduler.remove(fizz_id); 30 | println!("Fizz gone"); 31 | Timer::after(Duration::from_secs(5)).await; 32 | scheduler.remove(buzz_id); 33 | println!("Buzz gone"); 34 | Timer::after(Duration::from_secs(1)).await; 35 | scheduler.remove(bazz_id); 36 | println!("Bazz gone"); 37 | 38 | // `scheduler` is dropped here, which causes the sched_service to end. 39 | }; 40 | 41 | // Poll the dropper and scheduler service concurrently until both return. 42 | futures::future::join(sched_service, dropper).await; 43 | }); 44 | } 45 | -------------------------------------------------------------------------------- /daemon/src/utils.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2021-2022 System76 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | use anyhow::Context; 5 | use as_result::IntoResult; 6 | use pop_system_updater::dbus::server::{context, Server}; 7 | use tokio::process::Command; 8 | 9 | pub async fn async_commands(cmds: &[&[&str]]) -> anyhow::Result<()> { 10 | for command in cmds { 11 | async_command(command).await?; 12 | } 13 | 14 | Ok(()) 15 | } 16 | 17 | pub async fn async_command(args: &[&str]) -> anyhow::Result<()> { 18 | if args.is_empty() { 19 | return Err(anyhow::anyhow!( 20 | "async_command fn invoked without arguments" 21 | )); 22 | } 23 | 24 | let mut cmd = Command::new(args[0]); 25 | 26 | if args.len() > 1 { 27 | cmd.args(&args[1..]); 28 | } 29 | 30 | cmd.status() 31 | .await 32 | .and_then(IntoResult::into_result) 33 | .with_context(|| format!("command execution failed for {:?}", args))?; 34 | 35 | Ok(()) 36 | } 37 | 38 | pub fn command_exists(cmd: &str) -> bool { 39 | if let Ok(path) = std::env::var("PATH") { 40 | for location in path.split(':') { 41 | if std::fs::metadata(&[location, "/", cmd].concat()).is_ok() { 42 | return true; 43 | } 44 | } 45 | } 46 | 47 | false 48 | } 49 | 50 | pub async fn error_handler(conn: &zbus::Connection, source: &str, error: anyhow::Error) { 51 | use std::fmt::Write; 52 | 53 | let mut output = String::new(); 54 | 55 | { 56 | let mut chain = anyhow::Chain::new(error.as_ref()); 57 | if let Some(why) = chain.next() { 58 | let _ = write!(&mut output, "{}", why); 59 | output.push_str(&why.to_string()); 60 | for why in chain { 61 | let _ = write!(&mut output, ": {}", why); 62 | } 63 | } 64 | } 65 | 66 | error!("{}: {}", source, output); 67 | context(conn, |ctx| async move { 68 | Server::error(&ctx, source, &output).await 69 | }) 70 | .await; 71 | } 72 | -------------------------------------------------------------------------------- /daemon/src/package_managers/flatpak.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2021-2022 System76 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | use crate::utils; 5 | use futures::StreamExt; 6 | use std::path::Path; 7 | 8 | pub async fn update(conn: zbus::Connection) { 9 | const SOURCE: &str = "flatpak"; 10 | 11 | if !utils::command_exists(SOURCE) { 12 | return; 13 | } 14 | 15 | let system = async { 16 | info!("{}: updating software for system", SOURCE); 17 | let refresh = &[SOURCE, "update", "--noninteractive"]; 18 | let prune = &[SOURCE, "remove", "--unused", "--noninteractive"]; 19 | let repair = &[SOURCE, "repair"]; 20 | 21 | if utils::async_commands(&[refresh, prune]).await.is_err() { 22 | if let Err(why) = utils::async_commands(&[repair, refresh, prune]).await { 23 | utils::error_handler(&conn, SOURCE, why).await; 24 | } 25 | } 26 | info!("{}: updated software for system", SOURCE); 27 | }; 28 | 29 | let users = async { 30 | futures::stream::iter(crate::accounts::user_names()) 31 | .for_each_concurrent(None, |user| async { 32 | let accounts_service_file = ["/var/lib/AccountsService/users/", &user].concat(); 33 | if !Path::new(&accounts_service_file).exists() { 34 | return; 35 | } 36 | 37 | let user = user; 38 | info!("{}: updating software for {}", SOURCE, user); 39 | let refresh = &[ 40 | "runuser", 41 | "-u", 42 | &user, 43 | "--", 44 | SOURCE, 45 | "update", 46 | "--noninteractive", 47 | ]; 48 | 49 | let prune = &[ 50 | "runuser", 51 | "-u", 52 | &user, 53 | "--", 54 | SOURCE, 55 | "remove", 56 | "--unused", 57 | "--noninteractive", 58 | ]; 59 | 60 | let repair = &["runuser", "-u", &user, "--", SOURCE, "repair", "--user"]; 61 | 62 | if utils::async_commands(&[refresh, prune]).await.is_err() { 63 | if let Err(why) = utils::async_commands(&[repair, refresh, prune]).await { 64 | utils::error_handler(&conn, SOURCE, why).await; 65 | } 66 | } 67 | 68 | info!("{}: updated software for {}", SOURCE, user); 69 | }) 70 | .await; 71 | }; 72 | 73 | futures::join!(system, users); 74 | } 75 | -------------------------------------------------------------------------------- /daemon/src/dbus/server.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2021-2022 System76 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | use super::{Event, PopService}; 5 | use crate::config::Schedule; 6 | use std::future::Future; 7 | use std::sync::{ 8 | atomic::{AtomicBool, Ordering}, 9 | Arc, 10 | }; 11 | use zbus::SignalContext; 12 | 13 | pub struct Server { 14 | pub updating: Arc, 15 | pub service: PopService, 16 | } 17 | 18 | #[rustfmt::skip] 19 | #[dbus_interface(name = "com.system76.SystemUpdater")] 20 | impl Server { 21 | async fn auto_update_set(&mut self, enable: bool) -> zbus::fdo::Result<()> { 22 | self.service.send(Event::SetAutoUpdate(enable)).await 23 | } 24 | 25 | /// Check if any updates are available to install. 26 | async fn check_for_updates(&mut self) -> zbus::fdo::Result<()> { 27 | self.service.send(Event::CheckForUpdates).await 28 | } 29 | 30 | /// Check if a system update is currently being performed. 31 | async fn is_updating(&self) -> bool { 32 | self.updating.load(Ordering::SeqCst) 33 | } 34 | 35 | async fn repair(&mut self) -> zbus::fdo::Result<()> { 36 | self.service.send(Event::Repair).await 37 | } 38 | 39 | async fn update_scheduling_disable(&mut self) -> zbus::fdo::Result<()> { 40 | self.service.send(Event::SetSchedule(None)).await 41 | } 42 | 43 | async fn update_scheduling_set(&mut self, schedule: Schedule) -> zbus::fdo::Result<()> { 44 | self.service.send(Event::SetSchedule(Some(schedule))).await 45 | } 46 | 47 | /// Initiates a system update. 48 | async fn update_system(&mut self) -> zbus::fdo::Result<()> { 49 | if !self.updating.load(Ordering::SeqCst) { 50 | self.service.send(Event::Update).await?; 51 | } 52 | 53 | Ok(()) 54 | } 55 | 56 | #[dbus_interface(signal)] 57 | pub async fn error(ctx: &SignalContext<'_>, source: &str, why: &str) -> zbus::Result<()>; 58 | 59 | #[dbus_interface(signal)] 60 | pub async fn progress(ctx: &SignalContext<'_>, source: &str, percent: u8) -> zbus::Result<()>; 61 | 62 | #[dbus_interface(signal)] 63 | pub async fn repair_err(ctx: &SignalContext<'_>, why: &str) -> zbus::Result<()>; 64 | 65 | #[dbus_interface(signal)] 66 | pub async fn repair_ok(ctx: &SignalContext<'_>) -> zbus::Result<()>; 67 | 68 | #[dbus_interface(signal)] 69 | pub async fn updates_available(ctx: &SignalContext<'_>, available: bool) -> zbus::Result<()>; 70 | } 71 | 72 | pub async fn context<'a, C, F>(conn: &zbus::Connection, future: C) 73 | where 74 | C: FnOnce(SignalContext<'static>) -> F + 'a, 75 | F: Future> + 'a, 76 | { 77 | if let Ok(iface) = conn 78 | .object_server() 79 | .interface::<_, Server>(super::IFACE) 80 | .await 81 | { 82 | if let Err(why) = future(iface.signal_context().to_owned()).await { 83 | error!("context failed with {:?}", why); 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /justfile: -------------------------------------------------------------------------------- 1 | rootdir := '' 2 | prefix := '/usr' 3 | clean := '0' 4 | debug := '0' 5 | vendor := '0' 6 | target := if debug == '1' { 'debug' } else { 'release' } 7 | vendor_args := if vendor == '1' { '--frozen --offline' } else { '' } 8 | debug_args := if debug == '1' { '' } else { '--release' } 9 | cargo_args := vendor_args + ' ' + debug_args 10 | 11 | root := rootdir + prefix 12 | bindir := root + '/bin' 13 | includedir := root + '/include' 14 | sysconfdir := root + '/share' 15 | libdir := root + '/lib' 16 | 17 | id := 'com.system76.SystemUpdater' 18 | id_local := id + '.Local' 19 | binary := 'pop-system-updater' 20 | gtklib := 'pop_system_updater_gtk' 21 | 22 | target_bin := bindir + '/' + binary 23 | target_dbus_conf := sysconfdir + '/dbus-1/system.d/' + id + '.conf' 24 | target_systemd_service := rootdir + '/lib/systemd/system/' + id + '.service' 25 | target_session_service := rootdir + '/lib/systemd/user/' + id_local + '.service' 26 | 27 | # Compiles pop-system-updater. 28 | all: _extract_vendor 29 | cargo build -p pop-system-updater {{cargo_args}} 30 | cargo build -p gtk-ffi {{cargo_args}} 31 | just pkgconfig 32 | 33 | pkgconfig: 34 | #!/bin/bash 35 | cat > target/{{gtklib}}.pc <<- EOM 36 | libdir={{libdir}} 37 | includedir={{includedir}} 38 | $(cat target/{{gtklib}}.pc.stub) 39 | EOM 40 | 41 | # Remove Cargo build artifacts. 42 | clean: 43 | cargo clean 44 | 45 | # Also remove .cargo and vendored dependencies. 46 | distclean: 47 | rm -rf .cargo vendor vendor.tar target 48 | 49 | # Run the GTK UI for testing purposes. 50 | run: 51 | cargo run -p pop-system-updater-gtk 52 | 53 | # Run the systemd service for testing. 54 | run_service: 55 | just && sudo env RUST_BACKTRACE=1 RUST_LOG=debug ./target/release/pop-system-updater 56 | 57 | # Install the compiled project into the system. 58 | install: 59 | install -Dm0755 target/{{target}}/{{binary}} {{target_bin}} 60 | install -Dm0644 data/{{id}}.conf {{target_dbus_conf}} 61 | install -Dm0644 data/{{id}}.service {{target_systemd_service}} 62 | install -Dm0644 data/{{id_local}}.service {{target_session_service}} 63 | install -Dm0644 gtk-ffi/{{gtklib}}.h {{includedir}}/{{gtklib}}.h 64 | install -Dm0644 target/{{target}}/lib{{gtklib}}.so {{libdir}}/lib{{gtklib}}.so 65 | install -Dm0655 target/{{gtklib}}.pc {{libdir}}/pkgconfig/{{gtklib}}.pc 66 | 67 | # Uninstall the files that were installed. 68 | uninstall: 69 | rm {{target_bin}} {{target_dbus_conf}} {{target_systemd_service}} 70 | 71 | # Vendor Cargo dependencies locally. 72 | vendor: 73 | mkdir -p .cargo 74 | cargo vendor --sync gtk/Cargo.toml \ 75 | --sync daemon/Cargo.toml \ 76 | | head -n -1 > .cargo/config 77 | echo 'directory = "vendor"' >> .cargo/config 78 | tar pcf vendor.tar vendor 79 | rm -rf vendor 80 | 81 | # Used by packaging systems to generate a source package. 82 | package_source: 83 | #!/usr/bin/env sh 84 | if test {{clean}} = 1; then 85 | just clean 86 | fi 87 | 88 | if test {{vendor}} = 1; then 89 | ischroot || just vendor 90 | fi 91 | 92 | # Used by packaging systems to build a binary package. 93 | package_build: 94 | just debug={{debug}} vendor={{vendor}} sysconfdir='/usr/share' 95 | 96 | # Extracts vendored dependencies if vendor=1 97 | _extract_vendor: 98 | #!/usr/bin/env sh 99 | if test {{vendor}} = 1; then 100 | rm -rf vendor; tar pxf vendor.tar 101 | fi 102 | -------------------------------------------------------------------------------- /gtk/src/proxy.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2021-2022 System76 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | use pop_system_updater::config::{Config, Frequency}; 5 | use pop_system_updater::dbus::{client::ClientProxy, local_client::LocalClientProxy}; 6 | use postage::mpsc::{channel, Sender}; 7 | use postage::prelude::*; 8 | use zbus::Connection; 9 | 10 | #[derive(Debug)] 11 | pub enum Event { 12 | Exit, 13 | SetNotificationFrequency(Frequency), 14 | UpdateConfig(Config), 15 | } 16 | 17 | pub fn initialize_service() -> Sender { 18 | let (tx, mut rx) = channel(1); 19 | 20 | let background_process = async move { 21 | let connection = match Connection::system().await { 22 | Ok(connection) => connection, 23 | Err(why) => { 24 | eprintln!("could not initiate connection to service: {}", why); 25 | return; 26 | } 27 | }; 28 | 29 | let mut proxy = match ClientProxy::new(&connection).await { 30 | Ok(proxy) => proxy, 31 | Err(why) => { 32 | eprintln!("could not get proxy from connection: {}", why); 33 | return; 34 | } 35 | }; 36 | 37 | let session_connection = match Connection::session().await { 38 | Ok(connection) => connection, 39 | Err(why) => { 40 | eprintln!("could not initiate connection to service: {}", why); 41 | return; 42 | } 43 | }; 44 | 45 | let mut session_proxy = match LocalClientProxy::new(&session_connection).await { 46 | Ok(proxy) => proxy, 47 | Err(why) => { 48 | eprintln!("could not get proxy from connection: {}", why); 49 | return; 50 | } 51 | }; 52 | 53 | while let Some(event) = rx.recv().await { 54 | match event { 55 | Event::Exit => break, 56 | 57 | Event::UpdateConfig(config) => { 58 | if let Err(why) = proxy.auto_update_set(config.auto_update).await { 59 | eprintln!("failed to change auto-update setting: {}", why); 60 | } 61 | 62 | let result = match config.schedule { 63 | Some(schedule) => proxy.update_scheduling_set(schedule).await, 64 | None => proxy.update_scheduling_disable().await, 65 | }; 66 | 67 | if let Err(why) = result { 68 | eprintln!("failed to change scheduling: {}", why); 69 | } 70 | 71 | if let Err(why) = session_proxy 72 | .notifications_enabled(!config.auto_update) 73 | .await 74 | { 75 | eprintln!( 76 | "failed to set auto-update config with user session service: {:?}", 77 | why 78 | ); 79 | } 80 | } 81 | 82 | Event::SetNotificationFrequency(frequency) => { 83 | if let Err(why) = session_proxy.set_notification_frequency(frequency).await { 84 | eprintln!("failed to update notification frequency: {:?}", why); 85 | } 86 | } 87 | } 88 | } 89 | }; 90 | 91 | std::thread::spawn(move || { 92 | let runtime = tokio::runtime::Builder::new_current_thread() 93 | .enable_all() 94 | .build() 95 | .unwrap(); 96 | 97 | runtime.block_on(background_process); 98 | }); 99 | 100 | tx 101 | } 102 | -------------------------------------------------------------------------------- /scheduler/src/lib.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2021-2022 System76 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | //! Simple lightweight efficient runtime-agnostic async task scheduler with cron expression support 5 | //! 6 | //! # Features 7 | //! 8 | //! - **Simple**: The most important feature of all, integrate easily in any codebase. 9 | //! - **Lightweight**: Minimal dependencies with a small amount of code implementing it. 10 | //! - **Efficient**: Tickless design with no reference counters and light structs. 11 | //! - **Runtime-Agnostic**: Bring your own runtime. No runtime dependencies. 12 | //! - **Async**: A single future drives the entire scheduler service. 13 | //! - **Task Scheduling**: Schedule multiple jobs with varying timeframes between them. 14 | //! - **Cron Expressions**: Standardized format for scheduling syntax. 15 | //! 16 | //! # Tips 17 | //! 18 | //! Scheduled jobs block the executor when they are executing, so it's best to keep 19 | //! their execution short. It's recommended practice to either spawn tasks onto an 20 | //! executor, or send messages from a channel. The good news is that each job being 21 | //! executed has a unique ID associated with it, which you can use for tracking 22 | //! specific tasks. 23 | //! 24 | //! # Demo 25 | //! 26 | //! The entire API wrapped up in one example. 27 | //! 28 | //! ``` 29 | //! use chrono::offset::Local; 30 | //! use async_cron_scheduler::*; 31 | //! use smol::Timer; 32 | //! use std::time::Duration; 33 | //! 34 | //! smol::block_on(async move { 35 | //! // Creates a scheduler based on the Local timezone. Note that the `sched_service` 36 | //! // contains the background job as a future for the caller to decide how to await 37 | //! // it. When the scheduler is dropped, the scheduler service will exit as well. 38 | //! let (mut scheduler, sched_service) = Scheduler::::launch(Timer::after); 39 | //! 40 | //! // Creates a job which executes every 1 seconds. 41 | //! let job = Job::cron("1/1 * * * * *").unwrap(); 42 | //! let fizz_id = scheduler.insert(job, |id| println!("Fizz")); 43 | //! 44 | //! // Creates a job which executes every 3 seconds. 45 | //! let job = Job::cron("1/3 * * * * *").unwrap(); 46 | //! let buzz_id = scheduler.insert(job, |id| println!("Buzz")); 47 | //! 48 | //! // Creates a job which executes every 5 seconds. 49 | //! let job = Job::cron("1/5 * * * * *").unwrap(); 50 | //! let bazz_id = scheduler.insert(job, |id| println!("Bazz")); 51 | //! 52 | //! // A future which gradually drops jobs from the scheduler. 53 | //! let dropper = async move { 54 | //! Timer::after(Duration::from_secs(7)).await; 55 | //! scheduler.remove(fizz_id); 56 | //! println!("Fizz gone"); 57 | //! Timer::after(Duration::from_secs(5)).await; 58 | //! scheduler.remove(buzz_id); 59 | //! println!("Buzz gone"); 60 | //! Timer::after(Duration::from_secs(1)).await; 61 | //! scheduler.remove(bazz_id); 62 | //! println!("Bazz gone"); 63 | //! 64 | //! // `scheduler` is dropped here, which causes the sched_service to end. 65 | //! }; 66 | //! 67 | //! // Poll the dropper and scheduler service concurrently until both return. 68 | //! futures::future::join(sched_service, dropper).await; 69 | //! }); 70 | //! ``` 71 | 72 | use chrono::DateTime; 73 | use chrono::TimeZone; 74 | pub use cron; 75 | 76 | mod job; 77 | mod scheduler; 78 | 79 | pub use self::job::*; 80 | pub use self::scheduler::*; 81 | 82 | /// Extensions for the chrono timezone structs. 83 | pub trait TimeZoneExt: TimeZone + Copy + Clone { 84 | /// Constructs a default timezone struct for this timezone. 85 | fn timescale() -> Self; 86 | 87 | /// Get the current time in this timezone. 88 | fn now() -> DateTime; 89 | } 90 | 91 | impl TimeZoneExt for chrono::Local { 92 | fn timescale() -> Self { 93 | Self 94 | } 95 | fn now() -> DateTime { 96 | Self::now() 97 | } 98 | } 99 | 100 | impl TimeZoneExt for chrono::Utc { 101 | fn timescale() -> Self { 102 | Self 103 | } 104 | 105 | fn now() -> DateTime { 106 | Self::now() 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /daemon/src/config.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2021-2022 System76 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | use serde::{de::DeserializeOwned, Deserialize, Serialize}; 5 | use serde_repr::{Deserialize_repr, Serialize_repr}; 6 | use std::path::{Path, PathBuf}; 7 | use zvariant::Type; 8 | 9 | pub const SYSTEM_CACHE: &str = "/var/cache/pop-system-updater/cache.ron"; 10 | pub const SYSTEM_PATH: &str = "/etc/pop-system-updater/config.ron"; 11 | pub const LOCAL_CACHE: &str = ".cache/pop-system-updater/cache.ron"; 12 | pub const LOCAL_PATH: &str = ".config/pop-system-updater/config.ron"; 13 | 14 | #[derive(Clone, Debug, Default, Deserialize, Serialize, Type)] 15 | pub struct Cache { 16 | pub last_update: u64, 17 | } 18 | 19 | #[derive(Clone, Debug, Default, Deserialize, Serialize, Type)] 20 | pub struct LocalCache { 21 | pub last_update: u64, 22 | } 23 | 24 | #[derive(Clone, Debug, Deserialize, Serialize)] 25 | pub struct Config { 26 | /// If it should automatically update when updates are available. 27 | pub auto_update: bool, 28 | 29 | /// When updates should be scheduled, if updates should be scheduled for. 30 | pub schedule: Option, 31 | } 32 | 33 | impl Config { 34 | #[must_use] 35 | pub const fn default_schedule() -> Schedule { 36 | Schedule { 37 | interval: Interval::Weekdays, 38 | hour: 22, 39 | minute: 0, 40 | } 41 | } 42 | } 43 | 44 | impl Default for Config { 45 | fn default() -> Self { 46 | Self { 47 | auto_update: false, 48 | schedule: Some(Config::default_schedule()), 49 | } 50 | } 51 | } 52 | 53 | #[derive(Clone, Debug, Deserialize, Serialize, Type)] 54 | pub struct Local { 55 | pub enabled: bool, 56 | pub notification_frequency: Frequency, 57 | } 58 | 59 | impl Default for Local { 60 | fn default() -> Self { 61 | Self { 62 | enabled: true, 63 | notification_frequency: Frequency::Weekly, 64 | } 65 | } 66 | } 67 | 68 | #[derive(Copy, Clone, Debug, Deserialize, Serialize, Type)] 69 | #[repr(u32)] 70 | pub enum Frequency { 71 | Weekly = 0, 72 | Daily = 1, 73 | Monthly = 2, 74 | } 75 | 76 | #[derive(Clone, Debug, Deserialize, Serialize, Type)] 77 | pub struct Schedule { 78 | pub interval: Interval, 79 | pub hour: u8, 80 | pub minute: u8, 81 | } 82 | 83 | #[repr(u8)] 84 | #[derive(Copy, Clone, Debug, Deserialize_repr, Serialize_repr, Type)] 85 | pub enum Interval { 86 | Monday = 1, 87 | Tuesday = 1 << 1, 88 | Wednesday = 1 << 2, 89 | Thursday = 1 << 3, 90 | Friday = 1 << 4, 91 | Saturday = 1 << 5, 92 | Sunday = 1 << 6, 93 | Weekdays = 1 << 7, 94 | } 95 | 96 | pub async fn load_session() -> Local { 97 | load(&session_path()).await 98 | } 99 | 100 | pub async fn load_session_cache() -> LocalCache { 101 | load(&session_cache_path()).await 102 | } 103 | 104 | pub async fn load_system() -> Config { 105 | load(Path::new(SYSTEM_PATH)).await 106 | } 107 | 108 | pub async fn load_system_cache() -> Cache { 109 | load(Path::new(SYSTEM_CACHE)).await 110 | } 111 | 112 | pub async fn write_session(config: &Local) { 113 | write(&session_path(), config).await; 114 | } 115 | 116 | pub async fn write_session_cache(cache: &LocalCache) { 117 | write(&session_cache_path(), cache).await; 118 | } 119 | 120 | pub async fn write_system(config: &Config) { 121 | write(Path::new(SYSTEM_PATH), config).await; 122 | } 123 | 124 | pub async fn write_system_cache(cache: &Cache) { 125 | write(Path::new(SYSTEM_CACHE), cache).await; 126 | } 127 | 128 | async fn load(path: &Path) -> T { 129 | info!("loading config: {:?}", path); 130 | let file; 131 | if let Ok(file_) = tokio::fs::read_to_string(path).await { 132 | file = file_; 133 | match ron::from_str::(&file) { 134 | Ok(config) => return config, 135 | Err(why) => { 136 | error!("failed to read config: {}", why); 137 | } 138 | } 139 | } 140 | 141 | let config = T::default(); 142 | write(path, &config).await; 143 | config 144 | } 145 | 146 | async fn write(path: &Path, config: &T) { 147 | info!("writing config: {:?}", path); 148 | 149 | if let Some(parent) = path.parent() { 150 | let _res = tokio::fs::create_dir(parent).await; 151 | } 152 | 153 | let config = match ron::to_string(config) { 154 | Ok(config) => config, 155 | Err(why) => { 156 | error!("failed to serialize config: {}", why); 157 | return; 158 | } 159 | }; 160 | 161 | if let Err(why) = tokio::fs::write(path, config.as_bytes()).await { 162 | error!("failed to write config file: {}", why); 163 | } 164 | } 165 | 166 | fn session_path() -> PathBuf { 167 | #[allow(deprecated)] 168 | std::env::home_dir().expect("NO HOME").join(LOCAL_PATH) 169 | } 170 | 171 | fn session_cache_path() -> PathBuf { 172 | #[allow(deprecated)] 173 | std::env::home_dir().expect("NO HOME").join(LOCAL_CACHE) 174 | } 175 | -------------------------------------------------------------------------------- /daemon/src/service/session.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2021-2022 System76 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | use anyhow::Context; 5 | use config::{Frequency, Local, LocalCache}; 6 | use flume::Sender; 7 | use pop_system_updater::config; 8 | use pop_system_updater::dbus::PopService; 9 | use pop_system_updater::dbus::{ 10 | client::ClientProxy, local_server::LocalServer, LocalEvent, IFACE_LOCAL, 11 | }; 12 | use std::time::{Duration, SystemTime}; 13 | use tokio::task::JoinHandle; 14 | use zbus::Connection; 15 | 16 | pub async fn run() -> anyhow::Result<()> { 17 | if std::env::var("DISPLAY").is_err() { 18 | info!("Restarting service in 5s because DISPLAY could not be found"); 19 | tokio::time::sleep(Duration::from_secs(5)).await; 20 | return Ok(()); 21 | } 22 | 23 | let system_connection = Connection::system() 24 | .await 25 | .context("could not initiate connection to service")?; 26 | 27 | let _system_proxy = ClientProxy::new(&system_connection) 28 | .await 29 | .context("could not get proxy from connection")?; 30 | 31 | let mut config = config::load_session().await; 32 | let (sender, receiver) = flume::bounded(1); 33 | 34 | let connection = Connection::session() 35 | .await 36 | .context("failed to initiate session connection")?; 37 | 38 | connection 39 | .object_server() 40 | .at( 41 | IFACE_LOCAL, 42 | LocalServer { 43 | config: config.clone(), 44 | service: PopService { 45 | sender: sender.clone(), 46 | }, 47 | }, 48 | ) 49 | .await 50 | .context("failed to serve service")?; 51 | 52 | connection 53 | .request_name("com.system76.SystemUpdater.Local") 54 | .await 55 | .map_err(|why| match why { 56 | zbus::Error::NameTaken => anyhow::anyhow!("user service is already active"), 57 | other => anyhow::anyhow!("could not register user service: {}", other), 58 | })?; 59 | 60 | let mut state = State { 61 | cache: config::load_session_cache().await, 62 | schedule_handle: tokio::spawn(update_on( 63 | sender.clone(), 64 | Duration::from_secs(SECONDS_IN_DAY), 65 | )), 66 | sender, 67 | }; 68 | 69 | state.check_for_updates(&config).await; 70 | 71 | while let Ok(event) = receiver.recv_async().await { 72 | match event { 73 | LocalEvent::CheckUpdates => state.check_for_updates(&config).await, 74 | LocalEvent::UpdateConfig(conf) => { 75 | config = conf; 76 | state.check_for_updates(&config).await; 77 | } 78 | } 79 | } 80 | 81 | Ok(()) 82 | } 83 | 84 | pub struct State { 85 | cache: LocalCache, 86 | schedule_handle: JoinHandle<()>, 87 | sender: Sender, 88 | } 89 | 90 | impl State { 91 | async fn check_for_updates(&mut self, config: &Local) { 92 | self.schedule_handle.abort(); 93 | 94 | if !config.enabled { 95 | info!("notifications disabled"); 96 | return; 97 | } 98 | 99 | let now = SystemTime::now() 100 | .duration_since(SystemTime::UNIX_EPOCH) 101 | .map_or(0, |d| d.as_secs()); 102 | 103 | let next_update = next_update(config, &self.cache); 104 | 105 | if next_update > now { 106 | let next = next_update - now; 107 | info!("next update in {} seconds", next); 108 | let future = update_on(self.sender.clone(), Duration::from_secs(next)); 109 | self.schedule_handle = tokio::spawn(future); 110 | return; 111 | } 112 | 113 | self.schedule_handle = tokio::spawn(update_on( 114 | self.sender.clone(), 115 | Duration::from_secs(SECONDS_IN_DAY), 116 | )); 117 | 118 | self.cache.last_update = now; 119 | let f1 = config::write_session_cache(&self.cache); 120 | let f2 = async { 121 | if crate::package_managers::updates_are_available().await { 122 | info!("displaying notification of available updates"); 123 | let handle = tokio::runtime::Handle::current(); 124 | std::thread::spawn(move || { 125 | let _reactor_context = handle.enter(); 126 | crate::notify::updates_available(); 127 | }); 128 | } 129 | }; 130 | 131 | futures::join!(f1, f2); 132 | } 133 | } 134 | 135 | const SECONDS_IN_DAY: u64 = 60 * 60 * 24; 136 | 137 | fn next_update(config: &Local, cache: &LocalCache) -> u64 { 138 | match config.notification_frequency { 139 | Frequency::Daily => cache.last_update + SECONDS_IN_DAY, 140 | Frequency::Weekly => cache.last_update + SECONDS_IN_DAY * 7, 141 | Frequency::Monthly => cache.last_update + SECONDS_IN_DAY * 30, 142 | } 143 | } 144 | 145 | async fn update_on(sender: Sender, duration: Duration) { 146 | tokio::time::sleep(duration).await; 147 | let _ = sender.send_async(LocalEvent::CheckUpdates).await; 148 | } 149 | -------------------------------------------------------------------------------- /daemon/src/package_managers/apt.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2021-2022 System76 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | use crate::utils; 5 | use anyhow::Context; 6 | use apt_cmd::lock::apt_lock_wait; 7 | use apt_cmd::{AptGet, AptMark, Dpkg}; 8 | use futures::Stream; 9 | use futures::StreamExt; 10 | use std::pin::Pin; 11 | use std::process::Stdio; 12 | use tokio::process::{Child, Command}; 13 | 14 | pub async fn update(conn: zbus::Connection) -> bool { 15 | const SOURCE: &str = "apt"; 16 | 17 | if !utils::command_exists(SOURCE) { 18 | return false; 19 | } 20 | 21 | info!("performing system update with apt"); 22 | 23 | let mut service_requires_update = false; 24 | 25 | if system_update(&mut service_requires_update).await.is_err() { 26 | if let Ok(release) = os_release::OS_RELEASE.as_ref() { 27 | if release.name == "Pop!_OS" { 28 | let _res = super::apt_pop::regenerate(&release.version_codename).await; 29 | } 30 | } 31 | 32 | let mut count = 0; 33 | while let Err(why) = repair().await { 34 | if count == 2 { 35 | utils::error_handler(&conn, SOURCE, why).await; 36 | return false; 37 | } 38 | 39 | count += 1; 40 | } 41 | } 42 | 43 | info!("{}: updated software for system", SOURCE); 44 | service_requires_update 45 | } 46 | 47 | pub async fn repair() -> anyhow::Result<()> { 48 | let _res = AptMark::new().hold(["pop-system-updater"]).await; 49 | 50 | apt_lock_wait().await; 51 | let apt_get_result = AptGet::new() 52 | .noninteractive() 53 | .fix_broken() 54 | .force() 55 | .allow_downgrades() 56 | .status() 57 | .await 58 | .context("failed to repair broken packages with `apt-get install -f`"); 59 | 60 | apt_lock_wait().await; 61 | let dpkg_result = Dpkg::new() 62 | .configure_all() 63 | .status() 64 | .await 65 | .context("failed to configure packages with `dpkg --configure -a`"); 66 | 67 | let _res = AptMark::new().unhold(["pop-system-updater"]).await; 68 | 69 | apt_get_result.and(dpkg_result) 70 | } 71 | 72 | async fn system_update(service_requires_update: &mut bool) -> anyhow::Result<()> { 73 | update_package_lists().await; 74 | 75 | info!("getting list of packages to update"); 76 | let packages = packages_to_fetch() 77 | .await 78 | .context("could not get packages to fetch")?; 79 | 80 | let mut packages: Vec<&str> = packages.iter().map(String::as_str).collect(); 81 | 82 | if let Some(id) = packages.iter().position(|&p| p == "pop-system-updater") { 83 | info!("service requires update"); 84 | *service_requires_update = true; 85 | packages.swap_remove(id); 86 | } 87 | 88 | upgrade().await.context("could not upgrade packages")?; 89 | 90 | Ok(()) 91 | } 92 | 93 | pub async fn update_package_lists() { 94 | info!("updating package lists"); 95 | apt_lock_wait().await; 96 | let result = AptGet::new() 97 | .update() 98 | .await 99 | .context("could not `apt update` package lists"); 100 | 101 | if let Err(why) = result { 102 | error!("potential issue with package lists configuration: {}", why); 103 | } 104 | } 105 | 106 | pub async fn packages_to_fetch() -> anyhow::Result> { 107 | apt_lock_wait().await; 108 | 109 | let (mut child, packages) = upgradable_packages() 110 | .await 111 | .context("could not get system updates from apt")?; 112 | 113 | let packages = packages.collect::>().await; 114 | 115 | info!("debian packages requiring updates: {}", packages.len()); 116 | 117 | child 118 | .wait() 119 | .await 120 | .context("could not check for updates from apt")?; 121 | 122 | Ok(packages) 123 | } 124 | 125 | pub async fn upgrade() -> anyhow::Result<()> { 126 | apt_lock_wait().await; 127 | 128 | let _res = AptMark::new().hold(["pop-system-updater"]).await; 129 | 130 | let mut result = AptGet::new() 131 | .noninteractive() 132 | .force() 133 | .allow_downgrades() 134 | .upgrade() 135 | .await 136 | .context("failed to install updates"); 137 | 138 | let _res = AptMark::new().unhold(["pop-system-updater"]).await; 139 | 140 | if result.is_ok() { 141 | result = AptGet::new() 142 | .noninteractive() 143 | .autoremove() 144 | .force() 145 | .status() 146 | .await 147 | .context("failed to autoremove packages"); 148 | } 149 | 150 | result 151 | } 152 | 153 | pub type Packages = Pin + Send>>; 154 | 155 | // Fetch all upgradeable debian packages from system apt repositories. 156 | pub async fn upgradable_packages() -> anyhow::Result<(Child, Packages)> { 157 | let mut child = Command::new("apt-get") 158 | .args(&["full-upgrade", "--dry-run"]) 159 | .stdout(Stdio::piped()) 160 | .stderr(Stdio::null()) 161 | .spawn() 162 | .context("failed to launch `apt`")?; 163 | 164 | let stdout = child.stdout.take().unwrap(); 165 | 166 | let stream = Box::pin(async_stream::stream! { 167 | use tokio::io::AsyncBufReadExt; 168 | let mut reader = tokio::io::BufReader::new(stdout); 169 | let mut buffer = String::new(); 170 | 171 | while let Ok(read) = reader.read_line(&mut buffer).await { 172 | if read == 0 { 173 | break 174 | } 175 | 176 | let mut words = buffer.split_ascii_whitespace(); 177 | if let Some("Inst") = words.next() { 178 | if let Some(package) = words.next() { 179 | yield package.into(); 180 | } 181 | } 182 | 183 | buffer.clear(); 184 | } 185 | }); 186 | 187 | Ok((child, stream)) 188 | } 189 | -------------------------------------------------------------------------------- /gtk/src/bsb.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2022 System76 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | use gtk::prelude::*; 5 | use std::convert::TryFrom; 6 | 7 | pub struct BetterSpinButton { 8 | pub root: gtk::Box, 9 | entry: gtk::Entry, 10 | padding: u32, 11 | min: u32, 12 | max: u32, 13 | } 14 | 15 | impl std::ops::Deref for BetterSpinButton { 16 | type Target = gtk::Box; 17 | 18 | fn deref(&self) -> &Self::Target { 19 | &self.root 20 | } 21 | } 22 | 23 | impl BetterSpinButton { 24 | pub fn new(min: u32, max: u32, inc_small: u32, inc_large: u32, padding: u32) -> Self { 25 | // Increase the value while preventing it from exceeding the max. 26 | let increase = move |value: u32, inc: u32| { 27 | if value + inc > max { 28 | min 29 | } else { 30 | value + inc 31 | } 32 | }; 33 | 34 | // Decrease the value while preventing it from exceeding the min. 35 | let decrease = move |value: u32, inc: u32| { 36 | if min + inc > value { 37 | max 38 | } else { 39 | value - inc 40 | } 41 | }; 42 | 43 | let entry = cascade! { 44 | gtk::Entry::default(); 45 | ..set_max_width_chars(2); 46 | ..set_width_chars(2); 47 | // Configure arrow key presses to increment and decrement the value 48 | ..connect_key_press_event(move |entry, event| { 49 | let current = || entry.text().as_str().parse::().unwrap_or(min); 50 | let set = |value| entry.set_text(&*format_number(value, padding as usize)); 51 | 52 | match event.keycode() { 53 | Some(111) => set(increase(current(), inc_large)), 54 | Some(113) => set(decrease(current(), inc_small)), 55 | Some(114) => set(increase(current(), inc_small)), 56 | Some(116) => set(decrease(current(), inc_large)), 57 | _ => return gtk::Inhibit(false) 58 | } 59 | 60 | gtk::Inhibit(true) 61 | }); 62 | }; 63 | 64 | let plus = cascade! { 65 | create_button("value-increase-symbolic"); 66 | ..connect_clicked(on_click(&entry, move |value| increase(value, inc_small), padding)); 67 | }; 68 | 69 | let minus = cascade! { 70 | create_button("value-decrease-symbolic"); 71 | ..connect_clicked(on_click(&entry, move |value| decrease(value, inc_small), padding)); 72 | }; 73 | 74 | let root = cascade! { 75 | gtk::Box::default(); 76 | ..set_orientation(gtk::Orientation::Vertical); 77 | ..add(&plus); 78 | ..add(&entry); 79 | ..add(&minus); 80 | }; 81 | 82 | BetterSpinButton { 83 | root, 84 | entry, 85 | padding, 86 | min, 87 | max, 88 | } 89 | } 90 | 91 | pub fn connect_update(&self, func: impl Fn() + 'static) { 92 | let (tx, rx) = flume::unbounded(); 93 | 94 | let id = self.entry.connect_changed(move |entry| { 95 | let _res = tx.send(entry.text()); 96 | }); 97 | 98 | let mut last_known = self.entry.text().to_string(); 99 | 100 | let entry = self.entry.downgrade(); 101 | let min = self.min; 102 | let max = self.max; 103 | let padding = self.padding; 104 | 105 | crate::utils::glib_spawn(async move { 106 | while let Ok(text) = rx.recv_async().await { 107 | let new_value = match text.as_str().parse::() { 108 | Ok(value) => { 109 | if value < min { 110 | max 111 | } else if value > max { 112 | min 113 | } else { 114 | value 115 | } 116 | } 117 | 118 | Err(_) => min, 119 | }; 120 | 121 | let new_value = format_number(new_value, padding as usize); 122 | 123 | let entry = match entry.upgrade() { 124 | Some(entry) => entry, 125 | None => return, 126 | }; 127 | 128 | entry.block_signal(&id); 129 | entry.set_text(&*new_value); 130 | entry.set_position(i32::try_from(new_value.len()).unwrap_or(0)); 131 | entry.unblock_signal(&id); 132 | 133 | if new_value != last_known { 134 | last_known = new_value; 135 | 136 | func(); 137 | } 138 | } 139 | }); 140 | } 141 | 142 | pub fn set_value(&self, value: u32) { 143 | self.entry 144 | .set_text(&*format_number(value, self.padding as usize)); 145 | } 146 | 147 | pub fn value(&self) -> u32 { 148 | self.entry.text().as_str().parse::().unwrap_or(0) 149 | } 150 | } 151 | 152 | fn create_button(icon: &str) -> gtk::Button { 153 | cascade! { 154 | gtk::Button::from_icon_name(Some(icon), gtk::IconSize::Button); 155 | ..set_can_focus(false); 156 | } 157 | } 158 | 159 | fn format_number(value: u32, padding: usize) -> String { 160 | format!("{:01$}", value, padding) 161 | } 162 | 163 | fn on_click( 164 | entry: >k::Entry, 165 | func: impl Fn(u32) -> u32 + 'static, 166 | padding: u32, 167 | ) -> impl Fn(>k::Button) + 'static { 168 | glib::clone!(@weak entry => move |_| { 169 | if entry.text_length() > 0 { 170 | match entry.text().as_str().parse::() { 171 | Ok(value) => entry.set_text(&*format_number(func(value), padding as usize)), 172 | Err(_) => entry.set_text(""), 173 | } 174 | } 175 | }) 176 | } 177 | -------------------------------------------------------------------------------- /scheduler/src/scheduler.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2021-2022 System76 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | use crate::{Job, JobId, TimeZoneExt}; 5 | use chrono::DateTime; 6 | use flume::{RecvError, Sender}; 7 | use futures::future::Either; 8 | use slotmap::{DefaultKey, SecondaryMap, SlotMap}; 9 | use std::{future::Future, time::Duration}; 10 | 11 | /// A scheduled command associated with a job. 12 | pub type Command = Box; 13 | 14 | /// Messages going into the scheduler service. 15 | enum SchedMessage { 16 | Insert(JobId, Job, Command), 17 | Remove(JobId), 18 | } 19 | 20 | /// The interface for interacting with the scheduler. 21 | /// 22 | /// When launching a scheduler, the scheduler and its service are created 23 | /// simultaneously with a channel connecting them. Job insert and remove 24 | /// messages are sent to the service for automatic management. When the 25 | /// scheduler is dropped, so too will the service its attached to exit. 26 | /// 27 | /// ``` 28 | /// use smol::Timer; 29 | /// use chrono::offset::Local; 30 | /// 31 | /// let (mut scheduler, service) = Scheduler::::launch(Timer::after); 32 | /// 33 | /// // Creates a job which executes every 3 seconds. 34 | /// let job = Job::cron("1/3 * * * * *").unwrap(); 35 | /// let fizz_id = scheduler.insert(job, |id| println!("Fizz")); 36 | /// 37 | /// // Creates a job which executes every 5 seconds. 38 | /// let job = Job::cron("1/5 * * * * *").unwrap(); 39 | /// let buzz_id = scheduler.insert(job, |id| println!("Buzz")); 40 | /// 41 | /// service.await; 42 | /// ``` 43 | pub struct Scheduler { 44 | jobs: SlotMap, 45 | sender: Sender>, 46 | } 47 | 48 | impl Scheduler 49 | where 50 | Tz::Offset: Send + Sync, 51 | { 52 | /// Insert a job into the scheduler with the command to call when scheduled. 53 | /// 54 | /// ``` 55 | /// // Creates a job which executes every 3 seconds. 56 | /// let job = Job::cron("1/3 * * * * *").unwrap(); 57 | /// let fizz_id = scheduler.insert(job, |id| println!("Fizz")); 58 | /// ``` 59 | pub fn insert( 60 | &mut self, 61 | job: Job, 62 | command: impl Fn(JobId) + Send + Sync + 'static, 63 | ) -> JobId { 64 | let id = JobId(self.jobs.insert(())); 65 | let _result = self 66 | .sender 67 | .send(SchedMessage::Insert(id, job, Box::new(command))); 68 | id 69 | } 70 | 71 | /// Remove a scheduled job from the scheduler. 72 | /// 73 | /// ``` 74 | /// scheduler.remove(fizz_id); 75 | /// ``` 76 | pub fn remove(&mut self, job: JobId) { 77 | if self.jobs.remove(job.0).is_some() { 78 | let _res = self.sender.send(SchedMessage::Remove(job)); 79 | } 80 | } 81 | 82 | /// Initializes the scheduler and its connected service. 83 | /// 84 | /// The API is designed to not rely on any async runtimes. This is achieved by 85 | /// returning a future to allow the caller to decide how it should be executed, 86 | /// and taking a function for handling sleeps. You can choose to spawn the 87 | /// returned future, or avoid spawning altgether and await it directly from the 88 | /// same thread. 89 | /// 90 | /// ## Smol runtime 91 | /// 92 | /// ``` 93 | /// let (mut scheduler, sched_service) = Scheduler::::launch(smol::Timer::after); 94 | /// smol::spawn(sched_service).detach(); 95 | /// ``` 96 | /// 97 | /// ## Tokio runtime 98 | /// 99 | /// ``` 100 | /// let (mut scheduler, sched_service) = Scheduler::::launch(tokio::time::sleep); 101 | /// tokio::spawn(sched_service); 102 | /// ``` 103 | pub fn launch(timer: T) -> (Self, impl Future + Send + Sync + 'static) 104 | where 105 | F: Future + Send + Sync, 106 | T: Fn(Duration) -> F + Send + Sync + 'static, 107 | { 108 | let (sender, receiver) = flume::unbounded(); 109 | 110 | let task = async move { 111 | let mut state = SchedulerModel { 112 | tasks: SecondaryMap::new(), 113 | next: None, 114 | }; 115 | 116 | loop { 117 | match state.next.take() { 118 | Some((key, duration)) => { 119 | let message = receiver.recv_async(); 120 | let wait = timer(duration); 121 | 122 | futures::pin_mut!(message); 123 | futures::pin_mut!(wait); 124 | 125 | match futures::future::select(message, wait).await { 126 | Either::Left((message, _)) => match message { 127 | Ok(message) => state.update(message), 128 | Err(RecvError::Disconnected) => break, 129 | }, 130 | 131 | Either::Right((_, _)) => { 132 | state.call(key); 133 | state.next(); 134 | } 135 | } 136 | } 137 | 138 | None => match receiver.recv_async().await { 139 | Ok(message) => state.update(message), 140 | Err(RecvError::Disconnected) => break, 141 | }, 142 | }; 143 | } 144 | }; 145 | 146 | ( 147 | Self { 148 | sender, 149 | jobs: SlotMap::new(), 150 | }, 151 | task, 152 | ) 153 | } 154 | } 155 | struct SchedulerModel { 156 | tasks: SecondaryMap, Command)>, 157 | next: Option<(DefaultKey, Duration)>, 158 | } 159 | 160 | impl SchedulerModel { 161 | pub fn call(&mut self, key: DefaultKey) { 162 | if let Some((job, func)) = self.tasks.get_mut(key) { 163 | func(JobId(key)); 164 | 165 | if let Some(next) = job.iterator.next() { 166 | job.next = next; 167 | 168 | return; 169 | } 170 | 171 | self.tasks.remove(key); 172 | } 173 | } 174 | 175 | pub fn next(&mut self) { 176 | loop { 177 | let mut next: Option<(DefaultKey, DateTime)> = None; 178 | 179 | for (id, (job, _)) in self.tasks.iter() { 180 | if let Some((_, date_time)) = next.as_ref() { 181 | if date_time.timestamp() > job.next.timestamp() { 182 | next = Some((id, job.next.clone())); 183 | } 184 | 185 | continue; 186 | } 187 | 188 | next = Some((id, job.next.clone())); 189 | } 190 | 191 | if let Some((id, date)) = next { 192 | let seconds_until = date.signed_duration_since(Tz::now()).num_seconds(); 193 | if let Ok(seconds_until) = u64::try_from(seconds_until) { 194 | if seconds_until > 0 { 195 | #[cfg(feature = "logging")] 196 | tracing::info!("next job in {} seconds", seconds_until); 197 | 198 | let duration = Duration::from_secs(seconds_until as u64); 199 | self.next = Some((id, duration)); 200 | return; 201 | } 202 | } 203 | 204 | self.call(id); 205 | continue; 206 | } 207 | 208 | return; 209 | } 210 | } 211 | 212 | pub fn update(&mut self, message: SchedMessage) { 213 | match message { 214 | SchedMessage::Insert(id, job, func) => { 215 | self.tasks.insert(id.0, (job, func)); 216 | self.next(); 217 | } 218 | 219 | SchedMessage::Remove(id) => { 220 | self.tasks.remove(id.0); 221 | self.next(); 222 | } 223 | } 224 | } 225 | } 226 | -------------------------------------------------------------------------------- /gtk/src/dialog.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2021-2022 System76 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | use crate::fl; 5 | #[allow(clippy::wildcard_imports)] 6 | use crate::utils::*; 7 | use gtk::prelude::*; 8 | use pop_system_updater::config::{Config, Interval, Schedule}; 9 | use postage::prelude::*; 10 | use std::convert::TryFrom; 11 | use std::rc::Rc; 12 | 13 | pub struct Dialog(pub gtk::Dialog); 14 | 15 | impl Dialog { 16 | #[allow(clippy::too_many_lines)] 17 | pub fn new(widget: >k::Widget, func: impl Fn(Config) + 'static) -> Self { 18 | enum Event { 19 | AutoUpdateChanged, 20 | Exit, 21 | UpdateConfig, 22 | } 23 | 24 | let when_available; 25 | let interval; 26 | let hour; 27 | let minute; 28 | let time_of_day; 29 | let schedule_label; 30 | 31 | let (tx, mut rx) = postage::mpsc::channel(1); 32 | 33 | let content = cascade! { 34 | gtk::ListBox::new(); 35 | ..set_selection_mode(gtk::SelectionMode::None); 36 | ..set_header_func(Some(Box::new(separator_header))); 37 | ..add(&{ 38 | when_available = gtk::Switch::builder() 39 | .valign(gtk::Align::Center) 40 | .build(); 41 | 42 | let label = gtk::Label::builder() 43 | .label(&fl!("update-when-available-label")) 44 | .xalign(0.0) 45 | .hexpand(true) 46 | .vexpand(true) 47 | .mnemonic_widget(&when_available) 48 | .build(); 49 | 50 | cascade! { 51 | option_container(); 52 | ..attach(&label, 0, 0, 1, 1); 53 | ..attach(&when_available, 1, 0, 1, 1); 54 | ..show_all(); 55 | } 56 | }); 57 | ..add(&{ 58 | interval = cascade! { 59 | gtk::ComboBoxText::new(); 60 | ..set_valign(gtk::Align::Center); 61 | ..append_text(&fl!("time-monday")); 62 | ..append_text(&fl!("time-tuesday")); 63 | ..append_text(&fl!("time-wednesday")); 64 | ..append_text(&fl!("time-thursday")); 65 | ..append_text(&fl!("time-friday")); 66 | ..append_text(&fl!("time-saturday")); 67 | ..append_text(&fl!("time-sunday")); 68 | ..append_text(&fl!("time-weekdays")); 69 | }; 70 | 71 | schedule_label = gtk::Label::builder() 72 | .label(&fl!("schedule-label")) 73 | .xalign(0.0) 74 | .hexpand(true) 75 | .vexpand(true) 76 | .mnemonic_widget(&interval) 77 | .build(); 78 | 79 | hour = cascade! { 80 | crate::bsb::BetterSpinButton::new(1, 12, 1, 1, 2); 81 | ..set_valign(gtk::Align::Center); 82 | }; 83 | 84 | minute = cascade! { 85 | crate::bsb::BetterSpinButton::new(0, 59, 1, 1, 2); 86 | ..set_valign(gtk::Align::Center); 87 | }; 88 | 89 | time_of_day = cascade! { 90 | gtk::ComboBoxText::new(); 91 | ..set_valign(gtk::Align::Center); 92 | ..append_text(&fl!("time-am")); 93 | ..append_text(&fl!("time-pm")); 94 | }; 95 | 96 | let times = cascade! { 97 | gtk::Box::new(gtk::Orientation::Horizontal, 4); 98 | ..add(&interval); 99 | ..add(&*hour); 100 | ..add(&*minute); 101 | ..add(&time_of_day); 102 | }; 103 | 104 | cascade! { 105 | option_container(); 106 | ..attach(&schedule_label, 0, 0, 1, 1); 107 | ..attach(×, 1, 0, 1, 1); 108 | ..show_all(); 109 | } 110 | }); 111 | ..connect_destroy({ 112 | let tx = tx.clone(); 113 | move |_| glib_send(tx.clone(), Event::Exit) 114 | }); 115 | }; 116 | 117 | let dialog = gtk::Dialog::builder() 118 | .title(&fl!("schedule-dialog-title")) 119 | .attached_to(widget) 120 | .build(); 121 | 122 | dialog.content_area().add(&{ 123 | cascade! { 124 | option_frame(content.upcast_ref::()); 125 | ..set_margin_start(4); 126 | ..set_margin_end(4); 127 | ..set_margin_top(12); 128 | ..set_halign(gtk::Align::Center); 129 | ..set_hexpand(true); 130 | } 131 | }); 132 | 133 | glib_spawn(async move { 134 | let disable_scheduling = |insensitive: bool| { 135 | hour.set_sensitive(!insensitive); 136 | minute.set_sensitive(!insensitive); 137 | time_of_day.set_sensitive(!insensitive); 138 | interval.set_sensitive(!insensitive); 139 | 140 | let label_ctx = schedule_label.style_context(); 141 | 142 | if insensitive { 143 | label_ctx.add_class("dim-label"); 144 | } else { 145 | label_ctx.remove_class("dim-label"); 146 | } 147 | }; 148 | 149 | let config = pop_system_updater::config::load_system().await; 150 | 151 | let schedule = match config.schedule.as_ref() { 152 | Some(sched) => sched.clone(), 153 | None => Config::default_schedule(), 154 | }; 155 | 156 | interval.set_active(Some(match schedule.interval { 157 | Interval::Monday => 0, 158 | Interval::Tuesday => 1, 159 | Interval::Wednesday => 2, 160 | Interval::Thursday => 3, 161 | Interval::Friday => 4, 162 | Interval::Saturday => 5, 163 | Interval::Sunday => 6, 164 | Interval::Weekdays => 7, 165 | })); 166 | 167 | let (hour_value, am) = match crate::utils::as_12(schedule.hour) { 168 | (hour, false) => (hour, 0), 169 | (hour, true) => (hour, 1), 170 | }; 171 | 172 | time_of_day.set_active(Some(am)); 173 | hour.set_value(u32::from(hour_value)); 174 | minute.set_value(u32::from(schedule.minute)); 175 | 176 | if config.schedule.is_none() { 177 | disable_scheduling(true); 178 | when_available.set_active(true); 179 | } 180 | 181 | // Connect widgets now that state is set. 182 | let tx_ = tx.clone(); 183 | let update_config = Rc::new(Box::new(move || { 184 | glib_send(tx_.clone(), Event::UpdateConfig); 185 | })); 186 | 187 | let tx = tx.clone(); 188 | when_available.connect_changed_active({ 189 | let update_config = update_config.clone(); 190 | move |_| { 191 | glib_send(tx.clone(), Event::AutoUpdateChanged); 192 | 193 | update_config(); 194 | } 195 | }); 196 | 197 | interval.connect_changed({ 198 | let update_config = update_config.clone(); 199 | move |_| update_config() 200 | }); 201 | 202 | time_of_day.connect_changed({ 203 | let update_config = update_config.clone(); 204 | move |_| update_config() 205 | }); 206 | 207 | #[allow(clippy::redundant_closure)] 208 | hour.connect_update({ 209 | let update_config = update_config.clone(); 210 | move || update_config() 211 | }); 212 | 213 | #[allow(clippy::redundant_closure)] 214 | minute.connect_update({ 215 | let update_config = update_config.clone(); 216 | move || update_config() 217 | }); 218 | 219 | while let Some(event) = rx.recv().await { 220 | match event { 221 | Event::AutoUpdateChanged => { 222 | disable_scheduling(when_available.is_active()); 223 | } 224 | 225 | Event::UpdateConfig => { 226 | let pm = time_of_day.active() == Some(1); 227 | 228 | let mut hour = u8::try_from(hour.value()).unwrap_or(0); 229 | 230 | if hour == 12 { 231 | hour = 0; 232 | } 233 | 234 | if pm { 235 | hour += 12; 236 | } 237 | 238 | func(Config { 239 | auto_update: true, 240 | schedule: if when_available.is_active() { 241 | None 242 | } else { 243 | Some(Schedule { 244 | interval: match interval.active() { 245 | Some(0) => Interval::Monday, 246 | Some(1) => Interval::Tuesday, 247 | Some(2) => Interval::Wednesday, 248 | Some(3) => Interval::Thursday, 249 | Some(4) => Interval::Friday, 250 | Some(5) => Interval::Saturday, 251 | Some(6) => Interval::Sunday, 252 | Some(7) => Interval::Weekdays, 253 | _ => { 254 | eprintln!("Unknown interval option selected"); 255 | continue; 256 | } 257 | }, 258 | hour, 259 | minute: u8::try_from(minute.value()).unwrap_or(0), 260 | }) 261 | }, 262 | }); 263 | } 264 | Event::Exit => break, 265 | } 266 | } 267 | }); 268 | 269 | Dialog(dialog) 270 | } 271 | } 272 | -------------------------------------------------------------------------------- /gtk/src/widget.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2021-2022 System76 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | use crate::dialog::Dialog; 5 | use crate::fl; 6 | use crate::localize::localizer; 7 | use crate::proxy; 8 | use crate::utils::{glib_send, glib_spawn, option_container, option_frame, separator_header}; 9 | use gtk::prelude::*; 10 | use i18n_embed::DesktopLanguageRequester; 11 | use pop_system_updater::config::{Config, Frequency, Interval}; 12 | use postage::prelude::*; 13 | use std::cell::Cell; 14 | use std::rc::Rc; 15 | 16 | #[derive(Debug)] 17 | enum Event { 18 | AutomaticUpdatesToggled, 19 | ChangeNotificationSchedule, 20 | DialogComplete(Config), 21 | Exit, 22 | } 23 | 24 | #[allow(clippy::module_name_repetitions)] 25 | pub struct SettingsWidget { 26 | pub inner: gtk::Widget, 27 | } 28 | 29 | impl SettingsWidget { 30 | /// # Panics 31 | /// 32 | /// Panics if the tokio runtime fails to be built 33 | #[allow(clippy::new_without_default)] 34 | #[allow(clippy::too_many_lines)] 35 | #[must_use] 36 | pub fn new() -> Self { 37 | let localizer = localizer(); 38 | let requested_languages = DesktopLanguageRequester::requested_languages(); 39 | 40 | if let Err(error) = localizer.select(&requested_languages) { 41 | eprintln!( 42 | "Error while loading languages for pop-system-updater-gtk {}", 43 | error 44 | ); 45 | } 46 | 47 | let mut ptx = crate::proxy::initialize_service(); 48 | 49 | let (tx, mut rx) = postage::mpsc::channel(1); 50 | 51 | let automatic_updates; 52 | let schedule_label; 53 | let schedule_description; 54 | let notification_label; 55 | let notification_schedule; 56 | 57 | let content = cascade! { 58 | gtk::ListBox::new(); 59 | ..set_selection_mode(gtk::SelectionMode::None); 60 | ..set_header_func(Some(Box::new(separator_header))); 61 | ..add(&{ 62 | automatic_updates = gtk::Switch::builder() 63 | .valign(gtk::Align::Center) 64 | .build(); 65 | 66 | let label = gtk::Label::builder() 67 | .label(&fl!("automatic-updates-label")) 68 | .xalign(0.0) 69 | .hexpand(true) 70 | .vexpand(true) 71 | .mnemonic_widget(&automatic_updates) 72 | .build(); 73 | 74 | cascade! { 75 | option_container(); 76 | ..add(&label); 77 | ..add(&automatic_updates); 78 | ..show_all(); 79 | } 80 | }); 81 | ..add({ 82 | let button_image = gtk::Image::builder() 83 | .icon_name("go-next-symbolic") 84 | .icon_size(gtk::IconSize::Button) 85 | .build(); 86 | 87 | schedule_label = gtk::Label::builder() 88 | .label(&fl!("automatically-install-label")) 89 | .xalign(0.0) 90 | .hexpand(true) 91 | .vexpand(true) 92 | .build(); 93 | 94 | schedule_description = gtk::Label::builder() 95 | .xalign(0.0) 96 | .valign(gtk::Align::Center) 97 | .vexpand(true) 98 | .build(); 99 | 100 | let container = cascade! { 101 | gtk::Box::new(gtk::Orientation::Horizontal, 4); 102 | ..add(&schedule_description); 103 | ..add(&button_image); 104 | }; 105 | 106 | &cascade! { 107 | option_container(); 108 | ..add(&schedule_label); 109 | ..add(&container); 110 | ..show_all(); 111 | } 112 | }); 113 | ..add(&{ 114 | notification_label = gtk::Label::builder() 115 | .label(&fl!("update-notifications-label")) 116 | .xalign(0.0) 117 | .hexpand(true) 118 | .build(); 119 | 120 | notification_schedule = cascade! { 121 | gtk::ComboBoxText::new(); 122 | ..set_valign(gtk::Align::Center); 123 | ..append_text(&fl!("schedule-weekly")); 124 | ..append_text(&fl!("schedule-daily")); 125 | ..append_text(&fl!("schedule-monthly")); 126 | }; 127 | 128 | cascade! { 129 | option_container(); 130 | ..add(¬ification_label); 131 | ..add(¬ification_schedule); 132 | ..show_all(); 133 | } 134 | }); 135 | ..connect_destroy({ 136 | let tx = tx.clone(); 137 | let ptx = ptx.clone(); 138 | move |_| { 139 | let mut tx = tx.clone(); 140 | let mut ptx = ptx.clone(); 141 | glib_spawn(async move { 142 | let f1 = tx.send(Event::Exit); 143 | let f2 = ptx.send(proxy::Event::Exit); 144 | 145 | let _ = futures::join!(f1, f2); 146 | }); 147 | } 148 | }); 149 | }; 150 | 151 | let dialog_active = Rc::new(Cell::new(true)); 152 | 153 | content.connect_row_activated( 154 | glib::clone!(@weak dialog_active, @strong tx, @strong ptx => move |_, row| { 155 | if dialog_active.get() && row.index() == 1 { 156 | let tx = tx.clone(); 157 | let dialog = Dialog::new(row.upcast_ref::(), move |conf| { 158 | glib_send(tx.clone(), Event::DialogComplete(conf)); 159 | }); 160 | 161 | dialog.0.run(); 162 | 163 | unsafe { 164 | dialog.0.destroy(); 165 | } 166 | } 167 | }), 168 | ); 169 | 170 | let widget = Self { 171 | inner: option_frame(content.upcast_ref::()).upcast::(), 172 | }; 173 | 174 | let runtime = tokio::runtime::Builder::new_current_thread() 175 | .enable_all() 176 | .build() 177 | .unwrap(); 178 | 179 | glib_spawn(async move { 180 | let _context = runtime.enter(); 181 | let update_schedule_description = |config: &Config| { 182 | let text: String = if config.auto_update { 183 | if let Some(schedule) = config.schedule.as_ref() { 184 | let (hour, am_pm) = match crate::utils::as_12(schedule.hour) { 185 | (hour, false) => (hour, fl!("time-am")), 186 | (hour, true) => (hour, fl!("time-pm")), 187 | }; 188 | 189 | format!( 190 | "Update on {} at {:02}:{:02} {}", 191 | match schedule.interval { 192 | Interval::Monday => fl!("time-monday"), 193 | Interval::Tuesday => fl!("time-tuesday"), 194 | Interval::Wednesday => fl!("time-wednesday"), 195 | Interval::Thursday => fl!("time-thursday"), 196 | Interval::Friday => fl!("time-friday"), 197 | Interval::Saturday => fl!("time-saturday"), 198 | Interval::Sunday => fl!("time-sunday"), 199 | Interval::Weekdays => fl!("time-weekdays"), 200 | }, 201 | hour, 202 | schedule.minute, 203 | am_pm 204 | ) 205 | } else { 206 | fl!("update-when-available") 207 | } 208 | } else { 209 | fl!("off") 210 | }; 211 | 212 | schedule_description.set_text(&text); 213 | }; 214 | 215 | let change_scheduling_sensitivity = |config: &Config| { 216 | let label_ctx = schedule_label.style_context(); 217 | let description_ctx = schedule_description.style_context(); 218 | let nlabel_ctx = notification_label.style_context(); 219 | 220 | if config.auto_update { 221 | label_ctx.remove_class("dim-label"); 222 | description_ctx.remove_class("dim-label"); 223 | nlabel_ctx.add_class("dim-label"); 224 | notification_schedule.set_sensitive(false); 225 | } else { 226 | label_ctx.add_class("dim-label"); 227 | description_ctx.add_class("dim-label"); 228 | nlabel_ctx.remove_class("dim-label"); 229 | notification_schedule.set_sensitive(true); 230 | } 231 | 232 | dialog_active.set(config.auto_update); 233 | update_schedule_description(config); 234 | }; 235 | 236 | let (mut system_config, session_config) = futures::join!( 237 | pop_system_updater::config::load_system(), 238 | pop_system_updater::config::load_session() 239 | ); 240 | 241 | automatic_updates.set_active(system_config.auto_update); 242 | change_scheduling_sensitivity(&system_config); 243 | 244 | notification_schedule.set_active(Some(session_config.notification_frequency as u32)); 245 | 246 | automatic_updates.connect_changed_active(glib::clone!(@strong tx => move |_| { 247 | glib_send(tx.clone(), Event::AutomaticUpdatesToggled); 248 | })); 249 | 250 | notification_schedule.connect_changed(glib::clone!(@strong tx => move |_| { 251 | glib_send(tx.clone(), Event::ChangeNotificationSchedule); 252 | })); 253 | 254 | while let Some(event) = rx.recv().await { 255 | match event { 256 | Event::ChangeNotificationSchedule => { 257 | let _ = ptx 258 | .send(proxy::Event::SetNotificationFrequency( 259 | match notification_schedule.active() { 260 | Some(0) => Frequency::Weekly, 261 | Some(1) => Frequency::Daily, 262 | _ => Frequency::Monthly, 263 | }, 264 | )) 265 | .await; 266 | } 267 | 268 | Event::DialogComplete(conf) => { 269 | system_config = conf.clone(); 270 | 271 | change_scheduling_sensitivity(&conf); 272 | 273 | let _ = ptx.send(proxy::Event::UpdateConfig(conf)).await; 274 | } 275 | 276 | Event::AutomaticUpdatesToggled => { 277 | system_config.auto_update = automatic_updates.is_active(); 278 | 279 | change_scheduling_sensitivity(&system_config); 280 | 281 | let _ = ptx 282 | .send(proxy::Event::UpdateConfig(system_config.clone())) 283 | .await; 284 | } 285 | 286 | Event::Exit => break, 287 | } 288 | } 289 | }); 290 | 291 | widget 292 | } 293 | } 294 | -------------------------------------------------------------------------------- /daemon/src/service/system.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2021-2022 System76 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | use anyhow::Context; 5 | use async_cron_scheduler::{Job, JobId, Scheduler}; 6 | use chrono::Local; 7 | use config::{Interval, Schedule}; 8 | use flume::Sender; 9 | use futures::StreamExt; 10 | use pop_system_updater::config::{self, Config}; 11 | use pop_system_updater::dbus::PopService; 12 | use pop_system_updater::dbus::{ 13 | server::{self, Server}, 14 | Event, IFACE, 15 | }; 16 | use std::path::Path; 17 | use std::sync::atomic::{AtomicBool, Ordering}; 18 | use std::sync::Arc; 19 | use std::time::Duration; 20 | use tokio::task::JoinHandle; 21 | use zbus::Connection; 22 | 23 | pub struct Service { 24 | updating: Arc, 25 | update_task: Option>, 26 | update_job: Option, 27 | when_available_queue: Option>, 28 | scheduler: Scheduler, 29 | } 30 | 31 | impl Service { 32 | async fn auto_update(&mut self, connection: &zbus::Connection, sender: Sender) { 33 | if self.update_task.is_some() { 34 | info!("already performing an update"); 35 | return; 36 | } 37 | 38 | info!("system update initiated"); 39 | self.updating.store(true, Ordering::SeqCst); 40 | 41 | let connection = connection.clone(); 42 | let updating = self.updating.clone(); 43 | 44 | self.update_task = Some(tokio::task::spawn(async move { 45 | let _ = futures::join!( 46 | crate::package_managers::apt::update(connection.clone()), 47 | crate::package_managers::flatpak::update(connection.clone()), 48 | crate::package_managers::fwupd::update(connection.clone()), 49 | crate::package_managers::nix::update(&connection), 50 | crate::package_managers::snap::update(connection.clone()) 51 | ); 52 | 53 | updating.store(false, Ordering::SeqCst); 54 | let _ = sender.send_async(Event::UpdateComplete).await; 55 | info!("system update complete"); 56 | })); 57 | } 58 | 59 | async fn check_for_updates(&self) { 60 | if self.update_task.is_some() { 61 | info!("already performing an update"); 62 | return; 63 | } 64 | 65 | info!("checking for system updates"); 66 | apt_cmd::lock::apt_lock_wait().await; 67 | crate::package_managers::apt::update_package_lists().await; 68 | info!("check for system updates complete"); 69 | } 70 | 71 | async fn repair(&self, connection: &zbus::Connection) { 72 | if self.update_task.is_some() { 73 | info!("already performing an update"); 74 | return; 75 | } 76 | 77 | info!("performing a system repair"); 78 | 79 | let result = crate::package_managers::apt::repair().await; 80 | 81 | let response = |ctx| async move { 82 | match result { 83 | Ok(()) => Server::repair_ok(&ctx).await, 84 | Err(why) => Server::repair_err(&ctx, &why.to_string()).await, 85 | } 86 | }; 87 | 88 | server::context(connection, response).await; 89 | 90 | info!("system repair attempt complete"); 91 | } 92 | 93 | fn schedule_when_available(&mut self, sender: &Sender) { 94 | if let Some(id) = self.update_job.take() { 95 | self.scheduler.remove(id); 96 | } 97 | 98 | if let Some(task) = self.when_available_queue.take() { 99 | task.abort(); 100 | } 101 | 102 | self.update_job = Some(auto_job(&mut self.scheduler, sender)); 103 | } 104 | 105 | async fn update_notification(&self, connection: &zbus::Connection) { 106 | let response = |ctx| async move { 107 | Server::updates_available(&ctx, crate::package_managers::updates_are_available().await) 108 | .await 109 | }; 110 | 111 | server::context(connection, response).await; 112 | } 113 | 114 | fn update_scheduler(&mut self, config: &Config, sender: &Sender) { 115 | if let Some(id) = self.update_job.take() { 116 | self.scheduler.remove(id); 117 | } 118 | 119 | if let Some(task) = self.when_available_queue.take() { 120 | task.abort(); 121 | } 122 | 123 | if config.auto_update { 124 | if let Some(ref conf) = config.schedule { 125 | self.update_job = Some(schedule_job(&mut self.scheduler, conf, sender)); 126 | } else { 127 | let sender = sender.clone(); 128 | self.when_available_queue = Some(tokio::task::spawn(async move { 129 | info!("scheduling when available in 60 seconds"); 130 | tokio::time::sleep(Duration::from_secs(60)).await; 131 | let _ = sender.send_async(Event::ScheduleWhenAvailable).await; 132 | })); 133 | } 134 | } 135 | } 136 | } 137 | 138 | // Watches for an interrupt signal. 139 | async fn interrupt_handler(sender: Sender) { 140 | let sig_duration = std::time::Duration::from_secs(1); 141 | 142 | loop { 143 | tokio::time::sleep(sig_duration).await; 144 | if crate::signal_handler::status().is_some() { 145 | info!("Found interrupt"); 146 | let _ = sender.send_async(Event::Exit).await; 147 | break; 148 | } 149 | } 150 | } 151 | 152 | // Check for updates every 12 hours. 153 | async fn scheduled_check(sender: Sender) { 154 | let mut interval = tokio::time::interval(std::time::Duration::from_secs(60 * 60 * 12)); 155 | 156 | loop { 157 | interval.tick().await; 158 | let _ = sender.send_async(Event::CheckForUpdates).await; 159 | } 160 | } 161 | 162 | pub async fn run() -> anyhow::Result<()> { 163 | info!("initiating system service"); 164 | crate::signal_handler::init(); 165 | 166 | let (sender, receiver) = flume::bounded(1); 167 | 168 | let updating = Arc::new(AtomicBool::new(false)); 169 | 170 | let connection = Connection::system() 171 | .await 172 | .context("failed to initialize dbus connection")?; 173 | 174 | connection 175 | .object_server() 176 | .at( 177 | IFACE, 178 | Server { 179 | updating: updating.clone(), 180 | service: PopService { 181 | sender: sender.clone(), 182 | }, 183 | }, 184 | ) 185 | .await 186 | .context("failed to serve service")?; 187 | 188 | connection 189 | .request_name("com.system76.SystemUpdater") 190 | .await 191 | .map_err(|why| match why { 192 | zbus::Error::NameTaken => anyhow::anyhow!("system service is already active"), 193 | other => anyhow::anyhow!("could not register system service: {}", other), 194 | })?; 195 | 196 | info!("DBus connection established"); 197 | 198 | let mut config = config::load_system().await; 199 | 200 | let (scheduler, scheduler_service) = Scheduler::::launch(tokio::time::sleep); 201 | 202 | let mut service = Service { 203 | updating, 204 | update_job: None, 205 | update_task: None, 206 | when_available_queue: None, 207 | scheduler, 208 | }; 209 | 210 | service.update_scheduler(&config, &sender); 211 | 212 | futures::join!( 213 | scheduler_service, 214 | interrupt_handler(sender.clone()), 215 | restart_session_services(), 216 | scheduled_check(sender.clone()), 217 | // The event handler, which processes all requests from DBus and the scheduler. 218 | async move { 219 | info!("listening for events"); 220 | while let Ok(event) = receiver.recv_async().await { 221 | info!("received event: {:?}", event); 222 | match event { 223 | Event::CheckForUpdates => { 224 | service.check_for_updates().await; 225 | service.update_notification(&connection).await; 226 | } 227 | 228 | Event::Repair => service.repair(&connection).await, 229 | 230 | Event::ScheduleWhenAvailable => service.schedule_when_available(&sender), 231 | 232 | Event::Update => service.auto_update(&connection, sender.clone()).await, 233 | 234 | Event::UpdateComplete => service.update_task = None, 235 | 236 | Event::SetAutoUpdate(enable) => { 237 | info!("setting auto-update mode to {}", enable); 238 | 239 | config.auto_update = enable; 240 | 241 | service.update_scheduler(&config, &sender); 242 | 243 | let config = config.clone(); 244 | tokio::spawn(async move { 245 | config::write_system(&config).await; 246 | info!("system configuration file updated"); 247 | }); 248 | } 249 | 250 | Event::SetSchedule(schedule) => { 251 | info!("Changing scheduling to {:?}", schedule); 252 | 253 | config.schedule = schedule; 254 | 255 | service.update_scheduler(&config, &sender); 256 | 257 | let config = config.clone(); 258 | tokio::spawn(async move { 259 | config::write_system(&config).await; 260 | info!("system configuration file updated"); 261 | }); 262 | } 263 | 264 | Event::Exit => { 265 | info!("shutting down"); 266 | std::process::exit(0); 267 | } 268 | } 269 | } 270 | 271 | info!("stop listening for events"); 272 | } 273 | ); 274 | 275 | Ok(()) 276 | } 277 | 278 | /// Ensures that session services are always updated and restarted along with this service. 279 | async fn restart_session_services() { 280 | info!("restarting any session services"); 281 | futures::stream::iter(crate::accounts::user_names()) 282 | .for_each_concurrent(None, |user| async { 283 | let accounts_service_file = ["/var/lib/AccountsService/users/", &user].concat(); 284 | if !Path::new(&accounts_service_file).exists() { 285 | return; 286 | } 287 | 288 | let user = user; 289 | 290 | let _res = crate::utils::async_commands(&[&[ 291 | "runuser", 292 | "-u", 293 | &user, 294 | "--", 295 | "systemctl", 296 | "--user", 297 | "restart", 298 | "com.system76.SystemUpdater.Local", 299 | ]]) 300 | .await; 301 | }) 302 | .await; 303 | } 304 | 305 | fn auto_job(scheduler: &mut Scheduler, sender: &Sender) -> JobId { 306 | info!("scheduling every 12 hours"); 307 | tokio::spawn({ 308 | let sender = sender.clone(); 309 | async move { 310 | let _ = sender.send_async(Event::Update).await; 311 | } 312 | }); 313 | 314 | let sender = sender.clone(); 315 | scheduler.insert(Job::cron("0 0 */12 * * *").unwrap(), move |_| { 316 | let sender = sender.clone(); 317 | tokio::spawn(async move { 318 | let _ = sender.send_async(Event::Update).await; 319 | }); 320 | }) 321 | } 322 | 323 | fn schedule_job( 324 | scheduler: &mut Scheduler, 325 | schedule: &Schedule, 326 | sender: &Sender, 327 | ) -> JobId { 328 | info!("scheduling for {:?}", schedule); 329 | let sender = sender.clone(); 330 | scheduler.insert(Job::cron(&*cron_expression(schedule)).unwrap(), move |_| { 331 | let sender = sender.clone(); 332 | tokio::spawn(async move { 333 | let _ = sender.send_async(Event::Update).await; 334 | }); 335 | }) 336 | } 337 | 338 | fn cron_expression(schedule: &Schedule) -> String { 339 | let days: &str = match schedule.interval { 340 | Interval::Sunday => "SUN", 341 | Interval::Monday => "MON", 342 | Interval::Tuesday => "TUE", 343 | Interval::Wednesday => "WED", 344 | Interval::Thursday => "THU", 345 | Interval::Friday => "FRI", 346 | Interval::Saturday => "SAT", 347 | Interval::Weekdays => "1-5", 348 | }; 349 | 350 | let minute = schedule.minute.min(59); 351 | let hour = schedule.hour.min(23); 352 | 353 | let expression = format!("0 {minute} {hour} * * {days}"); 354 | info!("setting cron expression {}", expression); 355 | expression 356 | } 357 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Mozilla Public License Version 2.0 2 | ================================== 3 | 4 | 1. Definitions 5 | -------------- 6 | 7 | 1.1. "Contributor" 8 | means each individual or legal entity that creates, contributes to 9 | the creation of, or owns Covered Software. 10 | 11 | 1.2. "Contributor Version" 12 | means the combination of the Contributions of others (if any) used 13 | by a Contributor and that particular Contributor's Contribution. 14 | 15 | 1.3. "Contribution" 16 | means Covered Software of a particular Contributor. 17 | 18 | 1.4. "Covered Software" 19 | means Source Code Form to which the initial Contributor has attached 20 | the notice in Exhibit A, the Executable Form of such Source Code 21 | Form, and Modifications of such Source Code Form, in each case 22 | including portions thereof. 23 | 24 | 1.5. "Incompatible With Secondary Licenses" 25 | means 26 | 27 | (a) that the initial Contributor has attached the notice described 28 | in Exhibit B to the Covered Software; or 29 | 30 | (b) that the Covered Software was made available under the terms of 31 | version 1.1 or earlier of the License, but not also under the 32 | terms of a Secondary License. 33 | 34 | 1.6. "Executable Form" 35 | means any form of the work other than Source Code Form. 36 | 37 | 1.7. "Larger Work" 38 | means a work that combines Covered Software with other material, in 39 | a separate file or files, that is not Covered Software. 40 | 41 | 1.8. "License" 42 | means this document. 43 | 44 | 1.9. "Licensable" 45 | means having the right to grant, to the maximum extent possible, 46 | whether at the time of the initial grant or subsequently, any and 47 | all of the rights conveyed by this License. 48 | 49 | 1.10. "Modifications" 50 | means any of the following: 51 | 52 | (a) any file in Source Code Form that results from an addition to, 53 | deletion from, or modification of the contents of Covered 54 | Software; or 55 | 56 | (b) any new file in Source Code Form that contains any Covered 57 | Software. 58 | 59 | 1.11. "Patent Claims" of a Contributor 60 | means any patent claim(s), including without limitation, method, 61 | process, and apparatus claims, in any patent Licensable by such 62 | Contributor that would be infringed, but for the grant of the 63 | License, by the making, using, selling, offering for sale, having 64 | made, import, or transfer of either its Contributions or its 65 | Contributor Version. 66 | 67 | 1.12. "Secondary License" 68 | means either the GNU General Public License, Version 2.0, the GNU 69 | Lesser General Public License, Version 2.1, the GNU Affero General 70 | Public License, Version 3.0, or any later versions of those 71 | licenses. 72 | 73 | 1.13. "Source Code Form" 74 | means the form of the work preferred for making modifications. 75 | 76 | 1.14. "You" (or "Your") 77 | means an individual or a legal entity exercising rights under this 78 | License. For legal entities, "You" includes any entity that 79 | controls, is controlled by, or is under common control with You. For 80 | purposes of this definition, "control" means (a) the power, direct 81 | or indirect, to cause the direction or management of such entity, 82 | whether by contract or otherwise, or (b) ownership of more than 83 | fifty percent (50%) of the outstanding shares or beneficial 84 | ownership of such entity. 85 | 86 | 2. License Grants and Conditions 87 | -------------------------------- 88 | 89 | 2.1. Grants 90 | 91 | Each Contributor hereby grants You a world-wide, royalty-free, 92 | non-exclusive license: 93 | 94 | (a) under intellectual property rights (other than patent or trademark) 95 | Licensable by such Contributor to use, reproduce, make available, 96 | modify, display, perform, distribute, and otherwise exploit its 97 | Contributions, either on an unmodified basis, with Modifications, or 98 | as part of a Larger Work; and 99 | 100 | (b) under Patent Claims of such Contributor to make, use, sell, offer 101 | for sale, have made, import, and otherwise transfer either its 102 | Contributions or its Contributor Version. 103 | 104 | 2.2. Effective Date 105 | 106 | The licenses granted in Section 2.1 with respect to any Contribution 107 | become effective for each Contribution on the date the Contributor first 108 | distributes such Contribution. 109 | 110 | 2.3. Limitations on Grant Scope 111 | 112 | The licenses granted in this Section 2 are the only rights granted under 113 | this License. No additional rights or licenses will be implied from the 114 | distribution or licensing of Covered Software under this License. 115 | Notwithstanding Section 2.1(b) above, no patent license is granted by a 116 | Contributor: 117 | 118 | (a) for any code that a Contributor has removed from Covered Software; 119 | or 120 | 121 | (b) for infringements caused by: (i) Your and any other third party's 122 | modifications of Covered Software, or (ii) the combination of its 123 | Contributions with other software (except as part of its Contributor 124 | Version); or 125 | 126 | (c) under Patent Claims infringed by Covered Software in the absence of 127 | its Contributions. 128 | 129 | This License does not grant any rights in the trademarks, service marks, 130 | or logos of any Contributor (except as may be necessary to comply with 131 | the notice requirements in Section 3.4). 132 | 133 | 2.4. Subsequent Licenses 134 | 135 | No Contributor makes additional grants as a result of Your choice to 136 | distribute the Covered Software under a subsequent version of this 137 | License (see Section 10.2) or under the terms of a Secondary License (if 138 | permitted under the terms of Section 3.3). 139 | 140 | 2.5. Representation 141 | 142 | Each Contributor represents that the Contributor believes its 143 | Contributions are its original creation(s) or it has sufficient rights 144 | to grant the rights to its Contributions conveyed by this License. 145 | 146 | 2.6. Fair Use 147 | 148 | This License is not intended to limit any rights You have under 149 | applicable copyright doctrines of fair use, fair dealing, or other 150 | equivalents. 151 | 152 | 2.7. Conditions 153 | 154 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted 155 | in Section 2.1. 156 | 157 | 3. Responsibilities 158 | ------------------- 159 | 160 | 3.1. Distribution of Source Form 161 | 162 | All distribution of Covered Software in Source Code Form, including any 163 | Modifications that You create or to which You contribute, must be under 164 | the terms of this License. You must inform recipients that the Source 165 | Code Form of the Covered Software is governed by the terms of this 166 | License, and how they can obtain a copy of this License. You may not 167 | attempt to alter or restrict the recipients' rights in the Source Code 168 | Form. 169 | 170 | 3.2. Distribution of Executable Form 171 | 172 | If You distribute Covered Software in Executable Form then: 173 | 174 | (a) such Covered Software must also be made available in Source Code 175 | Form, as described in Section 3.1, and You must inform recipients of 176 | the Executable Form how they can obtain a copy of such Source Code 177 | Form by reasonable means in a timely manner, at a charge no more 178 | than the cost of distribution to the recipient; and 179 | 180 | (b) You may distribute such Executable Form under the terms of this 181 | License, or sublicense it under different terms, provided that the 182 | license for the Executable Form does not attempt to limit or alter 183 | the recipients' rights in the Source Code Form under this License. 184 | 185 | 3.3. Distribution of a Larger Work 186 | 187 | You may create and distribute a Larger Work under terms of Your choice, 188 | provided that You also comply with the requirements of this License for 189 | the Covered Software. If the Larger Work is a combination of Covered 190 | Software with a work governed by one or more Secondary Licenses, and the 191 | Covered Software is not Incompatible With Secondary Licenses, this 192 | License permits You to additionally distribute such Covered Software 193 | under the terms of such Secondary License(s), so that the recipient of 194 | the Larger Work may, at their option, further distribute the Covered 195 | Software under the terms of either this License or such Secondary 196 | License(s). 197 | 198 | 3.4. Notices 199 | 200 | You may not remove or alter the substance of any license notices 201 | (including copyright notices, patent notices, disclaimers of warranty, 202 | or limitations of liability) contained within the Source Code Form of 203 | the Covered Software, except that You may alter any license notices to 204 | the extent required to remedy known factual inaccuracies. 205 | 206 | 3.5. Application of Additional Terms 207 | 208 | You may choose to offer, and to charge a fee for, warranty, support, 209 | indemnity or liability obligations to one or more recipients of Covered 210 | Software. However, You may do so only on Your own behalf, and not on 211 | behalf of any Contributor. You must make it absolutely clear that any 212 | such warranty, support, indemnity, or liability obligation is offered by 213 | You alone, and You hereby agree to indemnify every Contributor for any 214 | liability incurred by such Contributor as a result of warranty, support, 215 | indemnity or liability terms You offer. You may include additional 216 | disclaimers of warranty and limitations of liability specific to any 217 | jurisdiction. 218 | 219 | 4. Inability to Comply Due to Statute or Regulation 220 | --------------------------------------------------- 221 | 222 | If it is impossible for You to comply with any of the terms of this 223 | License with respect to some or all of the Covered Software due to 224 | statute, judicial order, or regulation then You must: (a) comply with 225 | the terms of this License to the maximum extent possible; and (b) 226 | describe the limitations and the code they affect. Such description must 227 | be placed in a text file included with all distributions of the Covered 228 | Software under this License. Except to the extent prohibited by statute 229 | or regulation, such description must be sufficiently detailed for a 230 | recipient of ordinary skill to be able to understand it. 231 | 232 | 5. Termination 233 | -------------- 234 | 235 | 5.1. The rights granted under this License will terminate automatically 236 | if You fail to comply with any of its terms. However, if You become 237 | compliant, then the rights granted under this License from a particular 238 | Contributor are reinstated (a) provisionally, unless and until such 239 | Contributor explicitly and finally terminates Your grants, and (b) on an 240 | ongoing basis, if such Contributor fails to notify You of the 241 | non-compliance by some reasonable means prior to 60 days after You have 242 | come back into compliance. Moreover, Your grants from a particular 243 | Contributor are reinstated on an ongoing basis if such Contributor 244 | notifies You of the non-compliance by some reasonable means, this is the 245 | first time You have received notice of non-compliance with this License 246 | from such Contributor, and You become compliant prior to 30 days after 247 | Your receipt of the notice. 248 | 249 | 5.2. If You initiate litigation against any entity by asserting a patent 250 | infringement claim (excluding declaratory judgment actions, 251 | counter-claims, and cross-claims) alleging that a Contributor Version 252 | directly or indirectly infringes any patent, then the rights granted to 253 | You by any and all Contributors for the Covered Software under Section 254 | 2.1 of this License shall terminate. 255 | 256 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all 257 | end user license agreements (excluding distributors and resellers) which 258 | have been validly granted by You or Your distributors under this License 259 | prior to termination shall survive termination. 260 | 261 | ************************************************************************ 262 | * * 263 | * 6. Disclaimer of Warranty * 264 | * ------------------------- * 265 | * * 266 | * Covered Software is provided under this License on an "as is" * 267 | * basis, without warranty of any kind, either expressed, implied, or * 268 | * statutory, including, without limitation, warranties that the * 269 | * Covered Software is free of defects, merchantable, fit for a * 270 | * particular purpose or non-infringing. The entire risk as to the * 271 | * quality and performance of the Covered Software is with You. * 272 | * Should any Covered Software prove defective in any respect, You * 273 | * (not any Contributor) assume the cost of any necessary servicing, * 274 | * repair, or correction. This disclaimer of warranty constitutes an * 275 | * essential part of this License. No use of any Covered Software is * 276 | * authorized under this License except under this disclaimer. * 277 | * * 278 | ************************************************************************ 279 | 280 | ************************************************************************ 281 | * * 282 | * 7. Limitation of Liability * 283 | * -------------------------- * 284 | * * 285 | * Under no circumstances and under no legal theory, whether tort * 286 | * (including negligence), contract, or otherwise, shall any * 287 | * Contributor, or anyone who distributes Covered Software as * 288 | * permitted above, be liable to You for any direct, indirect, * 289 | * special, incidental, or consequential damages of any character * 290 | * including, without limitation, damages for lost profits, loss of * 291 | * goodwill, work stoppage, computer failure or malfunction, or any * 292 | * and all other commercial damages or losses, even if such party * 293 | * shall have been informed of the possibility of such damages. This * 294 | * limitation of liability shall not apply to liability for death or * 295 | * personal injury resulting from such party's negligence to the * 296 | * extent applicable law prohibits such limitation. Some * 297 | * jurisdictions do not allow the exclusion or limitation of * 298 | * incidental or consequential damages, so this exclusion and * 299 | * limitation may not apply to You. * 300 | * * 301 | ************************************************************************ 302 | 303 | 8. Litigation 304 | ------------- 305 | 306 | Any litigation relating to this License may be brought only in the 307 | courts of a jurisdiction where the defendant maintains its principal 308 | place of business and such litigation shall be governed by laws of that 309 | jurisdiction, without reference to its conflict-of-law provisions. 310 | Nothing in this Section shall prevent a party's ability to bring 311 | cross-claims or counter-claims. 312 | 313 | 9. Miscellaneous 314 | ---------------- 315 | 316 | This License represents the complete agreement concerning the subject 317 | matter hereof. If any provision of this License is held to be 318 | unenforceable, such provision shall be reformed only to the extent 319 | necessary to make it enforceable. Any law or regulation which provides 320 | that the language of a contract shall be construed against the drafter 321 | shall not be used to construe this License against a Contributor. 322 | 323 | 10. Versions of the License 324 | --------------------------- 325 | 326 | 10.1. New Versions 327 | 328 | Mozilla Foundation is the license steward. Except as provided in Section 329 | 10.3, no one other than the license steward has the right to modify or 330 | publish new versions of this License. Each version will be given a 331 | distinguishing version number. 332 | 333 | 10.2. Effect of New Versions 334 | 335 | You may distribute the Covered Software under the terms of the version 336 | of the License under which You originally received the Covered Software, 337 | or under the terms of any subsequent version published by the license 338 | steward. 339 | 340 | 10.3. Modified Versions 341 | 342 | If you create software not governed by this License, and you want to 343 | create a new license for such software, you may create and use a 344 | modified version of this License if you rename the license and remove 345 | any references to the name of the license steward (except to note that 346 | such modified license differs from this License). 347 | 348 | 10.4. Distributing Source Code Form that is Incompatible With Secondary 349 | Licenses 350 | 351 | If You choose to distribute Source Code Form that is Incompatible With 352 | Secondary Licenses under the terms of this version of the License, the 353 | notice described in Exhibit B of this License must be attached. 354 | 355 | Exhibit A - Source Code Form License Notice 356 | ------------------------------------------- 357 | 358 | This Source Code Form is subject to the terms of the Mozilla Public 359 | License, v. 2.0. If a copy of the MPL was not distributed with this 360 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 361 | 362 | If it is not possible or desirable to put the notice in a particular 363 | file, then You may include the notice in a location (such as a LICENSE 364 | file in a relevant directory) where a recipient would be likely to look 365 | for such a notice. 366 | 367 | You may add additional accurate notices of copyright ownership. 368 | 369 | Exhibit B - "Incompatible With Secondary Licenses" Notice 370 | --------------------------------------------------------- 371 | 372 | This Source Code Form is "Incompatible With Secondary Licenses", as 373 | defined by the Mozilla Public License, v. 2.0. -------------------------------------------------------------------------------- /scheduler/LICENSE: -------------------------------------------------------------------------------- 1 | Mozilla Public License Version 2.0 2 | ================================== 3 | 4 | 1. Definitions 5 | -------------- 6 | 7 | 1.1. "Contributor" 8 | means each individual or legal entity that creates, contributes to 9 | the creation of, or owns Covered Software. 10 | 11 | 1.2. "Contributor Version" 12 | means the combination of the Contributions of others (if any) used 13 | by a Contributor and that particular Contributor's Contribution. 14 | 15 | 1.3. "Contribution" 16 | means Covered Software of a particular Contributor. 17 | 18 | 1.4. "Covered Software" 19 | means Source Code Form to which the initial Contributor has attached 20 | the notice in Exhibit A, the Executable Form of such Source Code 21 | Form, and Modifications of such Source Code Form, in each case 22 | including portions thereof. 23 | 24 | 1.5. "Incompatible With Secondary Licenses" 25 | means 26 | 27 | (a) that the initial Contributor has attached the notice described 28 | in Exhibit B to the Covered Software; or 29 | 30 | (b) that the Covered Software was made available under the terms of 31 | version 1.1 or earlier of the License, but not also under the 32 | terms of a Secondary License. 33 | 34 | 1.6. "Executable Form" 35 | means any form of the work other than Source Code Form. 36 | 37 | 1.7. "Larger Work" 38 | means a work that combines Covered Software with other material, in 39 | a separate file or files, that is not Covered Software. 40 | 41 | 1.8. "License" 42 | means this document. 43 | 44 | 1.9. "Licensable" 45 | means having the right to grant, to the maximum extent possible, 46 | whether at the time of the initial grant or subsequently, any and 47 | all of the rights conveyed by this License. 48 | 49 | 1.10. "Modifications" 50 | means any of the following: 51 | 52 | (a) any file in Source Code Form that results from an addition to, 53 | deletion from, or modification of the contents of Covered 54 | Software; or 55 | 56 | (b) any new file in Source Code Form that contains any Covered 57 | Software. 58 | 59 | 1.11. "Patent Claims" of a Contributor 60 | means any patent claim(s), including without limitation, method, 61 | process, and apparatus claims, in any patent Licensable by such 62 | Contributor that would be infringed, but for the grant of the 63 | License, by the making, using, selling, offering for sale, having 64 | made, import, or transfer of either its Contributions or its 65 | Contributor Version. 66 | 67 | 1.12. "Secondary License" 68 | means either the GNU General Public License, Version 2.0, the GNU 69 | Lesser General Public License, Version 2.1, the GNU Affero General 70 | Public License, Version 3.0, or any later versions of those 71 | licenses. 72 | 73 | 1.13. "Source Code Form" 74 | means the form of the work preferred for making modifications. 75 | 76 | 1.14. "You" (or "Your") 77 | means an individual or a legal entity exercising rights under this 78 | License. For legal entities, "You" includes any entity that 79 | controls, is controlled by, or is under common control with You. For 80 | purposes of this definition, "control" means (a) the power, direct 81 | or indirect, to cause the direction or management of such entity, 82 | whether by contract or otherwise, or (b) ownership of more than 83 | fifty percent (50%) of the outstanding shares or beneficial 84 | ownership of such entity. 85 | 86 | 2. License Grants and Conditions 87 | -------------------------------- 88 | 89 | 2.1. Grants 90 | 91 | Each Contributor hereby grants You a world-wide, royalty-free, 92 | non-exclusive license: 93 | 94 | (a) under intellectual property rights (other than patent or trademark) 95 | Licensable by such Contributor to use, reproduce, make available, 96 | modify, display, perform, distribute, and otherwise exploit its 97 | Contributions, either on an unmodified basis, with Modifications, or 98 | as part of a Larger Work; and 99 | 100 | (b) under Patent Claims of such Contributor to make, use, sell, offer 101 | for sale, have made, import, and otherwise transfer either its 102 | Contributions or its Contributor Version. 103 | 104 | 2.2. Effective Date 105 | 106 | The licenses granted in Section 2.1 with respect to any Contribution 107 | become effective for each Contribution on the date the Contributor first 108 | distributes such Contribution. 109 | 110 | 2.3. Limitations on Grant Scope 111 | 112 | The licenses granted in this Section 2 are the only rights granted under 113 | this License. No additional rights or licenses will be implied from the 114 | distribution or licensing of Covered Software under this License. 115 | Notwithstanding Section 2.1(b) above, no patent license is granted by a 116 | Contributor: 117 | 118 | (a) for any code that a Contributor has removed from Covered Software; 119 | or 120 | 121 | (b) for infringements caused by: (i) Your and any other third party's 122 | modifications of Covered Software, or (ii) the combination of its 123 | Contributions with other software (except as part of its Contributor 124 | Version); or 125 | 126 | (c) under Patent Claims infringed by Covered Software in the absence of 127 | its Contributions. 128 | 129 | This License does not grant any rights in the trademarks, service marks, 130 | or logos of any Contributor (except as may be necessary to comply with 131 | the notice requirements in Section 3.4). 132 | 133 | 2.4. Subsequent Licenses 134 | 135 | No Contributor makes additional grants as a result of Your choice to 136 | distribute the Covered Software under a subsequent version of this 137 | License (see Section 10.2) or under the terms of a Secondary License (if 138 | permitted under the terms of Section 3.3). 139 | 140 | 2.5. Representation 141 | 142 | Each Contributor represents that the Contributor believes its 143 | Contributions are its original creation(s) or it has sufficient rights 144 | to grant the rights to its Contributions conveyed by this License. 145 | 146 | 2.6. Fair Use 147 | 148 | This License is not intended to limit any rights You have under 149 | applicable copyright doctrines of fair use, fair dealing, or other 150 | equivalents. 151 | 152 | 2.7. Conditions 153 | 154 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted 155 | in Section 2.1. 156 | 157 | 3. Responsibilities 158 | ------------------- 159 | 160 | 3.1. Distribution of Source Form 161 | 162 | All distribution of Covered Software in Source Code Form, including any 163 | Modifications that You create or to which You contribute, must be under 164 | the terms of this License. You must inform recipients that the Source 165 | Code Form of the Covered Software is governed by the terms of this 166 | License, and how they can obtain a copy of this License. You may not 167 | attempt to alter or restrict the recipients' rights in the Source Code 168 | Form. 169 | 170 | 3.2. Distribution of Executable Form 171 | 172 | If You distribute Covered Software in Executable Form then: 173 | 174 | (a) such Covered Software must also be made available in Source Code 175 | Form, as described in Section 3.1, and You must inform recipients of 176 | the Executable Form how they can obtain a copy of such Source Code 177 | Form by reasonable means in a timely manner, at a charge no more 178 | than the cost of distribution to the recipient; and 179 | 180 | (b) You may distribute such Executable Form under the terms of this 181 | License, or sublicense it under different terms, provided that the 182 | license for the Executable Form does not attempt to limit or alter 183 | the recipients' rights in the Source Code Form under this License. 184 | 185 | 3.3. Distribution of a Larger Work 186 | 187 | You may create and distribute a Larger Work under terms of Your choice, 188 | provided that You also comply with the requirements of this License for 189 | the Covered Software. If the Larger Work is a combination of Covered 190 | Software with a work governed by one or more Secondary Licenses, and the 191 | Covered Software is not Incompatible With Secondary Licenses, this 192 | License permits You to additionally distribute such Covered Software 193 | under the terms of such Secondary License(s), so that the recipient of 194 | the Larger Work may, at their option, further distribute the Covered 195 | Software under the terms of either this License or such Secondary 196 | License(s). 197 | 198 | 3.4. Notices 199 | 200 | You may not remove or alter the substance of any license notices 201 | (including copyright notices, patent notices, disclaimers of warranty, 202 | or limitations of liability) contained within the Source Code Form of 203 | the Covered Software, except that You may alter any license notices to 204 | the extent required to remedy known factual inaccuracies. 205 | 206 | 3.5. Application of Additional Terms 207 | 208 | You may choose to offer, and to charge a fee for, warranty, support, 209 | indemnity or liability obligations to one or more recipients of Covered 210 | Software. However, You may do so only on Your own behalf, and not on 211 | behalf of any Contributor. You must make it absolutely clear that any 212 | such warranty, support, indemnity, or liability obligation is offered by 213 | You alone, and You hereby agree to indemnify every Contributor for any 214 | liability incurred by such Contributor as a result of warranty, support, 215 | indemnity or liability terms You offer. You may include additional 216 | disclaimers of warranty and limitations of liability specific to any 217 | jurisdiction. 218 | 219 | 4. Inability to Comply Due to Statute or Regulation 220 | --------------------------------------------------- 221 | 222 | If it is impossible for You to comply with any of the terms of this 223 | License with respect to some or all of the Covered Software due to 224 | statute, judicial order, or regulation then You must: (a) comply with 225 | the terms of this License to the maximum extent possible; and (b) 226 | describe the limitations and the code they affect. Such description must 227 | be placed in a text file included with all distributions of the Covered 228 | Software under this License. Except to the extent prohibited by statute 229 | or regulation, such description must be sufficiently detailed for a 230 | recipient of ordinary skill to be able to understand it. 231 | 232 | 5. Termination 233 | -------------- 234 | 235 | 5.1. The rights granted under this License will terminate automatically 236 | if You fail to comply with any of its terms. However, if You become 237 | compliant, then the rights granted under this License from a particular 238 | Contributor are reinstated (a) provisionally, unless and until such 239 | Contributor explicitly and finally terminates Your grants, and (b) on an 240 | ongoing basis, if such Contributor fails to notify You of the 241 | non-compliance by some reasonable means prior to 60 days after You have 242 | come back into compliance. Moreover, Your grants from a particular 243 | Contributor are reinstated on an ongoing basis if such Contributor 244 | notifies You of the non-compliance by some reasonable means, this is the 245 | first time You have received notice of non-compliance with this License 246 | from such Contributor, and You become compliant prior to 30 days after 247 | Your receipt of the notice. 248 | 249 | 5.2. If You initiate litigation against any entity by asserting a patent 250 | infringement claim (excluding declaratory judgment actions, 251 | counter-claims, and cross-claims) alleging that a Contributor Version 252 | directly or indirectly infringes any patent, then the rights granted to 253 | You by any and all Contributors for the Covered Software under Section 254 | 2.1 of this License shall terminate. 255 | 256 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all 257 | end user license agreements (excluding distributors and resellers) which 258 | have been validly granted by You or Your distributors under this License 259 | prior to termination shall survive termination. 260 | 261 | ************************************************************************ 262 | * * 263 | * 6. Disclaimer of Warranty * 264 | * ------------------------- * 265 | * * 266 | * Covered Software is provided under this License on an "as is" * 267 | * basis, without warranty of any kind, either expressed, implied, or * 268 | * statutory, including, without limitation, warranties that the * 269 | * Covered Software is free of defects, merchantable, fit for a * 270 | * particular purpose or non-infringing. The entire risk as to the * 271 | * quality and performance of the Covered Software is with You. * 272 | * Should any Covered Software prove defective in any respect, You * 273 | * (not any Contributor) assume the cost of any necessary servicing, * 274 | * repair, or correction. This disclaimer of warranty constitutes an * 275 | * essential part of this License. No use of any Covered Software is * 276 | * authorized under this License except under this disclaimer. * 277 | * * 278 | ************************************************************************ 279 | 280 | ************************************************************************ 281 | * * 282 | * 7. Limitation of Liability * 283 | * -------------------------- * 284 | * * 285 | * Under no circumstances and under no legal theory, whether tort * 286 | * (including negligence), contract, or otherwise, shall any * 287 | * Contributor, or anyone who distributes Covered Software as * 288 | * permitted above, be liable to You for any direct, indirect, * 289 | * special, incidental, or consequential damages of any character * 290 | * including, without limitation, damages for lost profits, loss of * 291 | * goodwill, work stoppage, computer failure or malfunction, or any * 292 | * and all other commercial damages or losses, even if such party * 293 | * shall have been informed of the possibility of such damages. This * 294 | * limitation of liability shall not apply to liability for death or * 295 | * personal injury resulting from such party's negligence to the * 296 | * extent applicable law prohibits such limitation. Some * 297 | * jurisdictions do not allow the exclusion or limitation of * 298 | * incidental or consequential damages, so this exclusion and * 299 | * limitation may not apply to You. * 300 | * * 301 | ************************************************************************ 302 | 303 | 8. Litigation 304 | ------------- 305 | 306 | Any litigation relating to this License may be brought only in the 307 | courts of a jurisdiction where the defendant maintains its principal 308 | place of business and such litigation shall be governed by laws of that 309 | jurisdiction, without reference to its conflict-of-law provisions. 310 | Nothing in this Section shall prevent a party's ability to bring 311 | cross-claims or counter-claims. 312 | 313 | 9. Miscellaneous 314 | ---------------- 315 | 316 | This License represents the complete agreement concerning the subject 317 | matter hereof. If any provision of this License is held to be 318 | unenforceable, such provision shall be reformed only to the extent 319 | necessary to make it enforceable. Any law or regulation which provides 320 | that the language of a contract shall be construed against the drafter 321 | shall not be used to construe this License against a Contributor. 322 | 323 | 10. Versions of the License 324 | --------------------------- 325 | 326 | 10.1. New Versions 327 | 328 | Mozilla Foundation is the license steward. Except as provided in Section 329 | 10.3, no one other than the license steward has the right to modify or 330 | publish new versions of this License. Each version will be given a 331 | distinguishing version number. 332 | 333 | 10.2. Effect of New Versions 334 | 335 | You may distribute the Covered Software under the terms of the version 336 | of the License under which You originally received the Covered Software, 337 | or under the terms of any subsequent version published by the license 338 | steward. 339 | 340 | 10.3. Modified Versions 341 | 342 | If you create software not governed by this License, and you want to 343 | create a new license for such software, you may create and use a 344 | modified version of this License if you rename the license and remove 345 | any references to the name of the license steward (except to note that 346 | such modified license differs from this License). 347 | 348 | 10.4. Distributing Source Code Form that is Incompatible With Secondary 349 | Licenses 350 | 351 | If You choose to distribute Source Code Form that is Incompatible With 352 | Secondary Licenses under the terms of this version of the License, the 353 | notice described in Exhibit B of this License must be attached. 354 | 355 | Exhibit A - Source Code Form License Notice 356 | ------------------------------------------- 357 | 358 | This Source Code Form is subject to the terms of the Mozilla Public 359 | License, v. 2.0. If a copy of the MPL was not distributed with this 360 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 361 | 362 | If it is not possible or desirable to put the notice in a particular 363 | file, then You may include the notice in a location (such as a LICENSE 364 | file in a relevant directory) where a recipient would be likely to look 365 | for such a notice. 366 | 367 | You may add additional accurate notices of copyright ownership. 368 | 369 | Exhibit B - "Incompatible With Secondary Licenses" Notice 370 | --------------------------------------------------------- 371 | 372 | This Source Code Form is "Incompatible With Secondary Licenses", as 373 | defined by the Mozilla Public License, v. 2.0. --------------------------------------------------------------------------------