Merge pull request from safing/feature/tauri-beta-update

Feature Tauri beta update
This commit is contained in:
Daniel Hååvi 2024-07-19 16:11:31 +02:00 committed by GitHub
commit ef0085882d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
57 changed files with 15992 additions and 8993 deletions

View file

@ -56,4 +56,4 @@ jobs:
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build angular projects
run: earthly --ci --remote-cache=ghcr.io/safing/build-cache --push +angular-release
run: earthly --ci --remote-cache=ghcr.io/safing/build-cache --push +angular-ci

View file

@ -33,4 +33,4 @@ jobs:
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build Kernel Extension
run: earthly --ci --remote-cache=ghcr.io/safing/build-cache --push +kext-build
run: earthly --ci --remote-cache=ghcr.io/safing/build-cache --push +kext-ci

View file

@ -33,4 +33,4 @@ jobs:
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build tauri project
run: earthly --ci --remote-cache=ghcr.io/safing/build-cache --push +tauri-release
run: earthly --ci --remote-cache=ghcr.io/safing/build-cache --push +tauri-ci

388
Earthfile
View file

@ -12,24 +12,90 @@ ARG --global work_image = "alpine"
ARG --global outputDir = "./dist"
# Architectures:
# 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" \
"x86_64-pc-windows-gnu"
# TODO: Compile errors here:
# "aarch64-pc-windows-gnu" \
# "x86_64-apple-darwin" \
# "aarch64-apple-darwin"
# "armv7-unknown-linux-gnueabihf" \
# "arm-unknown-linux-gnueabi"
#
# Linux:
# x86_64-unknown-linux-gnu
# aarch64-unknown-linux-gnu
# armv7-unknown-linux-gnueabihf
# arm-unknown-linux-gnueabi
#
# Windows:
# x86_64-pc-windows-gnu
# aarch64-pc-windows-gnu
#
# Mac:
# x86_64-apple-darwin
# aarch64-apple-darwin
# 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
build:
# Build all Golang binaries:
# ./dist/linux_amd64/portmaster-core
# ./dist/linux_amd64/portmaster-start
# ./dist/linux_arm64/portmaster-core
# ./dist/linux_arm64/portmaster-start
# ./dist/windows_amd64/portmaster-core.exe
# ./dist/windows_amd64/portmaster-start.exe
# ./dist/windows_arm64/portmaster-core.exe
# ./dist/windows_arm64/portmaster-start.exe
BUILD +go-build --GOOS="linux" --GOARCH="amd64"
BUILD +go-build --GOOS="linux" --GOARCH="arm64"
BUILD +go-build --GOOS="windows" --GOARCH="amd64"
BUILD +go-build --GOOS="windows" --GOARCH="arm64"
# Build the Angular UI:
# ./dist/all/portmaster-ui.zip
BUILD +angular-release
# Build Tauri app binaries:
# ./dist/linux_amd64/portmaster-app
# ./dist/linux_amd64/Portmaster-0.1.0-1.x86_64.rpm
# ./dist/linux_amd64/Portmaster_0.1.0_amd64.deb
BUILD +tauri-build --target="x86_64-unknown-linux-gnu"
# TODO:
# BUILD +tauri-build --target="x86_64-pc-windows-gnu"
# Bild Tauri release bundle for Windows:
# ./dist/windows_amd64/portmaster-app_vX-X-X.zip
BUILD +tauri-windows-release-bundle
# Build UI assets:
# ./dist/all/assets.zip
BUILD +assets
go-ci:
BUILD +go-build --GOOS="linux" --GOARCH="amd64"
BUILD +go-build --GOOS="linux" --GOARCH="arm64"
BUILD +go-build --GOOS="windows" --GOARCH="amd64"
BUILD +go-build --GOOS="windows" --GOARCH="arm64"
BUILD +go-test
angular-ci:
BUILD +angular-release
tauri-ci:
BUILD +tauri-build --target="x86_64-unknown-linux-gnu"
BUILD +tauri-windows-release-bundle
kext-ci:
BUILD +kext-build
release:
LOCALLY
IF ! git diff --quiet
RUN echo -e "\033[1;31m Refusing to release a dirty git repository. Please commit your local changes first! \033[0m" ; exit 1
END
BUILD +build
go-deps:
FROM ${go_builder_image}
WORKDIR /go-workdir
@ -111,7 +177,7 @@ go-build:
ARG GOOS=linux
ARG GOARCH=amd64
ARG GOARM
ARG CMDS=portmaster-start portmaster-core hub notifier
ARG CMDS=portmaster-start portmaster-core
CACHE --sharing shared "$GOCACHE"
CACHE --sharing shared "$GOMODCACHE"
@ -182,6 +248,7 @@ go-test:
go-test-all:
FROM ${work_image}
ARG --required architectures
FOR arch IN ${architectures}
DO +RUST_TO_GO_ARCH_STRING --rustTarget="${arch}"
@ -197,6 +264,7 @@ go-lint:
# Builds portmaster-start, portmaster-core, hub and notifier for all supported platforms
go-release:
FROM ${work_image}
ARG --required architectures
FOR arch IN ${architectures}
DO +RUST_TO_GO_ARCH_STRING --rustTarget="${arch}"
@ -218,11 +286,6 @@ go-build-utils:
BUILD +go-build --CMDS="" --GOARCH=amd64 --GOOS=linux
BUILD +go-build --CMDS="" --GOARCH=amd64 --GOOS=windows
# All targets that should run in CI for go.
go-ci:
BUILD +go-release
BUILD +go-test
# Prepares the angular project by installing dependencies
angular-deps:
FROM ${node_builder_image}
@ -272,7 +335,7 @@ angular-project:
RUN --no-cache cwd=$(pwd) && cd "${dist}" && zip -r "${cwd}/${project}.zip" ./
SAVE ARTIFACT --keep-ts "${dist}" "./output/${project}"
# Save portmaster UI as local artifact.
IF [ "${project}" = "portmaster" ]
SAVE ARTIFACT --keep-ts "./${project}.zip" AS LOCAL ${outputDir}/all/${project}-ui.zip
@ -302,9 +365,6 @@ assets:
rust-base:
FROM ${rust_builder_image}
RUN dpkg --add-architecture armhf
RUN dpkg --add-architecture arm64
RUN apt-get update -qq
# Tools and libraries required for cross-compilation
@ -318,74 +378,39 @@ rust-base:
gcc-multilib \
linux-libc-dev \
linux-libc-dev-amd64-cross \
linux-libc-dev-arm64-cross \
linux-libc-dev-armel-cross \
linux-libc-dev-armhf-cross \
build-essential \
curl \
wget \
file \
libsoup-3.0-dev \
libwebkit2gtk-4.1-dev
libwebkit2gtk-4.1-dev \
gcc-mingw-w64-x86-64 \
zip
# Install library dependencies for all supported architectures
# required for succesfully linking.
FOR arch IN amd64 arm64 armhf
RUN apt-get install --no-install-recommends -qq \
libsoup-3.0-0:${arch} \
libwebkit2gtk-4.1-0:${arch} \
libssl3:${arch} \
libayatana-appindicator3-1:${arch} \
librsvg2-bin:${arch} \
libgtk-3-0:${arch} \
libjavascriptcoregtk-4.1-0:${arch} \
libssl-dev:${arch} \
libayatana-appindicator3-dev:${arch} \
librsvg2-dev:${arch} \
libgtk-3-dev:${arch} \
libjavascriptcoregtk-4.1-dev:${arch}
END
# Note(ppacher): I've no idea why we need to explicitly create those symlinks:
# Some how all the other libs work but libsoup and libwebkit2gtk do not create the link file
RUN cd /usr/lib/aarch64-linux-gnu && \
ln -s libwebkit2gtk-4.1.so.0 libwebkit2gtk-4.1.so && \
ln -s libsoup-3.0.so.0 libsoup-3.0.so
RUN cd /usr/lib/arm-linux-gnueabihf && \
ln -s libwebkit2gtk-4.1.so.0 libwebkit2gtk-4.1.so && \
ln -s libsoup-3.0.so.0 libsoup-3.0.so
# For what ever reason trying to install the gcc compilers together with the above
# command makes apt fail due to conflicts with gcc-multilib. Installing in a separate
# step seems to work ...
RUN apt-get install --no-install-recommends -qq \
g++-mingw-w64-x86-64 \
gcc-aarch64-linux-gnu \
gcc-arm-none-eabi \
gcc-arm-linux-gnueabi \
gcc-arm-linux-gnueabihf \
libc6-dev-arm64-cross \
libc6-dev-armel-cross \
libc6-dev-armhf-cross \
libc6-dev-amd64-cross
libsoup-3.0-0 \
libwebkit2gtk-4.1-0 \
libssl3 \
libayatana-appindicator3-1 \
librsvg2-bin \
libgtk-3-0 \
libjavascriptcoregtk-4.1-0 \
libssl-dev \
libayatana-appindicator3-dev \
librsvg2-dev \
libgtk-3-dev \
libjavascriptcoregtk-4.1-dev
# Add some required rustup components
RUN rustup component add clippy
RUN rustup component add rustfmt
# Install architecture 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"
# Required for cross compilation to work.
ENV PKG_CONFIG_ALLOW_CROSS=1
# For now we need tauri-cli 2.0.0 for bulding
DO rust+CARGO --args="install tauri-cli --version ^2.0.0-beta"
# Explicitly cache here.
SAVE IMAGE --cache-hint
@ -398,7 +423,7 @@ tauri-src:
# --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 assets/data ./../../assets/data
COPY packaging/linux ./../../packaging/linux
COPY (+angular-project/output/tauri-builtin --project=tauri-builtin --dist=./dist/tauri-builtin --configuration=production --baseHref="/") ./../angular/dist/tauri-builtin
@ -411,13 +436,151 @@ tauri-build:
FROM +tauri-src
ARG --required target
ARG output=".*/release/(([^\./]+|([^\./]+\.(dll|exe)))|bundle/.*\.(deb|msi|AppImage))"
ARG output=".*/release/(([^\./]+|([^\./]+\.(dll|exe)))|bundle/(deb|rpm)/.*\.(deb|rpm))"
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
# We already know 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.
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}"
# Our tauri app has externalBins configured so tauri will try to embed them when it finished compiling
# the app. Make sure we copy portmaster-start and portmaster-core in all architectures supported.
# See documentation for externalBins for more information on how tauri searches for the binaries.
COPY (+go-build/output --CMDS="portmaster-start portmaster-core" --GOOS="${GOOS}" --GOARCH="${GOARCH}" --GOARM="${GOARM}") /tmp/gobuild
# Place them in the correct folder with the rust target tripple attached.
FOR bin IN $(ls /tmp/gobuild)
# ${bin$.*} does not work in SET commands unfortunately so we use a shell
# snippet here:
RUN set -e ; \
dest="./binaries/${bin}-${target}" ; \
if [ -z "${bin##*.exe}" ]; then \
dest="./binaries/${bin%.*}-${target}.exe" ; \
fi ; \
cp "/tmp/gobuild/${bin}" "${dest}" ;
END
# Just for debugging ...
# RUN ls -R ./binaries
# 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 rustup target add "${target}"
RUN --mount=$EARTHLY_RUST_TARGET_CACHE cargo tauri build --ci --target="${target}"
DO rust+COPY_OUTPUT --output="${output}"
# BUG(cross-compilation):
#
# The above command seems to correctly compile for all architectures we want to support but fails during
# linking since the target libaries are not available for the requested platforms. Maybe we need to download
# the, manually ...
#
# The earthly rust lib also has support for using cross-rs for cross-compilation but that fails due to the
# fact that cross-rs base docker images used for building are heavily outdated (latest = ubunut:16.0, main = ubuntu:20.04)
# which does not ship recent enough glib versions (our glib dependency needs glib>2.70 but ubunut:20.04 only ships 2.64)
#
# The following would use the CROSS function from the earthly lib, this
# DO rust+CROSS --target="${target}"
RUN echo output: $(ls -R "target/${target}/release")
LET outbin="error"
FOR bin IN "portmaster Portmaster.exe WebView2Loader.dll"
# Modify output binary.
SET outbin="${bin}"
IF [ "${bin}" = "portmaster" ]
SET outbin="portmaster-app"
ELSE IF [ "${bin}" = "Portmaster.exe" ]
SET outbin="portmaster-app.exe"
END
# Save output binary as local artifact.
SAVE ARTIFACT --if-exists --keep-ts "target/${target}/release/${bin}" AS LOCAL "${outputDir}/${GO_ARCH_STRING}/${outbin}"
END
SAVE ARTIFACT --if-exists --keep-ts "target/${target}/release/bundle/deb/*.deb" AS LOCAL "${outputDir}/${GO_ARCH_STRING}/"
SAVE ARTIFACT --if-exists --keep-ts "target/${target}/release/bundle/rpm/*.rpm" AS LOCAL "${outputDir}/${GO_ARCH_STRING}/"
tauri-windows-release-bundle:
FROM +tauri-src
ARG target="x86_64-pc-windows-gnu"
ARG output=".*/release/(([^\./]+|([^\./]+\.(dll|exe))))"
ARG bundle="none"
ARG GOOS=windows
ARG GOARCH=amd64
ARG GOARM
# The binaries will not be used but we still need to create them. Tauri will check for them.
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}"
# Our tauri app has externalBins configured so tauri will look for them when it finished compiling
# the app. Make sure we copy portmaster-start and portmaster-core in all architectures supported.
# See documentation for externalBins for more information on how tauri searches for the binaries.
COPY (+go-build/output --GOOS="${GOOS}" --CMDS="portmaster-start portmaster-core" --GOARCH="${GOARCH}" --GOARM="${GOARM}") /tmp/gobuild
# Place them in the correct folder with the rust target tripple attached.
FOR bin IN $(ls /tmp/gobuild)
# ${bin$.*} does not work in SET commands unfortunately so we use a shell
# snippet here:
RUN set -e ; \
dest="./binaries/${bin}-${target}" ; \
if [ -z "${bin##*.exe}" ]; then \
dest="./binaries/${bin%.*}-${target}.exe" ; \
fi ; \
cp "/tmp/gobuild/${bin}" "${dest}" ;
END
# Just for debugging ...
# RUN ls -R ./binaries
DO rust+SET_CACHE_MOUNTS_ENV
RUN rustup target add "${target}"
RUN --mount=$EARTHLY_RUST_TARGET_CACHE cargo tauri build --no-bundle --ci --target="${target}"
DO rust+COPY_OUTPUT --output="${output}"
# Get version from git.
COPY .git .
LET version = "$(git tag --points-at || true)"
IF [ -z "${version}" ]
LET dev_version = "$(git describe --tags --first-parent --abbrev=0 || true)"
IF [ -n "${dev_version}" ]
SET version = "${dev_version}"
END
END
IF [ -z "${version}" ]
SET version = "v0.0.0"
END
ENV VERSION="${version}"
RUN echo "Version: $VERSION"
ENV VERSION_SUFFIX="$(echo $VERSION | tr '.' '-')"
RUN echo "Version Suffix: $VERSION_SUFFIX"
RUN echo output: $(ls -R "target/${target}/release")
RUN mv "target/${target}/release/app.exe" "target/${target}/release/portmaster-app_${VERSION_SUFFIX}.exe"
RUN zip "target/${target}/release/portmaster-app_${VERSION_SUFFIX}.zip" "target/${target}/release/portmaster-app_${VERSION_SUFFIX}.exe" -j portmaster-app${VERSION_SUFFIX}.exe "target/${target}/release/WebView2Loader.dll" -j WebView2Loader.dll
SAVE ARTIFACT --if-exists "target/${target}/release/portmaster-app_${VERSION_SUFFIX}.zip" AS LOCAL "${outputDir}/${GO_ARCH_STRING}/"
tauri-prep-windows:
FROM +angular-base --configuration=production
ARG target="x86_64-pc-windows-msvc"
# 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 know 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.
RUN mkdir /tmp/gobuild
RUN mkdir ./binaries
@ -443,55 +606,29 @@ tauri-build:
cp "/tmp/gobuild/${bin}" "${dest}" ;
END
# Copy source
COPY --keep-ts ./desktop/tauri/src-tauri src-tauri
COPY --keep-ts ./assets assets
# Build UI
ENV NODE_ENV="production"
RUN --no-cache ./node_modules/.bin/ng build --configuration production --base-href / "tauri-builtin"
# Just for debugging ...
RUN ls -R ./binaries
# RUN ls -R ./binaries
# RUN ls -R ./dist
# 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}"
# BUG(cross-compilation):
#
# The above command seems to correctly compile for all architectures we want to support but fails during
# linking since the target libaries are not available for the requested platforms. Maybe we need to download
# the, manually ...
#
# The earthly rust lib also has support for using cross-rs for cross-compilation but that fails due to the
# fact that cross-rs base docker images used for building are heavily outdated (latest = ubunut:16.0, main = ubuntu:20.04)
# which does not ship recent enough glib versions (our glib dependency needs glib>2.70 but ubunut:20.04 only ships 2.64)
#
# The following would use the CROSS function from the earthly lib, this
# DO rust+CROSS --target="${target}"
# RUN echo output: $(ls "target/${target}/release")
LET outbin="error"
FOR bin IN "portmaster Portmaster.exe WebView2Loader.dll"
# Modify output binary.
SET outbin="${bin}"
IF [ "${bin}" = "portmaster" ]
SET outbin="portmaster-app"
ELSE IF [ "${bin}" = "Portmaster.exe" ]
SET outbin="portmaster-app.exe"
END
# Save output binary as local artifact.
IF [ -f "target/${target}/release/${bin}" ]
SAVE ARTIFACT --keep-ts "target/${target}/release/${bin}" AS LOCAL "${outputDir}/${GO_ARCH_STRING}/${outbin}"
END
END
SAVE ARTIFACT "./dist/tauri-builtin" AS LOCAL "${outputDir}/${GO_ARCH_STRING}/desktop/angular/dist/"
SAVE ARTIFACT "./src-tauri" AS LOCAL "${outputDir}/${GO_ARCH_STRING}/desktop/tauri/src-tauri"
SAVE ARTIFACT "./binaries" AS LOCAL "${outputDir}/${GO_ARCH_STRING}/desktop/tauri/src-tauri/"
SAVE ARTIFACT "./assets" AS LOCAL "${outputDir}/${GO_ARCH_STRING}/assets"
tauri-release:
FROM ${work_image}
ARG bundle="none"
ARG --required architectures
FOR arch IN ${architectures}
BUILD +tauri-build --target="${arch}" --bundle="${bundle}"
BUILD +tauri-build --target="${arch}"
END
kext-build:
@ -515,21 +652,6 @@ kext-build:
SAVE ARTIFACT --keep-ts "portmaster-kext-release-bundle.zip" AS LOCAL "${outputDir}/windows_amd64/portmaster-kext-release-bundle.zip"
build:
BUILD +go-release
BUILD +angular-release
BUILD +tauri-release
BUILD +assets
release:
LOCALLY
IF ! git diff --quiet
RUN echo -e "\033[1;31m Refusing to release a dirty git repository. Please commit your local changes first! \033[0m" ; exit 1
END
BUILD +build
# 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}.

