Merge pull request #2067 from safing/feature/2050-pause
Feature: pause PM/SPN
This commit is contained in:
@@ -167,6 +167,23 @@ export class PortapiService {
|
||||
});
|
||||
}
|
||||
|
||||
/** Triggers a pause of the portmaster or SPN service
|
||||
* @param duration The duration of the pause in seconds
|
||||
* @param onlySPN Whether or not only the SPN should be paused
|
||||
*/
|
||||
pause(duration: number, onlySPN: boolean): Observable<any> {
|
||||
return this.http.post(`${this.httpEndpoint}/v1/control/pause`, { duration, onlySPN }, {
|
||||
observe: 'response',
|
||||
responseType: 'arraybuffer',
|
||||
});
|
||||
}
|
||||
/** Triggers a resume of the portmaster (and SPN) service */
|
||||
resume(): Observable<any> {
|
||||
return this.http.post(`${this.httpEndpoint}/v1/control/resume`, undefined, {
|
||||
observe: 'response',
|
||||
responseType: 'arraybuffer',
|
||||
});
|
||||
}
|
||||
/** Force the portmaster to check for updates */
|
||||
checkForUpdates(): Observable<any> {
|
||||
return this.http.post(`${this.httpEndpoint}/v1/updates/check`, undefined, {
|
||||
|
||||
@@ -153,7 +153,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="nav-lower-list">
|
||||
<div class="relative link" sfngTipUpTrigger="navTools" sfngTipUpPassive tooltip="Version and Tools"
|
||||
<div class="relative link" sfngTipUpTrigger="navTools" sfngTipUpPassive sfng-tooltip="Version and Tools"
|
||||
sfngTooltipDelay="1000" snfgTooltipPosition="right" (click)="settingsMenu.dropdown.toggle(settingsMenuTrigger)"
|
||||
cdkOverlayOrigin #settingsMenuTrigger="cdkOverlayOrigin" [class.active]="settingsMenu.dropdown.isOpen">
|
||||
|
||||
@@ -202,8 +202,47 @@
|
||||
<app-menu-item (click)="cleanupHistory($event)">Cleanup Network History</app-menu-item>
|
||||
</app-menu>
|
||||
|
||||
<!-- Pause Menu -->
|
||||
<div sfngTipUpTrigger="navPause" sfngTipUpPassive sfng-tooltip="Pause and Resume" sfngTooltipDelay="1000"
|
||||
snfgTooltipPosition="right" class="link" (click)="pauseMenu.dropdown.toggle(pauseMenuTrigger)" cdkOverlayOrigin
|
||||
#pauseMenuTrigger="cdkOverlayOrigin" [class.active]="pauseMenu.dropdown.isOpen">
|
||||
|
||||
<svg viewBox="0 0 24 24" class="help" xmlns="http://www.w3.org/2000/svg">
|
||||
<g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M21 12C21 16.9706 16.9706 21 12 21C7.02944 21 3 16.9706 3 12C3 7.02944 7.02944 3 12 3C16.9706 3 21 7.02944 21 12Z"></path>
|
||||
<path d="M14 9L14 15"></path>
|
||||
<path d="M10 9L10 15"></path>
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
</div>
|
||||
<app-menu #pauseMenu offsetY="0" offsetX="10" overlayClass="rounded-t">
|
||||
<div *ngIf="isPaused">
|
||||
<div class="flex flex-col p-4 text-xxs">
|
||||
<span class="text-secondary">
|
||||
<span class="text-secondary">{{ pauseInfo }} </span>
|
||||
</span>
|
||||
<span class="text-secondary">
|
||||
Auto-resume at <span class="text-secondary">{{ pauseInfoTillTime }} </span>
|
||||
</span>
|
||||
</div>
|
||||
<hr/>
|
||||
</div>
|
||||
|
||||
<!-- we show 'Pause SPN...' items even when isPausedSPN to allow pause time modification-->
|
||||
<app-menu-item (click)="pauseSPN($event, 60*5)" *ngIf="spnEnabled || isPausedSPN" [disabled]="isPausedInterception">Pause SPN for 5 minutes</app-menu-item>
|
||||
<app-menu-item (click)="pauseSPN($event, 60*15)" *ngIf="spnEnabled || isPausedSPN" [disabled]="isPausedInterception">Pause SPN for 15 minutes</app-menu-item>
|
||||
<app-menu-item (click)="pauseSPN($event, 60*60)" *ngIf="spnEnabled || isPausedSPN" [disabled]="isPausedInterception">Pause SPN for 1 hour</app-menu-item>
|
||||
<hr/>
|
||||
<app-menu-item (click)="pause($event, 60*5)">Pause for 5 minutes</app-menu-item>
|
||||
<app-menu-item (click)="pause($event, 60*15)">Pause for 15 minutes</app-menu-item>
|
||||
<app-menu-item (click)="pause($event, 60*60)">Pause for 1 hour</app-menu-item>
|
||||
<hr/>
|
||||
<app-menu-item (click)="resume($event)" [disabled]="!isPaused">Resume now</app-menu-item>
|
||||
</app-menu>
|
||||
|
||||
<!-- Power Menu -->
|
||||
<div sfngTipUpTrigger="navPower" sfngTipUpPassive tooltip="Shutdown and Restart" sfngTooltipDelay="1000"
|
||||
<div sfngTipUpTrigger="navPower" sfngTipUpPassive sfng-tooltip="Shutdown and Restart" sfngTooltipDelay="1000"
|
||||
snfgTooltipPosition="right" class="link" (click)="powerMenu.dropdown.toggle(powerMenuTrigger)" cdkOverlayOrigin
|
||||
#powerMenuTrigger="cdkOverlayOrigin" [class.active]="powerMenu.dropdown.isOpen">
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { INTEGRATION_SERVICE, IntegrationService } from 'src/app/integration';
|
||||
import { ConnectedPosition } from '@angular/cdk/overlay';
|
||||
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, Inject, OnInit, Output, inject } from '@angular/core';
|
||||
import { ConfigService, DebugAPI, PortapiService, SPNService, StringSetting } from '@safing/portmaster-api';
|
||||
import { ConfigService, DebugAPI, PortapiService, SPNService, StringSetting, BoolSetting } from '@safing/portmaster-api';
|
||||
import { tap } from 'rxjs/operators';
|
||||
import { AppComponent } from 'src/app/app.component';
|
||||
import { NotificationType, NotificationsService, StatusService, VersionStatus } from 'src/app/services';
|
||||
import { NotificationType, NotificationsService, StatusService, VersionStatus, GetModuleState, ControlPauseStateData } from 'src/app/services';
|
||||
import { ActionIndicatorService } from 'src/app/shared/action-indicator';
|
||||
import { fadeInAnimation, fadeOutAnimation } from 'src/app/shared/animations';
|
||||
import { ExitService } from 'src/app/shared/exit-screen';
|
||||
@@ -36,12 +36,34 @@ export class NavigationComponent implements OnInit {
|
||||
/** The color to use for the notifcation-available hint (dot) */
|
||||
notificationColor: string = 'text-green-300';
|
||||
|
||||
pauseState: ControlPauseStateData | null = null;
|
||||
get isPaused(): boolean { return this.pauseState?.Interception===true || this.pauseState?.SPN===true; }
|
||||
get isPausedInterception(): boolean { return this.pauseState?.Interception===true; }
|
||||
get isPausedSPN(): boolean { return this.pauseState?.SPN===true; }
|
||||
get pauseInfo(): string {
|
||||
if (this.pauseState?.Interception===true && this.pauseState?.SPN===true)
|
||||
return 'Portmaster and SPN are paused';
|
||||
else if (this.pauseState?.Interception===true)
|
||||
return 'Portmaster is paused';
|
||||
else if (this.pauseState?.SPN===true)
|
||||
return 'SPN is paused';
|
||||
return '';
|
||||
}
|
||||
get pauseInfoTillTime(): string {
|
||||
if (this.isPaused && this.pauseState?.TillTime)
|
||||
return new Date(this.pauseState.TillTime).toLocaleTimeString(undefined, { hour12: false });
|
||||
return '';
|
||||
}
|
||||
|
||||
/** Whether or not we have new, unseen prompts */
|
||||
hasNewPrompts = false;
|
||||
|
||||
/** Whether or not prompting is globally enabled. */
|
||||
globalPromptingEnabled = false;
|
||||
|
||||
/** Whether or not the SPN is currently enabled */
|
||||
spnEnabled = false;
|
||||
|
||||
@Output()
|
||||
sideDashChange = new EventEmitter<'collapsed' | 'expanded' | 'force-overlay'>();
|
||||
|
||||
@@ -93,12 +115,22 @@ export class NavigationComponent implements OnInit {
|
||||
this.cdr.markForCheck();
|
||||
});
|
||||
|
||||
this.statusService.status$.subscribe(status => {
|
||||
this.pauseState = GetModuleState(status, 'Control', 'control:paused')?.Data || null;
|
||||
});
|
||||
|
||||
this.configService.watch<StringSetting>('filter/defaultAction')
|
||||
.subscribe(defaultAction => {
|
||||
this.globalPromptingEnabled = defaultAction === 'ask';
|
||||
this.cdr.markForCheck();
|
||||
})
|
||||
|
||||
this.configService.watch<BoolSetting>("spn/enable")
|
||||
.subscribe(value => {
|
||||
this.spnEnabled = value;
|
||||
this.cdr.markForCheck();
|
||||
});
|
||||
|
||||
this.notificationService.new$
|
||||
.subscribe(notif => {
|
||||
|
||||
@@ -251,6 +283,43 @@ export class NavigationComponent implements OnInit {
|
||||
))
|
||||
}
|
||||
|
||||
pause(event: Event, duration: number) {
|
||||
// prevent default and stop-propagation to avoid
|
||||
// expanding the accordion body.
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
this.portapi.pause(duration, false)
|
||||
.subscribe(this.actionIndicator.httpObserver(
|
||||
'Pausing ...',
|
||||
'Failed to Pause',
|
||||
))
|
||||
}
|
||||
pauseSPN(event: Event, duration: number) {
|
||||
// prevent default and stop-propagation to avoid
|
||||
// expanding the accordion body.
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
this.portapi.pause(duration, true)
|
||||
.subscribe(this.actionIndicator.httpObserver(
|
||||
'Pausing SPN...',
|
||||
'Failed to Pause SPN',
|
||||
))
|
||||
}
|
||||
resume(event: Event) {
|
||||
// prevent default and stop-propagation to avoid
|
||||
// expanding the accordion body.
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
this.portapi.resume()
|
||||
.subscribe(this.actionIndicator.httpObserver(
|
||||
'Resuming ...',
|
||||
'Failed to Resume',
|
||||
))
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* Opens the data-directory of the portmaster installation.
|
||||
|
||||
@@ -14,9 +14,6 @@ export class StatusService {
|
||||
static trackSubsystem: TrackByFunction<Subsystem> = trackById;
|
||||
readonly trackSubsystem = StatusService.trackSubsystem;
|
||||
|
||||
readonly statusPrefix = "runtime:"
|
||||
readonly subsystemPrefix = this.statusPrefix + "subsystems/"
|
||||
|
||||
/**
|
||||
* status$ watches the global core status. It's mutlicasted using a BehaviorSubject so new
|
||||
* subscribers will automatically get the latest version while only one subscription
|
||||
@@ -48,48 +45,4 @@ export class StatusService {
|
||||
SelectedSecurityLevel: securityLevel,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Loads the current status of a subsystem.
|
||||
*
|
||||
* @param name The ID of the subsystem
|
||||
*/
|
||||
getSubsystemStatus(id: string): Observable<Subsystem> {
|
||||
return this.portapi.get(this.subsystemPrefix + id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the current status of all subsystems matching idPrefix.
|
||||
* If idPrefix is an empty string all subsystems are returned.
|
||||
*
|
||||
* @param idPrefix An optional ID prefix to limit the returned subsystems
|
||||
*/
|
||||
querySubsystem(idPrefix: string = ''): Observable<Subsystem[]> {
|
||||
return this.portapi.query<Subsystem>(this.subsystemPrefix + idPrefix)
|
||||
.pipe(
|
||||
map(reply => reply.data),
|
||||
toArray(),
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Watch a subsystem for changes. Completes when the subsystem is
|
||||
* deleted. See {@method PortAPI.watch} for more information.
|
||||
*
|
||||
* @param id The ID of the subsystem to watch.
|
||||
* @param opts Additional options for portapi.watch().
|
||||
*/
|
||||
watchSubsystem(id: string, opts?: WatchOpts): Observable<Subsystem> {
|
||||
return this.portapi.watch(this.subsystemPrefix + id, opts);
|
||||
}
|
||||
|
||||
/**
|
||||
* Watch for subsystem changes
|
||||
*
|
||||
* @param opts Additional options for portapi.sub().
|
||||
*/
|
||||
watchSubsystems(opts?: RetryableOpts): Observable<Subsystem[]> {
|
||||
return this.portapi.watchAll<Subsystem>(this.subsystemPrefix, opts);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ export function getOnlineStatusString(stat: OnlineStatus): string {
|
||||
export interface CoreStatus extends Record {
|
||||
OnlineStatus: OnlineStatus;
|
||||
CaptivePortal: CaptivePortal;
|
||||
// Modules: []ModuleState; // TODO: Do we need all modules?
|
||||
Modules: StateUpdate[]; // TODO: Do we need all modules?
|
||||
WorstState: {
|
||||
Module: string,
|
||||
ID: string,
|
||||
@@ -39,6 +39,20 @@ export interface CoreStatus extends Record {
|
||||
}
|
||||
}
|
||||
|
||||
export interface StateUpdate {
|
||||
Module: string;
|
||||
States: State[];
|
||||
}
|
||||
|
||||
export interface State {
|
||||
ID: string; // Program-unique identifier
|
||||
Name: string; // State name (may serve as notification title)
|
||||
Message?: string; // Detailed message about the state
|
||||
Type?: ModuleStateType; // State type
|
||||
Time?: Date; // Creation time
|
||||
Data?: any; // Additional data for processing
|
||||
}
|
||||
|
||||
export enum ModuleStateType {
|
||||
Undefined = "",
|
||||
Hint = "hint",
|
||||
@@ -110,3 +124,55 @@ export interface VersionStatus extends Record {
|
||||
[key: string]: Resource
|
||||
}
|
||||
}
|
||||
|
||||
function getModuleStates(status: CoreStatus, moduleID: string): State[] {
|
||||
const module = status.Modules?.find(m => m.Module === moduleID);
|
||||
return module?.States || [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a specific state from a module within the CoreStatus.
|
||||
* @param status The CoreStatus object containing module states.
|
||||
* @param moduleID The identifier of the module to search within.
|
||||
* @param stateID The identifier of the state to retrieve.
|
||||
* @returns The State object if found; otherwise, null.
|
||||
* @example
|
||||
* ```typescript
|
||||
* const state = GetModuleState(status, 'Control', 'control:paused');
|
||||
* if (state) {
|
||||
* console.log(`State found: ${state.Name}`);
|
||||
* } else {
|
||||
* console.log('State not found');
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export function GetModuleState(status: CoreStatus, moduleID: string, stateID: string): State | null {
|
||||
const states = getModuleStates(status, moduleID);
|
||||
for (const state of states) {
|
||||
if (state.ID === stateID) {
|
||||
return state;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Data structure for the 'control:paused' state from the 'Control' module.
|
||||
*
|
||||
* This interface defines the expected structure of the Data field when Portmaster
|
||||
* or its components are temporarily paused by the user.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const pausedState = GetModuleState(status, 'Control', 'control:paused');
|
||||
* if (pausedState?.Data) {
|
||||
* const pauseData = pausedState.Data as ControlPauseStateData;
|
||||
* console.log(`SPN paused: ${pauseData.SPN}`);
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export interface ControlPauseStateData {
|
||||
Interception: boolean; // Whether Portmaster interception is paused
|
||||
SPN: boolean; // Whether SPN is paused
|
||||
TillTime: string; // When the pause will end (JSON date as string, has to be converted to Date)
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, DestroyRef, Input, OnInit, inject } from "@angular/core";
|
||||
import { SecurityLevel } from "@safing/portmaster-api";
|
||||
import { combineLatest } from "rxjs";
|
||||
import { StatusService, ModuleStateType } from "src/app/services";
|
||||
import { StatusService, ModuleStateType, GetModuleState, ControlPauseStateData } from "src/app/services";
|
||||
import { fadeInAnimation, fadeOutAnimation } from "../animations";
|
||||
|
||||
interface SecurityOption {
|
||||
@@ -62,6 +62,17 @@ export class SecurityLockComponent implements OnInit {
|
||||
break;
|
||||
}
|
||||
|
||||
// Checking for Control:Paused state
|
||||
const pausedState = GetModuleState(status, 'Control', 'control:paused');
|
||||
if (pausedState?.Data) {
|
||||
const pauseData = pausedState.Data as ControlPauseStateData;
|
||||
if (pauseData.Interception === true) {
|
||||
this.lockLevel.displayText = 'Insecure: PAUSED';
|
||||
} else if (pauseData.SPN === true) {
|
||||
this.lockLevel.displayText = 'Secure (SPN Paused)';
|
||||
}
|
||||
}
|
||||
|
||||
this.cdr.markForCheck();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -99,6 +99,14 @@ navTools:
|
||||
title: Version and Tools
|
||||
content: |
|
||||
View the Portmaster's version and use special actions and tools.
|
||||
nextKey: navPause
|
||||
|
||||
navPause:
|
||||
title: Pause and Resume
|
||||
content: |
|
||||
Temporarily disable Portmaster's protection and network monitoring.
|
||||
|
||||
Choose to pause SPN only or disable all protection completely.
|
||||
nextKey: navPower
|
||||
|
||||
navPower:
|
||||
|
||||
3
desktop/tauri/src-tauri/Cargo.lock
generated
3
desktop/tauri/src-tauri/Cargo.lock
generated
@@ -859,8 +859,10 @@ checksum = "1a7964611d71df112cb1730f2ee67324fcf4d0fc6606acbbe9bfe06df124637c"
|
||||
dependencies = [
|
||||
"android-tzdata",
|
||||
"iana-time-zone",
|
||||
"js-sys",
|
||||
"num-traits",
|
||||
"serde",
|
||||
"wasm-bindgen",
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
@@ -3949,6 +3951,7 @@ version = "2.0.25"
|
||||
dependencies = [
|
||||
"assert_matches",
|
||||
"cached",
|
||||
"chrono",
|
||||
"clap_lex",
|
||||
"ctor",
|
||||
"dark-light",
|
||||
|
||||
@@ -52,6 +52,7 @@ reqwest = { version = "0.12", features = ["cookies", "json"] }
|
||||
|
||||
rfd = { version = "*", default-features = false, features = [ "tokio", "gtk3", "common-controls-v6" ] }
|
||||
open = "5.1.3"
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
|
||||
dark-light = { path = "../rust-dark-light" }
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
pub mod config;
|
||||
pub mod spn;
|
||||
pub mod notification;
|
||||
pub mod subsystem;
|
||||
pub mod system_status_types;
|
||||
@@ -1,45 +0,0 @@
|
||||
#![allow(dead_code)]
|
||||
use serde::*;
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
|
||||
pub struct ModuleStatus {
|
||||
#[serde(rename = "Name")]
|
||||
pub name: String,
|
||||
|
||||
#[serde(rename = "Enabled")]
|
||||
pub enabled: bool,
|
||||
|
||||
#[serde(rename = "Status")]
|
||||
pub status: u8,
|
||||
|
||||
#[serde(rename = "FailureStatus")]
|
||||
pub failure_status: u8,
|
||||
|
||||
#[serde(rename = "FailureID")]
|
||||
pub failure_id: String,
|
||||
|
||||
#[serde(rename = "FailureMsg")]
|
||||
pub failure_msg: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
|
||||
pub struct Subsystem {
|
||||
#[serde(rename = "ID")]
|
||||
pub id: String,
|
||||
|
||||
#[serde(rename = "Name")]
|
||||
pub name: String,
|
||||
|
||||
#[serde(rename = "Description")]
|
||||
pub description: String,
|
||||
|
||||
#[serde(rename = "Modules")]
|
||||
pub module_status: Vec<ModuleStatus>,
|
||||
|
||||
#[serde(rename = "FailureStatus")]
|
||||
pub failure_status: u8,
|
||||
}
|
||||
pub const FAILURE_NONE: u8 = 0;
|
||||
pub const FAILURE_HINT: u8 = 1;
|
||||
pub const FAILURE_WARNING: u8 = 2;
|
||||
pub const FAILURE_ERROR: u8 = 3;
|
||||
@@ -0,0 +1,81 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum StateType {
|
||||
#[serde(rename = "")]
|
||||
Undefined,
|
||||
#[serde(rename = "hint")]
|
||||
Hint,
|
||||
#[serde(rename = "warning")]
|
||||
Warning,
|
||||
#[serde(rename = "error")]
|
||||
Error,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct State {
|
||||
#[serde(rename = "ID")]
|
||||
pub id: String,
|
||||
#[serde(rename = "Name")]
|
||||
pub name: String,
|
||||
#[serde(rename = "Message")]
|
||||
pub message: Option<String>,
|
||||
#[serde(rename = "Type")]
|
||||
pub state_type: Option<StateType>,
|
||||
#[serde(rename = "Time")]
|
||||
pub time: Option<String>, // time.Time serialized by GoLang
|
||||
#[serde(rename = "Data")]
|
||||
pub data: Option<serde_json::Value>, // any type
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct StateUpdate {
|
||||
#[serde(rename = "Module")]
|
||||
pub module: String,
|
||||
#[serde(rename = "States")]
|
||||
pub states: Option<Vec<State>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct WorstState {
|
||||
#[serde(rename = "Module")]
|
||||
pub module: String,
|
||||
#[serde(flatten)]
|
||||
pub state: State,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct SystemStatus {
|
||||
#[serde(rename = "Modules")]
|
||||
pub modules: Vec<StateUpdate>,
|
||||
#[serde(rename = "WorstState")]
|
||||
pub worst_state: Option<WorstState>,
|
||||
|
||||
// add more fields when needed
|
||||
// ...
|
||||
}
|
||||
|
||||
// PauseInfo represents pause status data from "control:paused" state in Control module
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct PauseInfo {
|
||||
#[serde(rename = "Interception")]
|
||||
pub interception: bool,
|
||||
#[serde(rename = "SPN")]
|
||||
pub spn: bool,
|
||||
#[serde(rename = "TillTime")]
|
||||
pub till_time: String, // time.Time serialized as string by GoLang
|
||||
}
|
||||
|
||||
|
||||
impl SystemStatus {
|
||||
pub fn get_module_state(&self, module_name: &str, state_id: &str) -> Option<&State> {
|
||||
if let Some(module) = self.modules.iter().find(|m| m.module == module_name) {
|
||||
if let Some(states) = &module.states {
|
||||
return states.iter().find(|s| s.id == state_id);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
}
|
||||
@@ -217,6 +217,47 @@ impl<R: Runtime> PortmasterInterface<R> {
|
||||
});
|
||||
}
|
||||
|
||||
pub fn set_resume(&self) {
|
||||
tauri::async_runtime::spawn(async move {
|
||||
let client = reqwest::Client::new();
|
||||
match client
|
||||
.post(format!("{}control/resume", PORTMASTER_BASE_URL))
|
||||
.send()
|
||||
.await
|
||||
{
|
||||
Ok(v) => {
|
||||
debug!("resume request sent {:?}", v);
|
||||
}
|
||||
Err(err) => {
|
||||
error!("failed to send resume request {}", err);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
pub fn set_pause(&self, duration_seconds: u64, spn_only: bool) {
|
||||
tauri::async_runtime::spawn(async move {
|
||||
let client = reqwest::Client::new();
|
||||
match client
|
||||
.post(format!("{}control/pause", PORTMASTER_BASE_URL))
|
||||
.json(&serde_json::json!({
|
||||
"duration": duration_seconds,
|
||||
"onlySPN": spn_only
|
||||
}))
|
||||
.send()
|
||||
.await
|
||||
{
|
||||
Ok(v) => {
|
||||
debug!("pause request sent {:?}", v);
|
||||
}
|
||||
Err(err) => {
|
||||
error!("failed to send pause request {}", err);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
//// Internal functions
|
||||
fn start_notification_handler(&self) {
|
||||
if let Some(api) = self.get_api() {
|
||||
|
||||
@@ -134,8 +134,10 @@ pub async fn show_notification(cli: &PortAPI, key: String, n: Notification) {
|
||||
))
|
||||
.await;
|
||||
});
|
||||
}
|
||||
} else {
|
||||
error!("Notification clicked but no action associated.");
|
||||
// TODO(vladimir): If Action is None, the user clicked on the notification. Focus on the UI.
|
||||
}
|
||||
Ok(())
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
use std::ops::Deref;
|
||||
use std::sync::atomic::AtomicBool;
|
||||
use std::sync::RwLock;
|
||||
use std::{collections::HashMap, sync::atomic::Ordering};
|
||||
use std::{sync::atomic::Ordering};
|
||||
use chrono::{DateTime};
|
||||
|
||||
use log::{debug, error};
|
||||
use tauri::{
|
||||
@@ -16,11 +17,11 @@ use crate::config;
|
||||
use crate::{
|
||||
portapi::{
|
||||
client::PortAPI,
|
||||
message::ParseError,
|
||||
message::{ParseError},
|
||||
models::{
|
||||
config::BooleanValue,
|
||||
spn::SPNStatus,
|
||||
subsystem::{self, Subsystem},
|
||||
system_status_types::{self, SystemStatus},
|
||||
},
|
||||
types::{Request, Response},
|
||||
},
|
||||
@@ -59,6 +60,16 @@ const FORCE_SHOW_KEY: &str = "force-show";
|
||||
const PM_TRAY_ICON_ID: &str = "pm_icon";
|
||||
const PM_TRAY_MENU_ID: &str = "pm_tray_menu";
|
||||
|
||||
const PAUSE_SPN_5_KEY: &str = "pause_spn_5";
|
||||
const PAUSE_SPN_15_KEY: &str = "pause_spn_15";
|
||||
const PAUSE_SPN_60_KEY: &str = "pause_spn_60";
|
||||
const PAUSE_PM_5_KEY: &str = "pause_pm_5";
|
||||
const PAUSE_PM_15_KEY: &str = "pause_pm_15";
|
||||
const PAUSE_PM_60_KEY: &str = "pause_pm_60";
|
||||
const RESUME_KEY: &str = "resume_all";
|
||||
const PAUSE_INFO_KEY: &str = "pause_info";
|
||||
const PAUSE_INFO_TIME_KEY: &str = "pause_info_time";
|
||||
|
||||
// Icons
|
||||
|
||||
fn get_theme_mode() -> dark_light::Mode {
|
||||
@@ -125,6 +136,7 @@ fn build_tray_menu(
|
||||
app: &tauri::AppHandle,
|
||||
status: &str,
|
||||
spn_status_text: &str,
|
||||
pause_info: &system_status_types::PauseInfo,
|
||||
) -> core::result::Result<ContextMenu, Box<dyn std::error::Error>> {
|
||||
load_theme(app);
|
||||
|
||||
@@ -132,26 +144,51 @@ fn build_tray_menu(
|
||||
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))
|
||||
// Global status
|
||||
let global_status_text = if pause_info.interception {
|
||||
format!("Status: {} (PAUSED)", status)
|
||||
} else {
|
||||
format!("Status: {}", status)
|
||||
};
|
||||
let global_status = MenuItemBuilder::with_id(GLOBAL_STATUS_KEY, global_status_text)
|
||||
.enabled(false)
|
||||
.build(app)
|
||||
.unwrap();
|
||||
|
||||
// Setup SPN status
|
||||
let spn_status = MenuItemBuilder::with_id(SPN_STATUS_KEY, format!("SPN: {}", spn_status_text))
|
||||
.enabled(false)
|
||||
.build(app)
|
||||
.unwrap();
|
||||
// Pause items
|
||||
let (pause_status_item, pause_status_time_item, resume_item) = if pause_info.interception || pause_info.spn {
|
||||
let status_text = match (pause_info.interception, pause_info.spn) {
|
||||
(true, true) => "Portmaster and SPN are paused",
|
||||
(true, false) => "Portmaster is paused",
|
||||
(false, true) => "SPN is paused",
|
||||
_ => unreachable!(), // We already checked at least one is true
|
||||
};
|
||||
let status_item = MenuItemBuilder::with_id(PAUSE_INFO_KEY, status_text).enabled(false).build(app)?;
|
||||
|
||||
let formatted_time = DateTime::parse_from_rfc3339(&pause_info.till_time)
|
||||
.map(|dt| dt.with_timezone(&chrono::Local).format("%H:%M:%S").to_string())
|
||||
.unwrap_or_else(|_| pause_info.till_time.clone());
|
||||
let time_item = MenuItemBuilder::with_id(PAUSE_INFO_TIME_KEY, format!("Auto-resume at {}", formatted_time)).enabled(false).build(app)?;
|
||||
let resume_item = MenuItemBuilder::with_id(RESUME_KEY, "Resume now").build(app)?;
|
||||
(Some(status_item), Some(time_item), Some(resume_item))
|
||||
} else {
|
||||
(None, None, None)
|
||||
};
|
||||
|
||||
// Setup SPN button
|
||||
let spn_button_text = match spn_status_text {
|
||||
"disabled" => "Enable SPN",
|
||||
_ => "Disable SPN",
|
||||
};
|
||||
let spn_status = MenuItemBuilder::with_id(SPN_STATUS_KEY, format!("SPN: {}", spn_status_text))
|
||||
.enabled(false)
|
||||
.build(app)
|
||||
.unwrap();
|
||||
let spn_button = MenuItemBuilder::with_id(SPN_BUTTON_KEY, spn_button_text)
|
||||
.build(app)
|
||||
.unwrap();
|
||||
|
||||
// Setup Icon theme submenu
|
||||
let system_theme = MenuItemBuilder::with_id(SYSTEM_THEME_KEY, "System")
|
||||
.build(app)
|
||||
.unwrap();
|
||||
@@ -165,27 +202,74 @@ fn build_tray_menu(
|
||||
.items(&[&system_theme, &light_theme, &dark_theme])
|
||||
.build()?;
|
||||
|
||||
|
||||
// Setup Pause/Resume menu items
|
||||
let disabled_spn_pause = (spn_status_text == "disabled" && !pause_info.spn) || pause_info.interception;
|
||||
let pause_spn_5min_item = MenuItemBuilder::with_id(PAUSE_SPN_5_KEY, "Pause SPN for 5 minutes").enabled(!disabled_spn_pause).build(app)?;
|
||||
let pause_spn_15min_item = MenuItemBuilder::with_id(PAUSE_SPN_15_KEY, "Pause SPN for 15 minutes").enabled(!disabled_spn_pause).build(app)?;
|
||||
let pause_spn_1hour_item = MenuItemBuilder::with_id(PAUSE_SPN_60_KEY, "Pause SPN for 1 hour").enabled(!disabled_spn_pause).build(app)?;
|
||||
|
||||
let pause_pm_5min_item = MenuItemBuilder::with_id(PAUSE_PM_5_KEY, "Pause for 5 minutes").build(app)?;
|
||||
let pause_pm_15min_item = MenuItemBuilder::with_id(PAUSE_PM_15_KEY, "Pause for 15 minutes").build(app)?;
|
||||
let pause_pm_1hour_item = MenuItemBuilder::with_id(PAUSE_PM_60_KEY, "Pause for 1 hour").build(app)?;
|
||||
|
||||
let pause_menu = SubmenuBuilder::new(app, "Pause")
|
||||
.items(&[
|
||||
&pause_spn_5min_item,
|
||||
&pause_spn_15min_item,
|
||||
&pause_spn_1hour_item,
|
||||
&PredefinedMenuItem::separator(app)?,
|
||||
&pause_pm_5min_item,
|
||||
&pause_pm_15min_item,
|
||||
&pause_pm_1hour_item,
|
||||
])
|
||||
.build()?;
|
||||
|
||||
/* DEV MENU
|
||||
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()?;
|
||||
*/
|
||||
|
||||
// Assemble menu items
|
||||
let s = PredefinedMenuItem::separator(app)?;
|
||||
let mut items: Vec<&dyn tauri::menu::IsMenuItem<Wry>> = Vec::new();
|
||||
|
||||
|
||||
|
||||
items.push(&global_status);
|
||||
items.push(&s);
|
||||
|
||||
if let Some(ref pause_status_item) = pause_status_item {
|
||||
items.push(pause_status_item);
|
||||
}
|
||||
if let Some(ref pause_status_time_item) = pause_status_time_item {
|
||||
items.push(pause_status_time_item);
|
||||
}
|
||||
if let Some(ref resume_item) = resume_item {
|
||||
items.push(resume_item);
|
||||
}
|
||||
items.push(&pause_menu);
|
||||
items.push(&s);
|
||||
|
||||
items.push(&spn_status);
|
||||
items.push(&spn_button);
|
||||
items.push(&s);
|
||||
|
||||
items.push(&theme_menu);
|
||||
items.push(&s);
|
||||
|
||||
items.push(&open_btn);
|
||||
items.push(&s);
|
||||
|
||||
items.push(&exit_ui_btn);
|
||||
items.push(&shutdown_btn);
|
||||
//items.push(&developer_menu);
|
||||
|
||||
let menu = MenuBuilder::with_id(app, PM_TRAY_MENU_ID)
|
||||
.items(&[
|
||||
&open_btn,
|
||||
&PredefinedMenuItem::separator(app)?,
|
||||
&global_status,
|
||||
&PredefinedMenuItem::separator(app)?,
|
||||
&spn_status,
|
||||
&spn_button,
|
||||
&PredefinedMenuItem::separator(app)?,
|
||||
&theme_menu,
|
||||
&PredefinedMenuItem::separator(app)?,
|
||||
&exit_ui_btn,
|
||||
&shutdown_btn,
|
||||
&developer_menu,
|
||||
])
|
||||
.items(&items)
|
||||
.build()?;
|
||||
|
||||
return Ok(menu);
|
||||
@@ -194,7 +278,7 @@ fn build_tray_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 menu = build_tray_menu(app.handle(), "unknown", "disabled", &system_status_types::PauseInfo::default())?;
|
||||
|
||||
let icon = TrayIconBuilder::with_id(PM_TRAY_ICON_ID)
|
||||
.icon(Image::from_bytes(get_red_icon()).unwrap())
|
||||
@@ -250,6 +334,15 @@ pub fn setup_tray_menu(
|
||||
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),
|
||||
|
||||
PAUSE_SPN_5_KEY => app.portmaster().set_pause(60*5, true),
|
||||
PAUSE_SPN_15_KEY => app.portmaster().set_pause(60*15, true),
|
||||
PAUSE_SPN_60_KEY => app.portmaster().set_pause(60*60, true),
|
||||
PAUSE_PM_5_KEY => app.portmaster().set_pause(60*5, false),
|
||||
PAUSE_PM_15_KEY => app.portmaster().set_pause(60*15, false),
|
||||
PAUSE_PM_60_KEY => app.portmaster().set_pause(60*60, false),
|
||||
RESUME_KEY => app.portmaster().set_resume(),
|
||||
|
||||
other => {
|
||||
error!("unknown menu event id: {}", other);
|
||||
}
|
||||
@@ -275,36 +368,35 @@ pub fn setup_tray_menu(
|
||||
Ok(icon)
|
||||
}
|
||||
|
||||
pub fn update_icon(icon: AppIcon, subsystems: HashMap<String, Subsystem>, 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()),
|
||||
|mut acc, s| {
|
||||
for m in s {
|
||||
if m.failure_status > acc.0 {
|
||||
acc = (m.failure_status, m.failure_msg.clone())
|
||||
}
|
||||
}
|
||||
acc
|
||||
},
|
||||
);
|
||||
pub fn update_icon(icon: AppIcon, system_status: SystemStatus, spn_status: String) {
|
||||
// Extract the worst state type
|
||||
let worst_state_type = system_status.worst_state
|
||||
.as_ref()
|
||||
.and_then(|ws| ws.state.state_type.clone())
|
||||
.unwrap_or(system_status_types::StateType::Undefined);
|
||||
|
||||
let mut status = "Secured".to_owned();
|
||||
|
||||
if failure.0 != subsystem::FAILURE_NONE {
|
||||
status = failure.1;
|
||||
}
|
||||
|
||||
let icon_color = match failure.0 {
|
||||
subsystem::FAILURE_WARNING => IconColor::Yellow,
|
||||
subsystem::FAILURE_ERROR => IconColor::Red,
|
||||
_ => match spn_status.as_str() {
|
||||
// Determine status and icon color in a single match expression
|
||||
let (status, icon_color) = match worst_state_type {
|
||||
system_status_types::StateType::Error => ("Insecure", IconColor::Red),
|
||||
system_status_types::StateType::Warning => ("Insecure", IconColor::Yellow),
|
||||
_ => {
|
||||
let color = match spn_status.as_str() {
|
||||
"connected" | "connecting" => IconColor::Blue,
|
||||
_ => IconColor::Green,
|
||||
},
|
||||
};
|
||||
("Secured", color)
|
||||
}
|
||||
};
|
||||
|
||||
if let Ok(menu) = build_tray_menu(icon.app_handle(), status.as_ref(), spn_status.as_str()) {
|
||||
// Extract pause info from system status
|
||||
let pause_info = system_status
|
||||
.get_module_state("Control", "control:paused")
|
||||
.and_then(|state| state.data.as_ref())
|
||||
.and_then(|data| serde_json::from_value::<system_status_types::PauseInfo>(data.clone()).ok())
|
||||
.unwrap_or_default();
|
||||
|
||||
// Rebuild and set the tray menu
|
||||
if let Ok(menu) = build_tray_menu(icon.app_handle(), status, spn_status.as_str(), &pause_info) {
|
||||
if let Err(err) = icon.set_menu(Some(menu)) {
|
||||
error!("failed to set menu on tray icon: {}", err.to_string());
|
||||
}
|
||||
@@ -322,16 +414,16 @@ pub async fn tray_handler(cli: PortAPI, app: tauri::AppHandle) {
|
||||
}
|
||||
};
|
||||
|
||||
let mut subsystem_subscription = match cli
|
||||
let mut system_status_subscription = match cli
|
||||
.request(Request::QuerySubscribe(
|
||||
"query runtime:subsystems/".to_string(),
|
||||
"query runtime:system/status".to_string(),
|
||||
))
|
||||
.await
|
||||
{
|
||||
Ok(rx) => rx,
|
||||
Err(err) => {
|
||||
error!(
|
||||
"cancel try_handler: failed to subscribe to 'runtime:subsystems': {}",
|
||||
"cancel try_handler: failed to subscribe to 'runtime:system/status': {}",
|
||||
err
|
||||
);
|
||||
return;
|
||||
@@ -388,12 +480,12 @@ pub async fn tray_handler(cli: PortAPI, app: tauri::AppHandle) {
|
||||
|
||||
update_icon_color(&icon, IconColor::Blue);
|
||||
|
||||
let mut subsystems: HashMap<String, Subsystem> = HashMap::new();
|
||||
let mut system_status = SystemStatus::default();
|
||||
let mut spn_status: String = "".to_string();
|
||||
|
||||
loop {
|
||||
tokio::select! {
|
||||
msg = subsystem_subscription.recv() => {
|
||||
msg = system_status_subscription.recv() => {
|
||||
let msg = match msg {
|
||||
Some(m) => m,
|
||||
None => { break }
|
||||
@@ -407,17 +499,17 @@ pub async fn tray_handler(cli: PortAPI, app: tauri::AppHandle) {
|
||||
};
|
||||
|
||||
if let Some((_, payload)) = res {
|
||||
match payload.parse::<Subsystem>() {
|
||||
Ok(n) => {
|
||||
subsystems.insert(n.id.clone(), n);
|
||||
update_icon(icon.clone(), subsystems.clone(), spn_status.clone());
|
||||
match payload.parse::<SystemStatus>() {
|
||||
Ok(system_status_update) => {
|
||||
system_status.clone_from(&system_status_update);
|
||||
update_icon(icon.clone(), system_status.clone(), spn_status.clone());
|
||||
},
|
||||
Err(err) => match err {
|
||||
ParseError::Json(err) => {
|
||||
error!("failed to parse subsystem: {}", err);
|
||||
error!("failed to parse SystemStatus: {}", err);
|
||||
}
|
||||
_ => {
|
||||
error!("unknown error when parsing notifications payload");
|
||||
error!("unknown error when parsing SystemStatus payload");
|
||||
}
|
||||
},
|
||||
}
|
||||
@@ -441,7 +533,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(), subsystems.clone(), spn_status.clone());
|
||||
update_icon(icon.clone(), system_status.clone(), spn_status.clone());
|
||||
},
|
||||
Err(err) => match err {
|
||||
ParseError::Json(err) => {
|
||||
@@ -502,7 +594,18 @@ pub async fn tray_handler(cli: PortAPI, app: tauri::AppHandle) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
update_icon_nostate(icon.clone());
|
||||
}
|
||||
|
||||
pub fn update_icon_nostate(icon: AppIcon) {
|
||||
update_icon_color(&icon, IconColor::Red);
|
||||
|
||||
if let Ok(menu) = build_tray_menu(icon.app_handle(), "unknown", "unknown", &system_status_types::PauseInfo::default()) {
|
||||
if let Err(err) = icon.set_menu(Some(menu)) {
|
||||
error!("failed to set menu on tray icon: {}", err.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn update_icon_color(icon: &AppIcon, new_color: IconColor) {
|
||||
|
||||
@@ -78,12 +78,20 @@ func prep() error {
|
||||
}
|
||||
|
||||
func start() error {
|
||||
// Create fresh worker managers on Start() to enable clean restarts after Stop().
|
||||
// (after module.Manager().Stop() has been called, all workers are stopped and cannot be reused)
|
||||
module.selfcheckWorkerMgr = module.Manager().NewWorkerMgr("compatibility self-check", selfcheckTaskFunc, nil)
|
||||
module.cleanNotifyThresholdWorkerMgr = module.Manager().NewWorkerMgr("clean notify thresholds", cleanNotifyThreshold, nil)
|
||||
|
||||
startNotify()
|
||||
|
||||
selfcheckNetworkChangedFlag.Refresh()
|
||||
|
||||
// Schedule periodic self-checks.
|
||||
module.selfcheckWorkerMgr.Repeat(5 * time.Minute).Delay(selfcheckTaskRetryAfter)
|
||||
module.cleanNotifyThresholdWorkerMgr.Repeat(1 * time.Hour)
|
||||
|
||||
// Add network change callback to trigger immediate self-check.
|
||||
module.instance.NetEnv().EventNetworkChange.AddCallback("trigger compat self-check", func(_ *mgr.WorkerCtx, _ struct{}) (bool, error) {
|
||||
module.selfcheckWorkerMgr.Delay(selfcheckTaskRetryAfter)
|
||||
return false, nil
|
||||
@@ -92,13 +100,12 @@ func start() error {
|
||||
}
|
||||
|
||||
func stop() error {
|
||||
// selfcheckTask.Cancel()
|
||||
// selfcheckTask = nil
|
||||
|
||||
resetSelfCheckState()
|
||||
return nil
|
||||
}
|
||||
|
||||
func selfcheckTaskFunc(wc *mgr.WorkerCtx) error {
|
||||
|
||||
// Create tracing logger.
|
||||
ctx, tracer := log.AddTracer(wc.Ctx())
|
||||
defer tracer.Submit()
|
||||
@@ -138,12 +145,16 @@ func selfcheckTaskFunc(wc *mgr.WorkerCtx) error {
|
||||
}
|
||||
|
||||
// Reset self-check state.
|
||||
resetSelfCheckState()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func resetSelfCheckState() {
|
||||
selfcheckNetworkChangedFlag.Refresh()
|
||||
selfCheckIsFailing.UnSet()
|
||||
selfcheckFails = 0
|
||||
resetSystemIssue()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// SelfCheckIsFailing returns whether the self check is currently failing.
|
||||
@@ -167,10 +178,6 @@ func New(instance instance) (*Compat, error) {
|
||||
module = &Compat{
|
||||
mgr: m,
|
||||
instance: instance,
|
||||
|
||||
selfcheckWorkerMgr: m.NewWorkerMgr("compatibility self-check", selfcheckTaskFunc, nil),
|
||||
cleanNotifyThresholdWorkerMgr: m.NewWorkerMgr("clean notify thresholds", cleanNotifyThreshold, nil),
|
||||
|
||||
states: mgr.NewStateMgr(m),
|
||||
}
|
||||
if err := prep(); err != nil {
|
||||
|
||||
86
service/control/api.go
Normal file
86
service/control/api.go
Normal file
@@ -0,0 +1,86 @@
|
||||
package control
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/safing/portmaster/base/api"
|
||||
)
|
||||
|
||||
const (
|
||||
APIEndpointPause = "control/pause"
|
||||
APIEndpointResume = "control/resume"
|
||||
)
|
||||
|
||||
type pauseRequestParams struct {
|
||||
Duration int `json:"duration"` // Duration in seconds
|
||||
OnlySPN bool `json:"onlySPN"` // Whether to pause only the SPN service
|
||||
}
|
||||
|
||||
func (c *Control) registerAPIEndpoints() error {
|
||||
|
||||
if err := api.RegisterEndpoint(api.Endpoint{
|
||||
Path: APIEndpointPause,
|
||||
Write: api.PermitAdmin,
|
||||
ActionFunc: c.handlePause,
|
||||
Name: "Pause Portmaster",
|
||||
Description: "Pause the Portmaster Core Service.",
|
||||
Parameters: []api.Parameter{
|
||||
{
|
||||
Method: http.MethodPost,
|
||||
Field: "duration",
|
||||
Description: "Specify the duration to pause the service in seconds.",
|
||||
},
|
||||
{
|
||||
Method: http.MethodPost,
|
||||
Field: "onlySPN",
|
||||
Value: "false",
|
||||
Description: "Specify whether to pause only the SPN service.",
|
||||
}},
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := api.RegisterEndpoint(api.Endpoint{
|
||||
Path: APIEndpointResume,
|
||||
Write: api.PermitAdmin,
|
||||
ActionFunc: c.handleResume,
|
||||
Name: "Resume Portmaster",
|
||||
Description: "Resume the Portmaster Core Service.",
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Control) handlePause(r *api.Request) (msg string, err error) {
|
||||
params := pauseRequestParams{}
|
||||
if r.InputData != nil {
|
||||
if err := json.Unmarshal(r.InputData, ¶ms); err != nil {
|
||||
return "Bad Request: invalid input data", err
|
||||
}
|
||||
}
|
||||
|
||||
if params.OnlySPN {
|
||||
c.mgr.Info(fmt.Sprintf("Received SPN Pause(%v) action request ", params.Duration))
|
||||
} else {
|
||||
c.mgr.Info(fmt.Sprintf("Received Pause(%v) action request ", params.Duration))
|
||||
}
|
||||
|
||||
if err := c.pause(time.Duration(params.Duration)*time.Second, params.OnlySPN); err != nil {
|
||||
return "Failed to pause", err
|
||||
}
|
||||
return "Pause initiated", nil
|
||||
}
|
||||
|
||||
func (c *Control) handleResume(_ *api.Request) (msg string, err error) {
|
||||
c.mgr.Info("Received Resume action request")
|
||||
|
||||
if err := c.resume(); err != nil {
|
||||
return "Failed to resume", err
|
||||
}
|
||||
return "Resume initiated", nil
|
||||
}
|
||||
80
service/control/module.go
Normal file
80
service/control/module.go
Normal file
@@ -0,0 +1,80 @@
|
||||
package control
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/safing/portmaster/base/config"
|
||||
"github.com/safing/portmaster/base/notifications"
|
||||
"github.com/safing/portmaster/service/mgr"
|
||||
)
|
||||
|
||||
type PauseInfo struct {
|
||||
Interception bool // Whether Portmaster interception is paused
|
||||
SPN bool // Whether SPN is paused
|
||||
TillTime time.Time // When the pause will end
|
||||
}
|
||||
|
||||
type Control struct {
|
||||
mgr *mgr.Manager
|
||||
instance instance
|
||||
states *mgr.StateMgr
|
||||
|
||||
locker sync.Mutex
|
||||
resumeWorker *mgr.WorkerMgr
|
||||
pauseNotification *notifications.Notification
|
||||
pauseInfo PauseInfo
|
||||
}
|
||||
|
||||
type instance interface {
|
||||
Config() *config.Config
|
||||
InterceptionGroup() *mgr.GroupModule
|
||||
IsShuttingDown() bool
|
||||
}
|
||||
|
||||
var (
|
||||
singleton atomic.Bool
|
||||
)
|
||||
|
||||
func New(instance instance) (*Control, error) {
|
||||
if !singleton.CompareAndSwap(false, true) {
|
||||
return nil, fmt.Errorf("control: New failed: instance already created")
|
||||
}
|
||||
|
||||
m := mgr.New("Control")
|
||||
module := &Control{
|
||||
mgr: m,
|
||||
instance: instance,
|
||||
states: mgr.NewStateMgr(m),
|
||||
}
|
||||
if err := module.prep(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return module, nil
|
||||
}
|
||||
|
||||
func (c *Control) Manager() *mgr.Manager {
|
||||
return c.mgr
|
||||
}
|
||||
|
||||
func (u *Control) States() *mgr.StateMgr {
|
||||
return u.states
|
||||
}
|
||||
|
||||
func (c *Control) Start() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Control) Stop() error {
|
||||
c.locker.Lock()
|
||||
defer c.locker.Unlock()
|
||||
c.stopResumeWorker()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Control) prep() error {
|
||||
return c.registerAPIEndpoints()
|
||||
}
|
||||
230
service/control/pause.go
Normal file
230
service/control/pause.go
Normal file
@@ -0,0 +1,230 @@
|
||||
package control
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/safing/portmaster/base/config"
|
||||
"github.com/safing/portmaster/base/notifications"
|
||||
"github.com/safing/portmaster/service/mgr"
|
||||
)
|
||||
|
||||
func (c *Control) pause(duration time.Duration, onlySPN bool) (retErr error) {
|
||||
if c.instance.IsShuttingDown() {
|
||||
c.mgr.Debug("Cannot pause: system is shutting down")
|
||||
return nil
|
||||
}
|
||||
|
||||
c.locker.Lock()
|
||||
defer c.locker.Unlock()
|
||||
|
||||
defer func() {
|
||||
// update states after pause attempt
|
||||
c.updateStatesAndNotify()
|
||||
// log error if pause failed
|
||||
if retErr != nil {
|
||||
c.mgr.Error("Failed to pause: " + retErr.Error())
|
||||
}
|
||||
}()
|
||||
|
||||
if duration <= 0 {
|
||||
return errors.New("invalid pause duration")
|
||||
}
|
||||
|
||||
spn_enabled := config.GetAsBool("spn/enable", false)
|
||||
if onlySPN {
|
||||
if c.pauseInfo.Interception {
|
||||
return errors.New("cannot pause SPN separately when core is paused")
|
||||
}
|
||||
// If SPN is not running and not already paused, cannot pause it or change pause duration.
|
||||
if !spn_enabled() && !c.pauseInfo.SPN {
|
||||
return errors.New("cannot pause SPN when it is not running")
|
||||
}
|
||||
}
|
||||
|
||||
// Stop resume worker if running and start a new one later.
|
||||
c.stopResumeWorker()
|
||||
defer func() {
|
||||
if retErr == nil {
|
||||
// start new resume worker (with new duration) if no error
|
||||
c.startResumeWorker(duration)
|
||||
}
|
||||
}()
|
||||
|
||||
// Pause SPN if not already paused.
|
||||
if !c.pauseInfo.SPN {
|
||||
if spn_enabled() {
|
||||
// TODO: the 'pause' state must not make permanent config changes.
|
||||
// Consider possibility to not store permanent config changes.
|
||||
// E.g. SPN enabled -> pause SPN -> restart PC/Portmaster -> SPN should be enabled again.
|
||||
config.SetConfigOption("spn/enable", false)
|
||||
c.mgr.Info("SPN paused")
|
||||
c.pauseInfo.SPN = true
|
||||
}
|
||||
}
|
||||
|
||||
if onlySPN {
|
||||
return nil
|
||||
}
|
||||
|
||||
if !c.pauseInfo.Interception {
|
||||
if err := c.instance.InterceptionGroup().Stop(); err != nil {
|
||||
return err
|
||||
}
|
||||
c.mgr.Info("interception paused")
|
||||
c.pauseInfo.Interception = true
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Control) resume() (retErr error) {
|
||||
if c.instance.IsShuttingDown() {
|
||||
c.mgr.Debug("Cannot resume: system is shutting down")
|
||||
return nil
|
||||
}
|
||||
|
||||
c.locker.Lock()
|
||||
defer c.locker.Unlock()
|
||||
|
||||
defer func() {
|
||||
// update states after resume attempt
|
||||
c.updateStatesAndNotify()
|
||||
// log error if resume failed
|
||||
if retErr != nil {
|
||||
c.mgr.Error("Failed to resume: " + retErr.Error())
|
||||
}
|
||||
}()
|
||||
|
||||
c.stopResumeWorker()
|
||||
|
||||
if c.pauseInfo.Interception {
|
||||
if err := c.instance.InterceptionGroup().Start(); err != nil {
|
||||
return err
|
||||
}
|
||||
c.mgr.Info("interception resumed")
|
||||
c.pauseInfo.Interception = false
|
||||
}
|
||||
|
||||
if c.pauseInfo.SPN {
|
||||
enabled := config.GetAsBool("spn/enable", false)
|
||||
if !enabled() {
|
||||
config.SetConfigOption("spn/enable", true)
|
||||
c.mgr.Info("SPN resumed")
|
||||
}
|
||||
c.pauseInfo.SPN = false
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// stopResumeWorker stops any existing resume worker.
|
||||
// No thread safety, caller must hold c.locker.
|
||||
func (c *Control) stopResumeWorker() {
|
||||
c.pauseInfo.TillTime = time.Time{}
|
||||
|
||||
if c.resumeWorker != nil {
|
||||
c.resumeWorker.Stop()
|
||||
c.resumeWorker = nil
|
||||
}
|
||||
}
|
||||
|
||||
// startResumeWorker starts a worker that will resume normal operation after the specified duration.
|
||||
// No thread safety, caller must hold c.locker.
|
||||
func (c *Control) startResumeWorker(duration time.Duration) {
|
||||
c.pauseInfo.TillTime = time.Now().Add(duration)
|
||||
|
||||
resumerWorkerFunc := func(wc *mgr.WorkerCtx) error {
|
||||
wc.Info(fmt.Sprintf("Scheduling resume in %v", duration))
|
||||
|
||||
// Subscribe to config changes to detect SPN enable.
|
||||
cfgChangeEvt := c.instance.Config().EventConfigChange.Subscribe("control: spn enable check", 10)
|
||||
// Make sure to cancel subscription when worker stops.
|
||||
defer cfgChangeEvt.Cancel()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-wc.Ctx().Done():
|
||||
return nil
|
||||
case <-cfgChangeEvt.Events():
|
||||
spnEnabled := config.GetAsBool("spn/enable", false)
|
||||
if spnEnabled() {
|
||||
wc.Info("SPN enabled by user, resuming...")
|
||||
return c.resume()
|
||||
}
|
||||
case <-time.After(duration):
|
||||
wc.Info("Resuming...")
|
||||
|
||||
err := c.resume()
|
||||
if err == nil {
|
||||
n := ¬ifications.Notification{
|
||||
EventID: "control:resumed",
|
||||
Type: notifications.Info,
|
||||
Title: "Resumed",
|
||||
Message: "Automatically resumed from pause state",
|
||||
ShowOnSystem: true,
|
||||
Expires: time.Now().Add(15 * time.Second).Unix(),
|
||||
AvailableActions: []*notifications.Action{
|
||||
{
|
||||
ID: "ack",
|
||||
Text: "OK",
|
||||
},
|
||||
},
|
||||
}
|
||||
notifications.Notify(n)
|
||||
}
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
c.resumeWorker = c.mgr.NewWorkerMgr("resumer", resumerWorkerFunc, nil)
|
||||
c.resumeWorker.Go()
|
||||
}
|
||||
|
||||
// updateStatesAndNotify updates the paused states and sends notifications accordingly.
|
||||
// No thread safety, caller must hold c.locker.
|
||||
func (c *Control) updateStatesAndNotify() {
|
||||
if !c.pauseInfo.Interception && !c.pauseInfo.SPN {
|
||||
if c.pauseNotification != nil {
|
||||
c.pauseNotification.Delete()
|
||||
c.pauseNotification = nil
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
title := ""
|
||||
nType := notifications.Warning
|
||||
if c.pauseInfo.Interception && c.pauseInfo.SPN {
|
||||
title = "Portmaster and SPN paused"
|
||||
} else if c.pauseInfo.Interception {
|
||||
title = "Portmaster paused"
|
||||
} else if c.pauseInfo.SPN {
|
||||
title = "SPN paused"
|
||||
nType = notifications.Info // less severe notification for SPN-only pause
|
||||
}
|
||||
message := fmt.Sprintf("%s till %v", title, c.pauseInfo.TillTime.Format(time.TimeOnly))
|
||||
|
||||
c.pauseNotification = ¬ifications.Notification{
|
||||
EventID: "control:paused",
|
||||
Type: nType,
|
||||
Title: title,
|
||||
Message: message,
|
||||
ShowOnSystem: false, // TODO: Before enabling, ensure that UI client (Tauri implementation) supports ActionTypeWebhook.
|
||||
EventData: &c.pauseInfo,
|
||||
AvailableActions: []*notifications.Action{
|
||||
{
|
||||
Text: "Resume",
|
||||
Type: notifications.ActionTypeWebhook,
|
||||
Payload: ¬ifications.ActionTypeWebhookPayload{
|
||||
URL: APIEndpointResume,
|
||||
ResultAction: "display",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
notifications.Notify(c.pauseNotification)
|
||||
c.pauseNotification.SyncWithState(c.states)
|
||||
}
|
||||
@@ -24,10 +24,26 @@ func (i *Instance) GetWorkerInfo() (*mgr.WorkerInfo, error) {
|
||||
for _, m := range i.serviceGroup.Modules() {
|
||||
wi, _ := m.Manager().WorkerInfo(snapshot) // Does not fail when we provide a snapshot.
|
||||
infos = append(infos, wi)
|
||||
|
||||
// Check if module is a nested modules group
|
||||
if gm, ok := m.(*mgr.GroupModule); ok {
|
||||
for _, sm := range gm.Modules() {
|
||||
wi, _ := sm.Manager().WorkerInfo(snapshot) // Does not fail when we provide a snapshot.
|
||||
infos = append(infos, wi)
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, m := range i.SpnGroup.Modules() {
|
||||
wi, _ := m.Manager().WorkerInfo(snapshot) // Does not fail when we provide a snapshot.
|
||||
infos = append(infos, wi)
|
||||
|
||||
// Check if module is a nested modules group
|
||||
if gm, ok := m.(*mgr.GroupModule); ok {
|
||||
for _, sm := range gm.Modules() {
|
||||
wi, _ := sm.Manager().WorkerInfo(snapshot) // Does not fail when we provide a snapshot.
|
||||
infos = append(infos, wi)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return mgr.MergeWorkerInfo(infos...), nil
|
||||
|
||||
@@ -12,9 +12,7 @@ import (
|
||||
|
||||
var (
|
||||
packetMetricsDestination string
|
||||
metrics = &packetMetrics{
|
||||
done: make(chan struct{}),
|
||||
}
|
||||
metrics = &packetMetrics{}
|
||||
)
|
||||
|
||||
func init() {
|
||||
@@ -40,6 +38,10 @@ func (pm *packetMetrics) record(tp *tracedPacket, verdict string) {
|
||||
pm.l.Lock()
|
||||
defer pm.l.Unlock()
|
||||
|
||||
if pm.done == nil {
|
||||
return
|
||||
}
|
||||
|
||||
pm.records = append(pm.records, &performanceRecord{
|
||||
start: start,
|
||||
duration: duration,
|
||||
@@ -48,6 +50,18 @@ func (pm *packetMetrics) record(tp *tracedPacket, verdict string) {
|
||||
}(tp.start.UnixNano(), time.Since(tp.start))
|
||||
}
|
||||
|
||||
func (pm *packetMetrics) stop() {
|
||||
pm.l.Lock()
|
||||
defer pm.l.Unlock()
|
||||
|
||||
if pm.done == nil {
|
||||
return
|
||||
}
|
||||
|
||||
close(pm.done)
|
||||
pm.done = nil
|
||||
}
|
||||
|
||||
func (pm *packetMetrics) writeMetrics() {
|
||||
if packetMetricsDestination == "" {
|
||||
return
|
||||
@@ -62,9 +76,14 @@ func (pm *packetMetrics) writeMetrics() {
|
||||
_ = f.Close()
|
||||
}()
|
||||
|
||||
pm.l.Lock()
|
||||
pm.done = make(chan struct{})
|
||||
done := pm.done
|
||||
pm.l.Unlock()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-pm.done:
|
||||
case <-done:
|
||||
return
|
||||
case <-time.After(time.Second * 5):
|
||||
}
|
||||
|
||||
@@ -40,6 +40,7 @@ var (
|
||||
BandwidthUpdates = make(chan *packet.BandwidthUpdate, 1000)
|
||||
|
||||
disableInterception bool
|
||||
isStarted atomic.Bool
|
||||
)
|
||||
|
||||
func init() {
|
||||
@@ -53,6 +54,10 @@ func start() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
if !isStarted.CompareAndSwap(false, true) {
|
||||
return nil // already running
|
||||
}
|
||||
|
||||
inputPackets := Packets
|
||||
if packetMetricsDestination != "" {
|
||||
go metrics.writeMetrics()
|
||||
@@ -64,7 +69,17 @@ func start() error {
|
||||
}()
|
||||
}
|
||||
|
||||
return startInterception(inputPackets)
|
||||
err := startInterception(inputPackets)
|
||||
if err != nil {
|
||||
log.Errorf("interception: failed to start module: %q", err)
|
||||
log.Debug("interception: cleaning up after failed start...")
|
||||
metrics.stop()
|
||||
if e := stopInterception(); e != nil {
|
||||
log.Debugf("interception: error cleaning up after failed start: %q", e.Error())
|
||||
}
|
||||
isStarted.Store(false)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// Stop starts the interception.
|
||||
@@ -73,7 +88,11 @@ func stop() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
close(metrics.done)
|
||||
if !isStarted.CompareAndSwap(true, false) {
|
||||
return nil // not running
|
||||
}
|
||||
|
||||
metrics.stop()
|
||||
if err := stopInterception(); err != nil {
|
||||
log.Errorf("failed to stop interception module: %s", err)
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
package interception
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/coreos/go-iptables/iptables"
|
||||
"github.com/hashicorp/go-multierror"
|
||||
@@ -30,15 +30,10 @@ var (
|
||||
out6Queue nfQueue
|
||||
in6Queue nfQueue
|
||||
|
||||
isRunning atomic.Bool
|
||||
shutdownSignal = make(chan struct{})
|
||||
|
||||
experimentalNfqueueBackend bool
|
||||
)
|
||||
|
||||
func init() {
|
||||
flag.BoolVar(&experimentalNfqueueBackend, "experimental-nfqueue", false, "(deprecated flag; always used)")
|
||||
}
|
||||
|
||||
// nfQueue encapsulates nfQueue providers.
|
||||
type nfQueue interface {
|
||||
PacketChannel() <-chan packet.Packet
|
||||
@@ -262,12 +257,13 @@ func deactivateIPTables(protocol iptables.Protocol, rules, chains []string) erro
|
||||
|
||||
// StartNfqueueInterception starts the nfqueue interception.
|
||||
func StartNfqueueInterception(packets chan<- packet.Packet) (err error) {
|
||||
// @deprecated, remove in v1
|
||||
if experimentalNfqueueBackend {
|
||||
log.Warningf("[DEPRECATED] --experimental-nfqueue has been deprecated as the backend is now used by default")
|
||||
log.Warningf("[DEPRECATED] please remove the flag from your configuration!")
|
||||
if !isRunning.CompareAndSwap(false, true) {
|
||||
return nil // already running
|
||||
}
|
||||
|
||||
// Reset shutdown signal
|
||||
shutdownSignal = make(chan struct{})
|
||||
|
||||
err = activateNfqueueFirewall()
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not initialize nfqueue: %w", err)
|
||||
@@ -305,6 +301,11 @@ func StartNfqueueInterception(packets chan<- packet.Packet) (err error) {
|
||||
|
||||
// StopNfqueueInterception stops the nfqueue interception.
|
||||
func StopNfqueueInterception() error {
|
||||
if !isRunning.CompareAndSwap(true, false) {
|
||||
return nil // not running
|
||||
}
|
||||
|
||||
// Signal shutdown to packet handler
|
||||
defer close(shutdownSignal)
|
||||
|
||||
if out4Queue != nil {
|
||||
|
||||
@@ -16,6 +16,7 @@ import (
|
||||
"github.com/safing/portmaster/base/utils"
|
||||
"github.com/safing/portmaster/service/broadcasts"
|
||||
"github.com/safing/portmaster/service/compat"
|
||||
"github.com/safing/portmaster/service/control"
|
||||
"github.com/safing/portmaster/service/core"
|
||||
"github.com/safing/portmaster/service/core/base"
|
||||
"github.com/safing/portmaster/service/firewall"
|
||||
@@ -58,6 +59,7 @@ type Instance struct {
|
||||
cancelShutdownCtx context.CancelFunc
|
||||
|
||||
serviceGroup *mgr.Group
|
||||
serviceGroupInterception *mgr.GroupModule
|
||||
|
||||
binDir string
|
||||
dataDir string
|
||||
@@ -95,6 +97,7 @@ type Instance struct {
|
||||
process *process.ProcessModule
|
||||
resolver *resolver.ResolverModule
|
||||
sync *sync.Sync
|
||||
control *control.Control
|
||||
|
||||
access *access.Access
|
||||
|
||||
@@ -264,6 +267,10 @@ func New(svcCfg *ServiceConfig) (*Instance, error) { //nolint:maintidx
|
||||
if err != nil {
|
||||
return instance, fmt.Errorf("create sync module: %w", err)
|
||||
}
|
||||
instance.control, err = control.New(instance)
|
||||
if err != nil {
|
||||
return instance, fmt.Errorf("create control module: %w", err)
|
||||
}
|
||||
instance.access, err = access.New(instance)
|
||||
if err != nil {
|
||||
return instance, fmt.Errorf("create access module: %w", err)
|
||||
@@ -307,6 +314,12 @@ func New(svcCfg *ServiceConfig) (*Instance, error) { //nolint:maintidx
|
||||
return instance, fmt.Errorf("create terminal module: %w", err)
|
||||
}
|
||||
|
||||
// Grouped interception modules that can be paused/resumed together.
|
||||
instance.serviceGroupInterception = mgr.NewGroupModule("Interception Group",
|
||||
instance.interception,
|
||||
instance.dnsmonitor,
|
||||
instance.compat)
|
||||
|
||||
// Add all modules to instance group.
|
||||
instance.serviceGroup = mgr.NewGroup(
|
||||
instance.base,
|
||||
@@ -334,14 +347,18 @@ func New(svcCfg *ServiceConfig) (*Instance, error) { //nolint:maintidx
|
||||
instance.resolver,
|
||||
instance.filterLists,
|
||||
instance.customlist,
|
||||
instance.interception,
|
||||
instance.dnsmonitor,
|
||||
|
||||
instance.compat,
|
||||
// Grouped pausable interception modules:
|
||||
// instance.interception,
|
||||
// instance.dnsmonitor,
|
||||
// instance.compat
|
||||
instance.serviceGroupInterception,
|
||||
|
||||
instance.status,
|
||||
instance.broadcasts,
|
||||
instance.sync,
|
||||
instance.ui,
|
||||
instance.control,
|
||||
|
||||
instance.access,
|
||||
)
|
||||
@@ -542,6 +559,11 @@ func (i *Instance) Interception() *interception.Interception {
|
||||
return i.interception
|
||||
}
|
||||
|
||||
// InterceptionGroup returns the grouped interception modules that can be paused together.
|
||||
func (i *Instance) InterceptionGroup() *mgr.GroupModule {
|
||||
return i.serviceGroupInterception
|
||||
}
|
||||
|
||||
// DNSMonitor returns the dns-listener module.
|
||||
func (i *Instance) DNSMonitor() *dnsmonitor.DNSMonitor {
|
||||
return i.dnsmonitor
|
||||
|
||||
33
service/mgr/group_module.go
Normal file
33
service/mgr/group_module.go
Normal file
@@ -0,0 +1,33 @@
|
||||
package mgr
|
||||
|
||||
// GroupModule is a module that wraps a group of modules,
|
||||
// to allow nesting groups of modules in parent group.
|
||||
type GroupModule struct {
|
||||
mgr *Manager
|
||||
group *Group
|
||||
}
|
||||
|
||||
func NewGroupModule(name string, modules ...Module) *GroupModule {
|
||||
return &GroupModule{
|
||||
mgr: New(name),
|
||||
group: NewGroup(modules...),
|
||||
}
|
||||
}
|
||||
|
||||
func (gm *GroupModule) Manager() *Manager {
|
||||
return gm.mgr
|
||||
}
|
||||
|
||||
func (gm *GroupModule) Start() error {
|
||||
return gm.group.Start()
|
||||
}
|
||||
|
||||
func (gm *GroupModule) Stop() error {
|
||||
return gm.group.Stop()
|
||||
}
|
||||
|
||||
// Modules returns the modules in the group wrapped by this group module.
|
||||
// (mimics Group.Modules())
|
||||
func (gm *GroupModule) Modules() []Module {
|
||||
return gm.group.Modules()
|
||||
}
|
||||
@@ -35,10 +35,6 @@ func (s *Status) Manager() *mgr.Manager {
|
||||
|
||||
// Start starts the module.
|
||||
func (s *Status) Start() error {
|
||||
if err := s.setupRuntimeProvider(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
s.mgr.Go("status publisher", s.statusPublisher)
|
||||
|
||||
s.instance.NetEnv().EventOnlineStatusChange.AddCallback("update online status in system status",
|
||||
@@ -67,6 +63,14 @@ func (s *Status) Stop() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Status) prep() error {
|
||||
// register status provider as soon as possible
|
||||
if err := s.setupRuntimeProvider(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// AddToDebugInfo adds the system status to the given debug.Info.
|
||||
func AddToDebugInfo(di *debug.Info) {
|
||||
di.AddSection(
|
||||
@@ -96,6 +100,10 @@ func New(instance instance) (*Status, error) {
|
||||
notifications: make(map[string]map[string]*notifications.Notification),
|
||||
}
|
||||
|
||||
if err := module.prep(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return module, nil
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user