Merge branch 'v2.0' into feature/ui-security
This commit is contained in:
6
.github/workflows/release.yml
vendored
6
.github/workflows/release.yml
vendored
@@ -1,4 +1,4 @@
|
|||||||
name: Release
|
name: Release v2.X
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
@@ -36,6 +36,8 @@ jobs:
|
|||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
|
|
||||||
installer-linux:
|
installer-linux:
|
||||||
|
#JOB DISABLED FOR NOW
|
||||||
|
if: false
|
||||||
name: Installer linux
|
name: Installer linux
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs: release-prep
|
needs: release-prep
|
||||||
@@ -63,6 +65,8 @@ jobs:
|
|||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
|
|
||||||
installer-windows:
|
installer-windows:
|
||||||
|
#JOB DISABLED FOR NOW
|
||||||
|
if: false
|
||||||
name: Installer windows
|
name: Installer windows
|
||||||
runs-on: windows-latest
|
runs-on: windows-latest
|
||||||
needs: release-prep
|
needs: release-prep
|
||||||
|
|||||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -60,3 +60,7 @@ windows_core_dll/portmaster-core/x64/
|
|||||||
|
|
||||||
#Tauri-generated files
|
#Tauri-generated files
|
||||||
desktop/tauri/src-tauri/gen/
|
desktop/tauri/src-tauri/gen/
|
||||||
|
|
||||||
|
#Binaries used for installer gereneration for Windows
|
||||||
|
desktop/tauri/src-tauri/binary/
|
||||||
|
desktop/tauri/src-tauri/intel/
|
||||||
|
|||||||
@@ -603,6 +603,10 @@ installer-linux:
|
|||||||
SAVE ARTIFACT --if-exists --keep-ts "target/${target}/release/bundle/deb/*.deb" AS LOCAL "${outputDir}/${GO_ARCH_STRING}/"
|
SAVE ARTIFACT --if-exists --keep-ts "target/${target}/release/bundle/deb/*.deb" AS LOCAL "${outputDir}/${GO_ARCH_STRING}/"
|
||||||
SAVE ARTIFACT --if-exists --keep-ts "target/${target}/release/bundle/rpm/*.rpm" AS LOCAL "${outputDir}/${GO_ARCH_STRING}/"
|
SAVE ARTIFACT --if-exists --keep-ts "target/${target}/release/bundle/rpm/*.rpm" AS LOCAL "${outputDir}/${GO_ARCH_STRING}/"
|
||||||
|
|
||||||
|
all-artifacts:
|
||||||
|
BUILD +release-prep
|
||||||
|
BUILD +installer-linux
|
||||||
|
|
||||||
kext-build:
|
kext-build:
|
||||||
FROM ${rust_builder_image}
|
FROM ${rust_builder_image}
|
||||||
|
|
||||||
|
|||||||
74
base/utils/call_limiter2.go
Normal file
74
base/utils/call_limiter2.go
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CallLimiter2 bundles concurrent calls and optionally limits how fast a function is called.
|
||||||
|
type CallLimiter2 struct {
|
||||||
|
pause time.Duration
|
||||||
|
|
||||||
|
slot atomic.Int64
|
||||||
|
slotWait sync.RWMutex
|
||||||
|
|
||||||
|
executing atomic.Bool
|
||||||
|
lastExec time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewCallLimiter2 returns a new call limiter.
|
||||||
|
// Set minPause to zero to disable the minimum pause between calls.
|
||||||
|
func NewCallLimiter2(minPause time.Duration) *CallLimiter2 {
|
||||||
|
return &CallLimiter2{
|
||||||
|
pause: minPause,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Do executes the given function.
|
||||||
|
// All concurrent calls to Do are bundled and return when f() finishes.
|
||||||
|
// Waits until the minimum pause is over before executing f() again.
|
||||||
|
func (l *CallLimiter2) Do(f func()) {
|
||||||
|
// Get ticket number.
|
||||||
|
slot := l.slot.Load()
|
||||||
|
|
||||||
|
// Check if we can execute.
|
||||||
|
if l.executing.CompareAndSwap(false, true) {
|
||||||
|
// Make others wait.
|
||||||
|
l.slotWait.Lock()
|
||||||
|
defer l.slotWait.Unlock()
|
||||||
|
|
||||||
|
// Execute and return.
|
||||||
|
l.waitAndExec(f)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for slot to end and check if slot is done.
|
||||||
|
for l.slot.Load() == slot {
|
||||||
|
time.Sleep(100 * time.Microsecond)
|
||||||
|
l.slotWait.RLock()
|
||||||
|
l.slotWait.RUnlock() //nolint:staticcheck
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *CallLimiter2) waitAndExec(f func()) {
|
||||||
|
defer func() {
|
||||||
|
// Update last exec time.
|
||||||
|
l.lastExec = time.Now().UTC()
|
||||||
|
// Enable next execution first.
|
||||||
|
l.executing.Store(false)
|
||||||
|
// Move to next slot aftewards to prevent wait loops.
|
||||||
|
l.slot.Add(1)
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Wait for the minimum duration between executions.
|
||||||
|
if l.pause > 0 {
|
||||||
|
sinceLastExec := time.Since(l.lastExec)
|
||||||
|
if sinceLastExec < l.pause {
|
||||||
|
time.Sleep(l.pause - sinceLastExec)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute.
|
||||||
|
f()
|
||||||
|
}
|
||||||
@@ -13,7 +13,7 @@ func TestCallLimiter(t *testing.T) {
|
|||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
pause := 10 * time.Millisecond
|
pause := 10 * time.Millisecond
|
||||||
oa := NewCallLimiter(pause)
|
oa := NewCallLimiter2(pause)
|
||||||
executed := abool.New()
|
executed := abool.New()
|
||||||
var testWg sync.WaitGroup
|
var testWg sync.WaitGroup
|
||||||
|
|
||||||
@@ -41,14 +41,14 @@ func TestCallLimiter(t *testing.T) {
|
|||||||
executed.UnSet() // reset check
|
executed.UnSet() // reset check
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wait for pause to reset.
|
// Wait for 2x pause to reset.
|
||||||
time.Sleep(pause)
|
time.Sleep(2 * pause)
|
||||||
|
|
||||||
// Continuous use with re-execution.
|
// Continuous use with re-execution.
|
||||||
// Choose values so that about 10 executions are expected
|
// Choose values so that about 10 executions are expected
|
||||||
var execs uint32
|
var execs uint32
|
||||||
testWg.Add(200)
|
testWg.Add(100)
|
||||||
for range 200 {
|
for range 100 {
|
||||||
go func() {
|
go func() {
|
||||||
oa.Do(func() {
|
oa.Do(func() {
|
||||||
atomic.AddUint32(&execs, 1)
|
atomic.AddUint32(&execs, 1)
|
||||||
@@ -69,8 +69,8 @@ func TestCallLimiter(t *testing.T) {
|
|||||||
t.Errorf("unexpected high exec count: %d", execs)
|
t.Errorf("unexpected high exec count: %d", execs)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wait for pause to reset.
|
// Wait for 2x pause to reset.
|
||||||
time.Sleep(pause)
|
time.Sleep(2 * pause)
|
||||||
|
|
||||||
// Check if the limiter correctly handles panics.
|
// Check if the limiter correctly handles panics.
|
||||||
testWg.Add(100)
|
testWg.Add(100)
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
"github.com/safing/portmaster/base/log"
|
"github.com/safing/portmaster/base/log"
|
||||||
"github.com/safing/portmaster/base/notifications"
|
"github.com/safing/portmaster/base/notifications"
|
||||||
"github.com/safing/portmaster/service"
|
"github.com/safing/portmaster/service"
|
||||||
|
"github.com/safing/portmaster/service/ui"
|
||||||
"github.com/safing/portmaster/service/updates"
|
"github.com/safing/portmaster/service/updates"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -71,3 +72,4 @@ type updateDummyInstance struct{}
|
|||||||
func (udi *updateDummyInstance) Restart() {}
|
func (udi *updateDummyInstance) Restart() {}
|
||||||
func (udi *updateDummyInstance) Shutdown() {}
|
func (udi *updateDummyInstance) Shutdown() {}
|
||||||
func (udi *updateDummyInstance) Notifications() *notifications.Notifications { return nil }
|
func (udi *updateDummyInstance) Notifications() *notifications.Notifications { return nil }
|
||||||
|
func (udi *updateDummyInstance) UI() *ui.UI { return nil }
|
||||||
|
|||||||
@@ -4,13 +4,14 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/safing/portmaster/service/configure"
|
||||||
"github.com/safing/portmaster/service/updates"
|
"github.com/safing/portmaster/service/updates"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
scanConfig = updates.IndexScanConfig{
|
scanConfig = updates.IndexScanConfig{
|
||||||
Name: "Portmaster Binaries",
|
Name: configure.DefaultBinaryIndexName,
|
||||||
PrimaryArtifact: "linux_amd64/portmaster-core",
|
PrimaryArtifact: "linux_amd64/portmaster-core",
|
||||||
BaseURL: "https://updates.safing.io/",
|
BaseURL: "https://updates.safing.io/",
|
||||||
IgnoreFiles: []string{
|
IgnoreFiles: []string{
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "portmaster",
|
"name": "portmaster",
|
||||||
"version": "2.0.1",
|
"version": "2.0.2",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"ng": "ng",
|
"ng": "ng",
|
||||||
"start": "npm install && npm run build-libs:dev && ng serve --proxy-config ./proxy.json",
|
"start": "npm install && npm run build-libs:dev && ng serve --proxy-config ./proxy.json",
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
/// Struct representing an RGB color
|
/// Struct representing an RGB color
|
||||||
|
#[allow(dead_code)] // Suppress warnings for unused fields in this struct only
|
||||||
pub(crate) struct Rgb(pub(crate) u32, pub(crate) u32, pub(crate) u32);
|
pub(crate) struct Rgb(pub(crate) u32, pub(crate) u32, pub(crate) u32);
|
||||||
|
|
||||||
impl FromStr for Rgb {
|
impl FromStr for Rgb {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "portmaster"
|
name = "portmaster"
|
||||||
version = "2.0.0"
|
version = "2.0.1"
|
||||||
description = "Portmaster UI"
|
description = "Portmaster UI"
|
||||||
authors = ["Safing"]
|
authors = ["Safing"]
|
||||||
license = ""
|
license = ""
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
|
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
|
||||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||||
|
|
||||||
use std::{env, path::Path, time::Duration};
|
use std::{env, time::Duration};
|
||||||
|
|
||||||
use tauri::{AppHandle, Emitter, Listener, Manager, RunEvent, WindowEvent};
|
use tauri::{AppHandle, Emitter, Listener, Manager, RunEvent, WindowEvent};
|
||||||
|
|
||||||
@@ -25,6 +25,7 @@ use portmaster::PortmasterExt;
|
|||||||
use tauri_plugin_log::RotationStrategy;
|
use tauri_plugin_log::RotationStrategy;
|
||||||
use traymenu::setup_tray_menu;
|
use traymenu::setup_tray_menu;
|
||||||
use window::{close_splash_window, create_main_window, hide_splash_window};
|
use window::{close_splash_window, create_main_window, hide_splash_window};
|
||||||
|
use tauri_plugin_window_state::StateFlags;
|
||||||
|
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
extern crate lazy_static;
|
extern crate lazy_static;
|
||||||
@@ -140,7 +141,7 @@ fn main() {
|
|||||||
|
|
||||||
// TODO(vladimir): Permission for logs/app2 folder are not guaranteed. Use the default location for now.
|
// TODO(vladimir): Permission for logs/app2 folder are not guaranteed. Use the default location for now.
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
let log_target = if let Some(data_dir) = cli_args.data {
|
let log_target = if let Some(_) = cli_args.data {
|
||||||
tauri_plugin_log::Target::new(tauri_plugin_log::TargetKind::LogDir { file_name: None })
|
tauri_plugin_log::Target::new(tauri_plugin_log::TargetKind::LogDir { file_name: None })
|
||||||
} else {
|
} else {
|
||||||
tauri_plugin_log::Target::new(tauri_plugin_log::TargetKind::Stdout)
|
tauri_plugin_log::Target::new(tauri_plugin_log::TargetKind::Stdout)
|
||||||
@@ -174,7 +175,12 @@ fn main() {
|
|||||||
// OS Version and Architecture support
|
// OS Version and Architecture support
|
||||||
.plugin(tauri_plugin_os::init())
|
.plugin(tauri_plugin_os::init())
|
||||||
// Initialize save windows state plugin.
|
// Initialize save windows state plugin.
|
||||||
.plugin(tauri_plugin_window_state::Builder::default().build())
|
.plugin(tauri_plugin_window_state::Builder::default()
|
||||||
|
// Don't save visibility state, so it will not interfere with "--background" command line argument
|
||||||
|
.with_state_flags(StateFlags::all() & !StateFlags::VISIBLE)
|
||||||
|
// Don't save splash window state
|
||||||
|
.with_denylist(&["splash",])
|
||||||
|
.build())
|
||||||
// Single instance guard
|
// Single instance guard
|
||||||
.plugin(tauri_plugin_single_instance::init(|app, argv, cwd| {
|
.plugin(tauri_plugin_single_instance::init(|app, argv, cwd| {
|
||||||
// Send info to already dunning instance.
|
// Send info to already dunning instance.
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ use crate::portapi::client::*;
|
|||||||
use crate::portapi::message::*;
|
use crate::portapi::message::*;
|
||||||
use crate::portapi::models::notification::*;
|
use crate::portapi::models::notification::*;
|
||||||
use crate::portapi::types::*;
|
use crate::portapi::types::*;
|
||||||
use log::debug;
|
|
||||||
use log::error;
|
use log::error;
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
use tauri::async_runtime;
|
use tauri::async_runtime;
|
||||||
|
|||||||
@@ -4,15 +4,12 @@ use std::sync::RwLock;
|
|||||||
use std::{collections::HashMap, sync::atomic::Ordering};
|
use std::{collections::HashMap, sync::atomic::Ordering};
|
||||||
|
|
||||||
use log::{debug, error};
|
use log::{debug, error};
|
||||||
use tauri::menu::{Menu, MenuItemKind};
|
|
||||||
use tauri::tray::{MouseButton, MouseButtonState};
|
|
||||||
use tauri::{
|
use tauri::{
|
||||||
image::Image,
|
image::Image,
|
||||||
menu::{MenuBuilder, MenuItemBuilder, PredefinedMenuItem, SubmenuBuilder},
|
menu::{Menu, MenuBuilder, MenuItemBuilder, PredefinedMenuItem, SubmenuBuilder},
|
||||||
tray::{TrayIcon, TrayIconBuilder},
|
tray::{MouseButton, MouseButtonState, TrayIcon, TrayIconBuilder},
|
||||||
Wry,
|
Manager, Wry,
|
||||||
};
|
};
|
||||||
use tauri::{Manager, Runtime};
|
|
||||||
use tauri_plugin_window_state::{AppHandleExt, StateFlags};
|
use tauri_plugin_window_state::{AppHandleExt, StateFlags};
|
||||||
|
|
||||||
use crate::config;
|
use crate::config;
|
||||||
@@ -33,6 +30,7 @@ use crate::{
|
|||||||
use tauri_plugin_dialog::{DialogExt, MessageDialogButtons};
|
use tauri_plugin_dialog::{DialogExt, MessageDialogButtons};
|
||||||
|
|
||||||
pub type AppIcon = TrayIcon<Wry>;
|
pub type AppIcon = TrayIcon<Wry>;
|
||||||
|
pub type ContextMenu = Menu<Wry>;
|
||||||
|
|
||||||
static SPN_STATE: AtomicBool = AtomicBool::new(false);
|
static SPN_STATE: AtomicBool = AtomicBool::new(false);
|
||||||
|
|
||||||
@@ -46,12 +44,20 @@ enum IconColor {
|
|||||||
|
|
||||||
static CURRENT_ICON_COLOR: RwLock<IconColor> = RwLock::new(IconColor::Red);
|
static CURRENT_ICON_COLOR: RwLock<IconColor> = RwLock::new(IconColor::Red);
|
||||||
pub static USER_THEME: RwLock<dark_light::Mode> = RwLock::new(dark_light::Mode::Default);
|
pub static USER_THEME: RwLock<dark_light::Mode> = RwLock::new(dark_light::Mode::Default);
|
||||||
|
const OPEN_KEY: &str = "open";
|
||||||
static SPN_STATUS_KEY: &str = "spn_status";
|
const EXIT_UI_KEY: &str = "exit_ui";
|
||||||
static SPN_BUTTON_KEY: &str = "spn_toggle";
|
const SPN_STATUS_KEY: &str = "spn_status";
|
||||||
static GLOBAL_STATUS_KEY: &str = "global_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_ICON_ID: &str = "pm_icon";
|
||||||
|
const PM_TRAY_MENU_ID: &str = "pm_tray_menu";
|
||||||
|
|
||||||
// Icons
|
// Icons
|
||||||
|
|
||||||
@@ -115,51 +121,57 @@ fn get_icon(icon: IconColor) -> &'static [u8] {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn setup_tray_menu(
|
fn build_tray_menu(
|
||||||
app: &mut tauri::App,
|
app: &tauri::AppHandle,
|
||||||
) -> core::result::Result<AppIcon, Box<dyn std::error::Error>> {
|
status: &str,
|
||||||
// Tray menu
|
spn_status_text: &str,
|
||||||
load_theme(app.handle());
|
) -> core::result::Result<ContextMenu, Box<dyn std::error::Error>> {
|
||||||
let open_btn = MenuItemBuilder::with_id("open", "Open App").build(app)?;
|
load_theme(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)?;
|
|
||||||
|
|
||||||
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)
|
.enabled(false)
|
||||||
.build(app)
|
.build(app)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
// Setup SPN status
|
// 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)
|
.enabled(false)
|
||||||
.build(app)
|
.build(app)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
// Setup SPN button
|
// 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)
|
.build(app)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let system_theme = MenuItemBuilder::with_id("system_theme", "System")
|
let system_theme = MenuItemBuilder::with_id(SYSTEM_THEME_KEY, "System")
|
||||||
.build(app)
|
.build(app)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let light_theme = MenuItemBuilder::with_id("light_theme", "Light")
|
let light_theme = MenuItemBuilder::with_id(LIGHT_THEME_KEY, "Light")
|
||||||
.build(app)
|
.build(app)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let dark_theme = MenuItemBuilder::with_id("dark_theme", "Dark")
|
let dark_theme = MenuItemBuilder::with_id(DARK_THEME_KEY, "Dark")
|
||||||
.build(app)
|
.build(app)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let theme_menu = SubmenuBuilder::new(app, "Icon Theme")
|
let theme_menu = SubmenuBuilder::new(app, "Icon Theme")
|
||||||
.items(&[&system_theme, &light_theme, &dark_theme])
|
.items(&[&system_theme, &light_theme, &dark_theme])
|
||||||
.build()?;
|
.build()?;
|
||||||
|
|
||||||
let force_show_window = MenuItemBuilder::with_id("force-show", "Force Show UI").build(app)?;
|
let force_show_window = MenuItemBuilder::with_id(FORCE_SHOW_KEY, "Force Show UI").build(app)?;
|
||||||
let reload_btn = MenuItemBuilder::with_id("reload", "Reload User Interface").build(app)?;
|
let reload_btn = MenuItemBuilder::with_id(RELOAD_KEY, "Reload User Interface").build(app)?;
|
||||||
let developer_menu = SubmenuBuilder::new(app, "Developer")
|
let developer_menu = SubmenuBuilder::new(app, "Developer")
|
||||||
.items(&[&reload_btn, &force_show_window])
|
.items(&[&reload_btn, &force_show_window])
|
||||||
.build()?;
|
.build()?;
|
||||||
|
|
||||||
let menu = MenuBuilder::new(app)
|
let menu = MenuBuilder::with_id(app, PM_TRAY_MENU_ID)
|
||||||
.items(&[
|
.items(&[
|
||||||
&open_btn,
|
&open_btn,
|
||||||
&PredefinedMenuItem::separator(app)?,
|
&PredefinedMenuItem::separator(app)?,
|
||||||
@@ -176,11 +188,19 @@ pub fn setup_tray_menu(
|
|||||||
])
|
])
|
||||||
.build()?;
|
.build()?;
|
||||||
|
|
||||||
|
return Ok(menu);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn setup_tray_menu(
|
||||||
|
app: &mut tauri::App,
|
||||||
|
) -> core::result::Result<AppIcon, Box<dyn std::error::Error>> {
|
||||||
|
let menu = build_tray_menu(app.handle(), "Secured", "disabled")?;
|
||||||
|
|
||||||
let icon = TrayIconBuilder::with_id(PM_TRAY_ICON_ID)
|
let icon = TrayIconBuilder::with_id(PM_TRAY_ICON_ID)
|
||||||
.icon(Image::from_bytes(get_red_icon()).unwrap())
|
.icon(Image::from_bytes(get_red_icon()).unwrap())
|
||||||
.menu(&menu)
|
.menu(&menu)
|
||||||
.on_menu_event(move |app, event| match event.id().as_ref() {
|
.on_menu_event(move |app, event| match event.id().as_ref() {
|
||||||
"exit_ui" => {
|
EXIT_UI_KEY => {
|
||||||
let handle = app.clone();
|
let handle = app.clone();
|
||||||
app.dialog()
|
app.dialog()
|
||||||
.message("This does not stop the Portmaster system service")
|
.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);
|
let _ = open_window(app);
|
||||||
}
|
}
|
||||||
"reload" => {
|
RELOAD_KEY => {
|
||||||
if let Ok(mut win) = open_window(app) {
|
if let Ok(mut win) = open_window(app) {
|
||||||
may_navigate_to_ui(&mut win, true);
|
may_navigate_to_ui(&mut win, true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"force-show" => {
|
FORCE_SHOW_KEY => {
|
||||||
match create_main_window(app) {
|
match create_main_window(app) {
|
||||||
Ok(mut win) => {
|
Ok(mut win) => {
|
||||||
may_navigate_to_ui(&mut win, true);
|
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) {
|
if SPN_STATE.load(Ordering::Acquire) {
|
||||||
app.portmaster().set_spn_enabled(false);
|
app.portmaster().set_spn_enabled(false);
|
||||||
} else {
|
} else {
|
||||||
app.portmaster().set_spn_enabled(true);
|
app.portmaster().set_spn_enabled(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"shutdown" => {
|
SHUTDOWN_KEY => {
|
||||||
app.portmaster().trigger_shutdown();
|
app.portmaster().trigger_shutdown();
|
||||||
}
|
}
|
||||||
"system_theme" => update_icon_theme(app, dark_light::Mode::Default),
|
SYSTEM_THEME_KEY => update_icon_theme(app, dark_light::Mode::Default),
|
||||||
"dark_theme" => update_icon_theme(app, dark_light::Mode::Dark),
|
DARK_THEME_KEY => update_icon_theme(app, dark_light::Mode::Dark),
|
||||||
"light_theme" => update_icon_theme(app, dark_light::Mode::Light),
|
LIGHT_THEME_KEY => update_icon_theme(app, dark_light::Mode::Light),
|
||||||
other => {
|
other => {
|
||||||
error!("unknown menu event id: {}", other);
|
error!("unknown menu event id: {}", other);
|
||||||
}
|
}
|
||||||
@@ -251,15 +271,11 @@ pub fn setup_tray_menu(
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
.build(app)?;
|
.build(app)?;
|
||||||
|
|
||||||
Ok(icon)
|
Ok(icon)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn update_icon<R: Runtime>(
|
pub fn update_icon(icon: AppIcon, subsystems: HashMap<String, Subsystem>, spn_status: String) {
|
||||||
icon: AppIcon,
|
|
||||||
menu: Option<Menu<R>>,
|
|
||||||
subsystems: HashMap<String, Subsystem>,
|
|
||||||
spn_status: String,
|
|
||||||
) {
|
|
||||||
// iterate over the subsystems and check if there's a module failure
|
// iterate over the subsystems and check if there's a module failure
|
||||||
let failure = subsystems.values().map(|s| &s.module_status).fold(
|
let failure = subsystems.values().map(|s| &s.module_status).fold(
|
||||||
(subsystem::FAILURE_NONE, "".to_string()),
|
(subsystem::FAILURE_NONE, "".to_string()),
|
||||||
@@ -273,14 +289,10 @@ pub fn update_icon<R: Runtime>(
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
if let Some(menu) = menu {
|
let mut status = "Secured".to_owned();
|
||||||
if let Some(MenuItemKind::MenuItem(global_status)) = menu.get(GLOBAL_STATUS_KEY) {
|
|
||||||
if failure.0 == subsystem::FAILURE_NONE {
|
if failure.0 != subsystem::FAILURE_NONE {
|
||||||
_ = global_status.set_text("Status: Secured");
|
status = failure.1;
|
||||||
} else {
|
|
||||||
_ = global_status.set_text(format!("Status: {}", failure.1));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let icon_color = match failure.0 {
|
let icon_color = match failure.0 {
|
||||||
@@ -291,6 +303,13 @@ pub fn update_icon<R: Runtime>(
|
|||||||
_ => IconColor::Green,
|
_ => 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);
|
update_icon_color(&icon, icon_color);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -391,8 +410,7 @@ pub async fn tray_handler(cli: PortAPI, app: tauri::AppHandle) {
|
|||||||
match payload.parse::<Subsystem>() {
|
match payload.parse::<Subsystem>() {
|
||||||
Ok(n) => {
|
Ok(n) => {
|
||||||
subsystems.insert(n.id.clone(), n);
|
subsystems.insert(n.id.clone(), n);
|
||||||
|
update_icon(icon.clone(), subsystems.clone(), spn_status.clone());
|
||||||
update_icon(icon.clone(), app.menu(), subsystems.clone(), spn_status.clone());
|
|
||||||
},
|
},
|
||||||
Err(err) => match err {
|
Err(err) => match err {
|
||||||
ParseError::Json(err) => {
|
ParseError::Json(err) => {
|
||||||
@@ -423,8 +441,7 @@ pub async fn tray_handler(cli: PortAPI, app: tauri::AppHandle) {
|
|||||||
Ok(value) => {
|
Ok(value) => {
|
||||||
debug!("SPN status update: {}", value.status);
|
debug!("SPN status update: {}", value.status);
|
||||||
spn_status.clone_from(&value.status);
|
spn_status.clone_from(&value.status);
|
||||||
|
update_icon(icon.clone(), subsystems.clone(), spn_status.clone());
|
||||||
update_icon(icon.clone(), app.menu(), subsystems.clone(), spn_status.clone());
|
|
||||||
},
|
},
|
||||||
Err(err) => match err {
|
Err(err) => match err {
|
||||||
ParseError::Json(err) => {
|
ParseError::Json(err) => {
|
||||||
@@ -453,9 +470,7 @@ pub async fn tray_handler(cli: PortAPI, app: tauri::AppHandle) {
|
|||||||
if let Some((_, payload)) = res {
|
if let Some((_, payload)) = res {
|
||||||
match payload.parse::<BooleanValue>() {
|
match payload.parse::<BooleanValue>() {
|
||||||
Ok(value) => {
|
Ok(value) => {
|
||||||
if let Some(menu) = app.menu() {
|
SPN_STATE.store(value.value.unwrap_or(false), Ordering::Release);
|
||||||
update_spn_ui_state(menu, value.value.unwrap_or(false));
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
Err(err) => match err {
|
Err(err) => match err {
|
||||||
ParseError::Json(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);
|
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),
|
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<R: Runtime>(menu: Menu<R>, 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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -81,6 +81,15 @@ var dataDir
|
|||||||
|
|
||||||
SimpleSC::SetServiceDescription "PortmasterCore" "Portmaster Application Firewall - Core Service"
|
SimpleSC::SetServiceDescription "PortmasterCore" "Portmaster Application Firewall - Core Service"
|
||||||
|
|
||||||
|
;
|
||||||
|
; Auto start the UI
|
||||||
|
;
|
||||||
|
DetailPrint "Creating registry entry for autostart"
|
||||||
|
WriteRegStr HKLM "SOFTWARE\Microsoft\Windows\CurrentVersion\Run" "Portmaster" '"$INSTDIR\portmaster.exe" --with-prompts --with-notifications --background'
|
||||||
|
|
||||||
|
;
|
||||||
|
; MIGRATION FROM PMv1 TO PMv2
|
||||||
|
;
|
||||||
StrCpy $oldInstallationDir "$COMMONPROGRAMDATA\Safing\Portmaster"
|
StrCpy $oldInstallationDir "$COMMONPROGRAMDATA\Safing\Portmaster"
|
||||||
StrCpy $dataDir "$COMMONPROGRAMDATA\Portmaster"
|
StrCpy $dataDir "$COMMONPROGRAMDATA\Portmaster"
|
||||||
|
|
||||||
@@ -168,6 +177,10 @@ var dataDir
|
|||||||
Delete /REBOOTOK "$INSTDIR\assets.zip"
|
Delete /REBOOTOK "$INSTDIR\assets.zip"
|
||||||
RMDir /r /REBOOTOK "$INSTDIR"
|
RMDir /r /REBOOTOK "$INSTDIR"
|
||||||
|
|
||||||
|
; remove the registry entry for the autostart
|
||||||
|
DetailPrint "Removing registry entry for autostart"
|
||||||
|
DeleteRegKey HKLM "SOFTWARE\Microsoft\Windows\CurrentVersion\Run"
|
||||||
|
|
||||||
; delete data files
|
; delete data files
|
||||||
Delete /REBOOTOK "$COMMONPROGRAMDATA\Portmaster\databases\history.db"
|
Delete /REBOOTOK "$COMMONPROGRAMDATA\Portmaster\databases\history.db"
|
||||||
RMDir /r /REBOOTOK "$COMMONPROGRAMDATA\Portmaster\databases\cache"
|
RMDir /r /REBOOTOK "$COMMONPROGRAMDATA\Portmaster\databases\cache"
|
||||||
@@ -178,6 +191,9 @@ var dataDir
|
|||||||
RMDir /r /REBOOTOK "$COMMONPROGRAMDATA\Portmaster\exec"
|
RMDir /r /REBOOTOK "$COMMONPROGRAMDATA\Portmaster\exec"
|
||||||
RMDir /r /REBOOTOK "$COMMONPROGRAMDATA\Portmaster\logs"
|
RMDir /r /REBOOTOK "$COMMONPROGRAMDATA\Portmaster\logs"
|
||||||
|
|
||||||
|
; Remove PMv1 migration flag
|
||||||
|
Delete /REBOOTOK "$COMMONPROGRAMDATA\Safing\Portmaster\migrated.txt"
|
||||||
|
|
||||||
${If} $DeleteAppDataCheckboxState = 1
|
${If} $DeleteAppDataCheckboxState = 1
|
||||||
DetailPrint "Deleting the application data..."
|
DetailPrint "Deleting the application data..."
|
||||||
RMDir /r /REBOOTOK "$COMMONPROGRAMDATA\Portmaster"
|
RMDir /r /REBOOTOK "$COMMONPROGRAMDATA\Portmaster"
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ Name=Portmaster
|
|||||||
GenericName=Application Firewall
|
GenericName=Application Firewall
|
||||||
Exec={{exec}} --data=/opt/safing/portmaster --with-prompts --with-notifications
|
Exec={{exec}} --data=/opt/safing/portmaster --with-prompts --with-notifications
|
||||||
Icon={{icon}}
|
Icon={{icon}}
|
||||||
|
StartupWMClass=portmaster
|
||||||
Terminal=false
|
Terminal=false
|
||||||
Type=Application
|
Type=Application
|
||||||
Categories=System
|
Categories=System
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ Environment=LOGLEVEL=info
|
|||||||
Environment=PORTMASTER_ARGS=
|
Environment=PORTMASTER_ARGS=
|
||||||
EnvironmentFile=-/etc/default/portmaster
|
EnvironmentFile=-/etc/default/portmaster
|
||||||
ProtectSystem=true
|
ProtectSystem=true
|
||||||
|
ReadWritePaths=/usr/lib/portmaster
|
||||||
RestrictAddressFamilies=AF_UNIX AF_NETLINK AF_INET AF_INET6
|
RestrictAddressFamilies=AF_UNIX AF_NETLINK AF_INET AF_INET6
|
||||||
RestrictNamespaces=yes
|
RestrictNamespaces=yes
|
||||||
ProtectHome=read-only
|
ProtectHome=read-only
|
||||||
|
|||||||
@@ -28,6 +28,9 @@ if [ -d "$OLD_INSTALLATION_DIR" ]; then
|
|||||||
echo "[ ] V1 migration: Removing V1 shortcuts"
|
echo "[ ] V1 migration: Removing V1 shortcuts"
|
||||||
rm /etc/xdg/autostart/portmaster_notifier.desktop
|
rm /etc/xdg/autostart/portmaster_notifier.desktop
|
||||||
rm /usr/share/applications/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)
|
# Remove V1 files (except configuration)
|
||||||
# (keeping V1 configuration for a smooth downgrade, if needed)
|
# (keeping V1 configuration for a smooth downgrade, if needed)
|
||||||
|
|||||||
@@ -50,15 +50,19 @@
|
|||||||
#------------------------------------------------------------------------------
|
#------------------------------------------------------------------------------
|
||||||
#
|
#
|
||||||
# Optional arguments:
|
# 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)
|
# -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
|
# -v: (version) Explicitly set the version to use for the installer file name
|
||||||
|
# -e: (erase) Just erase work directories
|
||||||
#------------------------------------------------------------------------------
|
#------------------------------------------------------------------------------
|
||||||
param (
|
param (
|
||||||
[Alias('i')]
|
[Alias('i')]
|
||||||
[switch]$interactive,
|
[switch]$interactive,
|
||||||
|
|
||||||
[Alias('v')]
|
[Alias('v')]
|
||||||
[string]$version
|
[string]$version,
|
||||||
|
|
||||||
|
[Alias('e')]
|
||||||
|
[switch]$erase
|
||||||
)
|
)
|
||||||
|
|
||||||
# Save the current directory
|
# Save the current directory
|
||||||
@@ -185,7 +189,18 @@ try {
|
|||||||
$destinationDir = "desktop/tauri/src-tauri"
|
$destinationDir = "desktop/tauri/src-tauri"
|
||||||
$binaryDir = "$destinationDir/binary" #portmaster\desktop\tauri\src-tauri\binary
|
$binaryDir = "$destinationDir/binary" #portmaster\desktop\tauri\src-tauri\binary
|
||||||
$intelDir = "$destinationDir/intel" #portmaster\desktop\tauri\src-tauri\intel
|
$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
|
# Copying BINARY FILES
|
||||||
Write-Output "`n[+] Copying binary files:"
|
Write-Output "`n[+] Copying binary files:"
|
||||||
|
|||||||
@@ -92,7 +92,8 @@ func (sc *ServiceConfig) Init() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func getCurrentBinaryFolder() (string, error) {
|
// returns the absolute path of the currently running executable
|
||||||
|
func getCurrentBinaryPath() (string, error) {
|
||||||
// Get the path of the currently running executable
|
// Get the path of the currently running executable
|
||||||
exePath, err := os.Executable()
|
exePath, err := os.Executable()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -105,6 +106,16 @@ func getCurrentBinaryFolder() (string, error) {
|
|||||||
return "", fmt.Errorf("failed to get absolute path: %w", err)
|
return "", fmt.Errorf("failed to get absolute path: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return absPath, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getCurrentBinaryFolder() (string, error) {
|
||||||
|
// Get the absolute path of the currently running executable
|
||||||
|
absPath, err := getCurrentBinaryPath()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
// Get the directory of the executable
|
// Get the directory of the executable
|
||||||
installDir := filepath.Dir(absPath)
|
installDir := filepath.Dir(absPath)
|
||||||
|
|
||||||
@@ -115,12 +126,12 @@ func MakeUpdateConfigs(svcCfg *ServiceConfig) (binaryUpdateConfig, intelUpdateCo
|
|||||||
switch runtime.GOOS {
|
switch runtime.GOOS {
|
||||||
case "windows":
|
case "windows":
|
||||||
binaryUpdateConfig = &updates.Config{
|
binaryUpdateConfig = &updates.Config{
|
||||||
Name: "binaries",
|
Name: configure.DefaultBinaryIndexName,
|
||||||
Directory: svcCfg.BinDir,
|
Directory: svcCfg.BinDir,
|
||||||
DownloadDirectory: filepath.Join(svcCfg.DataDir, "download_binaries"),
|
DownloadDirectory: filepath.Join(svcCfg.DataDir, "download_binaries"),
|
||||||
PurgeDirectory: filepath.Join(svcCfg.BinDir, "upgrade_obsolete_binaries"),
|
PurgeDirectory: filepath.Join(svcCfg.BinDir, "upgrade_obsolete_binaries"),
|
||||||
Ignore: []string{"databases", "intel", "config.json"},
|
Ignore: []string{"uninstall.exe"}, // "databases", "intel" and "config.json" not needed here since they are not in the bin dir.
|
||||||
IndexURLs: svcCfg.BinariesIndexURLs, // May be changed by config during instance startup.
|
IndexURLs: svcCfg.BinariesIndexURLs, // May be changed by config during instance startup.
|
||||||
IndexFile: "index.json",
|
IndexFile: "index.json",
|
||||||
Verify: svcCfg.VerifyBinaryUpdates,
|
Verify: svcCfg.VerifyBinaryUpdates,
|
||||||
AutoCheck: true, // May be changed by config during instance startup.
|
AutoCheck: true, // May be changed by config during instance startup.
|
||||||
@@ -130,7 +141,7 @@ func MakeUpdateConfigs(svcCfg *ServiceConfig) (binaryUpdateConfig, intelUpdateCo
|
|||||||
Notify: true,
|
Notify: true,
|
||||||
}
|
}
|
||||||
intelUpdateConfig = &updates.Config{
|
intelUpdateConfig = &updates.Config{
|
||||||
Name: "intel",
|
Name: configure.DefaultIntelIndexName,
|
||||||
Directory: filepath.Join(svcCfg.DataDir, "intel"),
|
Directory: filepath.Join(svcCfg.DataDir, "intel"),
|
||||||
DownloadDirectory: filepath.Join(svcCfg.DataDir, "download_intel"),
|
DownloadDirectory: filepath.Join(svcCfg.DataDir, "download_intel"),
|
||||||
PurgeDirectory: filepath.Join(svcCfg.DataDir, "upgrade_obsolete_intel"),
|
PurgeDirectory: filepath.Join(svcCfg.DataDir, "upgrade_obsolete_intel"),
|
||||||
@@ -146,11 +157,11 @@ func MakeUpdateConfigs(svcCfg *ServiceConfig) (binaryUpdateConfig, intelUpdateCo
|
|||||||
|
|
||||||
case "linux":
|
case "linux":
|
||||||
binaryUpdateConfig = &updates.Config{
|
binaryUpdateConfig = &updates.Config{
|
||||||
Name: "binaries",
|
Name: configure.DefaultBinaryIndexName,
|
||||||
Directory: svcCfg.BinDir,
|
Directory: svcCfg.BinDir,
|
||||||
DownloadDirectory: filepath.Join(svcCfg.DataDir, "download_binaries"),
|
DownloadDirectory: filepath.Join(svcCfg.DataDir, "download_binaries"),
|
||||||
PurgeDirectory: filepath.Join(svcCfg.DataDir, "upgrade_obsolete_binaries"),
|
PurgeDirectory: filepath.Join(svcCfg.DataDir, "upgrade_obsolete_binaries"),
|
||||||
Ignore: []string{"databases", "intel", "config.json"},
|
Ignore: []string{}, // "databases", "intel" and "config.json" not needed here since they are not in the bin dir.
|
||||||
IndexURLs: svcCfg.BinariesIndexURLs, // May be changed by config during instance startup.
|
IndexURLs: svcCfg.BinariesIndexURLs, // May be changed by config during instance startup.
|
||||||
IndexFile: "index.json",
|
IndexFile: "index.json",
|
||||||
Verify: svcCfg.VerifyBinaryUpdates,
|
Verify: svcCfg.VerifyBinaryUpdates,
|
||||||
@@ -160,8 +171,23 @@ func MakeUpdateConfigs(svcCfg *ServiceConfig) (binaryUpdateConfig, intelUpdateCo
|
|||||||
NeedsRestart: true,
|
NeedsRestart: true,
|
||||||
Notify: true,
|
Notify: true,
|
||||||
}
|
}
|
||||||
|
if binPath, err := getCurrentBinaryPath(); err == nil {
|
||||||
|
binaryUpdateConfig.PostUpgradeCommands = []updates.UpdateCommandConfig{
|
||||||
|
// Restore SELinux context for the new core binary after upgrade
|
||||||
|
// (`restorecon /usr/lib/portmaster/portmaster-core`)
|
||||||
|
{
|
||||||
|
Command: "restorecon",
|
||||||
|
Args: []string{binPath},
|
||||||
|
TriggerArtifactFName: binPath,
|
||||||
|
FailOnError: false, // Ignore error: 'restorecon' may not be available on a non-SELinux systems.
|
||||||
|
},
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return nil, nil, fmt.Errorf("failed to get current binary path: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
intelUpdateConfig = &updates.Config{
|
intelUpdateConfig = &updates.Config{
|
||||||
Name: "intel",
|
Name: configure.DefaultIntelIndexName,
|
||||||
Directory: filepath.Join(svcCfg.DataDir, "intel"),
|
Directory: filepath.Join(svcCfg.DataDir, "intel"),
|
||||||
DownloadDirectory: filepath.Join(svcCfg.DataDir, "download_intel"),
|
DownloadDirectory: filepath.Join(svcCfg.DataDir, "download_intel"),
|
||||||
PurgeDirectory: filepath.Join(svcCfg.DataDir, "upgrade_obsolete_intel"),
|
PurgeDirectory: filepath.Join(svcCfg.DataDir, "upgrade_obsolete_intel"),
|
||||||
|
|||||||
@@ -5,6 +5,9 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
DefaultBinaryIndexName = "Portmaster Binaries"
|
||||||
|
DefaultIntelIndexName = "intel"
|
||||||
|
|
||||||
DefaultStableBinaryIndexURLs = []string{
|
DefaultStableBinaryIndexURLs = []string{
|
||||||
"https://updates.safing.io/stable.v3.json",
|
"https://updates.safing.io/stable.v3.json",
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -438,6 +438,15 @@ func (i *Instance) BinaryUpdates() *updates.Updater {
|
|||||||
return i.binaryUpdates
|
return i.binaryUpdates
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetBinaryUpdateFile returns the file path of a binary update file.
|
||||||
|
func (i *Instance) GetBinaryUpdateFile(name string) (path string, err error) {
|
||||||
|
file, err := i.binaryUpdates.GetFile(name)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return file.Path(), nil
|
||||||
|
}
|
||||||
|
|
||||||
// IntelUpdates returns the updates module.
|
// IntelUpdates returns the updates module.
|
||||||
func (i *Instance) IntelUpdates() *updates.Updater {
|
func (i *Instance) IntelUpdates() *updates.Updater {
|
||||||
return i.intelUpdates
|
return i.intelUpdates
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ var (
|
|||||||
// pidsByUserLock is also used for locking the socketInfo.PID on all socket.*Info structs.
|
// pidsByUserLock is also used for locking the socketInfo.PID on all socket.*Info structs.
|
||||||
pidsByUser = make(map[int][]int)
|
pidsByUser = make(map[int][]int)
|
||||||
pidsByUserLock sync.RWMutex
|
pidsByUserLock sync.RWMutex
|
||||||
fetchPidsByUser = utils.NewCallLimiter(10 * time.Millisecond)
|
fetchPidsByUser = utils.NewCallLimiter2(10 * time.Millisecond)
|
||||||
)
|
)
|
||||||
|
|
||||||
// getPidsByUser returns the cached PIDs for the given UID.
|
// getPidsByUser returns the cached PIDs for the given UID.
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ type tcpTable struct {
|
|||||||
// lastUpdateAt stores the time when the tables where last updated as unix nanoseconds.
|
// lastUpdateAt stores the time when the tables where last updated as unix nanoseconds.
|
||||||
lastUpdateAt atomic.Int64
|
lastUpdateAt atomic.Int64
|
||||||
|
|
||||||
fetchLimiter *utils.CallLimiter
|
fetchLimiter *utils.CallLimiter2
|
||||||
fetchTable func() (connections []*socket.ConnectionInfo, listeners []*socket.BindInfo, err error)
|
fetchTable func() (connections []*socket.ConnectionInfo, listeners []*socket.BindInfo, err error)
|
||||||
|
|
||||||
dualStack *tcpTable
|
dualStack *tcpTable
|
||||||
@@ -34,13 +34,13 @@ type tcpTable struct {
|
|||||||
var (
|
var (
|
||||||
tcp6Table = &tcpTable{
|
tcp6Table = &tcpTable{
|
||||||
version: 6,
|
version: 6,
|
||||||
fetchLimiter: utils.NewCallLimiter(minDurationBetweenTableUpdates),
|
fetchLimiter: utils.NewCallLimiter2(minDurationBetweenTableUpdates),
|
||||||
fetchTable: getTCP6Table,
|
fetchTable: getTCP6Table,
|
||||||
}
|
}
|
||||||
|
|
||||||
tcp4Table = &tcpTable{
|
tcp4Table = &tcpTable{
|
||||||
version: 4,
|
version: 4,
|
||||||
fetchLimiter: utils.NewCallLimiter(minDurationBetweenTableUpdates),
|
fetchLimiter: utils.NewCallLimiter2(minDurationBetweenTableUpdates),
|
||||||
fetchTable: getTCP4Table,
|
fetchTable: getTCP4Table,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ type udpTable struct {
|
|||||||
// lastUpdateAt stores the time when the tables where last updated as unix nanoseconds.
|
// lastUpdateAt stores the time when the tables where last updated as unix nanoseconds.
|
||||||
lastUpdateAt atomic.Int64
|
lastUpdateAt atomic.Int64
|
||||||
|
|
||||||
fetchLimiter *utils.CallLimiter
|
fetchLimiter *utils.CallLimiter2
|
||||||
fetchTable func() (binds []*socket.BindInfo, err error)
|
fetchTable func() (binds []*socket.BindInfo, err error)
|
||||||
|
|
||||||
states map[string]map[string]*udpState
|
states map[string]map[string]*udpState
|
||||||
@@ -52,14 +52,14 @@ const (
|
|||||||
var (
|
var (
|
||||||
udp6Table = &udpTable{
|
udp6Table = &udpTable{
|
||||||
version: 6,
|
version: 6,
|
||||||
fetchLimiter: utils.NewCallLimiter(minDurationBetweenTableUpdates),
|
fetchLimiter: utils.NewCallLimiter2(minDurationBetweenTableUpdates),
|
||||||
fetchTable: getUDP6Table,
|
fetchTable: getUDP6Table,
|
||||||
states: make(map[string]map[string]*udpState),
|
states: make(map[string]map[string]*udpState),
|
||||||
}
|
}
|
||||||
|
|
||||||
udp4Table = &udpTable{
|
udp4Table = &udpTable{
|
||||||
version: 4,
|
version: 4,
|
||||||
fetchLimiter: utils.NewCallLimiter(minDurationBetweenTableUpdates),
|
fetchLimiter: utils.NewCallLimiter2(minDurationBetweenTableUpdates),
|
||||||
fetchTable: getUDP4Table,
|
fetchTable: getUDP4Table,
|
||||||
states: make(map[string]map[string]*udpState),
|
states: make(map[string]map[string]*udpState),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,35 +2,21 @@ package ui
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/safing/portmaster/base/api"
|
"github.com/safing/portmaster/base/api"
|
||||||
"github.com/safing/portmaster/base/log"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func registerAPIEndpoints() error {
|
func (ui *UI) registerAPIEndpoints() error {
|
||||||
return api.RegisterEndpoint(api.Endpoint{
|
return api.RegisterEndpoint(api.Endpoint{
|
||||||
Path: "ui/reload",
|
Path: "ui/reload",
|
||||||
Write: api.PermitUser,
|
Write: api.PermitUser,
|
||||||
ActionFunc: reloadUI,
|
ActionFunc: ui.reloadUI,
|
||||||
Name: "Reload UI Assets",
|
Name: "Reload UI Assets",
|
||||||
Description: "Removes all assets from the cache and reloads the current (possibly updated) version from disk when requested.",
|
Description: "Removes all assets from the cache and reloads the current (possibly updated) version from disk when requested.",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func reloadUI(_ *api.Request) (msg string, err error) {
|
func (ui *UI) reloadUI(_ *api.Request) (msg string, err error) {
|
||||||
appsLock.Lock()
|
|
||||||
defer appsLock.Unlock()
|
|
||||||
|
|
||||||
// Close all archives.
|
// Close all archives.
|
||||||
for id, archiveFS := range apps {
|
ui.CloseArchives()
|
||||||
err := archiveFS.Close()
|
|
||||||
if err != nil {
|
|
||||||
log.Warningf("ui: failed to close archive %s: %s", id, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reset index.
|
|
||||||
for key := range apps {
|
|
||||||
delete(apps, key)
|
|
||||||
}
|
|
||||||
|
|
||||||
return "all ui archives successfully reloaded", nil
|
return "all ui archives successfully reloaded", nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,27 +1,55 @@
|
|||||||
package ui
|
package ui
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"sync"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
|
|
||||||
"github.com/safing/portmaster/base/api"
|
"github.com/safing/portmaster/base/api"
|
||||||
"github.com/safing/portmaster/base/log"
|
"github.com/safing/portmaster/base/log"
|
||||||
"github.com/safing/portmaster/base/utils"
|
"github.com/safing/portmaster/base/utils"
|
||||||
"github.com/safing/portmaster/service/mgr"
|
"github.com/safing/portmaster/service/mgr"
|
||||||
"github.com/safing/portmaster/service/updates"
|
"github.com/spkg/zipfs"
|
||||||
)
|
)
|
||||||
|
|
||||||
func prep() error {
|
// UI serves the user interface files.
|
||||||
if err := registerAPIEndpoints(); err != nil {
|
type UI struct {
|
||||||
return err
|
mgr *mgr.Manager
|
||||||
}
|
instance instance
|
||||||
|
|
||||||
return registerRoutes()
|
archives map[string]*zipfs.FileSystem
|
||||||
|
archivesLock sync.RWMutex
|
||||||
|
|
||||||
|
upgradeLock atomic.Bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func start() error {
|
// New returns a new UI module.
|
||||||
|
func New(instance instance) (*UI, error) {
|
||||||
|
m := mgr.New("UI")
|
||||||
|
ui := &UI{
|
||||||
|
mgr: m,
|
||||||
|
instance: instance,
|
||||||
|
|
||||||
|
archives: make(map[string]*zipfs.FileSystem),
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := ui.registerAPIEndpoints(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := ui.registerRoutes(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return ui, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ui *UI) Manager() *mgr.Manager {
|
||||||
|
return ui.mgr
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start starts the module.
|
||||||
|
func (ui *UI) Start() error {
|
||||||
// Create a dummy directory to which processes change their working directory
|
// Create a dummy directory to which processes change their working directory
|
||||||
// to. Currently this includes the App and the Notifier. The aim is protect
|
// to. Currently this includes the App and the Notifier. The aim is protect
|
||||||
// all other directories and increase compatibility should any process want
|
// all other directories and increase compatibility should any process want
|
||||||
@@ -30,7 +58,7 @@ func start() error {
|
|||||||
// may seem dangerous, but proper permission on the parent directory provide
|
// may seem dangerous, but proper permission on the parent directory provide
|
||||||
// (some) protection.
|
// (some) protection.
|
||||||
// Processes must _never_ read from this directory.
|
// Processes must _never_ read from this directory.
|
||||||
execDir := filepath.Join(module.instance.DataDir(), "exec")
|
execDir := filepath.Join(ui.instance.DataDir(), "exec")
|
||||||
err := os.MkdirAll(execDir, 0o0777) //nolint:gosec // This is intentional.
|
err := os.MkdirAll(execDir, 0o0777) //nolint:gosec // This is intentional.
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Warningf("ui: failed to create safe exec dir: %s", err)
|
log.Warningf("ui: failed to create safe exec dir: %s", err)
|
||||||
@@ -45,52 +73,67 @@ func start() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// UI serves the user interface files.
|
|
||||||
type UI struct {
|
|
||||||
mgr *mgr.Manager
|
|
||||||
|
|
||||||
instance instance
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ui *UI) Manager() *mgr.Manager {
|
|
||||||
return ui.mgr
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start starts the module.
|
|
||||||
func (ui *UI) Start() error {
|
|
||||||
return start()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Stop stops the module.
|
// Stop stops the module.
|
||||||
func (ui *UI) Stop() error {
|
func (ui *UI) Stop() error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
func (ui *UI) getArchive(name string) (archive *zipfs.FileSystem, ok bool) {
|
||||||
shimLoaded atomic.Bool
|
ui.archivesLock.RLock()
|
||||||
module *UI
|
defer ui.archivesLock.RUnlock()
|
||||||
)
|
|
||||||
|
|
||||||
// New returns a new UI module.
|
archive, ok = ui.archives[name]
|
||||||
func New(instance instance) (*UI, error) {
|
return
|
||||||
if !shimLoaded.CompareAndSwap(false, true) {
|
}
|
||||||
return nil, errors.New("only one instance allowed")
|
|
||||||
}
|
func (ui *UI) setArchive(name string, archive *zipfs.FileSystem) {
|
||||||
m := mgr.New("UI")
|
ui.archivesLock.Lock()
|
||||||
module = &UI{
|
defer ui.archivesLock.Unlock()
|
||||||
mgr: m,
|
|
||||||
instance: instance,
|
ui.archives[name] = archive
|
||||||
|
}
|
||||||
|
|
||||||
|
// CloseArchives closes all open archives.
|
||||||
|
func (ui *UI) CloseArchives() {
|
||||||
|
if ui == nil {
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := prep(); err != nil {
|
ui.archivesLock.Lock()
|
||||||
return nil, err
|
defer ui.archivesLock.Unlock()
|
||||||
|
|
||||||
|
// Close archives.
|
||||||
|
for _, archive := range ui.archives {
|
||||||
|
if err := archive.Close(); err != nil {
|
||||||
|
ui.mgr.Warn("failed to close ui archive", "err", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return module, nil
|
// Reset map.
|
||||||
|
clear(ui.archives)
|
||||||
|
}
|
||||||
|
|
||||||
|
// EnableUpgradeLock enables the upgrade lock and closes all open archives.
|
||||||
|
func (ui *UI) EnableUpgradeLock() {
|
||||||
|
if ui == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ui.upgradeLock.Store(true)
|
||||||
|
ui.CloseArchives()
|
||||||
|
}
|
||||||
|
|
||||||
|
// DisableUpgradeLock disables the upgrade lock.
|
||||||
|
func (ui *UI) DisableUpgradeLock() {
|
||||||
|
if ui == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ui.upgradeLock.Store(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
type instance interface {
|
type instance interface {
|
||||||
DataDir() string
|
DataDir() string
|
||||||
API() *api.API
|
API() *api.API
|
||||||
BinaryUpdates() *updates.Updater
|
GetBinaryUpdateFile(name string) (path string, err error)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,26 +9,19 @@ import (
|
|||||||
"net/url"
|
"net/url"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
|
||||||
|
|
||||||
"github.com/spkg/zipfs"
|
"github.com/spkg/zipfs"
|
||||||
|
|
||||||
"github.com/safing/portmaster/base/api"
|
"github.com/safing/portmaster/base/api"
|
||||||
"github.com/safing/portmaster/base/log"
|
"github.com/safing/portmaster/base/log"
|
||||||
"github.com/safing/portmaster/base/utils"
|
"github.com/safing/portmaster/base/utils"
|
||||||
"github.com/safing/portmaster/service/updates"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
func (ui *UI) registerRoutes() error {
|
||||||
apps = make(map[string]*zipfs.FileSystem)
|
|
||||||
appsLock sync.RWMutex
|
|
||||||
)
|
|
||||||
|
|
||||||
func registerRoutes() error {
|
|
||||||
// Server assets.
|
// Server assets.
|
||||||
api.RegisterHandler(
|
api.RegisterHandler(
|
||||||
"/assets/{resPath:[a-zA-Z0-9/\\._-]+}",
|
"/assets/{resPath:[a-zA-Z0-9/\\._-]+}",
|
||||||
&archiveServer{defaultModuleName: "assets"},
|
&archiveServer{ui: ui, defaultModuleName: "assets"},
|
||||||
)
|
)
|
||||||
|
|
||||||
// Add slash to plain module namespaces.
|
// Add slash to plain module namespaces.
|
||||||
@@ -38,7 +31,7 @@ func registerRoutes() error {
|
|||||||
)
|
)
|
||||||
|
|
||||||
// Serve modules.
|
// Serve modules.
|
||||||
srv := &archiveServer{}
|
srv := &archiveServer{ui: ui}
|
||||||
api.RegisterHandler("/ui/modules/{moduleName:[a-z]+}/", srv)
|
api.RegisterHandler("/ui/modules/{moduleName:[a-z]+}/", srv)
|
||||||
api.RegisterHandler("/ui/modules/{moduleName:[a-z]+}/{resPath:[a-zA-Z0-9/\\._-]+}", srv)
|
api.RegisterHandler("/ui/modules/{moduleName:[a-z]+}/{resPath:[a-zA-Z0-9/\\._-]+}", srv)
|
||||||
|
|
||||||
@@ -52,6 +45,7 @@ func registerRoutes() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type archiveServer struct {
|
type archiveServer struct {
|
||||||
|
ui *UI
|
||||||
defaultModuleName string
|
defaultModuleName string
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -82,39 +76,35 @@ func (bs *archiveServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||||||
resPath = "index.html"
|
resPath = "index.html"
|
||||||
}
|
}
|
||||||
|
|
||||||
appsLock.RLock()
|
archiveFS, ok := bs.ui.getArchive(moduleName)
|
||||||
archiveFS, ok := apps[moduleName]
|
|
||||||
appsLock.RUnlock()
|
|
||||||
if ok {
|
if ok {
|
||||||
ServeFileFromArchive(w, r, moduleName, archiveFS, resPath)
|
ServeFileFromArchive(w, r, moduleName, archiveFS, resPath)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if the upgrade lock is enabled.
|
||||||
|
if bs.ui.upgradeLock.Load() {
|
||||||
|
http.Error(w, "Resources locked, upgrade in progress.", http.StatusLocked)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// get file from update system
|
// get file from update system
|
||||||
zipFile, err := module.instance.BinaryUpdates().GetFile(fmt.Sprintf("%s.zip", moduleName))
|
zipFile, err := bs.ui.instance.GetBinaryUpdateFile(fmt.Sprintf("%s.zip", moduleName))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, updates.ErrNotFound) {
|
log.Tracef("ui: error loading module %s: %s", moduleName, err)
|
||||||
log.Tracef("ui: requested module %s does not exist", moduleName)
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
http.Error(w, err.Error(), http.StatusNotFound)
|
|
||||||
} else {
|
|
||||||
log.Tracef("ui: error loading module %s: %s", moduleName, err)
|
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
||||||
}
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Open archive from disk.
|
// Open archive from disk.
|
||||||
archiveFS, err = zipfs.New(zipFile.Path())
|
archiveFS, err = zipfs.New(zipFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Tracef("ui: error prepping module %s: %s", moduleName, err)
|
log.Tracef("ui: error prepping module %s: %s", moduleName, err)
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
appsLock.Lock()
|
bs.ui.setArchive(moduleName, archiveFS)
|
||||||
apps[moduleName] = archiveFS
|
|
||||||
appsLock.Unlock()
|
|
||||||
|
|
||||||
ServeFileFromArchive(w, r, moduleName, archiveFS, resPath)
|
ServeFileFromArchive(w, r, moduleName, archiveFS, resPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -118,6 +118,14 @@ type Index struct {
|
|||||||
Artifacts []*Artifact `json:"Artifacts"`
|
Artifacts []*Artifact `json:"Artifacts"`
|
||||||
|
|
||||||
versionNum *semver.Version
|
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.
|
// 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:
|
case index.Name != newIndex.Name:
|
||||||
return errors.New("new index name does not match")
|
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):
|
case index.Published.After(newIndex.Published):
|
||||||
return errors.New("new index is older (time)")
|
return errors.New("new index is older (time)")
|
||||||
|
|
||||||
|
|||||||
@@ -234,10 +234,11 @@ func GenerateIndexFromDir(sourceDir string, cfg IndexScanConfig) (*Index, error)
|
|||||||
|
|
||||||
// Create base index.
|
// Create base index.
|
||||||
index := &Index{
|
index := &Index{
|
||||||
Name: cfg.Name,
|
Name: cfg.Name,
|
||||||
Version: cfg.Version,
|
Version: cfg.Version,
|
||||||
Published: time.Now(),
|
Published: time.Now(),
|
||||||
versionNum: indexVersion,
|
versionNum: indexVersion,
|
||||||
|
isLocallyGenerated: true,
|
||||||
}
|
}
|
||||||
if index.Version == "" && cfg.PrimaryArtifact != "" {
|
if index.Version == "" && cfg.PrimaryArtifact != "" {
|
||||||
pv, ok := artifacts[cfg.PrimaryArtifact]
|
pv, ok := artifacts[cfg.PrimaryArtifact]
|
||||||
|
|||||||
@@ -18,7 +18,9 @@ import (
|
|||||||
"github.com/safing/portmaster/base/log"
|
"github.com/safing/portmaster/base/log"
|
||||||
"github.com/safing/portmaster/base/notifications"
|
"github.com/safing/portmaster/base/notifications"
|
||||||
"github.com/safing/portmaster/base/utils"
|
"github.com/safing/portmaster/base/utils"
|
||||||
|
"github.com/safing/portmaster/service/configure"
|
||||||
"github.com/safing/portmaster/service/mgr"
|
"github.com/safing/portmaster/service/mgr"
|
||||||
|
"github.com/safing/portmaster/service/ui"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -49,6 +51,22 @@ var (
|
|||||||
ErrActionRequired = errors.New("action required")
|
ErrActionRequired = errors.New("action required")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// UpdateCommandConfig defines the configuration for a shell command
|
||||||
|
// that is executed when an update is applied
|
||||||
|
type UpdateCommandConfig struct {
|
||||||
|
// Shell command to execute
|
||||||
|
Command string
|
||||||
|
// Arguments to pass to the command
|
||||||
|
Args []string
|
||||||
|
// Execute triggers: if not empty, the command will be executed only if specified file was updated
|
||||||
|
// if empty, the command will be executed always
|
||||||
|
TriggerArtifactFName string
|
||||||
|
// FailOnError defines whether the upgrade should fail if the command fails
|
||||||
|
// true - upgrade will fail if the command fails
|
||||||
|
// false - upgrade will continue even if the command fails
|
||||||
|
FailOnError bool
|
||||||
|
}
|
||||||
|
|
||||||
// Config holds the configuration for the updates module.
|
// Config holds the configuration for the updates module.
|
||||||
type Config struct {
|
type Config struct {
|
||||||
// Name of the updater.
|
// Name of the updater.
|
||||||
@@ -86,6 +104,9 @@ type Config struct {
|
|||||||
// Notify defines whether the user shall be informed about events via notifications.
|
// Notify defines whether the user shall be informed about events via notifications.
|
||||||
// If enabled, disables automatic restart after upgrade.
|
// If enabled, disables automatic restart after upgrade.
|
||||||
Notify bool
|
Notify bool
|
||||||
|
|
||||||
|
// list of shell commands needed to run after the upgrade (if any)
|
||||||
|
PostUpgradeCommands []UpdateCommandConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check looks for obvious configuration errors.
|
// Check looks for obvious configuration errors.
|
||||||
@@ -201,6 +222,7 @@ func New(instance instance, name string, cfg Config) (*Updater, error) {
|
|||||||
module.corruptedInstallation = fmt.Errorf("invalid index: %w", err)
|
module.corruptedInstallation = fmt.Errorf("invalid index: %w", err)
|
||||||
}
|
}
|
||||||
index, err = GenerateIndexFromDir(cfg.Directory, IndexScanConfig{
|
index, err = GenerateIndexFromDir(cfg.Directory, IndexScanConfig{
|
||||||
|
Name: configure.DefaultBinaryIndexName,
|
||||||
Version: info.VersionNumber(),
|
Version: info.VersionNumber(),
|
||||||
})
|
})
|
||||||
if err == nil && index.init(currentPlatform) == nil {
|
if err == nil && index.init(currentPlatform) == nil {
|
||||||
@@ -402,7 +424,7 @@ func (u *Updater) updateAndUpgrade(w *mgr.WorkerCtx, indexURLs []string, ignoreV
|
|||||||
Type: notifications.ActionTypeWebhook,
|
Type: notifications.ActionTypeWebhook,
|
||||||
Payload: notifications.ActionTypeWebhookPayload{
|
Payload: notifications.ActionTypeWebhookPayload{
|
||||||
Method: "POST",
|
Method: "POST",
|
||||||
URL: "updates/apply",
|
URL: "core/restart",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -643,4 +665,5 @@ type instance interface {
|
|||||||
Restart()
|
Restart()
|
||||||
Shutdown()
|
Shutdown()
|
||||||
Notifications() *notifications.Notifications
|
Notifications() *notifications.Notifications
|
||||||
|
UI() *ui.UI
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"slices"
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -24,8 +25,20 @@ func (u *Updater) upgrade(downloader *Downloader, ignoreVersion bool) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If we are running in a UI instance, we need to unload the UI assets
|
||||||
|
if u.instance != nil {
|
||||||
|
u.instance.UI().EnableUpgradeLock()
|
||||||
|
defer u.instance.UI().DisableUpgradeLock()
|
||||||
|
}
|
||||||
|
|
||||||
// Execute the upgrade.
|
// Execute the upgrade.
|
||||||
upgradeError := u.upgradeMoveFiles(downloader)
|
upgradeError := u.upgradeMoveFiles(downloader)
|
||||||
|
if upgradeError == nil {
|
||||||
|
// Files upgraded successfully.
|
||||||
|
// Applying post-upgrade tasks, if any.
|
||||||
|
upgradeError = u.applyPostUpgradeCommands()
|
||||||
|
}
|
||||||
|
|
||||||
if upgradeError == nil {
|
if upgradeError == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -73,6 +86,10 @@ func (u *Updater) upgradeMoveFiles(downloader *Downloader) error {
|
|||||||
if slices.Contains(u.cfg.Ignore, file.Name()) {
|
if slices.Contains(u.cfg.Ignore, file.Name()) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
// ignore PurgeDirectory itself
|
||||||
|
if strings.EqualFold(u.cfg.PurgeDirectory, filepath.Join(u.cfg.Directory, file.Name())) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
// Otherwise, move file to purge dir.
|
// Otherwise, move file to purge dir.
|
||||||
src := filepath.Join(u.cfg.Directory, file.Name())
|
src := filepath.Join(u.cfg.Directory, file.Name())
|
||||||
@@ -199,3 +216,41 @@ func (u *Updater) deleteUnfinishedFiles(dir string) error {
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (u *Updater) applyPostUpgradeCommands() error {
|
||||||
|
// At this point, we assume that the upgrade was successful and all files are in place.
|
||||||
|
// We need to execute the post-upgrade commands, if any.
|
||||||
|
|
||||||
|
if len(u.cfg.PostUpgradeCommands) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// collect full paths to files that were upgraded, required to check the trigger.
|
||||||
|
upgradedFiles := make(map[string]struct{})
|
||||||
|
for _, artifact := range u.index.Artifacts {
|
||||||
|
upgradedFiles[filepath.Join(u.cfg.Directory, artifact.Filename)] = struct{}{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute post-upgrade commands.
|
||||||
|
for _, puCmd := range u.cfg.PostUpgradeCommands {
|
||||||
|
|
||||||
|
// Check trigger to ensure that we need to run this command.
|
||||||
|
if len(puCmd.TriggerArtifactFName) > 0 {
|
||||||
|
if _, ok := upgradedFiles[puCmd.TriggerArtifactFName]; !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debugf("updates/%s: executing post-upgrade command: '%s %s'", u.cfg.Name, puCmd.Command, strings.Join(puCmd.Args, " "))
|
||||||
|
output, err := exec.Command(puCmd.Command, puCmd.Args...).CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
if puCmd.FailOnError {
|
||||||
|
return fmt.Errorf("post-upgrade command '%s %s' failed: %w, output: %s", puCmd.Command, strings.Join(puCmd.Args, " "), err, string(output))
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Warningf("updates/%s: post-upgrade command '%s %s' failed, but ignored. Error: %s", u.cfg.Name, puCmd.Command, strings.Join(puCmd.Args, " "), err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user