View file

@ -0,0 +1,14 @@
#!/bin/sh
# Traymenu icons. Sometimes to wrong size is selected so remove 256 and 64.
convert pm_dark_green_512.png -colors 256 -define icon:auto-resize=48,32,16 pm_dark_green.ico
convert pm_dark_blue_512.png -colors 256 -define icon:auto-resize=48,32,16 pm_dark_blue.ico
convert pm_dark_red_512.png -colors 256 -define icon:auto-resize=48,32,16 pm_dark_red.ico
convert pm_dark_yellow_512.png -colors 256 -define icon:auto-resize=48,32,16 pm_dark_yellow.ico
convert pm_light_blue_512.png -colors 256 -define icon:auto-resize=48,32,16 pm_light_blue.ico
convert pm_light_green_512.png -colors 256 -define icon:auto-resize=48,32,16 pm_light_green.ico
convert pm_light_red_512.png -colors 256 -define icon:auto-resize=48,32,16 pm_light_red.ico
convert pm_light_yellow_512.png -colors 256 -define icon:auto-resize=48,32,16 pm_light_yellow.ico
convert pm_dark_512.png -colors 256 -define icon:auto-resize=64,48,32,16 pm_dark.ico
convert pm_light_512.png -colors 256 -define icon:auto-resize=64,48,32,16 pm_light.ico

Binary file not shown.

After

(image error) Size: 31 KiB

Binary file not shown.

Before

(image error) Size: 108 KiB

Binary file not shown.

After

(image error) Size: 15 KiB

Binary file not shown.

Before

(image error) Size: 97 KiB

Binary file not shown.

After

(image error) Size: 15 KiB

Binary file not shown.

Before

(image error) Size: 110 KiB

Binary file not shown.

After

(image error) Size: 15 KiB

Binary file not shown.

Before

(image error) Size: 110 KiB

Binary file not shown.

After

(image error) Size: 15 KiB

Binary file not shown.

Before

(image error) Size: 109 KiB

Binary file not shown.

After

(image error) Size: 31 KiB

Binary file not shown.

Before

(image error) Size: 108 KiB

Binary file not shown.

After

(image error) Size: 15 KiB

Binary file not shown.

Before

(image error) Size: 110 KiB

Binary file not shown.

After

(image error) Size: 15 KiB

Binary file not shown.

Before

(image error) Size: 110 KiB

Binary file not shown.

After

(image error) Size: 15 KiB

Binary file not shown.

Before

(image error) Size: 110 KiB

Binary file not shown.

After

(image error) Size: 15 KiB

Binary file not shown.

Before

(image error) Size: 110 KiB

View file

