diff --git a/cmds/updatemgr/scan.go b/cmds/updatemgr/scan.go index 9ef29f15..e5458ee6 100644 --- a/cmds/updatemgr/scan.go +++ b/cmds/updatemgr/scan.go @@ -4,13 +4,14 @@ import ( "encoding/json" "fmt" + "github.com/safing/portmaster/service/configure" "github.com/safing/portmaster/service/updates" "github.com/spf13/cobra" ) var ( scanConfig = updates.IndexScanConfig{ - Name: "Portmaster Binaries", + Name: configure.DefaultBinaryIndexName, PrimaryArtifact: "linux_amd64/portmaster-core", BaseURL: "https://updates.safing.io/", IgnoreFiles: []string{ diff --git a/desktop/tauri/src-tauri/src/traymenu.rs b/desktop/tauri/src-tauri/src/traymenu.rs index 777b9d23..197bdabc 100644 --- a/desktop/tauri/src-tauri/src/traymenu.rs +++ b/desktop/tauri/src-tauri/src/traymenu.rs @@ -4,15 +4,12 @@ use std::sync::RwLock; use std::{collections::HashMap, sync::atomic::Ordering}; use log::{debug, error}; -use tauri::menu::{Menu, MenuItemKind}; -use tauri::tray::{MouseButton, MouseButtonState}; use tauri::{ image::Image, - menu::{MenuBuilder, MenuItemBuilder, PredefinedMenuItem, SubmenuBuilder}, - tray::{TrayIcon, TrayIconBuilder}, - Wry, + menu::{Menu, MenuBuilder, MenuItemBuilder, PredefinedMenuItem, SubmenuBuilder}, + tray::{MouseButton, MouseButtonState, TrayIcon, TrayIconBuilder}, + Manager, Wry, }; -use tauri::{Manager, Runtime}; use tauri_plugin_window_state::{AppHandleExt, StateFlags}; use crate::config; @@ -33,6 +30,7 @@ use crate::{ use tauri_plugin_dialog::{DialogExt, MessageDialogButtons}; pub type AppIcon = TrayIcon; +pub type ContextMenu = Menu; static SPN_STATE: AtomicBool = AtomicBool::new(false); @@ -46,12 +44,20 @@ enum IconColor { static CURRENT_ICON_COLOR: RwLock = RwLock::new(IconColor::Red); pub static USER_THEME: RwLock = RwLock::new(dark_light::Mode::Default); - -static SPN_STATUS_KEY: &str = "spn_status"; -static SPN_BUTTON_KEY: &str = "spn_toggle"; -static GLOBAL_STATUS_KEY: &str = "global_status"; +const OPEN_KEY: &str = "open"; +const EXIT_UI_KEY: &str = "exit_ui"; +const SPN_STATUS_KEY: &str = "spn_status"; +const SPN_BUTTON_KEY: &str = "spn_toggle"; +const GLOBAL_STATUS_KEY: &str = "global_status"; +const SHUTDOWN_KEY: &str = "shutdown"; +const SYSTEM_THEME_KEY: &str = "system_theme"; +const LIGHT_THEME_KEY: &str = "light_theme"; +const DARK_THEME_KEY: &str = "dark_theme"; +const RELOAD_KEY: &str = "reload"; +const FORCE_SHOW_KEY: &str = "force-show"; const PM_TRAY_ICON_ID: &str = "pm_icon"; +const PM_TRAY_MENU_ID: &str = "pm_tray_menu"; // Icons @@ -115,51 +121,57 @@ fn get_icon(icon: IconColor) -> &'static [u8] { } } -pub fn setup_tray_menu( - app: &mut tauri::App, -) -> core::result::Result> { - // Tray menu - load_theme(app.handle()); - let open_btn = MenuItemBuilder::with_id("open", "Open App").build(app)?; - let exit_ui_btn = MenuItemBuilder::with_id("exit_ui", "Exit UI").build(app)?; - let shutdown_btn = MenuItemBuilder::with_id("shutdown", "Shut Down Portmaster").build(app)?; +fn build_tray_menu( + app: &tauri::AppHandle, + status: &str, + spn_status_text: &str, +) -> core::result::Result> { + load_theme(app); - let global_status = MenuItemBuilder::with_id("global_status", "Status: Secured") + let open_btn = MenuItemBuilder::with_id(OPEN_KEY, "Open App").build(app)?; + let exit_ui_btn = MenuItemBuilder::with_id(EXIT_UI_KEY, "Exit UI").build(app)?; + let shutdown_btn = MenuItemBuilder::with_id(SHUTDOWN_KEY, "Shut Down Portmaster").build(app)?; + + let global_status = MenuItemBuilder::with_id(GLOBAL_STATUS_KEY, format!("Status: {}", status)) .enabled(false) .build(app) .unwrap(); // Setup SPN status - let spn_status = MenuItemBuilder::with_id(SPN_STATUS_KEY, "SPN: Disabled") + let spn_status = MenuItemBuilder::with_id(SPN_STATUS_KEY, format!("SPN: {}", spn_status_text)) .enabled(false) .build(app) .unwrap(); // Setup SPN button - let spn_button = MenuItemBuilder::with_id(SPN_BUTTON_KEY, "Enable SPN") + let spn_button_text = match spn_status_text { + "disabled" => "Enable SPN", + _ => "Disable SPN", + }; + let spn_button = MenuItemBuilder::with_id(SPN_BUTTON_KEY, spn_button_text) .build(app) .unwrap(); - let system_theme = MenuItemBuilder::with_id("system_theme", "System") + let system_theme = MenuItemBuilder::with_id(SYSTEM_THEME_KEY, "System") .build(app) .unwrap(); - let light_theme = MenuItemBuilder::with_id("light_theme", "Light") + let light_theme = MenuItemBuilder::with_id(LIGHT_THEME_KEY, "Light") .build(app) .unwrap(); - let dark_theme = MenuItemBuilder::with_id("dark_theme", "Dark") + let dark_theme = MenuItemBuilder::with_id(DARK_THEME_KEY, "Dark") .build(app) .unwrap(); let theme_menu = SubmenuBuilder::new(app, "Icon Theme") .items(&[&system_theme, &light_theme, &dark_theme]) .build()?; - let force_show_window = MenuItemBuilder::with_id("force-show", "Force Show UI").build(app)?; - let reload_btn = MenuItemBuilder::with_id("reload", "Reload User Interface").build(app)?; + let force_show_window = MenuItemBuilder::with_id(FORCE_SHOW_KEY, "Force Show UI").build(app)?; + let reload_btn = MenuItemBuilder::with_id(RELOAD_KEY, "Reload User Interface").build(app)?; let developer_menu = SubmenuBuilder::new(app, "Developer") .items(&[&reload_btn, &force_show_window]) .build()?; - let menu = MenuBuilder::new(app) + let menu = MenuBuilder::with_id(app, PM_TRAY_MENU_ID) .items(&[ &open_btn, &PredefinedMenuItem::separator(app)?, @@ -176,11 +188,19 @@ pub fn setup_tray_menu( ]) .build()?; + return Ok(menu); +} + +pub fn setup_tray_menu( + app: &mut tauri::App, +) -> core::result::Result> { + let menu = build_tray_menu(app.handle(), "Secured", "disabled")?; + let icon = TrayIconBuilder::with_id(PM_TRAY_ICON_ID) .icon(Image::from_bytes(get_red_icon()).unwrap()) .menu(&menu) .on_menu_event(move |app, event| match event.id().as_ref() { - "exit_ui" => { + EXIT_UI_KEY => { let handle = app.clone(); app.dialog() .message("This does not stop the Portmaster system service") @@ -196,15 +216,15 @@ pub fn setup_tray_menu( } }); } - "open" => { + OPEN_KEY => { let _ = open_window(app); } - "reload" => { + RELOAD_KEY => { if let Ok(mut win) = open_window(app) { may_navigate_to_ui(&mut win, true); } } - "force-show" => { + FORCE_SHOW_KEY => { match create_main_window(app) { Ok(mut win) => { may_navigate_to_ui(&mut win, true); @@ -217,19 +237,19 @@ pub fn setup_tray_menu( } }; } - "spn_toggle" => { + SPN_BUTTON_KEY => { if SPN_STATE.load(Ordering::Acquire) { app.portmaster().set_spn_enabled(false); } else { app.portmaster().set_spn_enabled(true); } } - "shutdown" => { + SHUTDOWN_KEY => { app.portmaster().trigger_shutdown(); } - "system_theme" => update_icon_theme(app, dark_light::Mode::Default), - "dark_theme" => update_icon_theme(app, dark_light::Mode::Dark), - "light_theme" => update_icon_theme(app, dark_light::Mode::Light), + SYSTEM_THEME_KEY => update_icon_theme(app, dark_light::Mode::Default), + DARK_THEME_KEY => update_icon_theme(app, dark_light::Mode::Dark), + LIGHT_THEME_KEY => update_icon_theme(app, dark_light::Mode::Light), other => { error!("unknown menu event id: {}", other); } @@ -251,15 +271,11 @@ pub fn setup_tray_menu( } }) .build(app)?; + Ok(icon) } -pub fn update_icon( - icon: AppIcon, - menu: Option>, - subsystems: HashMap, - spn_status: String, -) { +pub fn update_icon(icon: AppIcon, subsystems: HashMap, spn_status: String) { // iterate over the subsystems and check if there's a module failure let failure = subsystems.values().map(|s| &s.module_status).fold( (subsystem::FAILURE_NONE, "".to_string()), @@ -273,14 +289,10 @@ pub fn update_icon( }, ); - if let Some(menu) = menu { - if let Some(MenuItemKind::MenuItem(global_status)) = menu.get(GLOBAL_STATUS_KEY) { - if failure.0 == subsystem::FAILURE_NONE { - _ = global_status.set_text("Status: Secured"); - } else { - _ = global_status.set_text(format!("Status: {}", failure.1)); - } - } + let mut status = "Secured".to_owned(); + + if failure.0 != subsystem::FAILURE_NONE { + status = failure.1; } let icon_color = match failure.0 { @@ -291,6 +303,13 @@ pub fn update_icon( _ => IconColor::Green, }, }; + + if let Ok(menu) = build_tray_menu(icon.app_handle(), status.as_ref(), spn_status.as_str()) { + if let Err(err) = icon.set_menu(Some(menu)) { + error!("failed to set menu on tray icon: {}", err.to_string()); + } + } + update_icon_color(&icon, icon_color); } @@ -391,8 +410,7 @@ pub async fn tray_handler(cli: PortAPI, app: tauri::AppHandle) { match payload.parse::() { Ok(n) => { subsystems.insert(n.id.clone(), n); - - update_icon(icon.clone(), app.menu(), subsystems.clone(), spn_status.clone()); + update_icon(icon.clone(), subsystems.clone(), spn_status.clone()); }, Err(err) => match err { ParseError::Json(err) => { @@ -423,8 +441,7 @@ pub async fn tray_handler(cli: PortAPI, app: tauri::AppHandle) { Ok(value) => { debug!("SPN status update: {}", value.status); spn_status.clone_from(&value.status); - - update_icon(icon.clone(), app.menu(), subsystems.clone(), spn_status.clone()); + update_icon(icon.clone(), subsystems.clone(), spn_status.clone()); }, Err(err) => match err { ParseError::Json(err) => { @@ -453,9 +470,7 @@ pub async fn tray_handler(cli: PortAPI, app: tauri::AppHandle) { if let Some((_, payload)) = res { match payload.parse::() { Ok(value) => { - if let Some(menu) = app.menu() { - update_spn_ui_state(menu, value.value.unwrap_or(false)); - } + SPN_STATE.store(value.value.unwrap_or(false), Ordering::Release); }, Err(err) => match err { ParseError::Json(err) => { @@ -487,9 +502,6 @@ pub async fn tray_handler(cli: PortAPI, app: tauri::AppHandle) { } } } - if let Some(menu) = app.menu() { - update_spn_ui_state(menu, false); - } update_icon_color(&icon, IconColor::Red); } @@ -554,22 +566,4 @@ fn save_theme(app: &tauri::AppHandle, mode: dark_light::Mode) { } Err(err) => error!("failed to load config file: {}", err), } - if let Some(menu) = app.menu() { - update_spn_ui_state(menu, false); - } -} - -fn update_spn_ui_state(menu: Menu, enabled: bool) { - if let (Some(MenuItemKind::MenuItem(spn_status)), Some(MenuItemKind::MenuItem(spn_btn))) = - (menu.get(SPN_STATUS_KEY), menu.get(SPN_BUTTON_KEY)) - { - if enabled { - _ = spn_status.set_text("SPN: Connected"); - _ = spn_btn.set_text("Disable SPN"); - } else { - _ = spn_status.set_text("SPN: Disabled"); - _ = spn_btn.set_text("Enable SPN"); - } - SPN_STATE.store(enabled, Ordering::Release); - } } diff --git a/desktop/tauri/src-tauri/templates/nsis/install_hooks.nsh b/desktop/tauri/src-tauri/templates/nsis/install_hooks.nsh index fb8fccda..97958043 100644 --- a/desktop/tauri/src-tauri/templates/nsis/install_hooks.nsh +++ b/desktop/tauri/src-tauri/templates/nsis/install_hooks.nsh @@ -178,6 +178,9 @@ var dataDir RMDir /r /REBOOTOK "$COMMONPROGRAMDATA\Portmaster\exec" RMDir /r /REBOOTOK "$COMMONPROGRAMDATA\Portmaster\logs" + ; Remove PMv1 migration flag + Delete /REBOOTOK "$COMMONPROGRAMDATA\Safing\Portmaster\migrated.txt" + ${If} $DeleteAppDataCheckboxState = 1 DetailPrint "Deleting the application data..." RMDir /r /REBOOTOK "$COMMONPROGRAMDATA\Portmaster" diff --git a/packaging/linux/postinst b/packaging/linux/postinst index c99f8b4c..a54ee8e5 100644 --- a/packaging/linux/postinst +++ b/packaging/linux/postinst @@ -28,6 +28,9 @@ if [ -d "$OLD_INSTALLATION_DIR" ]; then echo "[ ] V1 migration: Removing V1 shortcuts" rm /etc/xdg/autostart/portmaster_notifier.desktop rm /usr/share/applications/portmaster_notifier.desktop + # app V1 shortcut + # NOTE: new V2 shortcut registered as "Portmaster.desktop" (first letter uppercase), so we can distinguish between V1 and V2 shortcuts. + rm /usr/share/applications/portmaster.desktop # Remove V1 files (except configuration) # (keeping V1 configuration for a smooth downgrade, if needed) diff --git a/packaging/windows/generate_windows_installers.ps1 b/packaging/windows/generate_windows_installers.ps1 index 5e638296..31c5ed40 100644 --- a/packaging/windows/generate_windows_installers.ps1 +++ b/packaging/windows/generate_windows_installers.ps1 @@ -50,15 +50,19 @@ #------------------------------------------------------------------------------ # # Optional arguments: -# -i, --interactive: Can prompt for user input (e.g. when a file is not found in the primary folder but found in the alternate folder) -# -v, --version: Explicitly set the version to use for the installer file name +# -i: (interactive) Can prompt for user input (e.g. when a file is not found in the primary folder but found in the alternate folder) +# -v: (version) Explicitly set the version to use for the installer file name +# -e: (erase) Just erase work directories #------------------------------------------------------------------------------ param ( [Alias('i')] [switch]$interactive, [Alias('v')] - [string]$version + [string]$version, + + [Alias('e')] + [switch]$erase ) # Save the current directory @@ -185,7 +189,18 @@ try { $destinationDir = "desktop/tauri/src-tauri" $binaryDir = "$destinationDir/binary" #portmaster\desktop\tauri\src-tauri\binary $intelDir = "$destinationDir/intel" #portmaster\desktop\tauri\src-tauri\intel - $targetDir = "$destinationDir/target/release" #portmaster\desktop\tauri\src-tauri\target\release + $targetBase= "$destinationDir/target" #portmaster\desktop\tauri\src-tauri\target + $targetDir = "$targetBase/release" #portmaster\desktop\tauri\src-tauri\target\release + + # Erasing work directories + Write-Output "[+] Erasing work directories: '$binaryDir', '$intelDir', '$targetBase'" + Remove-Item -Recurse -Force -Path $binaryDir -ErrorAction SilentlyContinue + Remove-Item -Recurse -Force -Path $intelDir -ErrorAction SilentlyContinue + Remove-Item -Recurse -Force -Path $targetBase -ErrorAction SilentlyContinue + if ($erase) { + Write-Output "[ ] Done" + exit 0 + } # Copying BINARY FILES Write-Output "`n[+] Copying binary files:" diff --git a/service/config.go b/service/config.go index 39ad2d03..bef4110e 100644 --- a/service/config.go +++ b/service/config.go @@ -115,7 +115,7 @@ func MakeUpdateConfigs(svcCfg *ServiceConfig) (binaryUpdateConfig, intelUpdateCo switch runtime.GOOS { case "windows": binaryUpdateConfig = &updates.Config{ - Name: "binaries", + Name: configure.DefaultBinaryIndexName, Directory: svcCfg.BinDir, DownloadDirectory: filepath.Join(svcCfg.DataDir, "download_binaries"), PurgeDirectory: filepath.Join(svcCfg.BinDir, "upgrade_obsolete_binaries"), @@ -130,7 +130,7 @@ func MakeUpdateConfigs(svcCfg *ServiceConfig) (binaryUpdateConfig, intelUpdateCo Notify: true, } intelUpdateConfig = &updates.Config{ - Name: "intel", + Name: configure.DefaultIntelIndexName, Directory: filepath.Join(svcCfg.DataDir, "intel"), DownloadDirectory: filepath.Join(svcCfg.DataDir, "download_intel"), PurgeDirectory: filepath.Join(svcCfg.DataDir, "upgrade_obsolete_intel"), @@ -146,7 +146,7 @@ func MakeUpdateConfigs(svcCfg *ServiceConfig) (binaryUpdateConfig, intelUpdateCo case "linux": binaryUpdateConfig = &updates.Config{ - Name: "binaries", + Name: configure.DefaultBinaryIndexName, Directory: svcCfg.BinDir, DownloadDirectory: filepath.Join(svcCfg.DataDir, "download_binaries"), PurgeDirectory: filepath.Join(svcCfg.DataDir, "upgrade_obsolete_binaries"), @@ -161,7 +161,7 @@ func MakeUpdateConfigs(svcCfg *ServiceConfig) (binaryUpdateConfig, intelUpdateCo Notify: true, } intelUpdateConfig = &updates.Config{ - Name: "intel", + Name: configure.DefaultIntelIndexName, Directory: filepath.Join(svcCfg.DataDir, "intel"), DownloadDirectory: filepath.Join(svcCfg.DataDir, "download_intel"), PurgeDirectory: filepath.Join(svcCfg.DataDir, "upgrade_obsolete_intel"), diff --git a/service/configure/updates.go b/service/configure/updates.go index 3fec4afc..0625ff26 100644 --- a/service/configure/updates.go +++ b/service/configure/updates.go @@ -5,6 +5,9 @@ import ( ) var ( + DefaultBinaryIndexName = "Portmaster Binaries" + DefaultIntelIndexName = "intel" + DefaultStableBinaryIndexURLs = []string{ "https://updates.safing.io/stable.v3.json", } diff --git a/service/updates/index.go b/service/updates/index.go index d98d92d7..13285f9d 100644 --- a/service/updates/index.go +++ b/service/updates/index.go @@ -118,6 +118,14 @@ type Index struct { Artifacts []*Artifact `json:"Artifacts"` versionNum *semver.Version + + // isLocallyGenerated indicates whether the index was generated from a local directory. + // + // When true: + // - The `Published` field represents the generation time, not a formal release date. + // This timestamp should be ignored when checking for online updates. + // - Downgrades from this locally generated version to an online index should be prevented. + isLocallyGenerated bool } // LoadIndex loads and parses an index from the given filename. @@ -235,6 +243,15 @@ func (index *Index) ShouldUpgradeTo(newIndex *Index) error { case index.Name != newIndex.Name: return errors.New("new index name does not match") + case index.isLocallyGenerated: + if newIndex.versionNum.GreaterThan(index.versionNum) { + // Upgrade! (from a locally generated index to an online index) + return nil + } else { + // "Do nothing". + return ErrSameIndex + } + case index.Published.After(newIndex.Published): return errors.New("new index is older (time)") diff --git a/service/updates/index_scan.go b/service/updates/index_scan.go index 1dc1a7f6..3ac52f5f 100644 --- a/service/updates/index_scan.go +++ b/service/updates/index_scan.go @@ -234,10 +234,11 @@ func GenerateIndexFromDir(sourceDir string, cfg IndexScanConfig) (*Index, error) // Create base index. index := &Index{ - Name: cfg.Name, - Version: cfg.Version, - Published: time.Now(), - versionNum: indexVersion, + Name: cfg.Name, + Version: cfg.Version, + Published: time.Now(), + versionNum: indexVersion, + isLocallyGenerated: true, } if index.Version == "" && cfg.PrimaryArtifact != "" { pv, ok := artifacts[cfg.PrimaryArtifact] diff --git a/service/updates/module.go b/service/updates/module.go index b57830b2..8a441ac9 100644 --- a/service/updates/module.go +++ b/service/updates/module.go @@ -18,6 +18,7 @@ import ( "github.com/safing/portmaster/base/log" "github.com/safing/portmaster/base/notifications" "github.com/safing/portmaster/base/utils" + "github.com/safing/portmaster/service/configure" "github.com/safing/portmaster/service/mgr" ) @@ -201,6 +202,7 @@ func New(instance instance, name string, cfg Config) (*Updater, error) { module.corruptedInstallation = fmt.Errorf("invalid index: %w", err) } index, err = GenerateIndexFromDir(cfg.Directory, IndexScanConfig{ + Name: configure.DefaultBinaryIndexName, Version: info.VersionNumber(), }) if err == nil && index.init(currentPlatform) == nil { diff --git a/service/updates/upgrade.go b/service/updates/upgrade.go index 9d18de98..250baede 100644 --- a/service/updates/upgrade.go +++ b/service/updates/upgrade.go @@ -73,6 +73,10 @@ func (u *Updater) upgradeMoveFiles(downloader *Downloader) error { if slices.Contains(u.cfg.Ignore, file.Name()) { continue } + // ignore PurgeDirectory itself + if strings.EqualFold(u.cfg.PurgeDirectory, filepath.Join(u.cfg.Directory, file.Name())) { + continue + } // Otherwise, move file to purge dir. src := filepath.Join(u.cfg.Directory, file.Name())