mirror of
https://github.com/safing/portmaster
synced 2025-04-03 18:49:11 +00:00
Migrate tauri from portmaster-ui to desktop/tauri. Update build system
This commit is contained in:
parent
ac23ce32a1
commit
d524bce166
35 changed files with 10960 additions and 42 deletions
.angulardoc.json.earthlyignore.gitignore
.vscode
Earthfiledesktop
angular
tauri
.gitkeepassets
src-tauri
.gitignoreCargo.lockCargo.tomlbuild.rs
src
tauri.conf.json
4
.angulardoc.json
Normal file
4
.angulardoc.json
Normal file
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"repoId": "8f466ce7-4b75-4048-8b8a-cad5bf173aa0",
|
||||
"lastSync": 0
|
||||
}
|
|
@ -12,4 +12,14 @@ desktop/angular/.angular
|
|||
|
||||
# Assets are ignored here because the symlink wouldn't work in
|
||||
# the buildkit container so we copy the assets directly in Earthfile.
|
||||
desktop/angular/assets
|
||||
desktop/angular/assets
|
||||
|
||||
|
||||
desktop/tauri/src-tauri/target
|
||||
.gitignore
|
||||
AUTHORS
|
||||
CODE_OF_CONDUCT.md
|
||||
LICENSE
|
||||
README.md
|
||||
TESTING.md
|
||||
TRADEMARKS
|
10
.gitignore
vendored
10
.gitignore
vendored
|
@ -1,12 +1,6 @@
|
|||
# Compiled binaries
|
||||
portmaster
|
||||
portmaster.exe
|
||||
dnsonly
|
||||
dnsonly.exe
|
||||
main
|
||||
main.exe
|
||||
integrationtest
|
||||
integrationtest.exe
|
||||
*.exe
|
||||
dist/
|
||||
|
||||
# Dist dir
|
||||
dist
|
||||
|
|
1
.vscode/settings.json
vendored
Normal file
1
.vscode/settings.json
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
{}
|
261
Earthfile
261
Earthfile
|
@ -1,10 +1,23 @@
|
|||
VERSION --arg-scope-and-set 0.8
|
||||
VERSION --arg-scope-and-set --global-cache 0.8
|
||||
|
||||
ARG --global go_version = 1.21
|
||||
ARG --global distro = alpine3.18
|
||||
ARG --global node_version = 18
|
||||
ARG --global outputDir = "./dist"
|
||||
|
||||
# The list of rust targets we support. They will be automatically converted
|
||||
# to GOOS, GOARCH and GOARM when building go binaries. See the +RUST_TO_GO_ARCH_STRING
|
||||
# helper method at the bottom of the file.
|
||||
ARG --global architectures = "x86_64-unknown-linux-gnu" \
|
||||
"aarch64-unknown-linux-gnu" \
|
||||
"armv7-unknown-linux-gnueabihf" \
|
||||
"arm-unknown-linux-gnueabi" \
|
||||
"x86_64-pc-windows-gnu"
|
||||
|
||||
# Import the earthly rust lib since it already provides some useful
|
||||
# build-targets and methods to initialize the rust toolchain.
|
||||
IMPORT github.com/earthly/lib/rust:3.0.2 AS rust
|
||||
|
||||
go-deps:
|
||||
FROM golang:${go_version}-${distro}
|
||||
WORKDIR /go-workdir
|
||||
|
@ -24,7 +37,6 @@ go-deps:
|
|||
COPY go.sum .
|
||||
RUN go mod download
|
||||
|
||||
|
||||
go-base:
|
||||
FROM +go-deps
|
||||
|
||||
|
@ -42,6 +54,15 @@ go-base:
|
|||
# ./assets
|
||||
COPY assets ./assets
|
||||
|
||||
# updates all go dependencies and runs go mod tidy, saving go.mod and go.sum locally.
|
||||
update-go-deps:
|
||||
FROM +go-base
|
||||
|
||||
RUN go get -u ./..
|
||||
RUN go mod tidy
|
||||
SAVE ARTIFACT go.mod AS LOCAL go.mod
|
||||
SAVE ARTIFACT --if-exists go.sum AS LOCAL go.sum
|
||||
|
||||
# mod-tidy runs 'go mod tidy', saving go.mod and go.sum locally.
|
||||
mod-tidy:
|
||||
FROM +go-base
|
||||
|
@ -61,6 +82,9 @@ build-go:
|
|||
ARG GOARM
|
||||
ARG CMDS=portmaster-start portmaster-core hub notifier
|
||||
|
||||
# Get the current version
|
||||
DO +GET_VERSION
|
||||
|
||||
CACHE --sharing shared "$GOCACHE"
|
||||
CACHE --sharing shared "$GOMODCACHE"
|
||||
|
||||
|
@ -73,20 +97,17 @@ build-go:
|
|||
|
||||
# Build all go binaries from the specified in CMDS
|
||||
FOR bin IN $CMDS
|
||||
RUN go build -o "/tmp/build/" ./cmds/${bin}
|
||||
RUN go build -o "/tmp/build/" ./cmds/${bin}
|
||||
END
|
||||
|
||||
LET NAME = ""
|
||||
|
||||
FOR bin IN $(ls -1 "/tmp/build/")
|
||||
SET NAME = "${outputDir}/${GOOS}_${GOARCH}/${bin}"
|
||||
IF [ "${GOARM}" != "" ]
|
||||
SET NAME = "${outputDir}/${GOOS}_${GOARCH}v${GOARM}/${bin}"
|
||||
END
|
||||
DO +GO_ARCH_STRING --goos="${GOOS}" --goarch="${GOARCH}" --goarm="${GOARM}"
|
||||
|
||||
SAVE ARTIFACT "/tmp/build/${bin}" AS LOCAL "${NAME}"
|
||||
SAVE ARTIFACT "/tmp/build/${bin}" AS LOCAL "${outputDir}/${GO_ARCH_STRING}/${bin}"
|
||||
END
|
||||
|
||||
SAVE ARTIFACT "/tmp/build/" ./output
|
||||
|
||||
# Test one or more go packages.
|
||||
# Run `earthly +test-go` to test all packages
|
||||
# Run `earthly +test-go --PKG="service/firewall"` to only test a specific package.
|
||||
|
@ -108,29 +129,19 @@ test-go:
|
|||
END
|
||||
|
||||
test-go-all-platforms:
|
||||
# Linux platforms:
|
||||
BUILD +test-go --GOARCH=amd64 --GOOS=linux
|
||||
BUILD +test-go --GOARCH=arm64 --GOOS=linux
|
||||
BUILD +test-go --GOARCH=arm --GOOS=linux --GOARM=5
|
||||
BUILD +test-go --GOARCH=arm --GOOS=linux --GOARM=6
|
||||
BUILD +test-go --GOARCH=arm --GOOS=linux --GOARM=7
|
||||
|
||||
# Windows platforms:
|
||||
BUILD +test-go --GOARCH=amd64 --GOOS=windows
|
||||
BUILD +test-go --GOARCH=arm64 --GOOS=windows
|
||||
LOCALLY
|
||||
FOR arch IN ${architectures}
|
||||
DO +RUST_TO_GO_ARCH_STRING --rustTarget="${arch}"
|
||||
BUILD +test-go --GOARCH="${GOARCH}" --GOOS="${GOOS}" --GOARM="${GOARM}"
|
||||
END
|
||||
|
||||
# Builds portmaster-start, portmaster-core, hub and notifier for all supported platforms
|
||||
build-go-release:
|
||||
# Linux platforms:
|
||||
BUILD +build-go --GOARCH=amd64 --GOOS=linux
|
||||
BUILD +build-go --GOARCH=arm64 --GOOS=linux
|
||||
BUILD +build-go --GOARCH=arm --GOOS=linux --GOARM=5
|
||||
BUILD +build-go --GOARCH=arm --GOOS=linux --GOARM=6
|
||||
BUILD +build-go --GOARCH=arm --GOOS=linux --GOARM=7
|
||||
|
||||
# Windows platforms:
|
||||
BUILD +build-go --GOARCH=amd64 --GOOS=windows
|
||||
BUILD +build-go --GOARCH=arm64 --GOOS=windows
|
||||
LOCALLY
|
||||
FOR arch IN ${architectures}
|
||||
DO +RUST_TO_GO_ARCH_STRING --rustTarget="${arch}"
|
||||
BUILD +build-go --GOARCH="${GOARCH}" --GOOS="${GOOS}" --GOARM="${GOARM}"
|
||||
END
|
||||
|
||||
# Builds all binaries from the cmds/ folder for linux/windows AMD64
|
||||
# Most utility binaries are never needed on other platforms.
|
||||
|
@ -187,13 +198,199 @@ angular-project:
|
|||
# Build the angular projects (portmaster-UI and tauri-builtin) in production mode
|
||||
angular-release:
|
||||
BUILD +angular-project --project=portmaster --dist=./dist --configuration=production --baseHref=/ui/modules/portmaster
|
||||
BUILD +angular-project --project=tauri-builtin --dist=./dist/tauri-builtin --configuration=production --baseHref="/"
|
||||
|
||||
# Build the angular projects (portmaster-UI and tauri-builtin) in dev mode
|
||||
angular-dev:
|
||||
BUILD +angular-project --project=portmaster --dist=./dist --configuration=development --baseHref=/ui/modules/portmaster
|
||||
BUILD +angular-project --project=tauri-builtin --dist=./dist/tauri-builtin --configuration=development --baseHref="/"
|
||||
|
||||
# A base target for rust to prepare the build container
|
||||
rust-base:
|
||||
FROM rust:1.76-bookworm
|
||||
|
||||
RUN apt-get update -qq
|
||||
RUN apt-get install --no-install-recommends -qq \
|
||||
autoconf \
|
||||
autotools-dev \
|
||||
libtool-bin \
|
||||
clang \
|
||||
cmake \
|
||||
bsdmainutils \
|
||||
g++-mingw-w64-x86-64 \
|
||||
gcc-aarch64-linux-gnu \
|
||||
gcc-arm-none-eabi \
|
||||
gcc-arm-linux-gnueabi \
|
||||
gcc-arm-linux-gnueabihf \
|
||||
libgtk-3-dev \
|
||||
libjavascriptcoregtk-4.1-dev \
|
||||
libsoup-3.0-dev \
|
||||
libwebkit2gtk-4.1-dev \
|
||||
build-essential \
|
||||
curl \
|
||||
wget \
|
||||
file \
|
||||
libssl-dev \
|
||||
libayatana-appindicator3-dev \
|
||||
librsvg2-dev
|
||||
|
||||
# Add some required rustup components
|
||||
RUN rustup component add clippy
|
||||
RUN rustup component add rustfmt
|
||||
|
||||
# Install toolchains and targets
|
||||
FOR arch IN ${architectures}
|
||||
RUN rustup target add ${arch}
|
||||
END
|
||||
|
||||
DO rust+INIT --keep_fingerprints=true
|
||||
|
||||
# For now we need tauri-cli 1.5 for bulding
|
||||
DO rust+CARGO --args="install tauri-cli --version ^1.5.11"
|
||||
|
||||
tauri-src:
|
||||
FROM +rust-base
|
||||
|
||||
WORKDIR /app/tauri
|
||||
|
||||
# --keep-ts is necessary to ensure that the timestamps of the source files
|
||||
# are preserved such that Rust's incremental compilation works correctly.
|
||||
COPY --keep-ts ./desktop/tauri/ .
|
||||
COPY assets/data ./assets
|
||||
COPY (+angular-project/dist/tauri-builtin --project=tauri-builtin --dist=./dist/tauri-builtin --configuration=production --baseHref="/") ./../angular/dist/tauri-builtin
|
||||
|
||||
build-tauri:
|
||||
FROM +tauri-src
|
||||
|
||||
ARG --required target
|
||||
ARG output="release/[^\./]+"
|
||||
ARG bundle="none"
|
||||
|
||||
# if we want tauri to create the installer bundles we also need to provide all external binaries
|
||||
# we need to do some magic here because tauri expects the binaries to include the rust target tripple.
|
||||
# We already knwo that triple because it's a required argument. From that triple, we use +RUST_TO_GO_ARCH_STRING
|
||||
# function from below to parse the triple and guess wich GOOS and GOARCH we need.
|
||||
IF [ "${bundle}" != "none" ]
|
||||
RUN mkdir /tmp/gobuild
|
||||
RUN mkdir ./binaries
|
||||
|
||||
DO +RUST_TO_GO_ARCH_STRING --rustTarget="${target}"
|
||||
RUN echo "GOOS=${GOOS} GOARCH=${GOARCH} GOARM=${GOARM} GO_ARCH_STRING=${GO_ARCH_STRING}"
|
||||
|
||||
COPY (+build-go/output --GOOS="${GOOS}" --CMDS="portmaster-start portmaster-core" --GOARCH="${GOARCH}" --GOARM="${GOARM}") /tmp/gobuild
|
||||
|
||||
LET dest=""
|
||||
FOR bin IN $(ls /tmp/gobuild)
|
||||
SET dest="./binaries/${bin}-${target}"
|
||||
|
||||
IF [ -z "${bin##*.exe}" ]
|
||||
SET dest = "./binaries/${bin%.*}-${target}.exe"
|
||||
END
|
||||
|
||||
RUN echo "Copying ${bin} to ${dest}"
|
||||
RUN cp "/tmp/gobuild/${bin}" "${dest}"
|
||||
END
|
||||
END
|
||||
|
||||
# The following is exected to work but doesn't. for whatever reason cargo-sweep errors out on the windows-toolchain.
|
||||
#
|
||||
# DO rust+CARGO --args="tauri build --bundles none --ci --target=${target}" --output="release/[^/\.]+"
|
||||
#
|
||||
# For, now, we just directly mount the rust target cache and call cargo ourself.
|
||||
|
||||
DO rust+SET_CACHE_MOUNTS_ENV
|
||||
RUN --mount=$EARTHLY_RUST_TARGET_CACHE cargo tauri build --bundles "${bundle}" --ci --target="${target}"
|
||||
DO rust+COPY_OUTPUT --output="${output}"
|
||||
|
||||
RUN ls target
|
||||
|
||||
tauri-release:
|
||||
LOCALLY
|
||||
|
||||
ARG bundle="none"
|
||||
|
||||
FOR arch IN ${architectures}
|
||||
BUILD +build-tauri --target="${arch}" --bundle="${bundle}"
|
||||
END
|
||||
|
||||
release:
|
||||
BUILD +build-go-release
|
||||
BUILD +angular-release
|
||||
|
||||
|
||||
# Takes GOOS, GOARCH and optionally GOARM and creates a string representation for file-names.
|
||||
# in the form of ${GOOS}_{GOARCH} if GOARM is empty, otherwise ${GOOS}_${GOARCH}v${GOARM}.
|
||||
# Thats the same format as expected and served by our update server.
|
||||
#
|
||||
# The result is available as GO_ARCH_STRING environment variable in the build context.
|
||||
GO_ARCH_STRING:
|
||||
FUNCTION
|
||||
ARG --required goos
|
||||
ARG --required goarch
|
||||
ARG goarm
|
||||
|
||||
LET result = "${goos}_${goarch}"
|
||||
IF [ "${goarm}" != "" ]
|
||||
SET result = "${goos}_${goarch}v${goarm}"
|
||||
END
|
||||
|
||||
ENV GO_ARCH_STRING="${result}"
|
||||
|
||||
# Takes a rust target (--rustTarget) and extracts architecture and OS and arm version
|
||||
# and finally calls GO_ARCH_STRING.
|
||||
#
|
||||
# The result is available as GO_ARCH_STRING environment variable in the build context.
|
||||
# It also exports GOOS, GOARCH and GOARM environment variables.
|
||||
RUST_TO_GO_ARCH_STRING:
|
||||
FUNCTION
|
||||
ARG --required rustTarget
|
||||
|
||||
LET goos=""
|
||||
IF [ -z "${rustTarget##*linux*}" ]
|
||||
SET goos="linux"
|
||||
ELSE
|
||||
SET goos="windows"
|
||||
END
|
||||
|
||||
|
||||
LET goarch=""
|
||||
LET goarm=""
|
||||
|
||||
IF [ -z "${rustTarget##*x86_64*}" ]
|
||||
SET goarch="amd64"
|
||||
ELSE IF [ -z "${rustTarget##*arm*}" ]
|
||||
SET goarch="arm"
|
||||
SET goarm="6"
|
||||
|
||||
IF [ -z "${rustTarget##*v7*}" ]
|
||||
SET goarm="7"
|
||||
END
|
||||
ELSE IF [ -z "${rustTarget##*aarch64*}" ]
|
||||
SET goarch="arm64"
|
||||
ELSE
|
||||
RUN echo "GOARCH not detected"; \
|
||||
exit 1;
|
||||
END
|
||||
|
||||
ENV GOOS="${goos}"
|
||||
ENV GOARCH="${goarch}"
|
||||
ENV GOARM="${goarm}"
|
||||
|
||||
DO +GO_ARCH_STRING --goos="${goos}" --goarch="${goarch}" --goarm="${goarm}"
|
||||
|
||||
GET_VERSION:
|
||||
FUNCTION
|
||||
LOCALLY
|
||||
|
||||
LET VERSION=$(git tag --points-at)
|
||||
IF [ -z "${VERSION}"]
|
||||
SET VERSION=$(git describe --tags --abbrev=0)§dev§build
|
||||
ELSE IF ! git diff --quite
|
||||
SET VERSION="${VERSION}§dev§build"
|
||||
END
|
||||
|
||||
RUN echo "Version is ${VERSION}"
|
||||
ENV VERSION="${VERSION}"
|
||||
|
||||
test:
|
||||
LOCALLY
|
||||
|
||||
DO +GET_VERSION
|
|
@ -15,7 +15,7 @@
|
|||
"chrome-extension": "NODE_ENV=production ng build --configuration production portmaster-chrome-extension",
|
||||
"chrome-extension:dev": "ng build --configuration development portmaster-chrome-extension --watch",
|
||||
"build": "npm run build-libs && NODE_ENV=production ng build --configuration production --base-href /ui/modules/portmaster/",
|
||||
"build-tauri": "npm run build-libs && NODE_ENV=production ng build --configuration production",
|
||||
"build-tauri": "npm run build-libs && NODE_ENV=production ng build --configuration production tauri-builtin",
|
||||
"serve-tauri-builtin": "ng serve tauri-builtin --port 4100",
|
||||
"serve-app": "ng serve --port 4200 --proxy-config ./proxy.json",
|
||||
"tauri-dev": "npm install && run-s build-libs:dev && run-p serve-app serve-tauri-builtin"
|
||||
|
|
1
desktop/tauri/assets
Symbolic link
1
desktop/tauri/assets
Symbolic link
|
@ -0,0 +1 @@
|
|||
../../assets/data
|
3
desktop/tauri/src-tauri/.gitignore
vendored
Normal file
3
desktop/tauri/src-tauri/.gitignore
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
# Generated by Cargo
|
||||
# will have compiled files and executables
|
||||
/target/
|
7286
desktop/tauri/src-tauri/Cargo.lock
generated
Normal file
7286
desktop/tauri/src-tauri/Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
75
desktop/tauri/src-tauri/Cargo.toml
Normal file
75
desktop/tauri/src-tauri/Cargo.toml
Normal file
|
@ -0,0 +1,75 @@
|
|||
[package]
|
||||
name = "app"
|
||||
version = "0.1.0"
|
||||
description = "Portmaster UI"
|
||||
authors = ["Safing"]
|
||||
license = ""
|
||||
repository = ""
|
||||
default-run = "app"
|
||||
edition = "2021"
|
||||
rust-version = "1.60"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build = { version = "2.0.0-alpha", features = [] }
|
||||
|
||||
[dependencies]
|
||||
# Tauri
|
||||
tauri = { version = "2.0.0-alpha", features = ["tray-icon", "icon-ico", "icon-png"] }
|
||||
tauri-plugin-shell = "2.0.0-alpha"
|
||||
tauri-plugin-dialog = "2.0.0-alpha"
|
||||
tauri-plugin-clipboard-manager = "2.0.0-alpha"
|
||||
tauri-plugin-os = "2.0.0-alpha"
|
||||
tauri-plugin-single-instance = "2.0.0-alpha"
|
||||
tauri-plugin-cli = "2.0.0-alpha"
|
||||
tauri-plugin-notification = "2.0.0-alpha"
|
||||
|
||||
# We still need the tauri-cli 1.5 for building
|
||||
tauri-cli = "1.5.11"
|
||||
|
||||
# General
|
||||
serde_json = "1.0"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
futures-util = { version = "0.3", features = ["sink"] }
|
||||
dirs = "1.0"
|
||||
rust-ini = "0.20.0"
|
||||
dataurl = "0.1.2"
|
||||
uuid = "1.6.1"
|
||||
lazy_static = "1.4.0"
|
||||
tokio = { version = "1.35.0", features = ["macros"] }
|
||||
cached = "0.46.1"
|
||||
notify-rust = "4.10.0"
|
||||
assert_matches = "1.5.0"
|
||||
tokio-websockets = { version = "0.5.0", features = ["client", "ring", "rand"] }
|
||||
sha = "1.0.3"
|
||||
http = "1.0.0"
|
||||
url = "2.5.0"
|
||||
thiserror = "1.0"
|
||||
log = "0.4.21"
|
||||
pretty_env_logger = "0.5.0"
|
||||
|
||||
# Linux only
|
||||
[target.'cfg(target_os = "linux")'.dependencies]
|
||||
glib = "0.18.4"
|
||||
gtk-sys = "0.18.0"
|
||||
glib-sys = "0.18.1"
|
||||
gdk-pixbuf = "0.18.3"
|
||||
gdk-pixbuf-sys = "0.18.0"
|
||||
gio-sys = "0.18.1"
|
||||
|
||||
# Windows only
|
||||
[target.'cfg(target_os = "windows")'.dependencies]
|
||||
windows-service = "0.6.0"
|
||||
windows = { version = "0.54.0", features = ["Win32_Foundation", "Win32_UI_Shell", "Win32_UI_WindowsAndMessaging"] }
|
||||
|
||||
[dev-dependencies]
|
||||
which = "6.0.0"
|
||||
gtk = "0.18"
|
||||
ctor = "0.2.6"
|
||||
|
||||
[features]
|
||||
# this feature is used for production builds or when `devPath` points to the filesystem and the built-in dev server is disabled.
|
||||
# If you use cargo directly instead of tauri's cli you can use this feature flag to switch between tauri's `dev` and `build` modes.
|
||||
# DO NOT REMOVE!!
|
||||
custom-protocol = [ "tauri/custom-protocol" ]
|
3
desktop/tauri/src-tauri/build.rs
Normal file
3
desktop/tauri/src-tauri/build.rs
Normal file
|
@ -0,0 +1,3 @@
|
|||
fn main() {
|
||||
tauri_build::build()
|
||||
}
|
204
desktop/tauri/src-tauri/src/main.rs
Normal file
204
desktop/tauri/src-tauri/src/main.rs
Normal file
|
@ -0,0 +1,204 @@
|
|||
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
|
||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||
|
||||
use tauri::{AppHandle, Manager, RunEvent, WindowEvent};
|
||||
use tauri_plugin_cli::CliExt;
|
||||
|
||||
// Library crates
|
||||
mod portapi;
|
||||
mod service;
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
mod xdg;
|
||||
|
||||
// App modules
|
||||
mod portmaster;
|
||||
mod traymenu;
|
||||
mod window;
|
||||
|
||||
use log::{debug, error, info, trace, warn};
|
||||
use portmaster::PortmasterExt;
|
||||
use traymenu::setup_tray_menu;
|
||||
use window::{close_splash_window, create_main_window};
|
||||
|
||||
#[macro_use]
|
||||
extern crate lazy_static;
|
||||
|
||||
#[derive(Clone, serde::Serialize)]
|
||||
struct Payload {
|
||||
args: Vec<String>,
|
||||
cwd: String,
|
||||
}
|
||||
|
||||
struct WsHandler {
|
||||
handle: AppHandle,
|
||||
background: bool,
|
||||
|
||||
is_first_connect: bool,
|
||||
}
|
||||
|
||||
impl portmaster::Handler for WsHandler {
|
||||
fn on_connect(&mut self, cli: portapi::client::PortAPI) -> () {
|
||||
// we successfully connected to Portmaster. Set is_first_connect to false
|
||||
// so we don't show the splash-screen when we loose connection.
|
||||
self.is_first_connect = false;
|
||||
|
||||
if let Err(err) = close_splash_window(&self.handle) {
|
||||
error!("failed to close splash window: {}", err.to_string());
|
||||
}
|
||||
|
||||
// create the main window now. It's not automatically visible by default.
|
||||
// Rather, the angular application will show the window itself when it finished
|
||||
// bootstrapping.
|
||||
if let Err(err) = create_main_window(&self.handle) {
|
||||
error!("failed to create main window: {}", err.to_string());
|
||||
}
|
||||
|
||||
let handle = self.handle.clone();
|
||||
tauri::async_runtime::spawn(async move {
|
||||
traymenu::tray_handler(cli, handle).await;
|
||||
});
|
||||
}
|
||||
|
||||
fn on_disconnect(&mut self) {
|
||||
// if we're not running in background and this was the first connection attempt
|
||||
// then display the splash-screen.
|
||||
//
|
||||
// Once we had a successful connection the splash-screen will not be shown anymore
|
||||
// since there's already a main window with the angular application.
|
||||
if !self.background && self.is_first_connect {
|
||||
let _ = window::create_splash_window(&self.handle.clone());
|
||||
|
||||
self.is_first_connect = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
pretty_env_logger::init();
|
||||
|
||||
let app = tauri::Builder::default()
|
||||
// Shell plugin for open_external support
|
||||
.plugin(tauri_plugin_shell::init())
|
||||
// Clipboard support
|
||||
.plugin(tauri_plugin_clipboard_manager::init())
|
||||
// Dialog (Save/Open) support
|
||||
.plugin(tauri_plugin_dialog::init())
|
||||
// OS Version and Architecture support
|
||||
.plugin(tauri_plugin_os::init())
|
||||
// Single instance guard
|
||||
.plugin(tauri_plugin_single_instance::init(|app, argv, cwd| {
|
||||
let _ = app.emit("single-instance", Payload { args: argv, cwd });
|
||||
}))
|
||||
// Custom CLI arguments
|
||||
.plugin(tauri_plugin_cli::init())
|
||||
// Notification support
|
||||
.plugin(tauri_plugin_notification::init())
|
||||
// Our Portmaster Plugin that handles communication between tauri and our angular app.
|
||||
.plugin(portmaster::init())
|
||||
// Setup the app an any listeners
|
||||
.setup(|app| {
|
||||
setup_tray_menu(app)?;
|
||||
|
||||
// Setup the single-instance event listener that will create/focus the main window
|
||||
// or the splash-screen.
|
||||
let handle = app.handle().clone();
|
||||
app.listen_global("single-instance", move |_event| {
|
||||
let _ = window::open_window(&handle);
|
||||
});
|
||||
|
||||
// Handle cli flags:
|
||||
//
|
||||
let mut background = false;
|
||||
match app.cli().matches() {
|
||||
Ok(matches) => {
|
||||
debug!("cli matches={:?}", matches);
|
||||
|
||||
if let Some(bg_flag) = matches.args.get("background") {
|
||||
match bg_flag.value.as_bool() {
|
||||
Some(value) => {
|
||||
background = value;
|
||||
app.portmaster().set_show_after_bootstrap(!background);
|
||||
}
|
||||
None => {}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(nf_flag) = matches.args.get("with-notifications") {
|
||||
match nf_flag.value.as_bool() {
|
||||
Some(v) => {
|
||||
app.portmaster().with_notification_support(v);
|
||||
}
|
||||
None => {}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(pf_flag) = matches.args.get("with-prompts") {
|
||||
match pf_flag.value.as_bool() {
|
||||
Some(v) => {
|
||||
app.portmaster().with_connection_prompts(v);
|
||||
}
|
||||
None => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
error!("failed to parse cli arguments: {}", err.to_string());
|
||||
}
|
||||
};
|
||||
|
||||
// prepare a custom portmaster plugin handler that will show the splash-screen
|
||||
// (if not in --background) and launch the tray-icon handler.
|
||||
let handler = WsHandler {
|
||||
handle: app.handle().clone(),
|
||||
background,
|
||||
is_first_connect: true,
|
||||
};
|
||||
|
||||
// register the custom handler
|
||||
app.portmaster().register_handler(handler);
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.any_thread()
|
||||
.build(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
|
||||
app.run(|handle, e| match e {
|
||||
RunEvent::WindowEvent { label, event, .. } => {
|
||||
if label != "main" {
|
||||
// We only have one window at most so any other label is unexpected
|
||||
return;
|
||||
}
|
||||
|
||||
// Do not let the user close the window, instead send an event to the main
|
||||
// window so we can show the "will not stop portmaster" dialog and let the window
|
||||
// close itself using
|
||||
//
|
||||
// window.__TAURI__.window.getCurrent().close()
|
||||
//
|
||||
// Note: the above javascript does NOT trigger the CloseRequested event so
|
||||
// there's no need to handle that case here.
|
||||
//
|
||||
match event {
|
||||
WindowEvent::CloseRequested { api, .. } => {
|
||||
debug!(
|
||||
"window (label={}) close request received, forwarding to user-interface.",
|
||||
label
|
||||
);
|
||||
|
||||
api.prevent_close();
|
||||
if let Some(window) = handle.get_window(label.as_str()) {
|
||||
let _ = window.emit("exit-requested", "");
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
RunEvent::ExitRequested { api, .. } => {
|
||||
api.prevent_exit();
|
||||
}
|
||||
_ => {}
|
||||
});
|
||||
}
|
191
desktop/tauri/src-tauri/src/portapi/client.rs
Normal file
191
desktop/tauri/src-tauri/src/portapi/client.rs
Normal file
|
@ -0,0 +1,191 @@
|
|||
use futures_util::{SinkExt, StreamExt};
|
||||
use http::Uri;
|
||||
use log::{debug, error, warn};
|
||||
use std::collections::HashMap;
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
use tokio::sync::mpsc::{channel, Receiver, Sender};
|
||||
use tokio::sync::RwLock;
|
||||
use tokio_websockets::{ClientBuilder, Error};
|
||||
|
||||
use super::message::*;
|
||||
use super::types::*;
|
||||
|
||||
/// An internal representation of a Command that
|
||||
/// contains the PortAPI message as well as a response
|
||||
/// channel that will receive all responses sent from the
|
||||
/// server.
|
||||
///
|
||||
/// Users should normally not need to use the Command struct
|
||||
/// directly since `PortAPI` already abstracts the creation of
|
||||
/// mpsc channels.
|
||||
struct Command {
|
||||
msg: Message,
|
||||
response: Sender<Response>,
|
||||
}
|
||||
|
||||
/// The client implementation for PortAPI.
|
||||
#[derive(Clone)]
|
||||
pub struct PortAPI {
|
||||
dispatch: Sender<Command>,
|
||||
}
|
||||
|
||||
/// The map type used to store message subscribers.
|
||||
type SubscriberMap = RwLock<HashMap<usize, Sender<Response>>>;
|
||||
|
||||
/// Connect to PortAPI at the specified URI.
|
||||
///
|
||||
/// This method will launch a new async thread on the `tauri::async_runtime`
|
||||
/// that will handle message to transmit and also multiplex server responses
|
||||
/// to the appropriate subscriber.
|
||||
pub async fn connect(uri: &str) -> Result<PortAPI, Error> {
|
||||
let parsed = match uri.parse::<Uri>() {
|
||||
Ok(u) => u,
|
||||
Err(_e) => {
|
||||
return Err(Error::NoUriConfigured); // TODO(ppacher): fix the return error type.
|
||||
}
|
||||
};
|
||||
|
||||
let (mut client, _) = ClientBuilder::from_uri(parsed).connect().await?;
|
||||
let (tx, mut dispatch) = channel::<Command>(64);
|
||||
|
||||
tauri::async_runtime::spawn(async move {
|
||||
let subscribers: SubscriberMap = RwLock::new(HashMap::new());
|
||||
let next_id = AtomicUsize::new(0);
|
||||
|
||||
loop {
|
||||
tokio::select! {
|
||||
msg = client.next() => {
|
||||
let msg = match msg {
|
||||
Some(msg) => msg,
|
||||
None => {
|
||||
warn!("websocket connection lost");
|
||||
|
||||
dispatch.close();
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
match msg {
|
||||
Err(err) => {
|
||||
error!("failed to receive frame from websocket: {}", err);
|
||||
|
||||
dispatch.close();
|
||||
return;
|
||||
},
|
||||
Ok(msg) => {
|
||||
let text = unsafe {
|
||||
std::str::from_utf8_unchecked(msg.as_payload())
|
||||
};
|
||||
|
||||
match text.parse::<Message>() {
|
||||
Ok(msg) => {
|
||||
let id = msg.id;
|
||||
let map = subscribers
|
||||
.read()
|
||||
.await;
|
||||
|
||||
if let Some(sub) = map.get(&id) {
|
||||
let res: Result<Response, MessageError> = msg.try_into();
|
||||
match res {
|
||||
Ok(response) => {
|
||||
if let Err(err) = sub.send(response).await {
|
||||
// The receiver side has been closed already,
|
||||
// drop the read lock and remove the subscriber
|
||||
// from our hashmap
|
||||
drop(map);
|
||||
|
||||
subscribers
|
||||
.write()
|
||||
.await
|
||||
.remove(&id);
|
||||
|
||||
debug!("subscriber for command {} closed read side: {}", id, err);
|
||||
}
|
||||
},
|
||||
Err(err) => {
|
||||
error!("invalid command: {}", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
Err(err) => {
|
||||
error!("failed to deserialize message: {}", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
Some(mut cmd) = dispatch.recv() => {
|
||||
let id = next_id.fetch_add(1, Ordering::Relaxed);
|
||||
cmd.msg.id = id;
|
||||
let blob: String = cmd.msg.into();
|
||||
|
||||
debug!("Sending websocket frame: {}", blob);
|
||||
|
||||
match client.send(tokio_websockets::Message::text(blob)).await {
|
||||
Ok(_) => {
|
||||
subscribers
|
||||
.write()
|
||||
.await
|
||||
.insert(id, cmd.response);
|
||||
},
|
||||
Err(err) => {
|
||||
error!("failed to dispatch command: {}", err);
|
||||
|
||||
// TODO(ppacher): we should send some error to cmd.response here.
|
||||
// Otherwise, the sender of cmd might get stuck waiting for responses
|
||||
// if they don't check for PortAPI.is_closed().
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Ok(PortAPI { dispatch: tx })
|
||||
}
|
||||
|
||||
impl PortAPI {
|
||||
/// `request` sends a PortAPI `portapi::types::Request` to the server and returns a mpsc receiver channel
|
||||
/// where all server responses are forwarded.
|
||||
///
|
||||
/// If the caller does not intend to read any responses the returned receiver may be closed or
|
||||
/// dropped. As soon as the async-thread launched in `connect` detects a closed receiver it is remove
|
||||
/// from the subscription map.
|
||||
///
|
||||
/// The default buffer size for the channel is 64. Use `request_with_buffer_size` to specify a dedicated buffer size.
|
||||
pub async fn request(
|
||||
&self,
|
||||
r: Request,
|
||||
) -> std::result::Result<Receiver<Response>, MessageError> {
|
||||
self.request_with_buffer_size(r, 64).await
|
||||
}
|
||||
|
||||
// Like `request` but supports explicitly specifying a channel buffer size.
|
||||
pub async fn request_with_buffer_size(
|
||||
&self,
|
||||
r: Request,
|
||||
buffer: usize,
|
||||
) -> std::result::Result<Receiver<Response>, MessageError> {
|
||||
let (tx, rx) = channel(buffer);
|
||||
|
||||
let msg: Message = r.try_into()?;
|
||||
|
||||
let _ = self.dispatch.send(Command { response: tx, msg }).await;
|
||||
|
||||
Ok(rx)
|
||||
}
|
||||
|
||||
/// Reports whether or not the websocket connection to the Portmaster Database API has been closed
|
||||
/// due to errors.
|
||||
///
|
||||
/// Users are expected to check this field on a regular interval to detect any issues and perform
|
||||
/// a clean re-connect by calling `connect` again.
|
||||
pub fn is_closed(&self) -> bool {
|
||||
self.dispatch.is_closed()
|
||||
}
|
||||
}
|
258
desktop/tauri/src-tauri/src/portapi/message.rs
Normal file
258
desktop/tauri/src-tauri/src/portapi/message.rs
Normal file
|
@ -0,0 +1,258 @@
|
|||
use thiserror::Error;
|
||||
|
||||
/// MessageError describes any error that is encountered when parsing
|
||||
/// PortAPI messages or when converting between the Request/Response types.
|
||||
#[derive(Debug, Error)]
|
||||
pub enum MessageError {
|
||||
#[error("missing command id")]
|
||||
MissingID,
|
||||
|
||||
#[error("invalid command id")]
|
||||
InvalidID,
|
||||
|
||||
#[error("missing command")]
|
||||
MissingCommand,
|
||||
|
||||
#[error("missing key")]
|
||||
MissingKey,
|
||||
|
||||
#[error("missing payload")]
|
||||
MissingPayload,
|
||||
|
||||
#[error("unknown or unsupported command: {0}")]
|
||||
UnknownCommand(String),
|
||||
|
||||
#[error(transparent)]
|
||||
InvalidPayload(#[from] serde_json::Error),
|
||||
}
|
||||
|
||||
|
||||
/// Payload defines the payload type and content of a PortAPI message.
|
||||
///
|
||||
/// For the time being, only JSON payloads (indicated by a prefixed 'J' of the payload content)
|
||||
/// is directly supported in `Payload::parse()`.
|
||||
///
|
||||
/// For other payload types (like CBOR, BSON, ...) it's the user responsibility to figure out
|
||||
/// appropriate decoding from the `Payload::UNKNOWN` variant.
|
||||
#[derive(PartialEq, Debug, Clone)]
|
||||
pub enum Payload {
|
||||
JSON(String),
|
||||
UNKNOWN(String),
|
||||
}
|
||||
|
||||
/// ParseError is returned from `Payload::parse()`.
|
||||
#[derive(Debug, Error)]
|
||||
pub enum ParseError {
|
||||
#[error(transparent)]
|
||||
JSON(#[from] serde_json::Error),
|
||||
|
||||
#[error("unknown error while parsing")]
|
||||
UNKNOWN
|
||||
}
|
||||
|
||||
|
||||
impl Payload {
|
||||
/// Parse the payload into T.
|
||||
///
|
||||
/// Only JSON parsing is supported for now. See [Payload] for more information.
|
||||
pub fn parse<'a, T>(self: &'a Self) -> std::result::Result<T, ParseError>
|
||||
where
|
||||
T: serde::de::Deserialize<'a> {
|
||||
|
||||
match self {
|
||||
Payload::JSON(blob) => Ok(serde_json::from_str::<T>(blob.as_str())?),
|
||||
Payload::UNKNOWN(_) => Err(ParseError::UNKNOWN),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Supports creating a Payload instance from a String.
|
||||
///
|
||||
/// See [Payload] for more information.
|
||||
impl std::convert::From<String> for Payload {
|
||||
fn from(value: String) -> Payload {
|
||||
let mut chars = value.chars();
|
||||
let first = chars.next();
|
||||
let rest = chars.as_str().to_string();
|
||||
|
||||
match first {
|
||||
Some(c) => match c {
|
||||
'J' => Payload::JSON(rest),
|
||||
_ => Payload::UNKNOWN(value),
|
||||
},
|
||||
None => Payload::UNKNOWN("".to_string())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Display implementation for Payload that just displays the raw payload.
|
||||
impl std::fmt::Display for Payload {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Payload::JSON(payload) => {
|
||||
write!(f, "J{}", payload)
|
||||
},
|
||||
Payload::UNKNOWN(payload) => {
|
||||
write!(f, "{}", payload)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Message is an internal representation of a PortAPI message.
|
||||
/// Users should more likely use `portapi::types::Request` and `portapi::types::Response`
|
||||
/// instead of directly using `Message`.
|
||||
///
|
||||
/// The struct is still public since it might be useful for debugging or to implement new
|
||||
/// commands not yet supported by the `portapi::types` crate.
|
||||
#[derive(PartialEq, Debug, Clone)]
|
||||
pub struct Message {
|
||||
pub id: usize,
|
||||
pub cmd: String,
|
||||
pub key: Option<String>,
|
||||
pub payload: Option<Payload>,
|
||||
}
|
||||
|
||||
/// Implementation to marshal a PortAPI message into it's wire-format representation
|
||||
/// (which is a string).
|
||||
///
|
||||
/// Note that this conversion does not check for invalid messages!
|
||||
impl std::convert::From<Message> for String {
|
||||
fn from(value: Message) -> Self {
|
||||
let mut result = "".to_owned();
|
||||
|
||||
result.push_str(value.id.to_string().as_str());
|
||||
result.push_str("|");
|
||||
result.push_str(&value.cmd);
|
||||
|
||||
if let Some(key) = value.key {
|
||||
result.push_str("|");
|
||||
result.push_str(key.as_str());
|
||||
}
|
||||
|
||||
if let Some(payload) = value.payload {
|
||||
result.push_str("|");
|
||||
result.push_str(payload.to_string().as_str())
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
/// An implementation for `String::parse()` to convert a wire-format representation
|
||||
/// of a PortAPI message to a Message instance.
|
||||
///
|
||||
/// Any errors returned from `String::parse()` will be of type `MessageError`
|
||||
impl std::str::FromStr for Message {
|
||||
type Err = MessageError;
|
||||
|
||||
fn from_str(line: &str) -> Result<Self, Self::Err> {
|
||||
let parts = line.split("|").collect::<Vec<&str>>();
|
||||
|
||||
let id = match parts.get(0) {
|
||||
Some(s) => match (*s).parse::<usize>() {
|
||||
Ok(id) => Ok(id),
|
||||
Err(_) => Err(MessageError::InvalidID),
|
||||
},
|
||||
None => Err(MessageError::MissingID),
|
||||
}?;
|
||||
|
||||
let cmd = match parts.get(1) {
|
||||
Some(s) => Ok(*s),
|
||||
None => Err(MessageError::MissingCommand),
|
||||
}?
|
||||
.to_string();
|
||||
|
||||
let key = parts.get(2)
|
||||
.and_then(|key| Some(key.to_string()));
|
||||
|
||||
let payload : Option<Payload> = parts.get(3)
|
||||
.and_then(|p| Some(p.to_string().into()));
|
||||
|
||||
return Ok(Message {
|
||||
id,
|
||||
cmd,
|
||||
key,
|
||||
payload: payload
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use serde::Deserialize;
|
||||
|
||||
#[derive(Debug, PartialEq, Deserialize)]
|
||||
struct Test {
|
||||
a: i64,
|
||||
s: String,
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn payload_to_string() {
|
||||
let p = Payload::JSON("{}".to_string());
|
||||
assert_eq!(p.to_string(), "J{}");
|
||||
|
||||
let p = Payload::UNKNOWN("some unknown content".to_string());
|
||||
assert_eq!(p.to_string(), "some unknown content");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn payload_from_string() {
|
||||
let p: Payload = "J{}".to_string().into();
|
||||
assert_eq!(p, Payload::JSON("{}".to_string()));
|
||||
|
||||
let p: Payload = "some unknown content".to_string().into();
|
||||
assert_eq!(p, Payload::UNKNOWN("some unknown content".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn payload_parse() {
|
||||
let p: Payload = "J{\"a\": 100, \"s\": \"string\"}".to_string().into();
|
||||
|
||||
let t: Test = p.parse()
|
||||
.expect("Expected payload parsing to work");
|
||||
|
||||
assert_eq!(t, Test{
|
||||
a: 100,
|
||||
s: "string".to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_message() {
|
||||
let m = "10|insert|some:key|J{}".parse::<Message>()
|
||||
.expect("Expected message to parse");
|
||||
|
||||
assert_eq!(m, Message{
|
||||
id: 10,
|
||||
cmd: "insert".to_string(),
|
||||
key: Some("some:key".to_string()),
|
||||
payload: Some(Payload::JSON("{}".to_string())),
|
||||
});
|
||||
|
||||
let m = "1|done".parse::<Message>()
|
||||
.expect("Expected message to parse");
|
||||
|
||||
assert_eq!(m, Message{
|
||||
id: 1,
|
||||
cmd: "done".to_string(),
|
||||
key: None,
|
||||
payload: None
|
||||
});
|
||||
|
||||
let m = "".parse::<Message>()
|
||||
.expect_err("Expected parsing to fail");
|
||||
if let MessageError::InvalidID = m {} else {
|
||||
panic!("unexpected error value: {}", m)
|
||||
}
|
||||
|
||||
let m = "1".parse::<Message>()
|
||||
.expect_err("Expected parsing to fail");
|
||||
|
||||
if let MessageError::MissingCommand = m {} else {
|
||||
panic!("unexpected error value: {}", m)
|
||||
}
|
||||
}
|
||||
}
|
4
desktop/tauri/src-tauri/src/portapi/mod.rs
Normal file
4
desktop/tauri/src-tauri/src/portapi/mod.rs
Normal file
|
@ -0,0 +1,4 @@
|
|||
pub mod client;
|
||||
pub mod message;
|
||||
pub mod types;
|
||||
pub mod models;
|
18
desktop/tauri/src-tauri/src/portapi/models/config.rs
Normal file
18
desktop/tauri/src-tauri/src/portapi/models/config.rs
Normal file
|
@ -0,0 +1,18 @@
|
|||
use serde::*;
|
||||
use super::super::message::Payload;
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
|
||||
pub struct BooleanValue {
|
||||
#[serde(rename = "Value")]
|
||||
pub value: Option<bool>,
|
||||
}
|
||||
|
||||
impl TryInto<Payload> for BooleanValue {
|
||||
type Error = serde_json::Error;
|
||||
|
||||
fn try_into(self) -> Result<Payload, Self::Error> {
|
||||
let str = serde_json::to_string(&self)?;
|
||||
|
||||
Ok(Payload::JSON(str))
|
||||
}
|
||||
}
|
4
desktop/tauri/src-tauri/src/portapi/models/mod.rs
Normal file
4
desktop/tauri/src-tauri/src/portapi/models/mod.rs
Normal file
|
@ -0,0 +1,4 @@
|
|||
pub mod config;
|
||||
pub mod spn;
|
||||
pub mod notification;
|
||||
pub mod subsystem;
|
70
desktop/tauri/src-tauri/src/portapi/models/notification.rs
Normal file
70
desktop/tauri/src-tauri/src/portapi/models/notification.rs
Normal file
|
@ -0,0 +1,70 @@
|
|||
use serde::*;
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, PartialEq)]
|
||||
pub struct Notification {
|
||||
#[serde(rename = "EventID")]
|
||||
pub event_id: String,
|
||||
|
||||
#[serde(rename = "GUID")]
|
||||
pub guid: String,
|
||||
|
||||
#[serde(rename = "Type")]
|
||||
pub notification_type: NotificationType,
|
||||
|
||||
#[serde(rename = "Message")]
|
||||
pub message: String,
|
||||
|
||||
#[serde(rename = "Title")]
|
||||
pub title: String,
|
||||
#[serde(rename = "Category")]
|
||||
pub category: String,
|
||||
|
||||
#[serde(rename = "EventData")]
|
||||
pub data: serde_json::Value,
|
||||
|
||||
#[serde(rename = "Expires")]
|
||||
pub expires: u64,
|
||||
|
||||
#[serde(rename = "State")]
|
||||
pub state: String,
|
||||
|
||||
#[serde(rename = "AvailableActions")]
|
||||
pub actions: Vec<Action>,
|
||||
|
||||
#[serde(rename = "SelectedActionID")]
|
||||
pub selected_action_id: String,
|
||||
|
||||
#[serde(rename = "ShowOnSystem")]
|
||||
pub show_on_system: bool,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, PartialEq)]
|
||||
pub struct Action {
|
||||
#[serde(rename = "ID")]
|
||||
pub id: String,
|
||||
|
||||
#[serde(rename = "Text")]
|
||||
pub text: String,
|
||||
|
||||
#[serde(rename = "Type")]
|
||||
pub action_type: String,
|
||||
|
||||
#[serde(rename = "Payload")]
|
||||
pub payload: serde_json::Value,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, PartialEq)]
|
||||
pub struct NotificationType(i32);
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub const INFO: NotificationType = NotificationType(0);
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub const WARN: NotificationType = NotificationType(1);
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub const PROMPT: NotificationType = NotificationType(2);
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub const ERROR: NotificationType = NotificationType(3);
|
||||
|
8
desktop/tauri/src-tauri/src/portapi/models/spn.rs
Normal file
8
desktop/tauri/src-tauri/src/portapi/models/spn.rs
Normal file
|
@ -0,0 +1,8 @@
|
|||
use serde::*;
|
||||
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
|
||||
pub struct SPNStatus {
|
||||
#[serde(rename = "Status")]
|
||||
pub status: String,
|
||||
}
|
45
desktop/tauri/src-tauri/src/portapi/models/subsystem.rs
Normal file
45
desktop/tauri/src-tauri/src/portapi/models/subsystem.rs
Normal file
|
@ -0,0 +1,45 @@
|
|||
#![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;
|
199
desktop/tauri/src-tauri/src/portapi/types.rs
Normal file
199
desktop/tauri/src-tauri/src/portapi/types.rs
Normal file
|
@ -0,0 +1,199 @@
|
|||
|
||||
use super::message::*;
|
||||
|
||||
/// Request is a strongly typed request message
|
||||
/// that can be converted to a `portapi::message::Message`
|
||||
/// object for further use by the client (`portapi::client::PortAPI`).
|
||||
#[derive(PartialEq, Debug)]
|
||||
pub enum Request {
|
||||
Get(String),
|
||||
Query(String),
|
||||
Subscribe(String),
|
||||
QuerySubscribe(String),
|
||||
Create(String, Payload),
|
||||
Update(String, Payload),
|
||||
Insert(String, Payload),
|
||||
Delete(String),
|
||||
Cancel,
|
||||
}
|
||||
|
||||
/// Implementation to convert a internal `portapi::message::Message` to a valid
|
||||
/// `Request` variant.
|
||||
///
|
||||
/// Any error returned will be of type `portapi::message::MessageError`.
|
||||
impl std::convert::TryFrom<Message> for Request {
|
||||
type Error = MessageError;
|
||||
|
||||
fn try_from(value: Message) -> Result<Self, Self::Error> {
|
||||
match value.cmd.as_str() {
|
||||
"get" => {
|
||||
let key = value.key.ok_or(MessageError::MissingKey)?;
|
||||
Ok(Request::Get(key))
|
||||
},
|
||||
"query" => {
|
||||
let key = value.key.ok_or(MessageError::MissingKey)?;
|
||||
Ok(Request::Query(key))
|
||||
},
|
||||
"sub" => {
|
||||
let key = value.key.ok_or(MessageError::MissingKey)?;
|
||||
Ok(Request::Subscribe(key))
|
||||
},
|
||||
"qsub" => {
|
||||
let key = value.key.ok_or(MessageError::MissingKey)?;
|
||||
Ok(Request::QuerySubscribe(key))
|
||||
},
|
||||
"create" => {
|
||||
let key = value.key.ok_or(MessageError::MissingKey)?;
|
||||
let payload = value.payload.ok_or(MessageError::MissingPayload)?;
|
||||
Ok(Request::Create(key, payload))
|
||||
},
|
||||
"update" => {
|
||||
let key = value.key.ok_or(MessageError::MissingKey)?;
|
||||
let payload = value.payload.ok_or(MessageError::MissingPayload)?;
|
||||
Ok(Request::Update(key, payload))
|
||||
},
|
||||
"insert" => {
|
||||
let key = value.key.ok_or(MessageError::MissingKey)?;
|
||||
let payload = value.payload.ok_or(MessageError::MissingPayload)?;
|
||||
Ok(Request::Insert(key, payload))
|
||||
},
|
||||
"delete" => {
|
||||
let key = value.key.ok_or(MessageError::MissingKey)?;
|
||||
Ok(Request::Delete(key))
|
||||
},
|
||||
"cancel" => {
|
||||
Ok(Request::Cancel)
|
||||
},
|
||||
cmd => {
|
||||
Err(MessageError::UnknownCommand(cmd.to_string()))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// An implementation to try to convert a `Request` variant into a valid
|
||||
/// `portapi::message::Message` struct.
|
||||
///
|
||||
/// While this implementation does not yet return any errors, it's expected that
|
||||
/// additional validation will be added in the future so users should already expect
|
||||
/// to receive `portapi::message::MessageError`s.
|
||||
impl std::convert::TryFrom<Request> for Message {
|
||||
type Error = MessageError;
|
||||
|
||||
fn try_from(value: Request) -> Result<Self, Self::Error> {
|
||||
match value {
|
||||
Request::Get(key) => Ok(Message { id: 0, cmd: "get".to_string(), key: Some(key), payload: None }),
|
||||
Request::Query(key) => Ok(Message { id: 0, cmd: "query".to_string(), key: Some(key), payload: None }),
|
||||
Request::Subscribe(key) => Ok(Message { id: 0, cmd: "sub".to_string(), key: Some(key), payload: None }),
|
||||
Request::QuerySubscribe(key) => Ok(Message { id: 0, cmd: "qsub".to_string(), key: Some(key), payload: None }),
|
||||
Request::Create(key, value) => Ok(Message{ id: 0, cmd: "create".to_string(), key: Some(key), payload: Some(value)}),
|
||||
Request::Update(key, value) => Ok(Message{ id: 0, cmd: "update".to_string(), key: Some(key), payload: Some(value)}),
|
||||
Request::Insert(key, value) => Ok(Message{ id: 0, cmd: "insert".to_string(), key: Some(key), payload: Some(value)}),
|
||||
Request::Delete(key) => Ok(Message { id: 0, cmd: "delete".to_string(), key: Some(key), payload: None }),
|
||||
Request::Cancel => Ok(Message { id: 0, cmd: "cancel".to_string(), key: None, payload: None }),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Response is strongly types PortAPI response message.
|
||||
/// that can be converted to a `portapi::message::Message`
|
||||
/// object for further use by the client (`portapi::client::PortAPI`).
|
||||
#[derive(PartialEq, Debug)]
|
||||
pub enum Response {
|
||||
Ok(String, Payload),
|
||||
Update(String, Payload),
|
||||
New(String, Payload),
|
||||
Delete(String),
|
||||
Success,
|
||||
Error(String),
|
||||
Warning(String),
|
||||
Done
|
||||
}
|
||||
|
||||
/// Implementation to convert a internal `portapi::message::Message` to a valid
|
||||
/// `Response` variant.
|
||||
///
|
||||
/// Any error returned will be of type `portapi::message::MessageError`.
|
||||
impl std::convert::TryFrom<Message> for Response {
|
||||
type Error = MessageError;
|
||||
|
||||
fn try_from(value: Message) -> Result<Self, MessageError> {
|
||||
match value.cmd.as_str() {
|
||||
"ok" => {
|
||||
let key = value.key.ok_or(MessageError::MissingKey)?;
|
||||
let payload = value.payload.ok_or(MessageError::MissingPayload)?;
|
||||
|
||||
Ok(Response::Ok(key, payload))
|
||||
},
|
||||
"upd" => {
|
||||
let key = value.key.ok_or(MessageError::MissingKey)?;
|
||||
let payload = value.payload.ok_or(MessageError::MissingPayload)?;
|
||||
|
||||
Ok(Response::Update(key, payload))
|
||||
},
|
||||
"new" => {
|
||||
let key = value.key.ok_or(MessageError::MissingKey)?;
|
||||
let payload = value.payload.ok_or(MessageError::MissingPayload)?;
|
||||
|
||||
Ok(Response::New(key, payload))
|
||||
},
|
||||
"del" => {
|
||||
let key = value.key.ok_or(MessageError::MissingKey)?;
|
||||
|
||||
Ok(Response::Delete(key))
|
||||
},
|
||||
"success" => {
|
||||
Ok(Response::Success)
|
||||
},
|
||||
"error" => {
|
||||
let key = value.key.ok_or(MessageError::MissingKey)?;
|
||||
|
||||
Ok(Response::Error(key))
|
||||
},
|
||||
"warning" => {
|
||||
let key = value.key.ok_or(MessageError::MissingKey)?;
|
||||
|
||||
Ok(Response::Warning(key))
|
||||
},
|
||||
"done" => {
|
||||
Ok(Response::Done)
|
||||
},
|
||||
cmd => Err(MessageError::UnknownCommand(cmd.to_string()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// An implementation to try to convert a `Response` variant into a valid
|
||||
/// `portapi::message::Message` struct.
|
||||
///
|
||||
/// While this implementation does not yet return any errors, it's expected that
|
||||
/// additional validation will be added in the future so users should already expect
|
||||
/// to receive `portapi::message::MessageError`s.
|
||||
impl std::convert::TryFrom<Response> for Message {
|
||||
type Error = MessageError;
|
||||
|
||||
fn try_from(value: Response) -> Result<Self, Self::Error> {
|
||||
match value {
|
||||
Response::Ok(key, payload) => Ok(Message{id: 0, cmd: "ok".to_string(), key: Some(key), payload: Some(payload)}),
|
||||
Response::Update(key, payload) => Ok(Message{id: 0, cmd: "upd".to_string(), key: Some(key), payload: Some(payload)}),
|
||||
Response::New(key, payload) => Ok(Message{id: 0, cmd: "new".to_string(), key: Some(key), payload: Some(payload)}),
|
||||
Response::Delete(key ) => Ok(Message{id: 0, cmd: "del".to_string(), key: Some(key), payload: None}),
|
||||
Response::Success => Ok(Message{id: 0, cmd: "success".to_string(), key: None, payload: None}),
|
||||
Response::Warning(key) => Ok(Message{id: 0, cmd: "warning".to_string(), key: Some(key), payload: None}),
|
||||
Response::Error(key) => Ok(Message{id: 0, cmd: "error".to_string(), key: Some(key), payload: None}),
|
||||
Response::Done => Ok(Message{id: 0, cmd: "done".to_string(), key: None, payload: None}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize)]
|
||||
#[serde(rename_all = "UPPERCASE")]
|
||||
pub struct Record {
|
||||
pub created: u64,
|
||||
pub deleted: u64,
|
||||
pub expires: u64,
|
||||
pub modified: u64,
|
||||
pub key: String,
|
||||
}
|
182
desktop/tauri/src-tauri/src/portmaster/commands.rs
Normal file
182
desktop/tauri/src-tauri/src/portmaster/commands.rs
Normal file
|
@ -0,0 +1,182 @@
|
|||
use super::PortmasterPlugin;
|
||||
use crate::service::get_service_manager;
|
||||
use crate::service::ServiceManager;
|
||||
use log::debug;
|
||||
use std::sync::atomic::Ordering;
|
||||
use tauri::{Manager, Runtime, State, Window};
|
||||
|
||||
pub type Result = std::result::Result<String, String>;
|
||||
|
||||
#[derive(Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct Error {
|
||||
pub error: String,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn should_show<R: Runtime>(
|
||||
_window: Window<R>,
|
||||
portmaster: State<'_, PortmasterPlugin<R>>,
|
||||
) -> Result {
|
||||
if portmaster.get_show_after_bootstrap() {
|
||||
debug!("[tauri:rpc:should_show] application should show after bootstrap");
|
||||
|
||||
Ok("show".to_string())
|
||||
} else {
|
||||
debug!("[tauri:rpc:should_show] application should hide after bootstrap");
|
||||
|
||||
Ok("hide".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn should_handle_prompts<R: Runtime>(
|
||||
_window: Window<R>,
|
||||
portmaster: State<'_, PortmasterPlugin<R>>,
|
||||
) -> Result {
|
||||
if portmaster.handle_prompts.load(Ordering::Relaxed) {
|
||||
Ok("true".to_string())
|
||||
} else {
|
||||
Ok("false".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn get_state<R: Runtime>(
|
||||
_window: Window<R>,
|
||||
portmaster: State<'_, PortmasterPlugin<R>>,
|
||||
key: String,
|
||||
) -> Result {
|
||||
let value = portmaster.get_state(key);
|
||||
|
||||
if let Some(value) = value {
|
||||
Ok(value)
|
||||
} else {
|
||||
Ok("".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn set_state<R: Runtime>(
|
||||
_window: Window<R>,
|
||||
portmaster: State<'_, PortmasterPlugin<R>>,
|
||||
key: String,
|
||||
value: String,
|
||||
) -> Result {
|
||||
portmaster.set_state(key, value);
|
||||
|
||||
Ok("".to_string())
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
#[tauri::command]
|
||||
pub fn get_app_info<R: Runtime>(
|
||||
window: Window<R>,
|
||||
response_id: String,
|
||||
matching_path: String,
|
||||
exec_path: String,
|
||||
pid: i64,
|
||||
cmdline: String,
|
||||
) -> Result {
|
||||
let mut id = response_id;
|
||||
|
||||
let info = crate::xdg::ProcessInfo {
|
||||
cmdline,
|
||||
exec_path,
|
||||
pid,
|
||||
matching_path,
|
||||
};
|
||||
|
||||
if id == "" {
|
||||
id = uuid::Uuid::new_v4().to_string()
|
||||
}
|
||||
let cloned = id.clone();
|
||||
|
||||
std::thread::spawn(move || match crate::xdg::get_app_info(info) {
|
||||
Ok(info) => window.emit(&id, info),
|
||||
Err(err) => window.emit(
|
||||
&id,
|
||||
Error {
|
||||
error: err.to_string(),
|
||||
},
|
||||
),
|
||||
});
|
||||
|
||||
Ok(cloned)
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
#[tauri::command]
|
||||
pub fn get_app_info<R: Runtime>(
|
||||
window: Window<R>,
|
||||
response_id: String,
|
||||
_matching_path: String,
|
||||
_exec_path: String,
|
||||
_pid: i64,
|
||||
_cmdline: String,
|
||||
) -> Result {
|
||||
let mut id = response_id;
|
||||
|
||||
if id == "" {
|
||||
id = uuid::Uuid::new_v4().to_string()
|
||||
}
|
||||
let cloned = id.clone();
|
||||
|
||||
std::thread::spawn(move || {
|
||||
let _ = window.emit(
|
||||
&id,
|
||||
Error {
|
||||
error: "Unsupported OS".to_string(),
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
Ok(cloned)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn get_service_manager_status<R: Runtime>(window: Window<R>, response_id: String) -> Result {
|
||||
let mut id = response_id;
|
||||
|
||||
if id == "" {
|
||||
id = uuid::Uuid::new_v4().to_string();
|
||||
}
|
||||
let cloned = id.clone();
|
||||
|
||||
std::thread::spawn(move || {
|
||||
let result = match get_service_manager() {
|
||||
Ok(sm) => sm.status().map_err(|err| err.to_string()),
|
||||
Err(err) => Err(err.to_string()),
|
||||
};
|
||||
|
||||
match result {
|
||||
Ok(result) => window.emit(&id, &result),
|
||||
Err(err) => window.emit(&id, Error { error: err }),
|
||||
}
|
||||
});
|
||||
|
||||
Ok(cloned)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn start_service<R: Runtime>(window: Window<R>, response_id: String) -> Result {
|
||||
let mut id = response_id;
|
||||
|
||||
if id == "" {
|
||||
id = uuid::Uuid::new_v4().to_string();
|
||||
}
|
||||
let cloned = id.clone();
|
||||
|
||||
std::thread::spawn(move || {
|
||||
let result = match get_service_manager() {
|
||||
Ok(sm) => sm.start().map_err(|err| err.to_string()),
|
||||
Err(err) => Err(err.to_string()),
|
||||
};
|
||||
|
||||
match result {
|
||||
Ok(result) => window.emit(&id, &result),
|
||||
Err(err) => window.emit(&id, Error { error: err }),
|
||||
}
|
||||
});
|
||||
|
||||
Ok(cloned)
|
||||
}
|
294
desktop/tauri/src-tauri/src/portmaster/mod.rs
Normal file
294
desktop/tauri/src-tauri/src/portmaster/mod.rs
Normal file
|
@ -0,0 +1,294 @@
|
|||
/// This module contains a custom tauri plugin that handles all communication
|
||||
/// with the angular app loaded from the portmaster api.
|
||||
///
|
||||
/// Using a custom-plugin for this has the advantage that all code that has
|
||||
/// access to a tauri::Window or a tauri::AppHandle can get access to the
|
||||
/// portmaster plugin using the Runtime/Manager extension by just calling
|
||||
/// window.portmaster() or app_handle.portmaster().
|
||||
///
|
||||
/// Any portmaster related features (like changing a portmaster setting) should
|
||||
/// live in this module.
|
||||
///
|
||||
/// Code that handles windows should NOT live here but should rather be placed
|
||||
/// in the crate root.
|
||||
// The commands module contains tauri commands that are available to Javascript
|
||||
// using the invoke() and our custom invokeAsync() command.
|
||||
mod commands;
|
||||
|
||||
// The websocket module spawns an async function on tauri's runtime that manages
|
||||
// a persistent connection to the Portmaster websocket API and updates the tauri Portmaster
|
||||
// Plugin instance.
|
||||
mod websocket;
|
||||
|
||||
// The notification module manages system notifications from portmaster.
|
||||
mod notifications;
|
||||
|
||||
use crate::portapi::{
|
||||
client::PortAPI, message::Payload, models::config::BooleanValue, types::Request,
|
||||
};
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
sync::atomic::{AtomicBool, Ordering},
|
||||
};
|
||||
|
||||
use log::debug;
|
||||
use serde;
|
||||
use std::sync::Mutex;
|
||||
use tauri::{
|
||||
plugin::{Builder, TauriPlugin},
|
||||
AppHandle, Manager, Runtime,
|
||||
};
|
||||
|
||||
pub trait Handler {
|
||||
fn on_connect(&mut self, cli: PortAPI) -> ();
|
||||
fn on_disconnect(&mut self);
|
||||
}
|
||||
|
||||
pub struct PortmasterPlugin<R: Runtime> {
|
||||
#[allow(dead_code)]
|
||||
app: AppHandle<R>,
|
||||
|
||||
// state allows the angular application to store arbitrary values in the
|
||||
// tauri application memory using the get_state and set_state
|
||||
// tauri::commands.
|
||||
state: Mutex<HashMap<String, String>>,
|
||||
|
||||
// an atomic boolean that indicates if we're currently connected to
|
||||
// portmaster or not.
|
||||
is_reachable: AtomicBool,
|
||||
|
||||
// holds the portapi client if any.
|
||||
api: Mutex<Option<PortAPI>>,
|
||||
|
||||
// a vector of handlers that should be invoked on connect and disconnect of
|
||||
// the portmaster API.
|
||||
handlers: Mutex<Vec<Box<dyn Handler + Send>>>,
|
||||
|
||||
// whether or not we should handle notifications here.
|
||||
handle_notifications: AtomicBool,
|
||||
|
||||
// whether or not we should handle prompts.
|
||||
handle_prompts: AtomicBool,
|
||||
|
||||
// whether or not the angular application should call window.show after it
|
||||
// finished bootstrapping.
|
||||
should_show_after_bootstrap: AtomicBool,
|
||||
}
|
||||
|
||||
impl<R: Runtime> PortmasterPlugin<R> {
|
||||
/// Returns a state stored in the portmaster plugin.
|
||||
pub fn get_state(&self, key: String) -> Option<String> {
|
||||
let map = self.state.lock();
|
||||
|
||||
if let Ok(map) = map {
|
||||
match map.get(&key) {
|
||||
Some(value) => Some(value.clone()),
|
||||
None => None,
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Adds a new state to the portmaster plugin.
|
||||
pub fn set_state(&self, key: String, value: String) {
|
||||
let map = self.state.lock();
|
||||
|
||||
if let Ok(mut map) = map {
|
||||
map.insert(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
/// Reports wheter or not we're currently connected to the Portmaster API.
|
||||
pub fn is_reachable(&self) -> bool {
|
||||
self.is_reachable.load(Ordering::Relaxed)
|
||||
}
|
||||
|
||||
/// Registers a new connection handler that is called on connect
|
||||
/// and disconnect of the Portmaster websocket API.
|
||||
pub fn register_handler(&self, mut handler: impl Handler + Send + 'static) {
|
||||
// register_handler can only be invoked after the plugin setup
|
||||
// completed. in this case, the websocket thread is already spawned and
|
||||
// we might already be connected or know that the connection failed.
|
||||
// Call the respective handler method immediately now.
|
||||
if let Some(api) = self.get_api() {
|
||||
handler.on_connect(api);
|
||||
} else {
|
||||
handler.on_disconnect();
|
||||
}
|
||||
|
||||
if let Ok(mut handlers) = self.handlers.lock() {
|
||||
handlers.push(Box::new(handler));
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the current portapi client.
|
||||
pub fn get_api(&self) -> Option<PortAPI> {
|
||||
if let Ok(mut api) = self.api.lock() {
|
||||
match &mut *api {
|
||||
Some(api) => Some(api.clone()),
|
||||
None => None,
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Feature functions (enable/disable certain features).
|
||||
|
||||
/// Configures whether or not our tauri app should show system
|
||||
/// notifications. This excludes connection prompts. Use
|
||||
/// with_connection_prompts to enable handling of connection prompts.
|
||||
pub fn with_notification_support(&self, enable: bool) {
|
||||
self.handle_notifications.store(enable, Ordering::Relaxed);
|
||||
|
||||
// kick of the notification handler if we are connected.
|
||||
if enable {
|
||||
self.start_notification_handler();
|
||||
}
|
||||
}
|
||||
|
||||
/// Configures whether or not our angular application should show connection
|
||||
/// prompts via tauri.
|
||||
pub fn with_connection_prompts(&self, enable: bool) {
|
||||
self.handle_prompts.store(enable, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
/// Whether or not the angular application should call window.show after it
|
||||
/// finished bootstrapping.
|
||||
pub fn set_show_after_bootstrap(&self, show: bool) {
|
||||
self.should_show_after_bootstrap
|
||||
.store(show, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
/// Returns whether or not the angular application should call window.show
|
||||
/// after it finished bootstrapping.
|
||||
pub fn get_show_after_bootstrap(&self) -> bool {
|
||||
self.should_show_after_bootstrap.load(Ordering::Relaxed)
|
||||
}
|
||||
|
||||
/// Tells the angular applicatoin to show the window by emitting an event.
|
||||
/// It calls set_show_after_bootstrap(true) automatically so the application
|
||||
/// also shows after bootstrapping.
|
||||
pub fn show_window(&self) {
|
||||
debug!("[tauri] showing main window");
|
||||
|
||||
// set show_after_bootstrap to true so the app will even show if it
|
||||
// misses the event below because it's still bootstrapping.
|
||||
self.set_show_after_bootstrap(true);
|
||||
|
||||
// ignore the error here, there's nothing we could do about it anyways.
|
||||
let _ = self.app.emit("portmaster:show", "");
|
||||
}
|
||||
|
||||
/// Enables or disables the SPN.
|
||||
pub fn set_spn_enabled(&self, enabled: bool) {
|
||||
if let Some(api) = self.get_api() {
|
||||
let body: Result<Payload, serde_json::Error> = BooleanValue {
|
||||
value: Some(enabled),
|
||||
}
|
||||
.try_into();
|
||||
|
||||
if let Ok(payload) = body {
|
||||
tauri::async_runtime::spawn(async move {
|
||||
_ = api
|
||||
.request(Request::Update("config:spn/enable".to_string(), payload))
|
||||
.await;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//// Internal functions
|
||||
fn start_notification_handler(&self) {
|
||||
if let Some(api) = self.get_api() {
|
||||
let cli = api.clone();
|
||||
tauri::async_runtime::spawn(async move {
|
||||
notifications::notification_handler(cli).await;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Internal method to call all on_connect handlers
|
||||
fn on_connect(&self, api: PortAPI) {
|
||||
self.is_reachable.store(true, Ordering::Relaxed);
|
||||
|
||||
// store the new api client.
|
||||
let mut guard = self.api.lock().unwrap();
|
||||
*guard = Some(api.clone());
|
||||
drop(guard);
|
||||
|
||||
// fire-off the notification handler.
|
||||
if self.handle_notifications.load(Ordering::Relaxed) {
|
||||
self.start_notification_handler();
|
||||
}
|
||||
|
||||
if let Ok(mut handlers) = self.handlers.lock() {
|
||||
for handler in handlers.iter_mut() {
|
||||
handler.on_connect(api.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Internal method to call all on_disconnect handlers
|
||||
fn on_disconnect(&self) {
|
||||
self.is_reachable.store(false, Ordering::Relaxed);
|
||||
|
||||
// clear the current api client reference.
|
||||
let mut guard = self.api.lock().unwrap();
|
||||
*guard = None;
|
||||
drop(guard);
|
||||
|
||||
if let Ok(mut handlers) = self.handlers.lock() {
|
||||
for handler in handlers.iter_mut() {
|
||||
handler.on_disconnect();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub trait PortmasterExt<R: Runtime> {
|
||||
fn portmaster(&self) -> &PortmasterPlugin<R>;
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize, Debug)]
|
||||
pub struct Config {}
|
||||
|
||||
impl<R: Runtime, T: Manager<R>> PortmasterExt<R> for T {
|
||||
fn portmaster(&self) -> &PortmasterPlugin<R> {
|
||||
self.state::<PortmasterPlugin<R>>().inner()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn init<R: Runtime>() -> TauriPlugin<R, Option<Config>> {
|
||||
Builder::<R, Option<Config>>::new("portmaster")
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
commands::get_app_info,
|
||||
commands::get_service_manager_status,
|
||||
commands::start_service,
|
||||
commands::get_state,
|
||||
commands::set_state,
|
||||
commands::should_show,
|
||||
commands::should_handle_prompts
|
||||
])
|
||||
.setup(|app, _api| {
|
||||
let plugin = PortmasterPlugin {
|
||||
app: app.clone(),
|
||||
state: Mutex::new(HashMap::new()),
|
||||
is_reachable: AtomicBool::new(false),
|
||||
handlers: Mutex::new(Vec::new()),
|
||||
api: Mutex::new(None),
|
||||
handle_notifications: AtomicBool::new(false),
|
||||
handle_prompts: AtomicBool::new(false),
|
||||
should_show_after_bootstrap: AtomicBool::new(true),
|
||||
};
|
||||
|
||||
app.manage(plugin);
|
||||
|
||||
// fire of the websocket handler
|
||||
websocket::start_websocket_thread(app.clone());
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.build()
|
||||
}
|
103
desktop/tauri/src-tauri/src/portmaster/notifications.rs
Normal file
103
desktop/tauri/src-tauri/src/portmaster/notifications.rs
Normal file
|
@ -0,0 +1,103 @@
|
|||
use crate::portapi::client::*;
|
||||
use crate::portapi::message::*;
|
||||
use crate::portapi::models::notification::*;
|
||||
use crate::portapi::types::*;
|
||||
use log::error;
|
||||
use notify_rust;
|
||||
use serde_json::json;
|
||||
#[allow(unused_imports)]
|
||||
use tauri::async_runtime;
|
||||
|
||||
pub async fn notification_handler(cli: PortAPI) {
|
||||
let res = cli
|
||||
.request(Request::QuerySubscribe("query notifications:".to_string()))
|
||||
.await;
|
||||
|
||||
if let Ok(mut rx) = res {
|
||||
while let Some(msg) = rx.recv().await {
|
||||
let res = match msg {
|
||||
Response::Ok(key, payload) => Some((key, payload)),
|
||||
Response::New(key, payload) => Some((key, payload)),
|
||||
Response::Update(key, payload) => Some((key, payload)),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
if let Some((key, payload)) = res {
|
||||
match payload.parse::<Notification>() {
|
||||
Ok(n) => {
|
||||
// Skip if this one should not be shown using the system notifications
|
||||
if !n.show_on_system {
|
||||
return;
|
||||
}
|
||||
|
||||
// Skip if this action has already been acted on
|
||||
if n.selected_action_id != "" {
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO(ppacher): keep a reference of open notifications and close them
|
||||
// if the user reacted inside the UI:
|
||||
|
||||
let mut notif = notify_rust::Notification::new();
|
||||
notif.body(&n.message);
|
||||
notif.timeout(notify_rust::Timeout::Never); // TODO(ppacher): use n.expires to calculate the timeout.
|
||||
notif.summary(&n.title);
|
||||
notif.icon("portmaster");
|
||||
|
||||
for action in n.actions {
|
||||
notif.action(&action.id, &action.text);
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
let cli_clone = cli.clone();
|
||||
async_runtime::spawn(async move {
|
||||
let res = notif.show();
|
||||
match res {
|
||||
Ok(handle) => {
|
||||
handle.wait_for_action(|action| {
|
||||
match action {
|
||||
"__closed" => {
|
||||
// timeout
|
||||
}
|
||||
|
||||
value => {
|
||||
let value = value.to_string().clone();
|
||||
|
||||
async_runtime::spawn(async move {
|
||||
let _ = cli_clone
|
||||
.request(Request::Update(
|
||||
key,
|
||||
Payload::JSON(
|
||||
json!({
|
||||
"SelectedActionID": value
|
||||
})
|
||||
.to_string(),
|
||||
),
|
||||
))
|
||||
.await;
|
||||
});
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
Err(err) => {
|
||||
error!("failed to display notification: {}", err);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
Err(err) => match err {
|
||||
ParseError::JSON(err) => {
|
||||
error!("failed to parse notification: {}", err);
|
||||
}
|
||||
_ => {
|
||||
error!("unknown error when parsing notifications payload");
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
45
desktop/tauri/src-tauri/src/portmaster/websocket.rs
Normal file
45
desktop/tauri/src-tauri/src/portmaster/websocket.rs
Normal file
|
@ -0,0 +1,45 @@
|
|||
use super::PortmasterExt;
|
||||
use crate::portapi::client::connect;
|
||||
use log::{debug, error, info, warn};
|
||||
use tauri::{AppHandle, Runtime};
|
||||
use tokio::time::{sleep, Duration};
|
||||
|
||||
/// Starts a backround thread (via tauri::async_runtime) that connects to the Portmaster
|
||||
/// Websocket database API.
|
||||
pub fn start_websocket_thread<R: Runtime>(app: AppHandle<R>) {
|
||||
let app = app.clone();
|
||||
|
||||
tauri::async_runtime::spawn(async move {
|
||||
loop {
|
||||
debug!("Trying to connect to websocket endpoint");
|
||||
|
||||
let api = connect("ws://127.0.0.1:817/api/database/v1").await;
|
||||
|
||||
match api {
|
||||
Ok(cli) => {
|
||||
let portmaster = app.portmaster();
|
||||
|
||||
info!("Successfully connected to portmaster");
|
||||
|
||||
portmaster.on_connect(cli.clone());
|
||||
|
||||
while !cli.is_closed() {
|
||||
let _ = sleep(Duration::from_secs(1)).await;
|
||||
}
|
||||
|
||||
portmaster.on_disconnect();
|
||||
|
||||
warn!("lost connection to portmaster, retrying ....")
|
||||
}
|
||||
Err(err) => {
|
||||
error!("failed to create portapi client: {}", err);
|
||||
|
||||
app.portmaster().on_disconnect();
|
||||
|
||||
// sleep and retry
|
||||
sleep(Duration::from_secs(2)).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
17
desktop/tauri/src-tauri/src/service/manager.rs
Normal file
17
desktop/tauri/src-tauri/src/service/manager.rs
Normal file
|
@ -0,0 +1,17 @@
|
|||
use std::process::{Command, ExitStatus, Stdio};
|
||||
use std::{fs, io};
|
||||
|
||||
use thiserror::Error;
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
|
||||
use super::status::StatusResult;
|
||||
|
||||
static SYSTEMCTL: &str = "systemctl";
|
||||
// TODO(ppacher): add support for kdesudo and gksudo
|
||||
|
||||
enum SudoCommand {
|
||||
Pkexec,
|
||||
Gksu,
|
||||
}
|
76
desktop/tauri/src-tauri/src/service/mod.rs
Normal file
76
desktop/tauri/src-tauri/src/service/mod.rs
Normal file
|
@ -0,0 +1,76 @@
|
|||
// pub mod manager;
|
||||
pub mod status;
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
mod systemd;
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
mod windows_service;
|
||||
|
||||
use std::process::ExitStatus;
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
use crate::service::systemd::SystemdServiceManager;
|
||||
|
||||
use log::info;
|
||||
use thiserror::Error;
|
||||
|
||||
use self::status::StatusResult;
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[derive(Error, Debug)]
|
||||
pub enum ServiceManagerError {
|
||||
#[error("unsupported service manager")]
|
||||
UnsupportedServiceManager,
|
||||
|
||||
#[error("unsupported operating system")]
|
||||
UnsupportedOperatingSystem,
|
||||
|
||||
#[error(transparent)]
|
||||
FromUtf8Error(#[from] std::string::FromUtf8Error),
|
||||
|
||||
#[error(transparent)]
|
||||
IoError(#[from] std::io::Error),
|
||||
|
||||
#[error("{0} output={1}")]
|
||||
Other(ExitStatus, String),
|
||||
|
||||
#[error("{0}")]
|
||||
WindowsError(String),
|
||||
}
|
||||
|
||||
pub type Result<T> = std::result::Result<T, ServiceManagerError>;
|
||||
|
||||
/// A common interface to the system manager service (might be systemd, openrc, sc.exe, ...)
|
||||
pub trait ServiceManager {
|
||||
fn status(&self) -> Result<StatusResult>;
|
||||
fn start(&self) -> Result<StatusResult>;
|
||||
}
|
||||
|
||||
struct EmptyServiceManager();
|
||||
|
||||
impl ServiceManager for EmptyServiceManager {
|
||||
fn status(&self) -> Result<StatusResult> {
|
||||
Err(ServiceManagerError::UnsupportedServiceManager)
|
||||
}
|
||||
|
||||
fn start(&self) -> Result<StatusResult> {
|
||||
Err(ServiceManagerError::UnsupportedServiceManager)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_service_manager() -> Result<impl ServiceManager> {
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
if SystemdServiceManager::is_installed() {
|
||||
info!("system service manager: systemd");
|
||||
|
||||
Ok(SystemdServiceManager {})
|
||||
} else {
|
||||
Err(ServiceManagerError::UnsupportedServiceManager)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
return Ok(windows_service::SERVICE_MANGER.clone());
|
||||
}
|
27
desktop/tauri/src-tauri/src/service/status.rs
Normal file
27
desktop/tauri/src-tauri/src/service/status.rs
Normal file
|
@ -0,0 +1,27 @@
|
|||
use serde::{Serialize, Deserialize};
|
||||
|
||||
/// SystemResult defines the "success" codes when querying or starting
|
||||
/// a system service.
|
||||
#[derive(Serialize, Deserialize, Debug, PartialEq)]
|
||||
pub enum StatusResult {
|
||||
// The requested system service is installed and currently running.
|
||||
Running,
|
||||
|
||||
// The requested system service is installed but currently stopped.
|
||||
Stopped,
|
||||
|
||||
// NotFound is returned when the system service (systemd unit for linux)
|
||||
// has not been found and the system and likely means the Portmaster installtion
|
||||
// is broken all together.
|
||||
NotFound,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for StatusResult {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
StatusResult::Running => write!(f, "running"),
|
||||
StatusResult::Stopped => write!(f, "stopped"),
|
||||
StatusResult::NotFound => write!(f, "not installed")
|
||||
}
|
||||
}
|
||||
}
|
246
desktop/tauri/src-tauri/src/service/systemd.rs
Normal file
246
desktop/tauri/src-tauri/src/service/systemd.rs
Normal file
|
@ -0,0 +1,246 @@
|
|||
use log::{debug, error};
|
||||
|
||||
use super::status::StatusResult;
|
||||
use super::{Result, ServiceManager, ServiceManagerError};
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
use std::{
|
||||
fs, io,
|
||||
process::{Command, ExitStatus, Stdio},
|
||||
};
|
||||
|
||||
static SYSTEMCTL: &str = "systemctl";
|
||||
// TODO(ppacher): add support for kdesudo and gksudo
|
||||
|
||||
enum SudoCommand {
|
||||
Pkexec,
|
||||
Gksu,
|
||||
}
|
||||
|
||||
impl From<std::process::Output> for ServiceManagerError {
|
||||
fn from(output: std::process::Output) -> Self {
|
||||
let msg = String::from_utf8(output.stderr)
|
||||
.ok()
|
||||
.filter(|s| !s.trim().is_empty())
|
||||
.or_else(|| {
|
||||
String::from_utf8(output.stdout)
|
||||
.ok()
|
||||
.filter(|s| !s.trim().is_empty())
|
||||
})
|
||||
.unwrap_or_else(|| format!("Failed to run `systemctl`"));
|
||||
|
||||
ServiceManagerError::Other(output.status, msg)
|
||||
}
|
||||
}
|
||||
|
||||
/// System Service manager implementation for Linux based distros.
|
||||
pub struct SystemdServiceManager {}
|
||||
|
||||
impl SystemdServiceManager {
|
||||
/// Checks if systemctl is available in /sbin/ /bin, /usr/bin or /usr/sbin.
|
||||
///
|
||||
/// Note that we explicitly check those paths to avoid returning true in case
|
||||
/// there's a systemctl binary in the cwd and PATH includes . since this may
|
||||
/// pose a security risk of running an untrusted binary with root privileges.
|
||||
pub fn is_installed() -> bool {
|
||||
let paths = vec![
|
||||
"/sbin/systemctl",
|
||||
"/bin/systemctl",
|
||||
"/usr/sbin/systemctl",
|
||||
"/usr/bin/systemctl",
|
||||
];
|
||||
|
||||
for path in paths {
|
||||
debug!("checking for systemctl at path {}", path);
|
||||
|
||||
match fs::metadata(path) {
|
||||
Ok(md) => {
|
||||
debug!("found systemctl at path {} ", path);
|
||||
|
||||
if md.is_file() && md.permissions().mode() & 0o111 != 0 {
|
||||
return true;
|
||||
}
|
||||
|
||||
error!(
|
||||
"systemctl binary found but invalid permissions: {}",
|
||||
md.permissions().mode().to_string()
|
||||
);
|
||||
}
|
||||
Err(err) => {
|
||||
error!(
|
||||
"failed to check systemctl binary at {}: {}",
|
||||
path,
|
||||
err.to_string()
|
||||
);
|
||||
|
||||
continue;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
error!("failed to find systemctl binary");
|
||||
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
impl ServiceManager for SystemdServiceManager {
|
||||
fn status(&self) -> super::Result<StatusResult> {
|
||||
let name = "portmaster.service";
|
||||
let result = systemctl("is-active", name, false);
|
||||
|
||||
match result {
|
||||
// If `systemctl is-active` returns without an error code and stdout matches "active" (just to guard againt
|
||||
// unhandled cases), the service can be considered running.
|
||||
Ok(stdout) => {
|
||||
let mut copy = stdout.to_owned();
|
||||
trim_newline(&mut copy);
|
||||
|
||||
if copy != "active" {
|
||||
// make sure the output is as we expected
|
||||
Err(ServiceManagerError::Other(ExitStatus::default(), stdout))
|
||||
} else {
|
||||
Ok(StatusResult::Running)
|
||||
}
|
||||
}
|
||||
|
||||
Err(e) => {
|
||||
if let ServiceManagerError::Other(_err, ref output) = e {
|
||||
let mut copy = output.to_owned();
|
||||
trim_newline(&mut copy);
|
||||
|
||||
if copy == "inactive" {
|
||||
return Ok(StatusResult::Stopped);
|
||||
}
|
||||
} else {
|
||||
error!("failed to run 'systemctl is-active': {}", e.to_string());
|
||||
}
|
||||
|
||||
// Failed to check if the unit is running
|
||||
match systemctl("cat", name, false) {
|
||||
// "systemctl cat" seems to no have stable exit codes so we need
|
||||
// to check the output if it looks like "No files found for yyyy.service"
|
||||
// At least, the exit code are not documented for systemd v255 (newest at the time of writing)
|
||||
Err(ServiceManagerError::Other(status, msg)) => {
|
||||
if msg.contains("No files found for") {
|
||||
Ok(StatusResult::NotFound)
|
||||
} else {
|
||||
Err(ServiceManagerError::Other(status, msg))
|
||||
}
|
||||
}
|
||||
|
||||
// Any other error type means something went completely wrong while running systemctl altogether.
|
||||
Err(e) => Err(e),
|
||||
|
||||
// Fine, systemctl cat worked so if the output is "inactive" we know the service is installed
|
||||
// but stopped.
|
||||
Ok(_) => {
|
||||
// Unit seems to be installed so check the output of result
|
||||
let mut stderr = e.to_string();
|
||||
trim_newline(&mut stderr);
|
||||
|
||||
if stderr == "inactive" {
|
||||
Ok(StatusResult::Stopped)
|
||||
} else {
|
||||
Err(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn start(&self) -> Result<StatusResult> {
|
||||
let name = "portmaster.service";
|
||||
|
||||
// This time we need to run as root through pkexec or similar binaries like kdesudo/gksudo.
|
||||
systemctl("start", name, true)?;
|
||||
|
||||
// Check the status again to be sure it's started now
|
||||
self.status()
|
||||
}
|
||||
}
|
||||
|
||||
fn systemctl(
|
||||
cmd: &str,
|
||||
unit: &str,
|
||||
run_as_root: bool,
|
||||
) -> std::result::Result<String, ServiceManagerError> {
|
||||
let output = run(run_as_root, SYSTEMCTL, vec![cmd, unit])?;
|
||||
|
||||
// The command have been able to run (i.e. has been spawned and executed by the kernel).
|
||||
// We now need to check the exit code and "stdout/stderr" output in case of an error.
|
||||
if output.status.success() {
|
||||
Ok(String::from_utf8(output.stdout)?)
|
||||
} else {
|
||||
Err(output.into())
|
||||
}
|
||||
}
|
||||
|
||||
fn run<'a>(root: bool, cmd: &'a str, args: Vec<&'a str>) -> std::io::Result<std::process::Output> {
|
||||
// clone the args vector so we can insert the actual command in case we're running
|
||||
// through pkexec or friends. This is just callled a couple of times on start-up
|
||||
// so cloning the vector does not add any mentionable performance impact here and it's better
|
||||
// than expecting a mutalble vector in the first place.
|
||||
|
||||
let mut args = args.to_vec();
|
||||
|
||||
let mut command = match root {
|
||||
true => {
|
||||
// if we run through pkexec and friends we need to append cmd as the second argument.
|
||||
|
||||
args.insert(0, cmd);
|
||||
match get_sudo_cmd() {
|
||||
Ok(cmd) => {
|
||||
match cmd {
|
||||
SudoCommand::Pkexec => {
|
||||
// disable the internal text-based prompt agent from pkexec because it won't work anyway.
|
||||
args.insert(0, "--disable-internal-agent");
|
||||
Command::new("/usr/bin/pkexec")
|
||||
}
|
||||
SudoCommand::Gksu => {
|
||||
args.insert(0, "--message=Please enter your password:");
|
||||
args.insert(1, "--sudo-mode");
|
||||
|
||||
Command::new("/usr/bin/gksudo")
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(err) => return Err(err),
|
||||
}
|
||||
}
|
||||
false => Command::new(cmd),
|
||||
};
|
||||
|
||||
command.env("LC_ALL", "C");
|
||||
|
||||
command
|
||||
.stdin(Stdio::null())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped());
|
||||
|
||||
command.args(args).output()
|
||||
}
|
||||
|
||||
fn trim_newline(s: &mut String) {
|
||||
if s.ends_with('\n') {
|
||||
s.pop();
|
||||
if s.ends_with('\r') {
|
||||
s.pop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn get_sudo_cmd() -> std::result::Result<SudoCommand, std::io::Error> {
|
||||
if let Ok(_) = fs::metadata("/usr/bin/pkexec") {
|
||||
return Ok(SudoCommand::Pkexec);
|
||||
}
|
||||
|
||||
if let Ok(_) = fs::metadata("/usr/bin/gksudo") {
|
||||
return Ok(SudoCommand::Gksu);
|
||||
}
|
||||
|
||||
Err(std::io::Error::new(
|
||||
io::ErrorKind::NotFound,
|
||||
"failed to detect sudo command",
|
||||
))
|
||||
}
|
167
desktop/tauri/src-tauri/src/service/windows_service.rs
Normal file
167
desktop/tauri/src-tauri/src/service/windows_service.rs
Normal file
|
@ -0,0 +1,167 @@
|
|||
use std::{
|
||||
sync::{Arc, Mutex},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use windows::{
|
||||
core::{HSTRING, PCWSTR},
|
||||
Win32::{Foundation::HWND, UI::WindowsAndMessaging::SHOW_WINDOW_CMD},
|
||||
};
|
||||
use windows_service::{
|
||||
service::{Service, ServiceAccess},
|
||||
service_manager::{ServiceManager, ServiceManagerAccess},
|
||||
};
|
||||
|
||||
const SERVICE_NAME: &str = "PortmasterCore";
|
||||
|
||||
pub struct WindowsServiceManager {
|
||||
manager: Option<ServiceManager>,
|
||||
service: Option<Service>,
|
||||
}
|
||||
|
||||
lazy_static! {
|
||||
pub static ref SERVICE_MANGER: Arc<Mutex<WindowsServiceManager>> =
|
||||
Arc::new(Mutex::new(WindowsServiceManager::new()));
|
||||
}
|
||||
|
||||
impl WindowsServiceManager {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
manager: None,
|
||||
service: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn init_manager(&mut self) -> super::Result<()> {
|
||||
// Initialize service manager. This connects to the active service database and can query status.
|
||||
let manager = match ServiceManager::local_computer(
|
||||
None::<&str>,
|
||||
ServiceManagerAccess::ENUMERATE_SERVICE, // Only query status is allowed form non privileged application.
|
||||
) {
|
||||
Ok(manager) => manager,
|
||||
Err(err) => {
|
||||
return Err(windows_to_manager_err(err));
|
||||
}
|
||||
};
|
||||
self.manager = Some(manager);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn open_service(&mut self) -> super::Result<bool> {
|
||||
if let None = self.manager {
|
||||
self.init_manager()?;
|
||||
}
|
||||
|
||||
if let Some(manager) = &self.manager {
|
||||
let service = match manager.open_service(SERVICE_NAME, ServiceAccess::QUERY_STATUS) {
|
||||
Ok(service) => service,
|
||||
Err(_) => {
|
||||
return Ok(false); // Service is not installed.
|
||||
}
|
||||
};
|
||||
// Service is installed and the state can be queried.
|
||||
self.service = Some(service);
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
return Err(super::ServiceManagerError::WindowsError(
|
||||
"failed to initialize manager".to_string(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
impl super::ServiceManager for Arc<Mutex<WindowsServiceManager>> {
|
||||
fn status(&self) -> super::Result<super::status::StatusResult> {
|
||||
if let Ok(mut manager) = self.lock() {
|
||||
if let None = manager.service {
|
||||
// Try to open service
|
||||
if !manager.open_service()? {
|
||||
// Service is not installed.
|
||||
return Ok(super::status::StatusResult::NotFound);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(service) = &manager.service {
|
||||
match service.query_status() {
|
||||
Ok(status) => match status.current_state {
|
||||
windows_service::service::ServiceState::Stopped
|
||||
| windows_service::service::ServiceState::StopPending
|
||||
| windows_service::service::ServiceState::PausePending
|
||||
| windows_service::service::ServiceState::StartPending
|
||||
| windows_service::service::ServiceState::ContinuePending
|
||||
| windows_service::service::ServiceState::Paused => {
|
||||
// Stopped or in a transition state.
|
||||
return Ok(super::status::StatusResult::Stopped);
|
||||
}
|
||||
windows_service::service::ServiceState::Running => {
|
||||
// Everything expect Running state is considered stopped.
|
||||
return Ok(super::status::StatusResult::Running);
|
||||
}
|
||||
},
|
||||
Err(err) => {
|
||||
return Err(super::ServiceManagerError::WindowsError(err.to_string()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// This should be unreachable.
|
||||
Ok(super::status::StatusResult::NotFound)
|
||||
}
|
||||
|
||||
fn start(&self) -> super::Result<super::status::StatusResult> {
|
||||
if let Ok(mut service_manager) = self.lock() {
|
||||
// Check if service is installed.
|
||||
if let None = &service_manager.service {
|
||||
if let Err(_) = service_manager.open_service() {
|
||||
return Ok(super::status::StatusResult::NotFound);
|
||||
}
|
||||
}
|
||||
|
||||
// Run service manager with elevated privileges. This will show access popup.
|
||||
unsafe {
|
||||
windows::Win32::UI::Shell::ShellExecuteW(
|
||||
HWND::default(),
|
||||
&HSTRING::from("runas"),
|
||||
&HSTRING::from("C:\\Windows\\System32\\sc.exe"),
|
||||
&HSTRING::from(format!("start {}", SERVICE_NAME)),
|
||||
PCWSTR::null(),
|
||||
SHOW_WINDOW_CMD(0),
|
||||
);
|
||||
}
|
||||
|
||||
// Wait for service to start. Timeout 10s (100 * 100ms).
|
||||
if let Some(service) = &service_manager.service {
|
||||
for _ in 0..100 {
|
||||
match service.query_status() {
|
||||
Ok(status) => {
|
||||
if let windows_service::service::ServiceState::Running =
|
||||
status.current_state
|
||||
{
|
||||
return Ok(super::status::StatusResult::Running);
|
||||
} else {
|
||||
std::thread::sleep(Duration::from_millis(100));
|
||||
}
|
||||
}
|
||||
Err(err) => return Err(windows_to_manager_err(err)),
|
||||
}
|
||||
}
|
||||
}
|
||||
// Timeout starting the service.
|
||||
return Ok(super::status::StatusResult::Stopped);
|
||||
}
|
||||
return Err(super::ServiceManagerError::WindowsError(
|
||||
"failed to start service".to_string(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
fn windows_to_manager_err(err: windows_service::Error) -> super::ServiceManagerError {
|
||||
if let windows_service::Error::Winapi(_) = err {
|
||||
// Winapi does not contain the full error. Get the actual error from windows.
|
||||
return super::ServiceManagerError::WindowsError(
|
||||
windows::core::Error::from_win32().to_string(), // Internally will call `GetLastError()` and parse the result.
|
||||
);
|
||||
} else {
|
||||
return super::ServiceManagerError::WindowsError(err.to_string());
|
||||
}
|
||||
}
|
344
desktop/tauri/src-tauri/src/traymenu.rs
Normal file
344
desktop/tauri/src-tauri/src/traymenu.rs
Normal file
|
@ -0,0 +1,344 @@
|
|||
use std::collections::HashMap;
|
||||
use std::sync::Mutex;
|
||||
|
||||
use log::{debug, error};
|
||||
use tauri::{
|
||||
menu::{
|
||||
CheckMenuItem, CheckMenuItemBuilder, MenuBuilder, MenuItemBuilder, PredefinedMenuItem,
|
||||
SubmenuBuilder,
|
||||
},
|
||||
tray::{ClickType, TrayIcon, TrayIconBuilder},
|
||||
Icon, Manager, Wry,
|
||||
};
|
||||
use tauri_plugin_dialog::DialogExt;
|
||||
|
||||
use crate::{
|
||||
portapi::{
|
||||
client::PortAPI,
|
||||
message::ParseError,
|
||||
models::{
|
||||
config::BooleanValue,
|
||||
spn::SPNStatus,
|
||||
subsystem::{self, Subsystem},
|
||||
},
|
||||
types::{Request, Response},
|
||||
},
|
||||
portmaster::PortmasterExt,
|
||||
window::{create_main_window, may_navigate_to_ui, open_window},
|
||||
};
|
||||
|
||||
pub type AppIcon = TrayIcon<Wry>;
|
||||
|
||||
lazy_static! {
|
||||
// Set once setup_tray_menu executed.
|
||||
static ref SPN_BUTTON: Mutex<Option<CheckMenuItem<Wry>>> = Mutex::new(None);
|
||||
}
|
||||
|
||||
// Icons
|
||||
//
|
||||
const BLUE_ICON: &'static [u8] =
|
||||
include_bytes!("../../assets/icons/pm_light_blue_512.ico");
|
||||
const RED_ICON: &'static [u8] =
|
||||
include_bytes!("../../assets/icons/pm_light_red_512.ico");
|
||||
const YELLOW_ICON: &'static [u8] =
|
||||
include_bytes!("../../assets/icons/pm_light_yellow_512.ico");
|
||||
const GREEN_ICON: &'static [u8] =
|
||||
include_bytes!("../../assets/icons/pm_light_green_512.ico");
|
||||
|
||||
pub fn setup_tray_menu(
|
||||
app: &mut tauri::App,
|
||||
) -> core::result::Result<AppIcon, Box<dyn std::error::Error>> {
|
||||
// Tray menu
|
||||
let close_btn = MenuItemBuilder::with_id("close", "Exit").build(app);
|
||||
let open_btn = MenuItemBuilder::with_id("open", "Open").build(app);
|
||||
|
||||
let spn = CheckMenuItemBuilder::with_id("spn", "Use SPN").build(app);
|
||||
|
||||
// Store the SPN button reference
|
||||
let mut button_ref = SPN_BUTTON.lock().unwrap();
|
||||
*button_ref = Some(spn.clone());
|
||||
|
||||
let force_show_window = MenuItemBuilder::with_id("force-show", "Force Show UI").build(app);
|
||||
let reload_btn = MenuItemBuilder::with_id("reload", "Reload User Interface").build(app);
|
||||
let developer_menu = SubmenuBuilder::new(app, "Developer")
|
||||
.items(&[&reload_btn, &force_show_window])
|
||||
.build()?;
|
||||
|
||||
// Drop the reference now so we unlock immediately.
|
||||
drop(button_ref);
|
||||
|
||||
let menu = MenuBuilder::new(app)
|
||||
.items(&[
|
||||
&spn,
|
||||
&PredefinedMenuItem::separator(app),
|
||||
&open_btn,
|
||||
&close_btn,
|
||||
&developer_menu,
|
||||
])
|
||||
.build()?;
|
||||
|
||||
let icon = TrayIconBuilder::new()
|
||||
.icon(Icon::Raw(RED_ICON.to_vec()))
|
||||
.menu(&menu)
|
||||
.on_menu_event(move |app, event| match event.id().as_ref() {
|
||||
"close" => {
|
||||
let handle = app.clone();
|
||||
app.dialog()
|
||||
.message("This does not stop the Portmaster system service")
|
||||
.title("Do you really want to quit the user interface?")
|
||||
.ok_button_label("Yes, exit")
|
||||
.cancel_button_label("No")
|
||||
.show(move |answer| {
|
||||
if answer {
|
||||
let _ = handle.emit("exit-requested", "");
|
||||
handle.exit(0);
|
||||
}
|
||||
});
|
||||
}
|
||||
"open" => {
|
||||
let _ = open_window(app);
|
||||
}
|
||||
"reload" => {
|
||||
if let Ok(mut win) = open_window(app) {
|
||||
may_navigate_to_ui(&mut win, true);
|
||||
}
|
||||
}
|
||||
"force-show" => {
|
||||
match create_main_window(app) {
|
||||
Ok(mut win) => {
|
||||
may_navigate_to_ui(&mut win, true);
|
||||
if let Err(err) = win.show() {
|
||||
error!("[tauri] failed to show window: {}", err.to_string());
|
||||
};
|
||||
}
|
||||
Err(err) => {
|
||||
error!("[tauri] failed to create main window: {}", err.to_string());
|
||||
}
|
||||
};
|
||||
}
|
||||
"spn" => {
|
||||
let btn = SPN_BUTTON.lock().unwrap();
|
||||
|
||||
if let Some(bt) = &*btn {
|
||||
if let Ok(is_checked) = bt.is_checked() {
|
||||
app.portmaster().set_spn_enabled(is_checked);
|
||||
}
|
||||
}
|
||||
}
|
||||
other => {
|
||||
error!("unknown menu event id: {}", other);
|
||||
}
|
||||
})
|
||||
.on_tray_icon_event(|tray, event| {
|
||||
// not supported on linux
|
||||
if event.click_type == ClickType::Left {
|
||||
let _ = open_window(tray.app_handle());
|
||||
}
|
||||
})
|
||||
.build(app)?;
|
||||
|
||||
Ok(icon)
|
||||
}
|
||||
|
||||
pub fn update_icon(icon: AppIcon, subsystems: HashMap<String, Subsystem>, spn_status: String) {
|
||||
// iterate over the subsytems and check if there's a module failure
|
||||
let failure = subsystems
|
||||
.values()
|
||||
.into_iter()
|
||||
.map(|s| s.failure_status)
|
||||
.fold(
|
||||
subsystem::FAILURE_NONE,
|
||||
|acc, s| {
|
||||
if s > acc {
|
||||
s
|
||||
} else {
|
||||
acc
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
let next_icon = match failure {
|
||||
subsystem::FAILURE_WARNING => YELLOW_ICON,
|
||||
subsystem::FAILURE_ERROR => RED_ICON,
|
||||
_ => match spn_status.as_str() {
|
||||
"connected" | "connecting" => BLUE_ICON,
|
||||
_ => GREEN_ICON,
|
||||
},
|
||||
};
|
||||
|
||||
_ = icon.set_icon(Some(Icon::Raw(next_icon.to_vec())));
|
||||
}
|
||||
|
||||
pub async fn tray_handler(cli: PortAPI, app: tauri::AppHandle) {
|
||||
let icon = match app.tray() {
|
||||
Some(icon) => icon,
|
||||
None => {
|
||||
error!("cancel try_handler: missing try icon");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let mut subsystem_subscription = match cli
|
||||
.request(Request::QuerySubscribe(
|
||||
"query runtime:subsystems/".to_string(),
|
||||
))
|
||||
.await
|
||||
{
|
||||
Ok(rx) => rx,
|
||||
Err(err) => {
|
||||
error!(
|
||||
"cancel try_handler: failed to subscribe to 'runtime:subsystems': {}",
|
||||
err
|
||||
);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let mut spn_status_subscription = match cli
|
||||
.request(Request::QuerySubscribe(
|
||||
"query runtime:spn/status".to_string(),
|
||||
))
|
||||
.await
|
||||
{
|
||||
Ok(rx) => rx,
|
||||
Err(err) => {
|
||||
error!(
|
||||
"cancel try_handler: failed to subscribe to 'runtime:spn/status': {}",
|
||||
err
|
||||
);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let mut spn_config_subscription = match cli
|
||||
.request(Request::QuerySubscribe(
|
||||
"query config:spn/enable".to_string(),
|
||||
))
|
||||
.await
|
||||
{
|
||||
Ok(rx) => rx,
|
||||
Err(err) => {
|
||||
error!(
|
||||
"cancel try_handler: failed to subscribe to 'runtime:spn/enable': {}",
|
||||
err
|
||||
);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
_ = icon.set_icon(Some(Icon::Raw(BLUE_ICON.to_vec())));
|
||||
|
||||
let mut subsystems: HashMap<String, Subsystem> = HashMap::new();
|
||||
let mut spn_status: String = "".to_string();
|
||||
|
||||
loop {
|
||||
tokio::select! {
|
||||
msg = subsystem_subscription.recv() => {
|
||||
let msg = match msg {
|
||||
Some(m) => m,
|
||||
None => { break }
|
||||
};
|
||||
|
||||
let res = match msg {
|
||||
Response::Ok(key, payload) => Some((key, payload)),
|
||||
Response::New(key, payload) => Some((key, payload)),
|
||||
Response::Update(key, payload) => Some((key, payload)),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
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());
|
||||
},
|
||||
Err(err) => match err {
|
||||
ParseError::JSON(err) => {
|
||||
error!("failed to parse subsystem: {}", err);
|
||||
}
|
||||
_ => {
|
||||
error!("unknown error when parsing notifications payload");
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
msg = spn_status_subscription.recv() => {
|
||||
let msg = match msg {
|
||||
Some(m) => m,
|
||||
None => { break }
|
||||
};
|
||||
|
||||
let res = match msg {
|
||||
Response::Ok(key, payload) => Some((key, payload)),
|
||||
Response::New(key, payload) => Some((key, payload)),
|
||||
Response::Update(key, payload) => Some((key, payload)),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
if let Some((_, payload)) = res {
|
||||
match payload.parse::<SPNStatus>() {
|
||||
Ok(value) => {
|
||||
debug!("SPN status update: {}", value.status);
|
||||
spn_status = value.status.clone();
|
||||
|
||||
update_icon(icon.clone(), subsystems.clone(), spn_status.clone());
|
||||
},
|
||||
Err(err) => match err {
|
||||
ParseError::JSON(err) => {
|
||||
error!("failed to parse spn status value: {}", err)
|
||||
},
|
||||
_ => {
|
||||
error!("unknown error when parsing spn status value")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
msg = spn_config_subscription.recv() => {
|
||||
let msg = match msg {
|
||||
Some(m) => m,
|
||||
None => { break }
|
||||
};
|
||||
|
||||
let res = match msg {
|
||||
Response::Ok(key, payload) => Some((key, payload)),
|
||||
Response::New(key, payload) => Some((key, payload)),
|
||||
Response::Update(key, payload) => Some((key, payload)),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
if let Some((_, payload)) = res {
|
||||
match payload.parse::<BooleanValue>() {
|
||||
Ok(value) => {
|
||||
let mut btn = SPN_BUTTON.lock().unwrap();
|
||||
|
||||
if let Some(btn) = &mut *btn {
|
||||
if let Some(value) = value.value {
|
||||
_ = btn.set_checked(value);
|
||||
} else {
|
||||
_ = btn.set_checked(false);
|
||||
}
|
||||
}
|
||||
},
|
||||
Err(err) => match err {
|
||||
ParseError::JSON(err) => {
|
||||
error!("failed to parse config value: {}", err)
|
||||
},
|
||||
_ => {
|
||||
error!("unknown error when parsing config value")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(btn) = &mut *(SPN_BUTTON.lock().unwrap()) {
|
||||
_ = btn.set_checked(false);
|
||||
}
|
||||
|
||||
_ = icon.set_icon(Some(Icon::Raw(RED_ICON.to_vec())));
|
||||
}
|
151
desktop/tauri/src-tauri/src/window.rs
Normal file
151
desktop/tauri/src-tauri/src/window.rs
Normal file
|
@ -0,0 +1,151 @@
|
|||
use log::{debug, error};
|
||||
use tauri::{AppHandle, Manager, Result, UserAttentionType, Window, WindowBuilder, WindowUrl};
|
||||
|
||||
use crate::portmaster::PortmasterExt;
|
||||
|
||||
/// Either returns the existing "main" window or creates a new one.
|
||||
///
|
||||
/// The window is not automatically shown (i.e it starts hidden).
|
||||
/// If a new main window is created (i.e. the tauri app was minimized to system-tray)
|
||||
/// then the window will be automatically navigated to the Portmaster UI endpoint
|
||||
/// if ::websocket::is_portapi_reachable returns true.
|
||||
///
|
||||
/// Either the existing or the newly created window is returned.
|
||||
pub fn create_main_window(app: &AppHandle) -> Result<Window> {
|
||||
let mut window = if let Some(window) = app.get_window("main") {
|
||||
debug!("[tauri] main window already created");
|
||||
|
||||
window
|
||||
} else {
|
||||
debug!("[tauri] creating main window");
|
||||
|
||||
let res = WindowBuilder::new(app, "main", WindowUrl::App("index.html".into()))
|
||||
.visible(false)
|
||||
.build();
|
||||
|
||||
match res {
|
||||
Ok(win) => {
|
||||
win.once("tauri://error", |event| {
|
||||
error!("failed to open tauri window: {}", event.payload());
|
||||
});
|
||||
|
||||
win
|
||||
}
|
||||
Err(err) => {
|
||||
error!("[tauri] failed to create main window: {}", err.to_string());
|
||||
|
||||
return Err(err);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// If the window is not yet navigated to the Portmaster UI, do it now.
|
||||
may_navigate_to_ui(&mut window, false);
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
if let Ok(_) = std::env::var("TAURI_SHOW_IMMEDIATELY") {
|
||||
debug!("[tauri] TAURI_SHOW_IMMEDIATELY is set, opening window");
|
||||
|
||||
if let Err(err) = window.show() {
|
||||
error!("[tauri] failed to show window: {}", err.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
Ok(window)
|
||||
}
|
||||
|
||||
pub fn create_splash_window(app: &AppHandle) -> Result<Window> {
|
||||
if let Some(window) = app.get_window("splash") {
|
||||
let _ = window.show();
|
||||
Ok(window)
|
||||
} else {
|
||||
let window = WindowBuilder::new(app, "splash", WindowUrl::App("index.html".into()))
|
||||
.center()
|
||||
.closable(false)
|
||||
.focused(true)
|
||||
.resizable(false)
|
||||
.visible(true)
|
||||
.title("Portmaster")
|
||||
.inner_size(600.0, 250.0)
|
||||
.build()?;
|
||||
|
||||
let _ = window.request_user_attention(Some(UserAttentionType::Informational));
|
||||
|
||||
Ok(window)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn close_splash_window(app: &AppHandle) -> Result<()> {
|
||||
if let Some(window) = app.get_window("splash") {
|
||||
return window.close();
|
||||
}
|
||||
return Err(tauri::Error::WindowNotFound);
|
||||
}
|
||||
|
||||
/// Opens a window for the tauri application.
|
||||
///
|
||||
/// If the main window has already been created, it is instructed to
|
||||
/// show even if we're currently not connected to Portmaster.
|
||||
/// This is safe since the main-window will only be created if Portmaster API
|
||||
/// was reachable so the angular application must have finished bootstrapping.
|
||||
///
|
||||
/// If there's not main window and the Portmaster API is reachable we create a new
|
||||
/// main window.
|
||||
///
|
||||
/// If the Portmaster API is unreachable and there's no main window yet, we show the
|
||||
/// splash-screen window.
|
||||
pub fn open_window(app: &AppHandle) -> Result<Window> {
|
||||
if app.portmaster().is_reachable() {
|
||||
match app.get_window("main") {
|
||||
Some(win) => {
|
||||
app.portmaster().show_window();
|
||||
|
||||
Ok(win)
|
||||
}
|
||||
None => {
|
||||
app.portmaster().show_window();
|
||||
|
||||
create_main_window(app)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
debug!("Show splash screen");
|
||||
create_splash_window(app)
|
||||
}
|
||||
}
|
||||
|
||||
/// If the Portmaster Websocket database API is reachable the window will be navigated
|
||||
/// to the HTTP endpoint of Portmaster to load the UI from there.
|
||||
///
|
||||
/// Note that only happens if the window URL does not already point to the PM API.
|
||||
///
|
||||
/// In #[cfg(debug_assertions)] the TAURI_PM_URL environment variable will be used
|
||||
/// if set.
|
||||
/// Otherwise or in release builds, it will be navigated to http://127.0.0.1:817.
|
||||
pub fn may_navigate_to_ui(win: &mut Window, force: bool) {
|
||||
if !win.app_handle().portmaster().is_reachable() && !force {
|
||||
error!("[tauri] portmaster API is not reachable, not navigating");
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if force || cfg!(debug_assertions) || win.url().host_str() != Some("localhost") {
|
||||
#[cfg(debug_assertions)]
|
||||
if let Ok(target_url) = std::env::var("TAURI_PM_URL") {
|
||||
debug!("[tauri] navigating to {}", target_url);
|
||||
|
||||
win.navigate(target_url.parse().unwrap());
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
debug!("[tauri] navigating to http://localhost:4200");
|
||||
win.navigate("http://localhost:4200".parse().unwrap());
|
||||
}
|
||||
|
||||
#[cfg(not(debug_assertions))]
|
||||
win.navigate("http://localhost:817".parse().unwrap());
|
||||
}
|
||||
}
|
585
desktop/tauri/src-tauri/src/xdg/mod.rs
Normal file
585
desktop/tauri/src-tauri/src/xdg/mod.rs
Normal file
|
@ -0,0 +1,585 @@
|
|||
use cached::proc_macro::once;
|
||||
use dataurl::DataUrl;
|
||||
use gdk_pixbuf::{Pixbuf, PixbufError};
|
||||
use gtk_sys::{
|
||||
gtk_icon_info_free, gtk_icon_info_get_filename, gtk_icon_theme_get_default,
|
||||
gtk_icon_theme_lookup_icon, GtkIconTheme,
|
||||
};
|
||||
use log::{debug, error};
|
||||
use std::collections::HashMap;
|
||||
use std::ffi::c_int;
|
||||
use std::ffi::{CStr, CString};
|
||||
use std::io;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::{Arc, RwLock};
|
||||
use std::{
|
||||
env, fs,
|
||||
io::{Error, ErrorKind},
|
||||
};
|
||||
use thiserror::Error;
|
||||
|
||||
use dirs;
|
||||
use ini::{Ini, ParseOption};
|
||||
|
||||
static mut GTK_DEFAULT_THEME: Option<*mut GtkIconTheme> = None;
|
||||
|
||||
lazy_static! {
|
||||
static ref APP_INFO_CACHE: Arc<RwLock<HashMap<String, Option<AppInfo>>>> =
|
||||
Arc::new(RwLock::new(HashMap::new()));
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum LookupError {
|
||||
#[error(transparent)]
|
||||
IoError(#[from] std::io::Error),
|
||||
}
|
||||
|
||||
pub type Result<T> = std::result::Result<T, LookupError>;
|
||||
|
||||
#[derive(Clone, serde::Serialize)]
|
||||
pub struct AppInfo {
|
||||
pub icon_name: String,
|
||||
pub app_name: String,
|
||||
pub icon_dataurl: String,
|
||||
pub comment: String,
|
||||
}
|
||||
|
||||
impl Default for AppInfo {
|
||||
fn default() -> Self {
|
||||
AppInfo {
|
||||
icon_dataurl: "".to_string(),
|
||||
icon_name: "".to_string(),
|
||||
app_name: "".to_string(),
|
||||
comment: "".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, serde::Serialize, Debug)]
|
||||
pub struct ProcessInfo {
|
||||
pub exec_path: String,
|
||||
pub cmdline: String,
|
||||
pub pid: i64,
|
||||
pub matching_path: String,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for ProcessInfo {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"{} (cmdline={}) (pid={}) (matching_path={})",
|
||||
self.exec_path, self.cmdline, self.pid, self.matching_path
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_app_info(process_info: ProcessInfo) -> Result<AppInfo> {
|
||||
{
|
||||
let cache = APP_INFO_CACHE.read().unwrap();
|
||||
|
||||
if let Some(value) = cache.get(process_info.exec_path.as_str()) {
|
||||
match value {
|
||||
Some(app_info) => return Ok(app_info.clone()),
|
||||
None => {
|
||||
return Err(LookupError::IoError(io::Error::new(
|
||||
io::ErrorKind::NotFound,
|
||||
"not found",
|
||||
)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut needles = Vec::new();
|
||||
if !process_info.exec_path.is_empty() {
|
||||
needles.push(process_info.exec_path.as_str())
|
||||
}
|
||||
if !process_info.cmdline.is_empty() {
|
||||
needles.push(process_info.cmdline.as_str())
|
||||
}
|
||||
if !process_info.matching_path.is_empty() {
|
||||
needles.push(process_info.matching_path.as_str())
|
||||
}
|
||||
|
||||
// sort and deduplicate
|
||||
needles.sort();
|
||||
needles.dedup();
|
||||
|
||||
debug!("Searching app info for {:?}", process_info);
|
||||
|
||||
let mut desktop_files = Vec::new();
|
||||
for dir in get_application_directories()? {
|
||||
let mut files = find_desktop_files(dir.as_path())?;
|
||||
desktop_files.append(&mut files);
|
||||
}
|
||||
|
||||
let mut matches = Vec::new();
|
||||
for needle in needles.clone() {
|
||||
debug!("Trying needle {} on exec path", needle);
|
||||
|
||||
match try_get_app_info(needle, CheckType::Exec, &desktop_files) {
|
||||
Ok(mut result) => {
|
||||
matches.append(&mut result);
|
||||
}
|
||||
Err(LookupError::IoError(ioerr)) => {
|
||||
if ioerr.kind() != ErrorKind::NotFound {
|
||||
return Err(ioerr.into());
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
match try_get_app_info(needle, CheckType::Name, &desktop_files) {
|
||||
Ok(mut result) => {
|
||||
matches.append(&mut result);
|
||||
}
|
||||
Err(LookupError::IoError(ioerr)) => {
|
||||
if ioerr.kind() != ErrorKind::NotFound {
|
||||
return Err(ioerr.into());
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
if matches.is_empty() {
|
||||
APP_INFO_CACHE
|
||||
.write()
|
||||
.unwrap()
|
||||
.insert(process_info.exec_path, None);
|
||||
|
||||
Err(Error::new(ErrorKind::NotFound, format!("failed to find app info")).into())
|
||||
} else {
|
||||
// sort matches by length
|
||||
matches.sort_by(|a, b| a.1.cmp(&b.1));
|
||||
|
||||
for mut info in matches {
|
||||
match get_icon_as_png_dataurl(&info.0.icon_name, 32) {
|
||||
Ok(du) => {
|
||||
debug!(
|
||||
"[xdg] best match for {:?} is {:?} with len {}",
|
||||
process_info, info.0.icon_name, info.1
|
||||
);
|
||||
|
||||
info.0.icon_dataurl = du.1;
|
||||
|
||||
APP_INFO_CACHE
|
||||
.write()
|
||||
.unwrap()
|
||||
.insert(process_info.exec_path, Some(info.0.clone()));
|
||||
|
||||
return Ok(info.0);
|
||||
}
|
||||
Err(err) => {
|
||||
error!(
|
||||
"{}: failed to get icon: {}",
|
||||
info.0.icon_name,
|
||||
err.to_string()
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
Err(Error::new(ErrorKind::NotFound, format!("failed to find app info")).into())
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a vector of application directories that are expected
|
||||
/// to contain all .desktop files the current user has access to.
|
||||
/// The result of this function is cached for 5 minutes as it's not expected
|
||||
/// that application directories actually change.
|
||||
#[once(time = 300, sync_writes = true, result = true)]
|
||||
fn get_application_directories() -> Result<Vec<PathBuf>> {
|
||||
let xdg_home = match env::var_os("XDG_DATA_HOME") {
|
||||
Some(path) => PathBuf::from(path),
|
||||
None => {
|
||||
let home = dirs::home_dir()
|
||||
.ok_or(Error::new(ErrorKind::Other, "Failed to get home directory"))?;
|
||||
|
||||
home.join(".local/share")
|
||||
}
|
||||
};
|
||||
|
||||
let extra_application_dirs = match env::var_os("XDG_DATA_DIRS") {
|
||||
Some(paths) => env::split_paths(&paths).map(PathBuf::from).collect(),
|
||||
None => {
|
||||
// Fallback if XDG_DATA_DIRS is not set. If it's set, it normally already contains /usr/share and
|
||||
// /usr/local/share
|
||||
vec![
|
||||
PathBuf::from("/usr/share"),
|
||||
PathBuf::from("/usr/local/share"),
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
let mut app_dirs = Vec::new();
|
||||
for extra_dir in extra_application_dirs {
|
||||
app_dirs.push(extra_dir.join("applications"));
|
||||
}
|
||||
|
||||
app_dirs.push(xdg_home.join("applications"));
|
||||
|
||||
Ok(app_dirs)
|
||||
}
|
||||
|
||||
// TODO(ppacher): cache the result of find_desktop_files as well.
|
||||
// Though, seems like we cannot use the #[cached::proc_macro::cached] or #[cached::proc_macro::once] macros here
|
||||
// because [`Result<Vec<fs::DirEntry>>>`] does not implement [`Clone`]
|
||||
fn find_desktop_files(path: &Path) -> Result<Vec<fs::DirEntry>> {
|
||||
match path.read_dir() {
|
||||
Ok(files) => {
|
||||
let desktop_files = files
|
||||
.filter_map(|entry| entry.ok())
|
||||
.filter(|entry| match entry.file_type() {
|
||||
Ok(ft) => ft.is_file() || ft.is_symlink(),
|
||||
_ => false,
|
||||
})
|
||||
.filter(|entry| entry.file_name().to_string_lossy().ends_with(".desktop"))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
Ok(desktop_files)
|
||||
}
|
||||
Err(err) => {
|
||||
// We ignore NotFound errors here because not all application
|
||||
// directories need to exist.
|
||||
if err.kind() == ErrorKind::NotFound {
|
||||
Ok(Vec::new())
|
||||
} else {
|
||||
Err(err.into())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum CheckType {
|
||||
Name,
|
||||
Exec,
|
||||
}
|
||||
|
||||
fn try_get_app_info(
|
||||
needle: &str,
|
||||
check: CheckType,
|
||||
desktop_files: &Vec<fs::DirEntry>,
|
||||
) -> Result<Vec<(AppInfo, usize)>> {
|
||||
let path = PathBuf::from(needle);
|
||||
|
||||
let file_name = path.as_path().file_name().unwrap_or_default().to_str();
|
||||
|
||||
let mut result = Vec::new();
|
||||
|
||||
for file in desktop_files {
|
||||
let content = Ini::load_from_file_opt(
|
||||
file.path(),
|
||||
ParseOption {
|
||||
enabled_escape: false,
|
||||
enabled_quote: true,
|
||||
},
|
||||
)
|
||||
.map_err(|err| Error::new(ErrorKind::Other, err.to_string()))?;
|
||||
|
||||
let desktop_section = match content.section(Some("Desktop Entry")) {
|
||||
Some(section) => section,
|
||||
None => {
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
let matches = match check {
|
||||
CheckType::Name => {
|
||||
let name = match desktop_section.get("Name") {
|
||||
Some(name) => name,
|
||||
None => {
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(file_name) = file_name {
|
||||
if name.to_lowercase().contains(file_name) {
|
||||
file_name.len()
|
||||
} else {
|
||||
0
|
||||
}
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
CheckType::Exec => {
|
||||
let exec = match desktop_section.get("Exec") {
|
||||
Some(exec) => exec,
|
||||
None => {
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
if exec.to_lowercase().contains(needle) {
|
||||
needle.len()
|
||||
} else if let Some(file_name) = file_name {
|
||||
if exec.to_lowercase().starts_with(file_name) {
|
||||
file_name.len()
|
||||
} else {
|
||||
0
|
||||
}
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if matches > 0 {
|
||||
debug!(
|
||||
"[xdg] found matching desktop for needle {} file at {}",
|
||||
needle,
|
||||
file.path().to_string_lossy()
|
||||
);
|
||||
|
||||
let info = parse_app_info(desktop_section);
|
||||
|
||||
result.push((info, matches));
|
||||
}
|
||||
}
|
||||
|
||||
if result.len() > 0 {
|
||||
Ok(result)
|
||||
} else {
|
||||
Err(Error::new(ErrorKind::NotFound, "no matching .desktop files found").into())
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_app_info(props: &ini::Properties) -> AppInfo {
|
||||
AppInfo {
|
||||
icon_dataurl: "".to_string(),
|
||||
app_name: props.get("Name").unwrap_or_default().to_string(),
|
||||
comment: props.get("Comment").unwrap_or_default().to_string(),
|
||||
icon_name: props.get("Icon").unwrap_or_default().to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
fn get_icon_as_png_dataurl(name: &str, size: i8) -> Result<(String, String)> {
|
||||
unsafe {
|
||||
if GTK_DEFAULT_THEME.is_none() {
|
||||
let theme = gtk_icon_theme_get_default();
|
||||
if theme.is_null() {
|
||||
debug!("You have to initialize GTK!");
|
||||
return Err(Error::new(ErrorKind::Other, "You have to initialize GTK!").into());
|
||||
}
|
||||
|
||||
let theme = gtk_icon_theme_get_default();
|
||||
GTK_DEFAULT_THEME = Some(theme);
|
||||
}
|
||||
}
|
||||
|
||||
let mut icons = Vec::new();
|
||||
|
||||
// push the name
|
||||
icons.push(name);
|
||||
|
||||
// if we don't find the icon by it's name and it includes an extension,
|
||||
// drop the extension and try without.
|
||||
let name_without_ext;
|
||||
if let Some(ext) = PathBuf::from(name).extension() {
|
||||
let ext = ext.to_str().unwrap();
|
||||
|
||||
let mut ext_dot = String::from(".").to_owned();
|
||||
ext_dot.push_str(ext);
|
||||
|
||||
name_without_ext = name.replace(ext_dot.as_str(), "");
|
||||
icons.push(name_without_ext.as_str());
|
||||
} else {
|
||||
name_without_ext = String::from(name);
|
||||
}
|
||||
|
||||
// The xdg-desktop icon specification allows a fallback for icons that contains dashes.
|
||||
// i.e. the following lookup order is used:
|
||||
// - network-wired-secure
|
||||
// - network-wired
|
||||
// - network
|
||||
//
|
||||
name_without_ext
|
||||
.split("-")
|
||||
.for_each(|part| icons.push(part));
|
||||
|
||||
for name in icons {
|
||||
debug!("trying to load icon {}", name);
|
||||
|
||||
unsafe {
|
||||
let c_str = CString::new(name).unwrap();
|
||||
|
||||
let icon_info = gtk_icon_theme_lookup_icon(
|
||||
GTK_DEFAULT_THEME.unwrap(),
|
||||
c_str.as_ptr() as *const i8,
|
||||
size as c_int,
|
||||
0,
|
||||
);
|
||||
if icon_info.is_null() {
|
||||
error!("failed to lookup icon {}", name);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
let filename = gtk_icon_info_get_filename(icon_info);
|
||||
|
||||
let filename = CStr::from_ptr(filename).to_str().unwrap().to_string();
|
||||
|
||||
gtk_icon_info_free(icon_info);
|
||||
|
||||
match read_and_convert_pixbuf(filename.clone()) {
|
||||
Ok(pb) => return Ok((filename, pb)),
|
||||
Err(err) => {
|
||||
error!("failed to load icon from {}: {}", filename, err.to_string());
|
||||
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Err(Error::new(ErrorKind::NotFound, "failed to find icon").into())
|
||||
}
|
||||
|
||||
/*
|
||||
fn get_icon_as_file_2(ext: &str, size: i32) -> io::Result<(String, Vec<u8>)> {
|
||||
let result: String;
|
||||
let buf: Vec<u8>;
|
||||
|
||||
unsafe {
|
||||
let filename = CString::new(ext).unwrap();
|
||||
let null: u8 = 0;
|
||||
let p_null = &null as *const u8;
|
||||
let nullsize: usize = 0;
|
||||
let mut res = 0;
|
||||
let p_res = &mut res as *mut i32;
|
||||
let p_res = gio_sys::g_content_type_guess(filename.as_ptr(), p_null, nullsize, p_res);
|
||||
let icon = gio_sys::g_content_type_get_icon(p_res);
|
||||
g_free(p_res as *mut c_void);
|
||||
if DEFAULT_THEME.is_none() {
|
||||
let theme = gtk_icon_theme_get_default();
|
||||
if theme.is_null() {
|
||||
println!("You have to initialize GTK!");
|
||||
return Err(io::Error::new(io::ErrorKind::Other, "You have to initialize GTK!"))
|
||||
}
|
||||
let theme = gtk_icon_theme_get_default();
|
||||
DEFAULT_THEME = Some(theme);
|
||||
}
|
||||
let icon_names = gio_sys::g_themed_icon_get_names(icon as *mut GThemedIcon) as *mut *const i8;
|
||||
let icon_info = gtk_icon_theme_choose_icon(DEFAULT_THEME.unwrap(), icon_names, size, GTK_ICON_LOOKUP_NO_SVG);
|
||||
let filename = gtk_icon_info_get_filename(icon_info);
|
||||
|
||||
gtk_icon_info_free(icon_info);
|
||||
|
||||
result = CStr::from_ptr(filename).to_str().unwrap().to_string();
|
||||
|
||||
buf = match read_and_convert_pixbuf(result.clone()) {
|
||||
Ok(pb) => pb,
|
||||
Err(_) => Vec::new(),
|
||||
};
|
||||
|
||||
g_object_unref(icon as *mut GObject);
|
||||
}
|
||||
|
||||
Ok((result, buf))
|
||||
|
||||
}
|
||||
*/
|
||||
|
||||
fn read_and_convert_pixbuf(result: String) -> std::result::Result<String, glib::Error> {
|
||||
let pixbuf = match Pixbuf::from_file(result.clone()) {
|
||||
Ok(data) => Ok(data),
|
||||
Err(err) => {
|
||||
error!("failed to load icon pixbuf: {}", err.to_string());
|
||||
|
||||
Pixbuf::from_resource(result.clone().as_str())
|
||||
}
|
||||
};
|
||||
|
||||
match pixbuf {
|
||||
Ok(data) => match data.save_to_bufferv("png", &[]) {
|
||||
Ok(data) => {
|
||||
let mut du = DataUrl::new();
|
||||
|
||||
du.set_media_type(Some("image/png".to_string()));
|
||||
du.set_data(&data);
|
||||
|
||||
Ok(du.to_string())
|
||||
}
|
||||
Err(err) => {
|
||||
return Err(glib::Error::new(
|
||||
PixbufError::Failed,
|
||||
err.to_string().as_str(),
|
||||
));
|
||||
}
|
||||
},
|
||||
Err(err) => Err(err),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use ctor::ctor;
|
||||
use log::warn;
|
||||
use which::which;
|
||||
|
||||
// Use the ctor create to setup a global initializer before our tests are executed.
|
||||
#[ctor]
|
||||
fn init() {
|
||||
// we need to initialize GTK before running our tests.
|
||||
// This is only required when unit tests are executed as
|
||||
// GTK will otherwise be initialize by Tauri.
|
||||
|
||||
gtk::init().expect("failed to initialize GTK for tests")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_find_info_success() {
|
||||
// we expect at least one of the following binaries to be installed
|
||||
// on a linux system
|
||||
let test_binaries = vec![
|
||||
"vim", // vim is mostly bundled with a .desktop file
|
||||
"blueman-manager", // blueman-manager is the default bluetooth manager on most DEs
|
||||
"nautilus", // nautlis: file-manager on GNOME DE
|
||||
"thunar", // thunar: file-manager on XFCE
|
||||
"dolphin", // dolphin: file-manager on KDE
|
||||
];
|
||||
|
||||
let mut bin_found = false;
|
||||
|
||||
for cmd in test_binaries {
|
||||
match which(cmd) {
|
||||
Ok(bin) => {
|
||||
bin_found = true;
|
||||
|
||||
let bin = bin.to_string_lossy().to_string();
|
||||
|
||||
let result = get_app_info(ProcessInfo {
|
||||
cmdline: cmd.to_string(),
|
||||
exec_path: bin.clone(),
|
||||
matching_path: bin.clone(),
|
||||
pid: 0,
|
||||
})
|
||||
.expect(
|
||||
format!(
|
||||
"expected to find app info for {} ({})",
|
||||
bin,
|
||||
cmd.to_string()
|
||||
)
|
||||
.as_str(),
|
||||
);
|
||||
|
||||
let empty_string = String::from("");
|
||||
|
||||
// just make sure all fields are populated
|
||||
assert_ne!(result.app_name, empty_string);
|
||||
assert_ne!(result.comment, empty_string);
|
||||
assert_ne!(result.icon_name, empty_string);
|
||||
assert_ne!(result.icon_dataurl, empty_string);
|
||||
}
|
||||
Err(_) => {
|
||||
// binary not found
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !bin_found {
|
||||
warn!("test_find_info_success: no test binary found, test was skipped")
|
||||
}
|
||||
}
|
||||
}
|
106
desktop/tauri/src-tauri/tauri.conf.json
Normal file
106
desktop/tauri/src-tauri/tauri.conf.json
Normal file
|
@ -0,0 +1,106 @@
|
|||
{
|
||||
"build": {
|
||||
"beforeDevCommand": {
|
||||
"script": "npm run tauri-dev",
|
||||
"cwd": "../../angular",
|
||||
"wait": false
|
||||
},
|
||||
"devPath": "http://localhost:4100",
|
||||
"distDir": "../../angular/dist/tauri-builtin",
|
||||
"withGlobalTauri": true
|
||||
},
|
||||
"package": {
|
||||
"productName": "Portmaster",
|
||||
"version": "0.1.0"
|
||||
},
|
||||
"plugins": {
|
||||
"cli": {
|
||||
"args": [
|
||||
{
|
||||
"short": "d",
|
||||
"name": "data",
|
||||
"description": "Path to the installation directory",
|
||||
"takesValue": true
|
||||
},
|
||||
{
|
||||
"short": "b",
|
||||
"name": "background",
|
||||
"description": "Start in the background without opening a window"
|
||||
},
|
||||
{
|
||||
"name": "with-notifications",
|
||||
"description": "Enable experimental notifications via Tauri. Replaces the notifier app."
|
||||
},
|
||||
{
|
||||
"name": "with-prompts",
|
||||
"description": "Enable experimental prompt support via Tauri. Replaces the notifier app."
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"tauri": {
|
||||
"bundle": {
|
||||
"active": true,
|
||||
"category": "Utility",
|
||||
"copyright": "",
|
||||
"deb": {
|
||||
"depends": []
|
||||
},
|
||||
"externalBin": [
|
||||
"binaries/portmaster-start",
|
||||
"binaries/portmaster-core"
|
||||
],
|
||||
"icon": [
|
||||
"../assets/icons/pm_dark_512.png",
|
||||
"../assets/icons/pm_dark_512.ico",
|
||||
"../assets/icons/pm_light_512.png",
|
||||
"../assets/icons/pm_light_512.ico"
|
||||
],
|
||||
"identifier": "io.safing.portmaster",
|
||||
"longDescription": "",
|
||||
"macOS": {
|
||||
"entitlements": null,
|
||||
"exceptionDomain": "",
|
||||
"frameworks": [],
|
||||
"providerShortName": null,
|
||||
"signingIdentity": null
|
||||
},
|
||||
"resources": [],
|
||||
"shortDescription": "",
|
||||
"targets": [
|
||||
"deb",
|
||||
"appimage",
|
||||
"nsis",
|
||||
"msi",
|
||||
"app"
|
||||
],
|
||||
"windows": {
|
||||
"certificateThumbprint": null,
|
||||
"digestAlgorithm": "sha256",
|
||||
"timestampUrl": ""
|
||||
}
|
||||
},
|
||||
"security": {
|
||||
"csp": null,
|
||||
"dangerousRemoteDomainIpcAccess": [
|
||||
{
|
||||
"windows": [
|
||||
"main",
|
||||
"prompt"
|
||||
],
|
||||
"plugins": [
|
||||
"shell",
|
||||
"os",
|
||||
"clipboard-manager",
|
||||
"event",
|
||||
"window",
|
||||
"cli",
|
||||
"portmaster"
|
||||
],
|
||||
"domain": "localhost"
|
||||
}
|
||||
]
|
||||
},
|
||||
"windows": []
|
||||
}
|
||||
}
|
Loading…
Add table
Reference in a new issue