@ -14,16 +14,16 @@ const (
// Icons.
var (
//go:embed data/icons/pm_light_green_512.ico
//go:embed data/icons/pm_light_green_256.png
GreenICO []byte
//go:embed data/icons/pm_light_yellow_512.ico
//go:embed data/icons/pm_light_yellow_256.png
YellowICO []byte
//go:embed data/icons/pm_light_red_512.ico
//go:embed data/icons/pm_light_red_256.png
RedICO []byte
//go:embed data/icons/pm_light_blue_512.ico
//go:embed data/icons/pm_light_blue_256.png
BlueICO []byte
// ColoredIcons holds all the icons as .ICOs

View file

@ -28,6 +28,7 @@ var (
maxRetries int
dataRoot *utils.DirStructure
logsRoot *utils.DirStructure
forceOldUI bool
updateURLFlag string
userAgentFlag string
@ -74,6 +75,7 @@ func init() {
flags.StringVar(&userAgentFlag, "update-agent", "", "Set an alternative user agent for requests to the update server")
flags.IntVar(&maxRetries, "max-retries", 5, "Maximum number of retries when starting a Portmaster component")
flags.BoolVar(&stdinSignals, "input-signals", false, "Emulate signals using stdin.")
flags.BoolVar(&forceOldUI, "old-ui", false, "Use the old ui. (Beta)")
_ = rootCmd.MarkPersistentFlagDirname("data")
_ = flags.MarkHidden("input-signals")
}

View file

@ -29,6 +29,10 @@ const (
// This disables retrying and exits with an error code.
ControlledFailureExitCode = 24
// StartOldUIExitCode is an exit code that is returned by the UI when there. This is manfully triaged by the user, if the new UI does not work for them.
StartOldUIExitCode = 77
MissingDependencyExitCode = 0xc0000135 // Windows STATUS_DLL_NOT_FOUND
exeSuffix = ".exe"
zipSuffix = ".zip"
)
@ -38,6 +42,8 @@ var (
onWindows = runtime.GOOS == "windows"
stdinSignals bool
childIsRunning = abool.NewBool(false)
fallBackToOldUI bool = false
)
// Options for starting component.
@ -55,7 +61,21 @@ type Options struct {
RestartOnFail bool // Try restarting automatically, if the started component fails.
}
// This is a temp value that will be used to test the new UI in beta.
var app2Options = Options{
Name: "Portmaster App2",
Identifier: "app2/portmaster-app",
AllowDownload: false,
AllowHidingWindow: false,
RestartOnFail: true,
}
func init() {
// Make sure the new UI has a proper extension.
if onWindows {
app2Options.Identifier += ".zip"
}
registerComponent([]Options{
{
Name: "Portmaster Core",
@ -70,6 +90,7 @@ func init() {
Identifier: "app/portmaster-app.zip",
AllowDownload: false,
AllowHidingWindow: false,
RestartOnFail: true,
},
{
Name: "Portmaster Notifier",
@ -89,6 +110,26 @@ func init() {
RestartOnFail: true,
},
})
if onWindows {
registerComponent([]Options{
{
Name: "Portmaster App2",
Identifier: "app2/portmaster-app.zip",
AllowDownload: false,
AllowHidingWindow: false,
},
})
} else {
registerComponent([]Options{
{
Name: "Portmaster App2",
Identifier: "app2/portmaster-app",
AllowDownload: false,
AllowHidingWindow: false,
},
})
}
}
func registerComponent(opts []Options) {
@ -311,6 +352,14 @@ func persistOutputStreams(opts *Options, version string, cmd *exec.Cmd) (chan st
}
func execute(opts *Options, args []string) (cont bool, err error) {
if !forceOldUI && registry.UsePreReleases && opts.ShortIdentifier == "app" {
// Check if new ui was already tried.
if !fallBackToOldUI {
opts = &app2Options
log.Println("Using new UI")
}
}
file, err := registry.GetFile(
helper.PlatformIdentifier(opts.Identifier),
)
@ -440,6 +489,12 @@ func parseExitError(err error) (restart bool, errWithCtx error) {
return true, nil
case ControlledFailureExitCode:
return false, errors.New("controlled failure, check logs")
case StartOldUIExitCode:
fallBackToOldUI = true
return true, errors.New("user requested old UI")
case MissingDependencyExitCode:
fallBackToOldUI = true
return true, errors.New("new UI failed with missing dependency")
default:
return true, fmt.Errorf("unknown exit code %w", exErr)
}

File diff suppressed because it is too large Load diff

View file

@ -37,13 +37,13 @@
"@fortawesome/free-brands-svg-icons": "^6.4.0",
"@fortawesome/free-regular-svg-icons": "^6.4.0",
"@fortawesome/free-solid-svg-icons": "^6.4.0",
"@tauri-apps/api": "^2.0.0-beta.3",
"@tauri-apps/plugin-cli": "^2.0.0-beta.1",
"@tauri-apps/plugin-clipboard-manager": "^2.0.0-alpha.4",
"@tauri-apps/plugin-dialog": "^2.0.0-alpha.4",
"@tauri-apps/plugin-notification": "^2.0.0-alpha.4",
"@tauri-apps/plugin-os": "^2.0.0-alpha.5",
"@tauri-apps/plugin-shell": "^2.0.0-alpha.4",
"@tauri-apps/api": ">=2.0.0-beta.0",
"@tauri-apps/plugin-cli": ">=2.0.0-beta.0",
"@tauri-apps/plugin-clipboard-manager": ">=2.0.0-beta.0",
"@tauri-apps/plugin-dialog": ">=2.0.0-beta.0",
"@tauri-apps/plugin-notification": ">=2.0.0-beta.0",
"@tauri-apps/plugin-os": ">=2.0.0-beta.0",
"@tauri-apps/plugin-shell": "^2.0.0-beta",
"autoprefixer": "^10.4.14",
"d3": "^7.8.4",
"data-urls": "^5.0.0",
@ -101,4 +101,4 @@
"webpack-ext-reloader": "^1.1.9",
"zip-a-folder": "^1.1.5"
}
}
}

View file

@ -28,7 +28,7 @@ function asyncInvoke<T>(method: string, args: object): Promise<T> {
if (typeof event.payload === 'object' && 'error' in event.payload) {
reject(event.payload);
return
};
}
resolve(event.payload);
})
@ -75,7 +75,7 @@ export class TauriIntegrationService implements IntegrationService {
}
getAppInfo(info: ProcessInfo): Promise<AppInfo> {
return asyncInvoke("plugin:portmaster|get_app_info", {
return asyncInvoke("get_app_info", {
...info,
})
}
@ -112,7 +112,7 @@ export class TauriIntegrationService implements IntegrationService {
async shouldShow(): Promise<boolean> {
try {
const response = await invoke<string>("plugin:portmaster|should_show");
const response = await invoke<string>("should_show");
return response === "show";
} catch (err) {
console.error(err);
@ -122,7 +122,7 @@ export class TauriIntegrationService implements IntegrationService {
async shouldHandlePrompts(): Promise<boolean> {
try {
const response = await invoke<string>("plugin:portmaster|should_handle_prompts")
const response = await invoke<string>("should_handle_prompts")
return response === "true"
} catch (err) {
console.error(err);
@ -130,27 +130,27 @@ export class TauriIntegrationService implements IntegrationService {
}
}
get_state(key: string): Promise<string> {
return invoke<string>("plugin:portmaster|get_state");
get_state(_: string): Promise<string> {
return invoke<string>("get_state");
}
set_state(key: string, value: string): Promise<void> {
return invoke<void>("plugin:portmaster|set_state", {
return invoke<void>("set_state", {
key,
value
})
}
getServiceManagerStatus(): Promise<ServiceManagerStatus> {
return asyncInvoke("plugin:portmaster|get_service_manager_status", {})
return asyncInvoke("get_service_manager_status", {})
}
startService(): Promise<any> {
return asyncInvoke("plugin:portmaster|start_service", {});
return asyncInvoke("start_service", {});
}
onExitRequest(cb: () => void): () => void {
let unlisten: () => void = () => { };
let unlisten: () => void = () => undefined;
listen('exit-requested', () => {
cb();
@ -189,7 +189,7 @@ export class TauriIntegrationService implements IntegrationService {
return;
}
let promptWindow = new Window("prompt", {
const promptWindow = new Window("prompt", {
alwaysOnTop: true,
decorations: false,
minimizable: false,

View file

@ -1 +0,0 @@
../../assets/data

File diff suppressed because it is too large Load diff

View file

@ -12,21 +12,21 @@ 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 = [] }
tauri-build = { version = "2.0.0-beta.18", 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"
tauri = { version = "2.0.0-beta.23", features = ["tray-icon", "image-png", "config-json5"] }
tauri-plugin-shell = "2.0.0-beta"
tauri-plugin-dialog = "2.0.0-beta"
tauri-plugin-clipboard-manager = "2.0.0-beta"
tauri-plugin-os = "2.0.0-beta"
tauri-plugin-single-instance = "2.0.0-beta"
tauri-plugin-cli = "2.0.0-beta"
tauri-plugin-notification = "2.0.0-beta"
tauri-plugin-log = "2.0.0-beta"
# We still need the tauri-cli 1.5 for building
tauri-cli = "1.5.11"
tauri-cli = "2.0.0-beta.21"
# General
serde_json = "1.0"
@ -47,7 +47,10 @@ http = "1.0.0"
url = "2.5.0"
thiserror = "1.0"
log = "0.4.21"
pretty_env_logger = "0.5.0"
reqwest = { version = "0.12" }
rfd = { version = "*", default-features = false, features = [ "tokio", "gtk3", "common-controls-v6" ] }
open = "5.1.3"
# Linux only
[target.'cfg(target_os = "linux")'.dependencies]
@ -62,6 +65,7 @@ gio-sys = "0.18.1"
[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"] }
tauri-winrt-notification = "0.3.0"
[dev-dependencies]
which = "6.0.0"

View file

@ -0,0 +1,32 @@
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "default",
"description": "Capability for the main window",
"windows": [
"main",
"splash"
],
"remote": {
"urls": [
"http://localhost:817"
]
},
"permissions": [
"path:default",
"event:allow-listen",
"event:allow-unlisten",
"event:allow-emit",
"event:allow-emit-to",
"window:allow-hide",
"window:allow-show",
"window:allow-is-visible",
"window:allow-set-focus",
"app:default",
"image:default",
"resources:default",
"menu:default",
"tray:default",
"shell:allow-open",
"notification:default"
]
}

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1 @@
{"default":{"identifier":"default","description":"Capability for the main window","remote":{"urls":["http://localhost:817"]},"local":true,"windows":["main","splash"],"permissions":["path:default","event:allow-listen","event:allow-unlisten","event:allow-emit","event:allow-emit-to","window:allow-hide","window:allow-show","window:allow-is-visible","window:allow-set-focus","app:default","image:default","resources:default","menu:default","tray:default","shell:allow-open","notification:default"]}}

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,8 @@
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
use std::time::Duration;
use tauri::{AppHandle, Manager, RunEvent, WindowEvent};
use tauri_plugin_cli::CliExt;
@ -16,7 +18,7 @@ mod portmaster;
mod traymenu;
mod window;
use log::{debug, error, info, trace, warn};
use log::{debug, error, info};
use portmaster::PortmasterExt;
use traymenu::setup_tray_menu;
use window::{close_splash_window, create_main_window};
@ -24,6 +26,8 @@ use window::{close_splash_window, create_main_window};
#[macro_use]
extern crate lazy_static;
const FALLBACK_TO_OLD_UI_EXIT_CODE: i32 = 77;
#[derive(Clone, serde::Serialize)]
struct Payload {
args: Vec<String>,
@ -82,12 +86,38 @@ impl portmaster::Handler for WsHandler {
}
}
fn show_webview_not_installed_dialog() -> i32 {
use rfd::MessageDialog;
let result = MessageDialog::new()
.set_title("Portmaster")
.set_description("Webkit is not installed. Please install it and run portmaster again")
.set_buttons(rfd::MessageButtons::OkCancelCustom(
"Go to install page".to_owned(),
"Use old UI".to_owned(),
))
.show();
println!("{:?}", result);
if let rfd::MessageDialogResult::Custom(result) = result {
if result.eq("Go to install page") {
_ = open::that("https://wiki.safing.io/en/Portmaster/Install/Webview");
std::thread::sleep(Duration::from_secs(2));
return 0;
}
}
return FALLBACK_TO_OLD_UI_EXIT_CODE;
}
fn main() {
pretty_env_logger::init();
if let Err(_) = tauri::webview_version() {
std::process::exit(show_webview_not_installed_dialog());
}
let app = tauri::Builder::default()
// Shell plugin for open_external support
.plugin(tauri_plugin_shell::init())
.plugin(tauri_plugin_log::Builder::default().build())
// Clipboard support
.plugin(tauri_plugin_clipboard_manager::init())
// Dialog (Save/Open) support
@ -102,16 +132,24 @@ fn main() {
.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())
.invoke_handler(tauri::generate_handler![
portmaster::commands::get_app_info,
portmaster::commands::get_service_manager_status,
portmaster::commands::start_service,
portmaster::commands::get_state,
portmaster::commands::set_state,
portmaster::commands::should_show,
portmaster::commands::should_handle_prompts
])
// Setup the app an any listeners
.setup(|app| {
setup_tray_menu(app)?;
portmaster::setup(app.handle().clone());
// 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| {
app.listen_any("single-instance", move |_event| {
let _ = window::open_window(&handle);
});
@ -196,17 +234,23 @@ fn main() {
);
api.prevent_close();
if let Some(window) = handle.get_window(label.as_str()) {
let _ = window.emit("exit-requested", "");
if let Some(window) = handle.get_webview_window(label.as_str()) {
let result = window.emit("exit-requested", "");
if let Err(err) = result {
error!("failed to emit event: {}", err.to_string());
}
} else {
error!("window was None");
}
}
_ => {}
}
}
RunEvent::ExitRequested { api, .. } => {
api.prevent_exit();
}
// TODO(vladimir): why was this needed?
// RunEvent::ExitRequested { api, .. } => {
// api.prevent_exit();
// }
_ => {}
});
}

View file

@ -1,4 +1,4 @@
use super::PortmasterPlugin;
use super::PortmasterInterface;
use crate::service::get_service_manager;
use crate::service::ServiceManager;
use log::debug;
@ -15,7 +15,7 @@ pub struct Error {
#[tauri::command]
pub fn should_show<R: Runtime>(
_window: Window<R>,
portmaster: State<'_, PortmasterPlugin<R>>,
portmaster: State<'_, PortmasterInterface<R>>,
) -> Result {
if portmaster.get_show_after_bootstrap() {
debug!("[tauri:rpc:should_show] application should show after bootstrap");
@ -31,7 +31,7 @@ pub fn should_show<R: Runtime>(
#[tauri::command]
pub fn should_handle_prompts<R: Runtime>(
_window: Window<R>,
portmaster: State<'_, PortmasterPlugin<R>>,
portmaster: State<'_, PortmasterInterface<R>>,
) -> Result {
if portmaster.handle_prompts.load(Ordering::Relaxed) {
Ok("true".to_string())
@ -43,7 +43,7 @@ pub fn should_handle_prompts<R: Runtime>(
#[tauri::command]
pub fn get_state<R: Runtime>(
_window: Window<R>,
portmaster: State<'_, PortmasterPlugin<R>>,
portmaster: State<'_, PortmasterInterface<R>>,
key: String,
) -> Result {
let value = portmaster.get_state(key);
@ -58,7 +58,7 @@ pub fn get_state<R: Runtime>(
#[tauri::command]
pub fn set_state<R: Runtime>(
_window: Window<R>,
portmaster: State<'_, PortmasterPlugin<R>>,
portmaster: State<'_, PortmasterInterface<R>>,
key: String,
value: String,
) -> Result {

View file

@ -13,7 +13,7 @@
/// 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;
pub 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
@ -34,10 +34,9 @@ use std::{
use log::{debug, error};
use serde;
use std::sync::Mutex;
use tauri::{
plugin::{Builder, TauriPlugin},
AppHandle, Manager, Runtime,
};
use tauri::{AppHandle, Manager, Runtime};
const PORTMASTER_BASE_URL: &'static str = "http://127.0.0.1:817/api/v1/";
pub trait Handler {
fn on_connect(&mut self, cli: PortAPI) -> ();
@ -45,7 +44,7 @@ pub trait Handler {
fn name(&self) -> String;
}
pub struct PortmasterPlugin<R: Runtime> {
pub struct PortmasterInterface<R: Runtime> {
#[allow(dead_code)]
app: AppHandle<R>,
@ -76,7 +75,7 @@ pub struct PortmasterPlugin<R: Runtime> {
should_show_after_bootstrap: AtomicBool,
}
impl<R: Runtime> PortmasterPlugin<R> {
impl<R: Runtime> PortmasterInterface<R> {
/// Returns a state stored in the portmaster plugin.
pub fn get_state(&self, key: String) -> Option<String> {
let map = self.state.lock();
@ -118,13 +117,12 @@ impl<R: Runtime> PortmasterPlugin<R> {
handler.on_connect(api);
} else {
debug!("not yet connected to Portmaster API, calling on_disconnect()");
debug!("not yet connected to Portmaster API, calling on_disconnect()");
handler.on_disconnect();
}
handlers.push(Box::new(handler));
debug!("number of registered handlers: {}", handlers.len());
}
}
@ -174,7 +172,7 @@ impl<R: Runtime> PortmasterPlugin<R> {
self.should_show_after_bootstrap.load(Ordering::Relaxed)
}
/// Tells the angular applicatoin to show the window by emitting an event.
/// Tells the angular application 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) {
@ -184,8 +182,9 @@ impl<R: Runtime> PortmasterPlugin<R> {
// 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", "");
if let Err(err) = self.app.emit("portmaster:show", "") {
error!("failed to emit show event: {}", err.to_string());
}
}
/// Enables or disables the SPN.
@ -206,12 +205,30 @@ impl<R: Runtime> PortmasterPlugin<R> {
}
}
/// Send Shutdown request to portmaster
pub fn trigger_shutdown(&self) {
tauri::async_runtime::spawn(async move {
let client = reqwest::Client::new();
match client
.post(format!("{}core/shutdown", PORTMASTER_BASE_URL))
.send()
.await
{
Ok(v) => {
debug!("shutdown request sent {:?}", v);
}
Err(err) => {
error!("failed to send shutdown request {}", err);
}
}
});
}
//// 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;
notifications::notification_handler(api).await;
});
}
}
@ -223,9 +240,10 @@ impl<R: Runtime> PortmasterPlugin<R> {
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);
{
let mut guard = self.api.lock().unwrap();
*guard = Some(api.clone());
}
// fire-off the notification handler.
if self.handle_notifications.load(Ordering::Relaxed) {
@ -249,9 +267,10 @@ impl<R: Runtime> PortmasterPlugin<R> {
self.is_reachable.store(false, Ordering::Relaxed);
// clear the current api client reference.
let mut guard = self.api.lock().unwrap();
*guard = None;
drop(guard);
{
let mut guard = self.api.lock().unwrap();
*guard = None;
}
if let Ok(mut handlers) = self.handlers.lock() {
for handler in handlers.iter_mut() {
@ -262,47 +281,32 @@ impl<R: Runtime> PortmasterPlugin<R> {
}
pub trait PortmasterExt<R: Runtime> {
fn portmaster(&self) -> &PortmasterPlugin<R>;
fn portmaster(&self) -> &PortmasterInterface<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()
fn portmaster(&self) -> &PortmasterInterface<R> {
self.state::<PortmasterInterface<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),
};
pub fn setup(app: AppHandle) {
let interface = PortmasterInterface {
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);
app.manage(interface);
// fire of the websocket handler
websocket::start_websocket_thread(app.clone());
Ok(())
})
.build()
// fire of the websocket handler
websocket::start_websocket_thread(app.clone());
}

View file

@ -3,9 +3,7 @@ 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) {
@ -34,59 +32,7 @@ pub async fn notification_handler(cli: PortAPI) {
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);
}
}
});
}
show_notification(&cli, key, n).await;
}
Err(err) => match err {
ParseError::JSON(err) => {
@ -101,3 +47,99 @@ pub async fn notification_handler(cli: PortAPI) {
}
}
}
#[cfg(target_os = "linux")]
pub async fn show_notification(cli: &PortAPI, key: String, n: Notification) {
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);
}
{
let cli_clone = cli.clone();
async_runtime::spawn(async move {
let res = notif.show();
// TODO(ppacher): keep a reference of open notifications and close them
// if the user reacted inside the UI:
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);
}
}
});
}
}
#[cfg(target_os = "windows")]
pub async fn show_notification(cli: &PortAPI, key: String, n: Notification) {
use tauri_winrt_notification::{Duration, Sound, Toast};
let mut toast = Toast::new("io.safing.portmaster")
.title(&n.title)
.text1(&n.message)
.sound(Some(Sound::Default))
.duration(Duration::Long);
for action in n.actions {
toast = toast.add_button(&action.text, &action.id);
}
{
let cli = cli.clone();
toast = toast.on_activated(move |action| -> windows::core::Result<()> {
if let Some(value) = action {
let cli = cli.clone();
let key = key.clone();
async_runtime::spawn(async move {
let _ = cli
.request(Request::Update(
key,
Payload::JSON(
json!({
"SelectedActionID": value
})
.to_string(),
),
))
.await;
});
}
// TODO(vladimir): If Action is None, the user clicked on the notification. Focus on the UI.
Ok(())
});
}
toast.show().expect("unable to send notification");
// TODO(vladimir): keep a reference of open notifications and close them
// if the user reacted inside the UI:
}

View file

@ -12,7 +12,6 @@ use std::process::ExitStatus;
#[cfg(target_os = "linux")]
use crate::service::systemd::SystemdServiceManager;
use log::info;
use thiserror::Error;
use self::status::StatusResult;
@ -63,7 +62,7 @@ pub fn get_service_manager() -> Result<impl ServiceManager> {
#[cfg(target_os = "linux")]
{
if SystemdServiceManager::is_installed() {
info!("system service manager: systemd");
log::info!("system service manager: systemd");
Ok(SystemdServiceManager {})
} else {

View file

@ -1,16 +1,15 @@
use std::collections::HashMap;
use std::sync::atomic::AtomicBool;
use std::sync::Mutex;
use std::{collections::HashMap, sync::atomic::Ordering};
use log::{debug, error};
use tauri::tray::{MouseButton, MouseButtonState};
use tauri::{
menu::{
CheckMenuItem, CheckMenuItemBuilder, MenuBuilder, MenuItemBuilder, PredefinedMenuItem,
SubmenuBuilder,
},
tray::{ClickType, TrayIcon, TrayIconBuilder},
Icon, Manager, Wry,
image::Image,
menu::{MenuBuilder, MenuItem, MenuItemBuilder, PredefinedMenuItem, SubmenuBuilder},
tray::{TrayIcon, TrayIconBuilder},
Wry,
};
use tauri_plugin_dialog::DialogExt;
use crate::{
portapi::{
@ -26,62 +25,90 @@ use crate::{
portmaster::PortmasterExt,
window::{create_main_window, may_navigate_to_ui, open_window},
};
use tauri_plugin_dialog::DialogExt;
pub type AppIcon = TrayIcon<Wry>;
static SPN_STATE: AtomicBool = AtomicBool::new(false);
lazy_static! {
// Set once setup_tray_menu executed.
static ref SPN_BUTTON: Mutex<Option<CheckMenuItem<Wry>>> = Mutex::new(None);
static ref SPN_STATUS: Mutex<Option<MenuItem<Wry>>> = Mutex::new(None);
static ref SPN_BUTTON: Mutex<Option<MenuItem<Wry>>> = Mutex::new(None);
static ref GLOBAL_STATUS: Mutex<Option<MenuItem<Wry>>> = Mutex::new(None);
}
const PM_TRAY_ICON_ID: &'static str = "pm_icon";
// 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 BLUE_ICON: &'static [u8] = include_bytes!("../../../../assets/data/icons/pm_light_blue.ico");
const RED_ICON: &'static [u8] = include_bytes!("../../../../assets/data/icons/pm_light_red.ico");
const YELLOW_ICON: &'static [u8] =
include_bytes!("../../assets/icons/pm_light_yellow_512.ico");
include_bytes!("../../../../assets/data/icons/pm_light_yellow.ico");
const GREEN_ICON: &'static [u8] =
include_bytes!("../../assets/icons/pm_light_green_512.ico");
include_bytes!("../../../../assets/data/icons/pm_light_green.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 open_btn = MenuItemBuilder::with_id("open", "Open App").build(app)?;
let exit_ui_btn = MenuItemBuilder::with_id("exit_ui", "Exit UI").build(app)?;
let shutdown_btn = MenuItemBuilder::with_id("shutdown", "Shut Down Portmaster").build(app)?;
let spn = CheckMenuItemBuilder::with_id("spn", "Use SPN").build(app);
let global_status = MenuItemBuilder::with_id("global_status", "Status: Secured")
.enabled(false)
.build(app)
.unwrap();
{
let mut button_ref = GLOBAL_STATUS.lock()?;
*button_ref = Some(global_status.clone());
}
// Store the SPN button reference
let mut button_ref = SPN_BUTTON.lock().unwrap();
*button_ref = Some(spn.clone());
// Setup SPN status
let spn_status = MenuItemBuilder::with_id("spn_status", "SPN: Disabled")
.enabled(false)
.build(app)
.unwrap();
{
let mut button_ref = SPN_STATUS.lock()?;
*button_ref = Some(spn_status.clone());
}
// Setup SPN button
let spn = MenuItemBuilder::with_id("spn_toggle", "Enable SPN")
.build(app)
.unwrap();
{
let mut button_ref = SPN_BUTTON.lock()?;
*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 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,
&PredefinedMenuItem::separator(app)?,
&global_status,
&PredefinedMenuItem::separator(app)?,
&spn_status,
&spn,
&PredefinedMenuItem::separator(app)?,
&exit_ui_btn,
&shutdown_btn,
&developer_menu,
])
.build()?;
let icon = TrayIconBuilder::new()
.icon(Icon::Raw(RED_ICON.to_vec()))
let icon = TrayIconBuilder::with_id(PM_TRAY_ICON_ID)
.icon(Image::from_bytes(RED_ICON).unwrap())
.menu(&menu)
.on_menu_event(move |app, event| match event.id().as_ref() {
"close" => {
"exit_ui" => {
let handle = app.clone();
app.dialog()
.message("This does not stop the Portmaster system service")
@ -90,7 +117,7 @@ pub fn setup_tray_menu(
.cancel_button_label("No")
.show(move |answer| {
if answer {
let _ = handle.emit("exit-requested", "");
// let _ = handle.emit("exit-requested", "");
handle.exit(0);
}
});
@ -116,27 +143,39 @@ pub fn setup_tray_menu(
}
};
}
"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);
}
"spn_toggle" => {
if SPN_STATE.load(Ordering::Acquire) {
app.portmaster().set_spn_enabled(false);
} else {
app.portmaster().set_spn_enabled(true);
}
}
"shutdown" => {
app.portmaster().trigger_shutdown();
}
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());
if let tauri::tray::TrayIconEvent::Click {
id: _,
position: _,
rect: _,
button,
button_state,
} = event
{
if let MouseButton::Left = button {
if let MouseButtonState::Down = button_state {
let _ = open_window(tray.app_handle());
}
}
}
})
.build(app)?;
Ok(icon)
}
@ -145,19 +184,27 @@ pub fn update_icon(icon: AppIcon, subsystems: HashMap<String, Subsystem>, spn_st
let failure = subsystems
.values()
.into_iter()
.map(|s| s.failure_status)
.fold(
subsystem::FAILURE_NONE,
|acc, s| {
if s > acc {
s
} else {
acc
.map(|s| &s.module_status)
.fold((subsystem::FAILURE_NONE, "".to_string()), |mut acc, s| {
for m in s {
if m.failure_status > acc.0 {
acc = (m.failure_status, m.failure_msg.clone())
}
},
);
}
acc
});
let next_icon = match failure {
if failure.0 == subsystem::FAILURE_NONE {
if let Some(global_status) = &mut *(GLOBAL_STATUS.lock().unwrap()) {
_ = global_status.set_text("Status: Secured");
}
} else {
if let Some(global_status) = &mut *(GLOBAL_STATUS.lock().unwrap()) {
_ = global_status.set_text(format!("Status: {}", failure.1));
}
}
let next_icon = match failure.0 {
subsystem::FAILURE_WARNING => YELLOW_ICON,
subsystem::FAILURE_ERROR => RED_ICON,
_ => match spn_status.as_str() {
@ -166,11 +213,11 @@ pub fn update_icon(icon: AppIcon, subsystems: HashMap<String, Subsystem>, spn_st
},
};
_ = icon.set_icon(Some(Icon::Raw(next_icon.to_vec())));
_ = icon.set_icon(Some(Image::from_bytes(next_icon).unwrap()));
}
pub async fn tray_handler(cli: PortAPI, app: tauri::AppHandle) {
let icon = match app.tray() {
let icon = match app.tray_by_id(PM_TRAY_ICON_ID) {
Some(icon) => icon,
None => {
error!("cancel try_handler: missing try icon");
@ -226,7 +273,23 @@ pub async fn tray_handler(cli: PortAPI, app: tauri::AppHandle) {
}
};
_ = icon.set_icon(Some(Icon::Raw(BLUE_ICON.to_vec())));
let mut portmaster_shutdown_event_subscription = match cli
.request(Request::Subscribe(
"query runtime:modules/core/event/shutdown".to_string(),
))
.await
{
Ok(rx) => rx,
Err(err) => {
error!(
"cancel try_handler: failed to subscribe to 'runtime:modules/core/event/shutdown': {}",
err
);
return;
}
};
_ = icon.set_icon(Some(Image::from_bytes(BLUE_ICON).unwrap()));
let mut subsystems: HashMap<String, Subsystem> = HashMap::new();
let mut spn_status: String = "".to_string();
@ -312,15 +375,7 @@ pub async fn tray_handler(cli: PortAPI, app: tauri::AppHandle) {
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);
}
}
update_spn_ui_state(value.value.unwrap_or(false));
},
Err(err) => match err {
ParseError::JSON(err) => {
@ -332,13 +387,40 @@ pub async fn tray_handler(cli: PortAPI, app: tauri::AppHandle) {
}
}
}
},
msg = portmaster_shutdown_event_subscription.recv() => {
let msg = match msg {
Some(m) => m,
None => { break }
};
debug!("Shutdown request received: {:?}", msg);
match msg {
Response::Ok(_, _) | Response::New(_, _) | Response::Update(_, _) => app.exit(0),
_ => {},
}
}
}
}
if let Some(btn) = &mut *(SPN_BUTTON.lock().unwrap()) {
_ = btn.set_checked(false);
}
_ = icon.set_icon(Some(Icon::Raw(RED_ICON.to_vec())));
update_spn_ui_state(false);
_ = icon.set_icon(Some(Image::from_bytes(RED_ICON).unwrap()));
}
fn update_spn_ui_state(enabled: bool) {
let mut spn_status = SPN_STATUS.lock().unwrap();
let Some(spn_status_ref) = &mut *spn_status else {
return;
};
let mut spn_btn = SPN_BUTTON.lock().unwrap();
let Some(spn_btn_ref) = &mut *spn_btn else {
return;
};
if enabled {
_ = spn_status_ref.set_text("SPN: Connected");
_ = spn_btn_ref.set_text("Disable SPN");
} else {
_ = spn_status_ref.set_text("SPN: Disabled");
_ = spn_btn_ref.set_text("Enable SPN");
}
SPN_STATE.store(enabled, Ordering::SeqCst);
}

View file

@ -1,5 +1,7 @@
use log::{debug, error};
use tauri::{AppHandle, Manager, Result, UserAttentionType, Window, WindowBuilder, WindowUrl};
use tauri::{
AppHandle, Manager, Result, UserAttentionType, WebviewUrl, WebviewWindow, WebviewWindowBuilder,
};
use crate::portmaster::PortmasterExt;
@ -11,15 +13,16 @@ use crate::portmaster::PortmasterExt;
/// 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") {
pub fn create_main_window(app: &AppHandle) -> Result<WebviewWindow> {
let mut window = if let Some(window) = app.get_webview_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()))
let res = WebviewWindowBuilder::new(app, "main", WebviewUrl::App("index.html".into()))
.title("Portmaster")
.visible(false)
.build();
@ -54,12 +57,12 @@ pub fn create_main_window(app: &AppHandle) -> Result<Window> {
Ok(window)
}
pub fn create_splash_window(app: &AppHandle) -> Result<Window> {
if let Some(window) = app.get_window("splash") {
pub fn create_splash_window(app: &AppHandle) -> Result<WebviewWindow> {
if let Some(window) = app.get_webview_window("splash") {
let _ = window.show();
Ok(window)
} else {
let window = WindowBuilder::new(app, "splash", WindowUrl::App("index.html".into()))
let window = WebviewWindowBuilder::new(app, "splash", WebviewUrl::App("index.html".into()))
.center()
.closable(false)
.focused(true)
@ -76,8 +79,9 @@ pub fn create_splash_window(app: &AppHandle) -> Result<Window> {
}
pub fn close_splash_window(app: &AppHandle) -> Result<()> {
if let Some(window) = app.get_window("splash") {
return window.close();
if let Some(window) = app.get_webview_window("splash") {
let _ = window.hide();
return window.destroy();
}
return Err(tauri::Error::WindowNotFound);
}
@ -94,9 +98,9 @@ pub fn close_splash_window(app: &AppHandle) -> Result<()> {
///
/// 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> {
pub fn open_window(app: &AppHandle) -> Result<WebviewWindow> {
if app.portmaster().is_reachable() {
match app.get_window("main") {
match app.get_webview_window("main") {
Some(win) => {
app.portmaster().show_window();
@ -122,32 +126,41 @@ pub fn open_window(app: &AppHandle) -> Result<Window> {
/// 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) {
pub fn may_navigate_to_ui(win: &mut WebviewWindow, 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().as_str() == "tauri://localhost" {
if force || win.label().eq("main") {
#[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());
_ = win.navigate(target_url.parse().unwrap());
return;
}
#[cfg(debug_assertions)]
{
// Only for dev build
// Allow connection to http://localhost:4200
let capabilities = include_str!("../capabilities/default.json")
.replace("http://localhost:817", "http://localhost:4200");
let _ = win.add_capability(capabilities);
debug!("[tauri] navigating to http://localhost:4200");
win.navigate("http://localhost:4200".parse().unwrap());
_ = win.navigate("http://localhost:4200".parse().unwrap());
}
#[cfg(not(debug_assertions))]
win.navigate("http://localhost:817".parse().unwrap());
{
_ = win.navigate("http://localhost:817".parse().unwrap());
}
} else {
error!("not navigating to user interface: current url: {}", win.url().as_str());
error!(
"not navigating to user interface: current url: {}",
win.url().unwrap().as_str()
);
}
}

View file

@ -1,116 +0,0 @@
{
"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": "Safing Limited Inc",
"deb": {
"depends": [
"libayatana-appindicator3"
],
"desktopTemplate": "../../../packaging/linux/portmaster.desktop",
"files": {
"/usr/lib/systemd/system/portmaster.service": "../../../packaging/linux/portmaster.service",
"/etc/xdg/autostart/portmaster.desktop": "../../../packaging/linux/portmaster-autostart.desktop",
"/var/": "../../../packaging/linux/var",
"../control/postinst": "../../../packaging/linux/debian/postinst",
"../control/postrm": "../../../packaging/linux/debian/postrm"
}
},
"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": []
}
}

View file

@ -0,0 +1,106 @@
{
"build": {
"beforeDevCommand": {
"script": "npm run tauri-dev",
"cwd": "../../angular",
"wait": false
},
"frontendDist": "../../angular/dist/tauri-builtin",
"devUrl": "http://localhost:4100"
},
"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."
}
]
}
},
"productName": "Portmaster",
"version": "0.1.0",
"identifier": "io.safing.portmaster", // this is added as a property to the shortcut on windows (ApplicationUserModelID). Used for notifications.
"app": {
"withGlobalTauri": true,
"security": {
"csp": null
}
},
"bundle": {
"active": true,
"category": "Utility",
"copyright": "Safing Limited Inc",
"linux": {
"deb": {
"depends": [
"libayatana-appindicator3"
],
"desktopTemplate": "../../../packaging/linux/portmaster.desktop",
"files": {
"/usr/lib/systemd/system/portmaster.service": "../../../packaging/linux/portmaster.service",
"/etc/xdg/autostart/portmaster.desktop": "../../../packaging/linux/portmaster-autostart.desktop"
},
"postInstallScript": "../../../packaging/linux/postinst",
"postRemoveScript": "../../../packaging/linux/postrm"
},
"rpm": {
"depends": [
"libayatana-appindicator-gtk3"
],
"desktopTemplate": "../../../packaging/linux/portmaster.desktop",
"release": "1",
"files": {
"/usr/lib/systemd/system/portmaster.service": "../../../packaging/linux/portmaster.service",
"/etc/xdg/autostart/portmaster.desktop": "../../../packaging/linux/portmaster-autostart.desktop"
},
"postInstallScript": "../../../packaging/linux/postinst",
"postRemoveScript": "../../../packaging/linux/postrm"
}
},
"windows": {
"nsis": {
"installMode": "perMachine",
"installerHooks": "templates/nsis_install_hooks.nsh",
"installerIcon": "../../../assets/data/icons/pm_light.ico"
},
"wix": {
"fragmentPaths": [
"templates/service.wxs"
],
"template": "templates/main.wxs"
}
},
"externalBin": [
"binaries/portmaster-start",
"binaries/portmaster-core"
],
"targets": [
"deb",
"rpm",
"nsis",
"msi"
],
"icon": [
"../../../assets/data/icons/pm_dark_512.png",
"../../../assets/data/icons/pm_dark.ico",
"../../../assets/data/icons/pm_light_512.png",
"../../../assets/data/icons/pm_light.ico"
]
}
}

View file

@ -0,0 +1,354 @@
<?if $(sys.BUILDARCH)="x86"?>
<?define Win64 = "no" ?>
<?define PlatformProgramFilesFolder = "ProgramFilesFolder" ?>
<?elseif $(sys.BUILDARCH)="x64"?>
<?define Win64 = "yes" ?>
<?define PlatformProgramFilesFolder = "ProgramFiles64Folder" ?>
<?elseif $(sys.BUILDARCH)="arm64"?>
<?define Win64 = "yes" ?>
<?define PlatformProgramFilesFolder = "ProgramFiles64Folder" ?>
<?else?>
<?error Unsupported value of sys.BUILDARCH=$(sys.BUILDARCH)?>
<?endif?>
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">
<Product
Id="*"
Name="{{product_name}}"
UpgradeCode="{{upgrade_code}}"
Language="!(loc.TauriLanguage)"
Manufacturer="{{manufacturer}}"
Version="{{version}}">
<Package Id="*"
Keywords="Installer"
InstallerVersion="450"
Languages="0"
Compressed="yes"
InstallScope="perMachine"
SummaryCodepage="!(loc.TauriCodepage)"/>
<!-- https://docs.microsoft.com/en-us/windows/win32/msi/reinstallmode -->
<!-- reinstall all files; rewrite all registry entries; reinstall all shortcuts -->
<Property Id="REINSTALLMODE" Value="amus" />
<!-- Auto launch app after installation, useful for passive mode which usually used in updates -->
<Property Id="AUTOLAUNCHAPP" Secure="yes" />
<!-- Property to forward cli args to the launched app to not lose those of the pre-update instance -->
<Property Id="LAUNCHAPPARGS" Secure="yes" />
{{#if allow_downgrades}}
<MajorUpgrade Schedule="afterInstallInitialize" AllowDowngrades="yes" />
{{else}}
<MajorUpgrade Schedule="afterInstallInitialize" DowngradeErrorMessage="!(loc.DowngradeErrorMessage)" AllowSameVersionUpgrades="yes" />
{{/if}}
<InstallExecuteSequence>
<RemoveShortcuts>Installed AND NOT UPGRADINGPRODUCTCODE</RemoveShortcuts>
</InstallExecuteSequence>
<Media Id="1" Cabinet="app.cab" EmbedCab="yes" />
{{#if banner_path}}
<WixVariable Id="WixUIBannerBmp" Value="{{banner_path}}" />
{{/if}}
{{#if dialog_image_path}}
<WixVariable Id="WixUIDialogBmp" Value="{{dialog_image_path}}" />
{{/if}}
{{#if license}}
<WixVariable Id="WixUILicenseRtf" Value="{{license}}" />
{{/if}}
<Icon Id="ProductIcon" SourceFile="{{icon_path}}"/>
<Property Id="ARPPRODUCTICON" Value="ProductIcon" />
<Property Id="ARPNOREPAIR" Value="yes" Secure="yes" /> <!-- Remove repair -->
<SetProperty Id="ARPNOMODIFY" Value="1" After="InstallValidate" Sequence="execute"/>
{{#if homepage}}
<Property Id="ARPURLINFOABOUT" Value="{{homepage}}"/>
<Property Id="ARPHELPLINK" Value="{{homepage}}"/>
<Property Id="ARPURLUPDATEINFO" Value="{{homepage}}"/>
{{/if}}
<!-- initialize with previous InstallDir -->
<Property Id="INSTALLDIR">
<RegistrySearch Id="PrevInstallDirReg" Root="HKCU" Key="Software\\{{manufacturer}}\\{{product_name}}" Name="InstallDir" Type="raw"/>
</Property>
<!-- launch app checkbox -->
<Property Id="WIXUI_EXITDIALOGOPTIONALCHECKBOXTEXT" Value="!(loc.LaunchApp)" />
<Property Id="WIXUI_EXITDIALOGOPTIONALCHECKBOX" Value="1"/>
<CustomAction Id="LaunchApplication" Impersonate="yes" FileKey="Path" ExeCommand="[LAUNCHAPPARGS]" Return="asyncNoWait" />
<UI>
<!-- launch app checkbox -->
<Publish Dialog="ExitDialog" Control="Finish" Event="DoAction" Value="LaunchApplication">WIXUI_EXITDIALOGOPTIONALCHECKBOX = 1 and NOT Installed</Publish>
<Property Id="WIXUI_INSTALLDIR" Value="INSTALLDIR" />
{{#unless license}}
<!-- Skip license dialog -->
<Publish Dialog="WelcomeDlg"
Control="Next"
Event="NewDialog"
Value="InstallDirDlg"
Order="2">1</Publish>
<Publish Dialog="InstallDirDlg"
Control="Back"
Event="NewDialog"
Value="WelcomeDlg"
Order="2">1</Publish>
{{/unless}}
</UI>
<UIRef Id="WixUI_InstallDir" />
<Directory Id="TARGETDIR" Name="SourceDir">
<Directory Id="DesktopFolder" Name="Desktop">
<Component Id="ApplicationShortcutDesktop" Guid="*">
<Shortcut Id="ApplicationDesktopShortcut" Name="{{product_name}}" Description="Runs {{product_name}}" Target="[!Path]" WorkingDirectory="INSTALLDIR" />
<RemoveFolder Id="DesktopFolder" On="uninstall" />
<RegistryValue Root="HKCU" Key="Software\\{{manufacturer}}\\{{product_name}}" Name="Desktop Shortcut" Type="integer" Value="1" KeyPath="yes" />
</Component>
</Directory>
<Directory Id="$(var.PlatformProgramFilesFolder)" Name="PFiles">
<Directory Id="INSTALLDIR" Name="{{product_name}}"/>
</Directory>
<Directory Id="ProgramMenuFolder">
<Directory Id="ApplicationProgramsFolder" Name="{{product_name}}"/>
</Directory>
</Directory>
<DirectoryRef Id="INSTALLDIR">
<Component Id="RegistryEntries" Guid="*">
<RegistryKey Root="HKCU" Key="Software\\{{manufacturer}}\\{{product_name}}">
<RegistryValue Name="InstallDir" Type="string" Value="[INSTALLDIR]" KeyPath="yes" />
</RegistryKey>
<!-- Change the Root to HKCU for perUser installations -->
{{#each deep_link_protocols as |protocol| ~}}
<RegistryKey Root="HKLM" Key="Software\Classes\\{{protocol}}">
<RegistryValue Type="string" Name="URL Protocol" Value=""/>
<RegistryValue Type="string" Value="URL:{{bundle_id}} protocol"/>
<RegistryKey Key="DefaultIcon">
<RegistryValue Type="string" Value="&quot;[!Path]&quot;,0" />
</RegistryKey>
<RegistryKey Key="shell\open\command">
<RegistryValue Type="string" Value="&quot;[!Path]&quot; &quot;%1&quot;" />
</RegistryKey>
</RegistryKey>
{{/each~}}
</Component>
<Component Id="Path" Guid="{{path_component_guid}}" Win64="$(var.Win64)">
<File Id="Path" Source="{{app_exe_source}}" KeyPath="yes" Checksum="yes"/>
{{#each file_associations as |association| ~}}
{{#each association.ext as |ext| ~}}
<ProgId Id="{{../../product_name}}.{{ext}}" Advertise="yes" Description="{{association.description}}">
<Extension Id="{{ext}}" Advertise="yes">
<Verb Id="open" Command="Open with {{../../product_name}}" Argument="&quot;%1&quot;" />
</Extension>
</ProgId>
{{/each~}}
{{/each~}}
</Component>
{{#each binaries as |bin| ~}}
<Component Id="{{ bin.id }}" Guid="{{bin.guid}}" Win64="$(var.Win64)">
<File Id="Bin_{{ bin.id }}" Source="{{bin.path}}" KeyPath="yes"/>
</Component>
{{/each~}}
{{#if enable_elevated_update_task}}
<Component Id="UpdateTask" Guid="C492327D-9720-4CD5-8DB8-F09082AF44BE" Win64="$(var.Win64)">
<File Id="UpdateTask" Source="update.xml" KeyPath="yes" Checksum="yes"/>
</Component>
<Component Id="UpdateTaskInstaller" Guid="011F25ED-9BE3-50A7-9E9B-3519ED2B9932" Win64="$(var.Win64)">
<File Id="UpdateTaskInstaller" Source="install-task.ps1" KeyPath="yes" Checksum="yes"/>
</Component>
<Component Id="UpdateTaskUninstaller" Guid="D4F6CC3F-32DC-5FD0-95E8-782FFD7BBCE1" Win64="$(var.Win64)">
<File Id="UpdateTaskUninstaller" Source="uninstall-task.ps1" KeyPath="yes" Checksum="yes"/>
</Component>
{{/if}}
{{resources}}
<Component Id="CMP_UninstallShortcut" Guid="*">
<Shortcut Id="UninstallShortcut"
Name="Uninstall {{product_name}}"
Description="Uninstalls {{product_name}}"
Target="[System64Folder]msiexec.exe"
Arguments="/x [ProductCode]" />
<RemoveFolder Id="INSTALLDIR"
On="uninstall" />
<RegistryValue Root="HKCU"
Key="Software\\{{manufacturer}}\\{{product_name}}"
Name="Uninstaller Shortcut"
Type="integer"
Value="1"
KeyPath="yes" />
</Component>
</DirectoryRef>
<DirectoryRef Id="ApplicationProgramsFolder">
<Component Id="ApplicationShortcut" Guid="*">
<Shortcut Id="ApplicationStartMenuShortcut"
Name="{{product_name}}"
Description="Runs {{product_name}}"
Target="[!Path]"
Icon="ProductIcon"
WorkingDirectory="INSTALLDIR">
<ShortcutProperty Key="System.AppUserModel.ID" Value="{{bundle_id}}"/>
</Shortcut>
<RemoveFolder Id="ApplicationProgramsFolder" On="uninstall"/>
<RegistryValue Root="HKCU" Key="Software\\{{manufacturer}}\\{{product_name}}" Name="Start Menu Shortcut" Type="integer" Value="1" KeyPath="yes"/>
</Component>
</DirectoryRef>
{{#each merge_modules as |msm| ~}}
<DirectoryRef Id="TARGETDIR">
<Merge Id="{{ msm.name }}" SourceFile="{{ msm.path }}" DiskId="1" Language="!(loc.TauriLanguage)" />
</DirectoryRef>
<Feature Id="{{ msm.name }}" Title="{{ msm.name }}" AllowAdvertise="no" Display="hidden" Level="1">
<MergeRef Id="{{ msm.name }}"/>
</Feature>
{{/each~}}
<Feature
Id="MainProgram"
Title="Application"
Description="!(loc.InstallAppFeature)"
Level="1"
ConfigurableDirectory="INSTALLDIR"
AllowAdvertise="no"
Display="expand"
Absent="disallow">
<ComponentRef Id="RegistryEntries"/>
{{#each resource_file_ids as |resource_file_id| ~}}
<ComponentRef Id="{{ resource_file_id }}"/>
{{/each~}}
{{#if enable_elevated_update_task}}
<ComponentRef Id="UpdateTask" />
<ComponentRef Id="UpdateTaskInstaller" />
<ComponentRef Id="UpdateTaskUninstaller" />
{{/if}}
<Feature Id="ShortcutsFeature"
Title="Shortcuts"
Level="1">
<ComponentRef Id="Path"/>
<ComponentRef Id="CMP_UninstallShortcut" />
<ComponentRef Id="ApplicationShortcut" />
<ComponentRef Id="ApplicationShortcutDesktop" />
</Feature>
<Feature
Id="Environment"
Title="PATH Environment Variable"
Description="!(loc.PathEnvVarFeature)"
Level="1"
Absent="allow">
<ComponentRef Id="Path"/>
{{#each binaries as |bin| ~}}
<ComponentRef Id="{{ bin.id }}"/>
{{/each~}}
</Feature>
</Feature>
<Feature Id="External" AllowAdvertise="no" Absent="disallow">
{{#each component_group_refs as |id| ~}}
<ComponentGroupRef Id="{{ id }}"/>
{{/each~}}
{{#each component_refs as |id| ~}}
<ComponentRef Id="{{ id }}"/>
{{/each~}}
{{#each feature_group_refs as |id| ~}}
<FeatureGroupRef Id="{{ id }}"/>
{{/each~}}
{{#each feature_refs as |id| ~}}
<FeatureRef Id="{{ id }}"/>
{{/each~}}
{{#each merge_refs as |id| ~}}
<MergeRef Id="{{ id }}"/>
{{/each~}}
</Feature>
{{#if install_webview}}
<!-- WebView2 -->
<Property Id="WVRTINSTALLED">
<RegistrySearch Id="WVRTInstalledSystem" Root="HKLM" Key="SOFTWARE\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}" Name="pv" Type="raw" Win64="no" />
<RegistrySearch Id="WVRTInstalledUser" Root="HKCU" Key="SOFTWARE\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}" Name="pv" Type="raw"/>
</Property>
{{#if download_bootstrapper}}
<CustomAction Id='DownloadAndInvokeBootstrapper' Directory="INSTALLDIR" Execute="deferred" ExeCommand='powershell.exe -NoProfile -windowstyle hidden try [\{] [\[]Net.ServicePointManager[\]]::SecurityProtocol = [\[]Net.SecurityProtocolType[\]]::Tls12 [\}] catch [\{][\}]; Invoke-WebRequest -Uri "https://go.microsoft.com/fwlink/p/?LinkId=2124703" -OutFile "$env:TEMP\MicrosoftEdgeWebview2Setup.exe" ; Start-Process -FilePath "$env:TEMP\MicrosoftEdgeWebview2Setup.exe" -ArgumentList ({{webview_installer_args}} &apos;/install&apos;) -Wait' Return='check'/>
<InstallExecuteSequence>
<Custom Action='DownloadAndInvokeBootstrapper' Before='InstallFinalize'>
<![CDATA[NOT(REMOVE OR WVRTINSTALLED)]]>
</Custom>
</InstallExecuteSequence>
{{/if}}
<!-- Embedded webview bootstrapper mode -->
{{#if webview2_bootstrapper_path}}
<Binary Id="MicrosoftEdgeWebview2Setup.exe" SourceFile="{{webview2_bootstrapper_path}}"/>
<CustomAction Id='InvokeBootstrapper' BinaryKey='MicrosoftEdgeWebview2Setup.exe' Execute="deferred" ExeCommand='{{webview_installer_args}} /install' Return='check' />
<InstallExecuteSequence>
<Custom Action='InvokeBootstrapper' Before='InstallFinalize'>
<![CDATA[NOT(REMOVE OR WVRTINSTALLED)]]>
</Custom>
</InstallExecuteSequence>
{{/if}}
<!-- Embedded offline installer -->
{{#if webview2_installer_path}}
<Binary Id="MicrosoftEdgeWebView2RuntimeInstaller.exe" SourceFile="{{webview2_installer_path}}"/>
<CustomAction Id='InvokeStandalone' BinaryKey='MicrosoftEdgeWebView2RuntimeInstaller.exe' Execute="deferred" ExeCommand='{{webview_installer_args}} /install' Return='check' />
<InstallExecuteSequence>
<Custom Action='InvokeStandalone' Before='InstallFinalize'>
<![CDATA[NOT(REMOVE OR WVRTINSTALLED)]]>
</Custom>
</InstallExecuteSequence>
{{/if}}
{{/if}}
{{#if enable_elevated_update_task}}
<!-- Install an elevated update task within Windows Task Scheduler -->
<CustomAction
Id="CreateUpdateTask"
Return="check"
Directory="INSTALLDIR"
Execute="commit"
Impersonate="yes"
ExeCommand="powershell.exe -WindowStyle hidden .\install-task.ps1" />
<InstallExecuteSequence>
<Custom Action='CreateUpdateTask' Before='InstallFinalize'>
NOT(REMOVE)
</Custom>
</InstallExecuteSequence>
<!-- Remove elevated update task during uninstall -->
<CustomAction
Id="DeleteUpdateTask"
Return="check"
Directory="INSTALLDIR"
ExeCommand="powershell.exe -WindowStyle hidden .\uninstall-task.ps1" />
<InstallExecuteSequence>
<Custom Action="DeleteUpdateTask" Before='InstallFinalize'>
(REMOVE = "ALL") AND NOT UPGRADINGPRODUCTCODE
</Custom>
</InstallExecuteSequence>
{{/if}}
<CustomActionRef Id='InstallPortmasterService' />
<CustomActionRef Id='StopPortmasterService' />
<CustomActionRef Id='DeletePortmasterService' />
<InstallExecuteSequence>
<Custom Action="LaunchApplication" After="InstallFinalize">AUTOLAUNCHAPP AND NOT Installed</Custom>
</InstallExecuteSequence>
<SetProperty Id="ARPINSTALLLOCATION" Value="[INSTALLDIR]" After="CostFinalize"/>
</Product>
</Wix>

View file

@ -0,0 +1,350 @@
<?if $(sys.BUILDARCH)="x86"?>
<?define Win64 = "no" ?>
<?define PlatformProgramFilesFolder = "ProgramFilesFolder" ?>
<?elseif $(sys.BUILDARCH)="x64"?>
<?define Win64 = "yes" ?>
<?define PlatformProgramFilesFolder = "ProgramFiles64Folder" ?>
<?elseif $(sys.BUILDARCH)="arm64"?>
<?define Win64 = "yes" ?>
<?define PlatformProgramFilesFolder = "ProgramFiles64Folder" ?>
<?else?>
<?error Unsupported value of sys.BUILDARCH=$(sys.BUILDARCH)?>
<?endif?>
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">
<Product
Id="*"
Name="{{product_name}}"
UpgradeCode="{{upgrade_code}}"
Language="!(loc.TauriLanguage)"
Manufacturer="{{manufacturer}}"
Version="{{version}}">
<Package Id="*"
Keywords="Installer"
InstallerVersion="450"
Languages="0"
Compressed="yes"
InstallScope="perMachine"
SummaryCodepage="!(loc.TauriCodepage)"/>
<!-- https://docs.microsoft.com/en-us/windows/win32/msi/reinstallmode -->
<!-- reinstall all files; rewrite all registry entries; reinstall all shortcuts -->
<Property Id="REINSTALLMODE" Value="amus" />
<!-- Auto launch app after installation, useful for passive mode which usually used in updates -->
<Property Id="AUTOLAUNCHAPP" Secure="yes" />
<!-- Property to forward cli args to the launched app to not lose those of the pre-update instance -->
<Property Id="LAUNCHAPPARGS" Secure="yes" />
{{#if allow_downgrades}}
<MajorUpgrade Schedule="afterInstallInitialize" AllowDowngrades="yes" />
{{else}}
<MajorUpgrade Schedule="afterInstallInitialize" DowngradeErrorMessage="!(loc.DowngradeErrorMessage)" AllowSameVersionUpgrades="yes" />
{{/if}}
<InstallExecuteSequence>
<RemoveShortcuts>Installed AND NOT UPGRADINGPRODUCTCODE</RemoveShortcuts>
</InstallExecuteSequence>
<Media Id="1" Cabinet="app.cab" EmbedCab="yes" />
{{#if banner_path}}
<WixVariable Id="WixUIBannerBmp" Value="{{banner_path}}" />
{{/if}}
{{#if dialog_image_path}}
<WixVariable Id="WixUIDialogBmp" Value="{{dialog_image_path}}" />
{{/if}}
{{#if license}}
<WixVariable Id="WixUILicenseRtf" Value="{{license}}" />
{{/if}}
<Icon Id="ProductIcon" SourceFile="{{icon_path}}"/>
<Property Id="ARPPRODUCTICON" Value="ProductIcon" />
<Property Id="ARPNOREPAIR" Value="yes" Secure="yes" /> <!-- Remove repair -->
<SetProperty Id="ARPNOMODIFY" Value="1" After="InstallValidate" Sequence="execute"/>
{{#if homepage}}
<Property Id="ARPURLINFOABOUT" Value="{{homepage}}"/>
<Property Id="ARPHELPLINK" Value="{{homepage}}"/>
<Property Id="ARPURLUPDATEINFO" Value="{{homepage}}"/>
{{/if}}
<!-- initialize with previous InstallDir -->
<Property Id="INSTALLDIR">
<RegistrySearch Id="PrevInstallDirReg" Root="HKCU" Key="Software\\{{manufacturer}}\\{{product_name}}" Name="InstallDir" Type="raw"/>
</Property>
<!-- launch app checkbox -->
<Property Id="WIXUI_EXITDIALOGOPTIONALCHECKBOXTEXT" Value="!(loc.LaunchApp)" />
<Property Id="WIXUI_EXITDIALOGOPTIONALCHECKBOX" Value="1"/>
<CustomAction Id="LaunchApplication" Impersonate="yes" FileKey="Path" ExeCommand="[LAUNCHAPPARGS]" Return="asyncNoWait" />
<UI>
<!-- launch app checkbox -->
<Publish Dialog="ExitDialog" Control="Finish" Event="DoAction" Value="LaunchApplication">WIXUI_EXITDIALOGOPTIONALCHECKBOX = 1 and NOT Installed</Publish>
<Property Id="WIXUI_INSTALLDIR" Value="INSTALLDIR" />
{{#unless license}}
<!-- Skip license dialog -->
<Publish Dialog="WelcomeDlg"
Control="Next"
Event="NewDialog"
Value="InstallDirDlg"
Order="2">1</Publish>
<Publish Dialog="InstallDirDlg"
Control="Back"
Event="NewDialog"
Value="WelcomeDlg"
Order="2">1</Publish>
{{/unless}}
</UI>
<UIRef Id="WixUI_InstallDir" />
<Directory Id="TARGETDIR" Name="SourceDir">
<Directory Id="DesktopFolder" Name="Desktop">
<Component Id="ApplicationShortcutDesktop" Guid="*">
<Shortcut Id="ApplicationDesktopShortcut" Name="{{product_name}}" Description="Runs {{product_name}}" Target="[!Path]" WorkingDirectory="INSTALLDIR" />
<RemoveFolder Id="DesktopFolder" On="uninstall" />
<RegistryValue Root="HKCU" Key="Software\\{{manufacturer}}\\{{product_name}}" Name="Desktop Shortcut" Type="integer" Value="1" KeyPath="yes" />
</Component>
</Directory>
<Directory Id="$(var.PlatformProgramFilesFolder)" Name="PFiles">
<Directory Id="INSTALLDIR" Name="{{product_name}}"/>
</Directory>
<Directory Id="ProgramMenuFolder">
<Directory Id="ApplicationProgramsFolder" Name="{{product_name}}"/>
</Directory>
</Directory>
<DirectoryRef Id="INSTALLDIR">
<Component Id="RegistryEntries" Guid="*">
<RegistryKey Root="HKCU" Key="Software\\{{manufacturer}}\\{{product_name}}">
<RegistryValue Name="InstallDir" Type="string" Value="[INSTALLDIR]" KeyPath="yes" />
</RegistryKey>
<!-- Change the Root to HKCU for perUser installations -->
{{#each deep_link_protocols as |protocol| ~}}
<RegistryKey Root="HKLM" Key="Software\Classes\\{{protocol}}">
<RegistryValue Type="string" Name="URL Protocol" Value=""/>
<RegistryValue Type="string" Value="URL:{{bundle_id}} protocol"/>
<RegistryKey Key="DefaultIcon">
<RegistryValue Type="string" Value="&quot;[!Path]&quot;,0" />
</RegistryKey>
<RegistryKey Key="shell\open\command">
<RegistryValue Type="string" Value="&quot;[!Path]&quot; &quot;%1&quot;" />
</RegistryKey>
</RegistryKey>
{{/each~}}
</Component>
<Component Id="Path" Guid="{{path_component_guid}}" Win64="$(var.Win64)">
<File Id="Path" Source="{{app_exe_source}}" KeyPath="yes" Checksum="yes"/>
{{#each file_associations as |association| ~}}
{{#each association.ext as |ext| ~}}
<ProgId Id="{{../../product_name}}.{{ext}}" Advertise="yes" Description="{{association.description}}">
<Extension Id="{{ext}}" Advertise="yes">
<Verb Id="open" Command="Open with {{../../product_name}}" Argument="&quot;%1&quot;" />
</Extension>
</ProgId>
{{/each~}}
{{/each~}}
</Component>
{{#each binaries as |bin| ~}}
<Component Id="{{ bin.id }}" Guid="{{bin.guid}}" Win64="$(var.Win64)">
<File Id="Bin_{{ bin.id }}" Source="{{bin.path}}" KeyPath="yes"/>
</Component>
{{/each~}}
{{#if enable_elevated_update_task}}
<Component Id="UpdateTask" Guid="C492327D-9720-4CD5-8DB8-F09082AF44BE" Win64="$(var.Win64)">
<File Id="UpdateTask" Source="update.xml" KeyPath="yes" Checksum="yes"/>
</Component>
<Component Id="UpdateTaskInstaller" Guid="011F25ED-9BE3-50A7-9E9B-3519ED2B9932" Win64="$(var.Win64)">
<File Id="UpdateTaskInstaller" Source="install-task.ps1" KeyPath="yes" Checksum="yes"/>
</Component>
<Component Id="UpdateTaskUninstaller" Guid="D4F6CC3F-32DC-5FD0-95E8-782FFD7BBCE1" Win64="$(var.Win64)">
<File Id="UpdateTaskUninstaller" Source="uninstall-task.ps1" KeyPath="yes" Checksum="yes"/>
</Component>
{{/if}}
{{resources}}
<Component Id="CMP_UninstallShortcut" Guid="*">
<Shortcut Id="UninstallShortcut"
Name="Uninstall {{product_name}}"
Description="Uninstalls {{product_name}}"
Target="[System64Folder]msiexec.exe"
Arguments="/x [ProductCode]" />
<RemoveFolder Id="INSTALLDIR"
On="uninstall" />
<RegistryValue Root="HKCU"
Key="Software\\{{manufacturer}}\\{{product_name}}"
Name="Uninstaller Shortcut"
Type="integer"
Value="1"
KeyPath="yes" />
</Component>
</DirectoryRef>
<DirectoryRef Id="ApplicationProgramsFolder">
<Component Id="ApplicationShortcut" Guid="*">
<Shortcut Id="ApplicationStartMenuShortcut"
Name="{{product_name}}"
Description="Runs {{product_name}}"
Target="[!Path]"
Icon="ProductIcon"
WorkingDirectory="INSTALLDIR">
<ShortcutProperty Key="System.AppUserModel.ID" Value="{{bundle_id}}"/>
</Shortcut>
<RemoveFolder Id="ApplicationProgramsFolder" On="uninstall"/>
<RegistryValue Root="HKCU" Key="Software\\{{manufacturer}}\\{{product_name}}" Name="Start Menu Shortcut" Type="integer" Value="1" KeyPath="yes"/>
</Component>
</DirectoryRef>
{{#each merge_modules as |msm| ~}}
<DirectoryRef Id="TARGETDIR">
<Merge Id="{{ msm.name }}" SourceFile="{{ msm.path }}" DiskId="1" Language="!(loc.TauriLanguage)" />
</DirectoryRef>
<Feature Id="{{ msm.name }}" Title="{{ msm.name }}" AllowAdvertise="no" Display="hidden" Level="1">
<MergeRef Id="{{ msm.name }}"/>
</Feature>
{{/each~}}
<Feature
Id="MainProgram"
Title="Application"
Description="!(loc.InstallAppFeature)"
Level="1"
ConfigurableDirectory="INSTALLDIR"
AllowAdvertise="no"
Display="expand"
Absent="disallow">
<ComponentRef Id="RegistryEntries"/>
{{#each resource_file_ids as |resource_file_id| ~}}
<ComponentRef Id="{{ resource_file_id }}"/>
{{/each~}}
{{#if enable_elevated_update_task}}
<ComponentRef Id="UpdateTask" />
<ComponentRef Id="UpdateTaskInstaller" />
<ComponentRef Id="UpdateTaskUninstaller" />
{{/if}}
<Feature Id="ShortcutsFeature"
Title="Shortcuts"
Level="1">
<ComponentRef Id="Path"/>
<ComponentRef Id="CMP_UninstallShortcut" />
<ComponentRef Id="ApplicationShortcut" />
<ComponentRef Id="ApplicationShortcutDesktop" />
</Feature>
<Feature
Id="Environment"
Title="PATH Environment Variable"
Description="!(loc.PathEnvVarFeature)"
Level="1"
Absent="allow">
<ComponentRef Id="Path"/>
{{#each binaries as |bin| ~}}
<ComponentRef Id="{{ bin.id }}"/>
{{/each~}}
</Feature>
</Feature>
<Feature Id="External" AllowAdvertise="no" Absent="disallow">
{{#each component_group_refs as |id| ~}}
<ComponentGroupRef Id="{{ id }}"/>
{{/each~}}
{{#each component_refs as |id| ~}}
<ComponentRef Id="{{ id }}"/>
{{/each~}}
{{#each feature_group_refs as |id| ~}}
<FeatureGroupRef Id="{{ id }}"/>
{{/each~}}
{{#each feature_refs as |id| ~}}
<FeatureRef Id="{{ id }}"/>
{{/each~}}
{{#each merge_refs as |id| ~}}
<MergeRef Id="{{ id }}"/>
{{/each~}}
</Feature>
{{#if install_webview}}
<!-- WebView2 -->
<Property Id="WVRTINSTALLED">
<RegistrySearch Id="WVRTInstalledSystem" Root="HKLM" Key="SOFTWARE\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}" Name="pv" Type="raw" Win64="no" />
<RegistrySearch Id="WVRTInstalledUser" Root="HKCU" Key="SOFTWARE\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}" Name="pv" Type="raw"/>
</Property>
{{#if download_bootstrapper}}
<CustomAction Id='DownloadAndInvokeBootstrapper' Directory="INSTALLDIR" Execute="deferred" ExeCommand='powershell.exe -NoProfile -windowstyle hidden try [\{] [\[]Net.ServicePointManager[\]]::SecurityProtocol = [\[]Net.SecurityProtocolType[\]]::Tls12 [\}] catch [\{][\}]; Invoke-WebRequest -Uri "https://go.microsoft.com/fwlink/p/?LinkId=2124703" -OutFile "$env:TEMP\MicrosoftEdgeWebview2Setup.exe" ; Start-Process -FilePath "$env:TEMP\MicrosoftEdgeWebview2Setup.exe" -ArgumentList ({{webview_installer_args}} &apos;/install&apos;) -Wait' Return='check'/>
<InstallExecuteSequence>
<Custom Action='DownloadAndInvokeBootstrapper' Before='InstallFinalize'>
<![CDATA[NOT(REMOVE OR WVRTINSTALLED)]]>
</Custom>
</InstallExecuteSequence>
{{/if}}
<!-- Embedded webview bootstrapper mode -->
{{#if webview2_bootstrapper_path}}
<Binary Id="MicrosoftEdgeWebview2Setup.exe" SourceFile="{{webview2_bootstrapper_path}}"/>
<CustomAction Id='InvokeBootstrapper' BinaryKey='MicrosoftEdgeWebview2Setup.exe' Execute="deferred" ExeCommand='{{webview_installer_args}} /install' Return='check' />
<InstallExecuteSequence>
<Custom Action='InvokeBootstrapper' Before='InstallFinalize'>
<![CDATA[NOT(REMOVE OR WVRTINSTALLED)]]>
</Custom>
</InstallExecuteSequence>
{{/if}}
<!-- Embedded offline installer -->
{{#if webview2_installer_path}}
<Binary Id="MicrosoftEdgeWebView2RuntimeInstaller.exe" SourceFile="{{webview2_installer_path}}"/>
<CustomAction Id='InvokeStandalone' BinaryKey='MicrosoftEdgeWebView2RuntimeInstaller.exe' Execute="deferred" ExeCommand='{{webview_installer_args}} /install' Return='check' />
<InstallExecuteSequence>
<Custom Action='InvokeStandalone' Before='InstallFinalize'>
<![CDATA[NOT(REMOVE OR WVRTINSTALLED)]]>
</Custom>
</InstallExecuteSequence>
{{/if}}
{{/if}}
{{#if enable_elevated_update_task}}
<!-- Install an elevated update task within Windows Task Scheduler -->
<CustomAction
Id="CreateUpdateTask"
Return="check"
Directory="INSTALLDIR"
Execute="commit"
Impersonate="yes"
ExeCommand="powershell.exe -WindowStyle hidden .\install-task.ps1" />
<InstallExecuteSequence>
<Custom Action='CreateUpdateTask' Before='InstallFinalize'>
NOT(REMOVE)
</Custom>
</InstallExecuteSequence>
<!-- Remove elevated update task during uninstall -->
<CustomAction
Id="DeleteUpdateTask"
Return="check"
Directory="INSTALLDIR"
ExeCommand="powershell.exe -WindowStyle hidden .\uninstall-task.ps1" />
<InstallExecuteSequence>
<Custom Action="DeleteUpdateTask" Before='InstallFinalize'>
(REMOVE = "ALL") AND NOT UPGRADINGPRODUCTCODE
</Custom>
</InstallExecuteSequence>
{{/if}}
<InstallExecuteSequence>
<Custom Action="LaunchApplication" After="InstallFinalize">AUTOLAUNCHAPP AND NOT Installed</Custom>
</InstallExecuteSequence>
<SetProperty Id="ARPINSTALLLOCATION" Value="[INSTALLDIR]" After="CostFinalize"/>
</Product>
</Wix>

View file

@ -0,0 +1,14 @@
!define NSIS_HOOK_POSTINSTALL "NSIS_HOOK_POSTINSTALL_"
!macro NSIS_HOOK_POSTINSTALL_
ExecWait '"$INSTDIR\portmaster-start.exe" install core-service --data="$INSTDIR\data"'
!macroend
!define NSIS_HOOK_PREUNINSTALL "NSIS_HOOK_PREUNINSTALL_"
!macro NSIS_HOOK_PREUNINSTALL_
ExecWait 'sc.exe stop PortmasterCore'
ExecWait 'sc.exe delete PortmasterCore'
!macroend

View file

@ -0,0 +1,37 @@
<?xml version="1.0" encoding="UTF-8"?>
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">
<Fragment>
<CustomAction Id="InstallPortmasterService"
Directory="INSTALLDIR"
ExeCommand="&quot;[INSTALLDIR]portmaster-start.exe&quot; install core-service --data=&quot;[INSTALLDIR]data&quot;"
Execute="commit"
Return="check"
Impersonate="no"
/>
<CustomAction Id="StopPortmasterService"
Directory="INSTALLDIR"
ExeCommand="sc.exe stop PortmasterCore"
Execute="commit"
Return="ignore"
Impersonate="no"
/>
<CustomAction Id="DeletePortmasterService"
Directory="INSTALLDIR"
ExeCommand="sc.exe delete PortmasterCore"
Execute="commit"
Return="ignore"
Impersonate="no"
/>
<InstallExecuteSequence>
<Custom Action="InstallPortmasterService" Before='InstallFinalize'>
<![CDATA[NOT Installed]]>
</Custom>
<Custom Action="StopPortmasterService" Before='InstallFinalize'>
REMOVE
</Custom>
<Custom Action="DeletePortmasterService" Before='InstallFinalize'>
REMOVE
</Custom>
</InstallExecuteSequence>
</Fragment>
</Wix>

View file

@ -33,10 +33,9 @@ PrivateDevices=yes
AmbientCapabilities=cap_chown cap_kill cap_net_admin cap_net_bind_service cap_net_broadcast cap_net_raw cap_sys_module cap_sys_ptrace cap_dac_override cap_fowner cap_fsetid
CapabilityBoundingSet=cap_chown cap_kill cap_net_admin cap_net_bind_service cap_net_broadcast cap_net_raw cap_sys_module cap_sys_ptrace cap_dac_override cap_fowner cap_fsetid
StateDirectory=portmaster
ExecStartPre=-/usr/bin/portmaster-start --data $STATE_DIRECTORY clean-structure
# TODO(ppacher): add --disable-software-updates once it's merged and the release process changed.
ExecStart=/usr/bin/portmaster-core --data $STATE_DIRECTORY $PORTMASTER_ARGS
ExecStartPost=-/usr/bin/portmaster-start recover-iptables
ExecStart=/usr/bin/portmaster-start --data /opt/safing/portmaster core -- $PORTMASTER_ARGS
ExecStopPost=-/usr/bin/portmaster-start recover-iptables
[Install]
WantedBy=multi-user.target

View file

@ -57,6 +57,7 @@ func MandatoryUpdates() (identifiers []string) {
PlatformIdentifier("start/portmaster-start.exe"),
PlatformIdentifier("notifier/portmaster-notifier.exe"),
PlatformIdentifier("notifier/portmaster-wintoast.dll"),
PlatformIdentifier("app2/portmaster-app.zip"),
)
} else {
identifiers = append(
@ -64,6 +65,7 @@ func MandatoryUpdates() (identifiers []string) {
PlatformIdentifier("core/portmaster-core"),
PlatformIdentifier("start/portmaster-start"),
PlatformIdentifier("notifier/portmaster-notifier"),
PlatformIdentifier("app2/portmaster-app"),
)
}
@ -88,5 +90,6 @@ func AutoUnpackUpdates() []string {
return []string{
PlatformIdentifier("app/portmaster-app.zip"),
PlatformIdentifier("app2/portmaster-app.zip"),
}
}