chore(rust/gui-client): migrate to Tauri v2 (#6996)

Closes #4883 

Refs #7005 

Adds support for Ubuntu 24.04, drops support for Ubuntu 20.04

Known issues:
- On Ubuntu 22.04, sometimes GNOME shows the wrong tray icon
- On Ubuntu 24.04, the first time you open the tray menu, GNOME takes a
long time to open the menu.

---------

Signed-off-by: Reactor Scram <ReactorScram@users.noreply.github.com>
This commit is contained in:
Reactor Scram
2024-10-24 11:31:28 -05:00
committed by GitHub
parent 5f91259d31
commit 4fe4001760
37 changed files with 2284 additions and 1361 deletions

View File

@@ -15,7 +15,7 @@ runs:
shell: bash
- name: Install Tauri build deps
if: ${{ runner.os == 'Linux' }}
run: sudo apt-get --yes install build-essential libwebkit2gtk-4.0-dev libssl-dev libgtk-3-dev libayatana-appindicator3-dev librsvg2-dev
run: sudo apt-get --yes install build-essential curl file libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev libssl-dev libxdo-dev wget
shell: bash
- name: Install gnome-keyring
if: ${{ runner.os == 'Linux' }}
@@ -49,3 +49,7 @@ runs:
# Currently the MSI does this and it's a little janky.
run: Start-Process WebView2Installer.exe -ArgumentList "/install" -Wait
shell: pwsh
# Otherwise one of the Tauri macros panics in static analysis
- name: Create `rust/gui-client/dist`
run: mkdir "$GITHUB_WORKSPACE/rust/gui-client/dist"
shell: bash

2
.github/codespellrc vendored
View File

@@ -1,3 +1,3 @@
[codespell]
skip = ./**/*.svg,./elixir/deps,./**/*.min.js,./kotlin/android/app/build,./kotlin/android/build,./e2e/pnpm-lock.yaml,./website/.next,./website/pnpm-lock.yaml,./rust/connlib/tunnel/testcases,./rust/target,Cargo.lock,./website/docs/reference/api/*.mdx,./**/erl_crash.dump,./cover,./vendor,*.json,seeds.exs,./**/node_modules,./deps,./priv/static,./priv/plts,./**/priv/static,./.git,./_build,*.cast,./**/proptest-regressions
skip = ./**/*.svg,./elixir/deps,./**/*.min.js,./kotlin/android/app/build,./kotlin/android/build,./e2e/pnpm-lock.yaml,./website/.next,./website/pnpm-lock.yaml,./rust/connlib/tunnel/testcases,./rust/gui-client/dist,./rust/target,Cargo.lock,./website/docs/reference/api/*.mdx,./**/erl_crash.dump,./cover,./vendor,*.json,seeds.exs,./**/node_modules,./deps,./priv/static,./priv/plts,./**/priv/static,./.git,./_build,*.cast,./**/proptest-regressions
ignore-words-list = optin,crate,keypair,keypairs,iif,statics,wee,anull,commitish,inout,fo,superceded

View File

@@ -52,7 +52,7 @@ jobs:
- uses: actions/checkout@v4
- uses: ./.github/actions/setup-rust
id: setup-rust
- uses: ./.github/actions/setup-tauri
- uses: ./.github/actions/setup-tauri-v2
timeout-minutes: 5
- uses: taiki-e/install-action@cargo-udeps
env:
@@ -76,8 +76,8 @@ jobs:
matrix:
# TODO: https://github.com/rust-lang/cargo/issues/5220
runs-on: [
ubuntu-20.04,
ubuntu-22.04,
ubuntu-24.04,
macos-12,
macos-13,
macos-14,
@@ -89,7 +89,7 @@ jobs:
- uses: actions/checkout@v4
- uses: ./.github/actions/setup-rust
id: setup-rust
- uses: ./.github/actions/setup-tauri
- uses: ./.github/actions/setup-tauri-v2
- uses: taiki-e/install-action@v2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -132,8 +132,8 @@ jobs:
fail-fast: false
matrix:
runs-on: [
ubuntu-20.04,
ubuntu-22.04,
ubuntu-24.04,
windows-2019,
windows-2022
]
@@ -147,7 +147,7 @@ jobs:
- uses: actions/checkout@v4
- uses: ./.github/actions/setup-node
- uses: ./.github/actions/setup-rust
- uses: ./.github/actions/setup-tauri
- uses: ./.github/actions/setup-tauri-v2
timeout-minutes: 5
with:
runtime: true
@@ -176,13 +176,13 @@ jobs:
fail-fast: false
matrix:
# TODO: Add Windows as part of issue #3782
runs-on: [ubuntu-20.04, ubuntu-22.04]
runs-on: [ubuntu-22.04, ubuntu-24.04]
test: [linux-group, token-path]
runs-on: ${{ matrix.runs-on }}
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/setup-rust
- uses: ./.github/actions/setup-tauri
- uses: ./.github/actions/setup-tauri-v2
timeout-minutes: 5
- run: scripts/tests/${{ matrix.test }}.sh
name: "test script"

View File

@@ -29,59 +29,46 @@ jobs:
fail-fast: false
matrix:
include:
- runs-on: ubuntu-20.04
# mark:next-gui-version
binary-dest-path: firezone-client-gui-linux_1.3.10_x86_64
rename-script: ../../scripts/build/tauri-rename-ubuntu.sh
upload-script: ../../scripts/build/tauri-upload-ubuntu.sh
# mark:next-gui-version
syms-artifact: rust/gui-client/firezone-client-gui-linux_1.3.10_x86_64.dwp
# mark:next-gui-version
pkg-artifact: rust/gui-client/firezone-client-gui-linux_1.3.10_x86_64.deb
- runs-on: ubuntu-22.04
arch: x86_64
os: linux
pkg-extension: deb
syms-extension: dwp
- runs-on: ubuntu-22.04-arm
# mark:next-gui-version
binary-dest-path: firezone-client-gui-linux_1.3.10_aarch64
rename-script: ../../scripts/build/tauri-rename-ubuntu.sh
upload-script: ../../scripts/build/tauri-upload-ubuntu.sh
# mark:next-gui-version
syms-artifact: rust/gui-client/firezone-client-gui-linux_1.3.10_aarch64.dwp
# mark:next-gui-version
pkg-artifact: rust/gui-client/firezone-client-gui-linux_1.3.10_aarch64.deb
arch: aarch64
os: linux
pkg-extension: deb
syms-extension: dwp
- runs-on: windows-2019
# mark:next-gui-version
binary-dest-path: firezone-client-gui-windows_1.3.10_x86_64
rename-script: ../../scripts/build/tauri-rename-windows.sh
upload-script: ../../scripts/build/tauri-upload-windows.sh
# mark:next-gui-version
syms-artifact: rust/gui-client/firezone-client-gui-windows_1.3.10_x86_64.pdb
# mark:next-gui-version
pkg-artifact: rust/gui-client/firezone-client-gui-windows_1.3.10_x86_64.msi
arch: x86_64
os: windows
pkg-extension: msi
syms-extension: pdb
env:
BINARY_DEST_PATH: ${{ matrix.binary-dest-path }}
# mark:next-gui-version
ARTIFACT_SRC: ./rust/gui-client/firezone-client-gui-${{ matrix.os }}_1.3.10_${{ matrix.arch }}
# mark:next-gui-version
ARTIFACT_DST: firezone-client-gui-${{ matrix.os }}_1.3.10_${{ matrix.arch }}
AZURE_KEY_VAULT_URI: ${{ secrets.AZURE_KEY_VAULT_URI }}
AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }}
AZURE_CERT_NAME: ${{ secrets.AZURE_CERT_NAME }}
# mark:next-gui-version
BINARY_DEST_PATH: firezone-client-gui-${{ matrix.os }}_1.3.10_${{ matrix.arch }}
# Seems like there's no way to de-dupe env vars that depend on each other
# mark:next-gui-version
FIREZONE_GUI_VERSION: 1.3.10
RENAME_SCRIPT: ../../scripts/build/tauri-rename-${{ matrix.os }}.sh
TARGET_DIR: ../target
UPLOAD_SCRIPT: ../../scripts/build/tauri-upload-${{ matrix.os }}.sh
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/setup-node
- uses: ./.github/actions/setup-rust
- uses: ./.github/actions/setup-tauri
- uses: ./.github/actions/setup-tauri-v2
# Installing new packages can take time
timeout-minutes: 10
# the arm64 images don't have the GH cli installed.
# Remove this when https://github.com/actions/runner-images/issues/10192 is resolved.
- name: Ubuntu arm workaround
if: ${{ matrix.runs-on == 'ubuntu-22.04-arm' }}
run: |
(type -p wget >/dev/null || (sudo apt update && sudo apt-get install wget -y)) \
&& sudo mkdir -p -m 755 /etc/apt/keyrings \
&& wget -qO- https://cli.github.com/packages/githubcli-archive-keyring.gpg | sudo tee /etc/apt/keyrings/githubcli-archive-keyring.gpg > /dev/null \
&& sudo chmod go+r /etc/apt/keyrings/githubcli-archive-keyring.gpg \
&& echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | sudo tee /etc/apt/sources.list.d/github-cli.list > /dev/null \
&& sudo apt update \
&& sudo apt install gh -y
- name: Install pnpm deps
run: pnpm install
- name: Install AzureSignTool
@@ -89,7 +76,15 @@ jobs:
shell: bash
# AzureSignTool >= 5 needs .NET 8. windows-2019 runner only has .NET 7.
run: dotnet tool install --global AzureSignTool --version 4.0.1
- name: Check if swap needed
if: ${{ runner.os == 'Linux' }}
run: free -m
- name: Enable swap
if: ${{ runner.os == 'Linux' }}
run: sudo fallocate -l 8G /swapfile && sudo chmod 600 /swapfile && sudo mkswap /swapfile && sudo swapon /swapfile && free -m
- name: Build release exe and MSI / deb
env:
CARGO_PROFILE_RELEASE_LTO: thin # Fat LTO is getting too slow / RAM-hungry on Tauri builds
# Signs the exe before bundling it into the MSI
run: pnpm build
- name: Ensure unmodified Git workspace
@@ -102,32 +97,28 @@ jobs:
- name: Sign the MSI
if: ${{ runner.os == 'Windows' }}
shell: bash
# mark:next-gui-version
run: ../../scripts/build/sign.sh ../target/release/bundle/msi/Firezone_1.3.10_x64_en-US.msi
run: ../../scripts/build/sign.sh ../target/release/bundle/msi/Firezone_${{ env.FIREZONE_GUI_VERSION }}_x64_en-US.msi
- name: Rename artifacts and compute SHA256
shell: bash
run: ${{ matrix.rename-script }}
run: ${{ env.RENAME_SCRIPT }}
- name: Upload debug symbols
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.binary-dest-path }}-syms
path: |
${{ matrix.syms-artifact }}
name: ${{ env.ARTIFACT_DST }}-syms
path: ${{ env.ARTIFACT_SRC }}.${{ matrix.syms-extension }}
if-no-files-found: error
- name: Upload package
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.binary-dest-path }}-pkg
path: |
${{ matrix.pkg-artifact }}
name: ${{ env.ARTIFACT_DST }}-pkg
path: ${{ env.ARTIFACT_SRC }}.${{ matrix.pkg-extension }}
if-no-files-found: error
- name: Upload Release Assets
# Only upload the windows build to the drafted release on main builds
# Only upload the GUI Client build to the drafted release on main builds
if: ${{ github.ref_name == 'main' }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
REPOSITORY: ${{ github.repository }}
# mark:next-gui-version
TAG_NAME: gui-client-1.3.10
TAG_NAME: gui-client-${{ env.FIREZONE_GUI_VERSION }}
shell: bash
run: ${{ matrix.upload-script }}
run: ${{ env.UPLOAD_SCRIPT }}

1713
rust/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -26,3 +26,9 @@ src/output.css
# JS output from the TypeScript compiler
src/**/*.js
# Vite output
dist
# Some new generated files in Tauri v2
src-tauri/gen

View File

@@ -8,7 +8,7 @@ To compile natively for x86_64 Linux:
1. [Install rustup](https://rustup.rs/)
1. Install [pnpm](https://pnpm.io/installation)
1. `sudo apt-get install at-spi2-core gcc libwebkit2gtk-4.0-dev libssl-dev libgtk-3-dev libayatana-appindicator3-dev librsvg2-dev pkg-config xvfb`
1. `sudo apt-get install build-essential curl file libayatana-appindicator3-dev librsvg2-dev libssl-dev libwebkit2gtk-4.1-dev libxdo-dev wget`
## Setup (Windows)

View File

@@ -4,11 +4,11 @@ setlocal enabledelayedexpansion
REM Copy frontend dependencies
copy "node_modules\flowbite\dist\flowbite.min.js" "src\"
REM Compile TypeScript
call pnpm tsc
REM Compile CSS
call pnpm tailwindcss -i src\input.css -o src\output.css
REM bundle web assets
call pnpm vite build
REM Compile Rust and bundle
call pnpm tauri build

View File

@@ -8,12 +8,12 @@ BUNDLES_DIR=../target/release/bundle/deb
# Copy frontend dependencies
cp node_modules/flowbite/dist/flowbite.min.js src/
# Compile TypeScript
pnpm tsc
# Compile CSS
pnpm tailwindcss -i src/input.css -o src/output.css
# Bundle all web assets
pnpm vite build
# Get rid of any existing debs, since we need to discover the path later
rm -rf "$BUNDLES_DIR"

View File

@@ -1,4 +1,8 @@
{
"name": "firezone-gui-client",
"private": true,
"version": "0.1.0",
"type": "module",
"scripts": {
"build": "run-script-os",
"build:win32": "call build.bat",
@@ -11,15 +15,16 @@
"tauri": "tauri"
},
"dependencies": {
"@tauri-apps/api": "^1.6",
"@tauri-apps/api": ">=2.0.0",
"flowbite": "^2.5.2"
},
"devDependencies": {
"@tauri-apps/cli": "^1.6",
"@tauri-apps/cli": ">=2.0.0",
"@types/node": "22",
"http-server": "^14.1.1",
"run-script-os": "^1.1.6",
"tailwindcss": "^3.4.14",
"typescript": "^5.6.3"
"typescript": "^5.6.3",
"vite": "^5.3.1"
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -97,6 +97,7 @@ pub async fn clear_gui_logs() -> Result<()> {
/// * `stem` - A directory containing all the log files inside the zip archive, to avoid creating a ["tar bomb"](https://www.linfo.org/tarbomb.html). This comes from the automatically-generated name of the archive, even if the user changes it to e.g. `logs.zip`
pub async fn export_logs_to(path: PathBuf, stem: PathBuf) -> Result<()> {
tracing::info!("Exporting logs to {path:?}");
let start = std::time::Instant::now();
// Use a temp path so that if the export fails we don't end up with half a zip file
let temp_path = path.with_extension(".zip-partial");
@@ -113,6 +114,7 @@ pub async fn export_logs_to(path: PathBuf, stem: PathBuf) -> Result<()> {
})
.await
.context("Failed to join zip export task")??;
tracing::debug!(elapsed_s = ?start.elapsed(), "Exported logs");
Ok(())
}

View File

@@ -32,6 +32,15 @@ pub struct AppState {
pub release: Option<Release>,
}
impl Default for AppState {
fn default() -> AppState {
AppState {
connlib: ConnlibState::Loading,
release: None,
}
}
}
impl AppState {
pub fn into_menu(self) -> Menu {
let quit_text = match &self.connlib {
@@ -130,7 +139,7 @@ impl SignedIn {
}
}
#[derive(PartialEq)]
#[derive(Clone, PartialEq)]
pub struct Icon {
pub base: IconBase,
pub update_ready: bool,
@@ -144,7 +153,7 @@ pub(crate) fn icon_terminating() -> Icon {
}
}
#[derive(PartialEq)]
#[derive(Clone, PartialEq)]
pub enum IconBase {
/// Must be equivalent to the default app icon, since we assume this is set when we start
Busy,

View File

@@ -8,16 +8,14 @@ pub const INTERNET_RESOURCE_DESCRIPTION: &str = "All network traffic";
/// A menu that can either be assigned to the system tray directly or used as a submenu in another menu.
///
/// Equivalent to `tauri::SystemTrayMenu`
#[derive(Debug, Default, PartialEq, Serialize)]
/// Equivalent to `tauri::menu::Menu` or `tauri::menu::Submenu`
#[derive(Clone, Debug, Default, PartialEq, Serialize)]
pub struct Menu {
pub entries: Vec<Entry>,
}
/// Something that can be shown in a menu, including text items, separators, and submenus
///
/// Equivalent to `tauri::SystemTrayMenuEntry`
#[derive(Debug, PartialEq, Serialize)]
#[derive(Clone, Debug, PartialEq, Serialize)]
pub enum Entry {
Item(Item),
Separator,
@@ -27,7 +25,7 @@ pub enum Entry {
/// Something that shows text and may be clickable
///
/// Equivalent to `tauri::CustomMenuItem`
#[derive(Debug, PartialEq, Serialize)]
#[derive(Clone, Debug, PartialEq, Serialize)]
pub struct Item {
/// An event to send to the app when the item is clicked.
///
@@ -40,7 +38,7 @@ pub struct Item {
}
/// Events that the menu can send to the app
#[derive(Debug, Deserialize, PartialEq, Serialize)]
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
pub enum Event {
/// Marks this Resource as favorite
AddFavorite(ResourceId),
@@ -73,7 +71,7 @@ pub enum Event {
DisableInternetResource,
}
#[derive(Debug, Deserialize, PartialEq, Serialize)]
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
pub enum Window {
About,
Settings,

View File

@@ -8,14 +8,14 @@ default-run = "firezone-gui-client"
authors = ["Firezone, Inc."]
[build-dependencies]
anyhow = { version = "1.0" }
tauri-build = { version = "1.5", features = [] }
anyhow = { version = "1.0.89" }
tauri-build = { version = "2.0.1", features = [] }
[dependencies]
anyhow = { version = "1.0" }
anyhow = { version = "1.0.89" }
atomicwrites = { workspace = true }
chrono = { workspace = true }
clap = { version = "4.5", features = ["derive", "env"] }
clap = { version = "4.5.20", features = ["derive", "env"] }
connlib-client-shared = { workspace = true }
connlib-model = { workspace = true }
firezone-bin-shared = { workspace = true }
@@ -28,14 +28,16 @@ rand = "0.8.5"
rustls = { workspace = true }
sadness-generator = "0.6.0"
secrecy = { workspace = true }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
serde = { version = "1.0.210", features = ["derive"] }
serde_json = "1.0.132"
subtle = "2.5.0"
# `notification` is not needed on Windows, but if we remove it, Tauri's bundler will just add it back. #6597
tauri = { version = "1.7.1", features = ["dialog", "icon-png", "notification", "shell-open-api", "system-tray"] }
tauri-runtime = "0.14.5"
tauri-utils = "1.6.0"
thiserror = { version = "1.0", default-features = false }
tauri = { version = "2.0.3", features = ["tray-icon", "image-png"] }
tauri-plugin-dialog = "2.0.1"
tauri-plugin-notification = "2.0.1"
tauri-plugin-shell = "2.0.1"
tauri-runtime = "2.1.0"
tauri-utils = "2.0.1"
thiserror = { version = "1.0.64", default-features = false }
tokio = { workspace = true, features = ["signal", "time", "macros", "rt", "rt-multi-thread"] }
tokio-util = { version = "0.7.11", features = ["codec"] }
tracing = { workspace = true }

View File

@@ -21,7 +21,8 @@ use firezone_headless_client::LogFilterReloader;
use firezone_telemetry as telemetry;
use secrecy::{ExposeSecret as _, SecretString};
use std::{str::FromStr, time::Duration};
use tauri::{Manager, SystemTrayEvent};
use tauri::Manager;
use tauri_plugin_shell::ShellExt as _;
use tokio::sync::{mpsc, oneshot};
use tracing::instrument;
@@ -61,7 +62,7 @@ impl GuiIntegration for TauriIntegration {
fn set_welcome_window_visible(&self, visible: bool) -> Result<()> {
let win = self
.app
.get_window("welcome")
.get_webview_window("welcome")
.context("Couldn't get handle to Welcome window")?;
if visible {
@@ -73,7 +74,7 @@ impl GuiIntegration for TauriIntegration {
}
fn open_url<P: AsRef<str>>(&self, url: P) -> Result<()> {
Ok(tauri::api::shell::open(&self.app.shell_scope(), url, None)?)
Ok(self.app.shell().open(url.as_ref(), None)?)
}
fn set_tray_icon(&mut self, icon: common::system_tray::Icon) -> Result<()> {
@@ -85,11 +86,11 @@ impl GuiIntegration for TauriIntegration {
}
fn show_notification(&self, title: &str, body: &str) -> Result<()> {
os::show_notification(title, body)
os::show_notification(&self.app, title, body)
}
fn show_update_notification(&self, ctlr_tx: CtlrTx, title: &str, url: url::Url) -> Result<()> {
os::show_update_notification(ctlr_tx, title, url)
os::show_update_notification(&self.app, ctlr_tx, title, url)
}
fn show_window(&self, window: common::system_tray::Window) -> Result<()> {
@@ -100,7 +101,7 @@ impl GuiIntegration for TauriIntegration {
let win = self
.app
.get_window(id)
.get_webview_window(id)
.context("Couldn't get handle to `{id}` window")?;
// Needed to bring shown windows to the front
@@ -139,16 +140,16 @@ pub(crate) fn run(
inject_faults: cli.inject_faults,
};
let (setup_result_tx, mut setup_result_rx) = oneshot::channel::<Result<(), Error>>();
let (tray_tx, tray_rx) = oneshot::channel();
let app = tauri::Builder::default()
.manage(managed)
.on_window_event(|event| {
if let tauri::WindowEvent::CloseRequested { api, .. } = event.event() {
.on_window_event(|window, event| {
if let tauri::WindowEvent::CloseRequested { api, .. } = event {
// Keep the frontend running but just hide this webview
// Per https://tauri.app/v1/guides/features/system-tray/#preventing-the-app-from-closing
// Closing the window fully seems to deallocate it or something.
event.window().hide().unwrap();
window.hide().unwrap();
api.prevent_close();
}
})
@@ -163,23 +164,9 @@ pub(crate) fn run(
settings::get_advanced_settings,
crate::client::welcome::sign_in,
])
.system_tray(system_tray::loading())
.on_system_tray_event(|app, event| {
if let SystemTrayEvent::MenuItemClick { id, .. } = event {
tracing::debug!(?id, "SystemTrayEvent::MenuItemClick");
let event = match serde_json::from_str::<TrayMenuEvent>(&id) {
Ok(x) => x,
Err(e) => {
tracing::error!("{e}");
return;
}
};
match handle_system_tray_event(app, event) {
Ok(_) => {}
Err(e) => tracing::error!("{e}"),
}
}
})
.plugin(tauri_plugin_dialog::init())
.plugin(tauri_plugin_notification::init())
.plugin(tauri_plugin_shell::init())
.setup(move |app| {
let setup_inner = move || {
// Check for updates
@@ -241,16 +228,20 @@ pub(crate) fn run(
assert_eq!(
firezone_bin_shared::BUNDLE_ID,
app.handle().config().tauri.bundle.identifier,
app.handle().config().identifier,
"BUNDLE_ID should match bundle ID in tauri.conf.json"
);
let app_handle = app.handle();
let tray = tray_rx.blocking_recv().expect("tray_rx failed");
let tray = system_tray::Tray::new(app.handle().clone(), tray);
let integration = TauriIntegration { app: app.handle().clone(), tray };
let app_handle = app.handle().clone();
let _ctlr_task = tokio::spawn(async move {
// Spawn two nested Tasks so the outer can catch panics from the inner
let task = tokio::spawn(run_controller(
app_handle.clone(),
ctlr_tx,
integration,
ctlr_rx,
advanced_settings,
reloader,
@@ -285,25 +276,27 @@ pub(crate) fn run(
// In a normal Rust application, Sentry's guard would flush on drop: https://docs.sentry.io/platforms/rust/configuration/draining/
// But due to a limit in `tao` we cannot return from the event loop and must call `std::process::exit` (or Tauri's wrapper), so we explicitly flush here.
// TODO: This limit may not exist in Tauri v2
telemetry.stop();
tracing::info!(?exit_code);
app_handle.exit(exit_code);
// In Tauri v1, calling `App::exit` internally exited the process.
// In Tauri v2, that doesn't happen, but `App::run` still doesn't return, so we have to bail out of the process manually.
std::process::exit(exit_code);
});
Ok(())
};
setup_result_tx.send(setup_inner()).expect("should be able to send setup result");
let result = setup_inner();
if let Err(error) = &result {
tracing::error!(?error, "Tauri setup failed");
}
Ok(())
result
});
tracing::debug!("Building Tauri app...");
let app = app.build(tauri::generate_context!());
setup_result_rx
.try_recv()
.context("couldn't receive result of setup")??;
let app = match app {
Ok(x) => x,
Err(error) => {
@@ -318,6 +311,36 @@ pub(crate) fn run(
}
};
let tray = tauri::tray::TrayIconBuilder::new()
.icon(system_tray::icon_to_tauri_icon(
&firezone_gui_client_common::system_tray::Icon::default(),
))
.menu(&system_tray::build_app_state(
app.handle(),
&firezone_gui_client_common::system_tray::AppState::default().into_menu(),
)?)
.on_menu_event(|app, event| {
let id = &event.id.0;
tracing::debug!(?id, "SystemTrayEvent::MenuItemClick");
let event = match serde_json::from_str::<TrayMenuEvent>(id) {
Ok(x) => x,
Err(e) => {
tracing::error!("{e}");
return;
}
};
match handle_system_tray_event(app, event) {
Ok(_) => {}
Err(e) => tracing::error!("{e}"),
}
})
.tooltip("Firezone")
.build(&app)
.context("Cannot build Tauri tray icon")?;
if tray_tx.send(tray).is_err() {
panic!("Couldn't send tray through the channel");
}
app.run(|_app_handle, event| {
if let tauri::RunEvent::ExitRequested { api, .. } = event {
// Don't exit if we close our main window
@@ -326,6 +349,7 @@ pub(crate) fn run(
api.prevent_exit();
}
});
tracing::warn!("app.run returned, this is normally unreachable even in Tauri v2");
Ok(())
}
@@ -439,8 +463,8 @@ fn handle_system_tray_event(app: &tauri::AppHandle, event: TrayMenuEvent) -> Res
// TODO: Move this into `impl Controller`
async fn run_controller(
app: tauri::AppHandle,
ctlr_tx: CtlrTx,
integration: TauriIntegration,
rx: mpsc::Receiver<ControllerRequest>,
advanced_settings: AdvancedSettings,
log_filter_reloader: LogFilterReloader,
@@ -448,11 +472,11 @@ async fn run_controller(
updates_rx: mpsc::Receiver<Option<updates::Notification>>,
) -> Result<(), Error> {
tracing::debug!("Entered `run_controller`");
let tray = system_tray::Tray::new(app.tray_handle());
let controller = firezone_gui_client_common::controller::Builder {
advanced_settings,
ctlr_tx,
integration: TauriIntegration { app, tray },
integration,
log_filter_reloader,
rx,
telemetry,

View File

@@ -1,6 +1,6 @@
use anyhow::{Context as _, Result};
use firezone_bin_shared::BUNDLE_ID;
use tauri::api::notification::Notification;
use tauri::AppHandle;
use tauri_plugin_notification::NotificationExt as _;
pub(crate) async fn set_autostart(enabled: bool) -> Result<()> {
let dir = dirs::config_local_dir()
@@ -35,17 +35,19 @@ pub(crate) async fn set_autostart(enabled: bool) -> Result<()> {
/// Since clickable notifications don't work on Linux yet, the update text
/// must be different on different platforms
pub(crate) fn show_update_notification(
app: &AppHandle,
_ctlr_tx: super::CtlrTx,
title: &str,
download_url: url::Url,
) -> Result<()> {
show_notification(title, download_url.to_string().as_ref())?;
show_notification(app, title, download_url.to_string().as_ref())?;
Ok(())
}
/// Show a notification in the bottom right of the screen
pub(crate) fn show_notification(title: &str, body: &str) -> Result<()> {
Notification::new(BUNDLE_ID)
pub(crate) fn show_notification(app: &AppHandle, title: &str, body: &str) -> Result<()> {
app.notification()
.builder()
.title(title)
.body(body)
.show()?;

View File

@@ -1,6 +1,7 @@
use super::{ControllerRequest, CtlrTx};
use anyhow::{Context, Result};
use firezone_bin_shared::BUNDLE_ID;
use tauri::AppHandle;
pub(crate) async fn set_autostart(_enabled: bool) -> Result<()> {
todo!()
@@ -9,6 +10,7 @@ pub(crate) async fn set_autostart(_enabled: bool) -> Result<()> {
/// Since clickable notifications don't work on Linux yet, the update text
/// must be different on different platforms
pub(crate) fn show_update_notification(
_app: &AppHandle,
ctlr_tx: CtlrTx,
title: &str,
download_url: url::Url,
@@ -29,7 +31,7 @@ pub(crate) fn show_update_notification(
///
/// TODO: Warn about silent failure if the AppID is not installed:
/// <https://github.com/tauri-apps/winrt-notification/issues/17#issuecomment-1988715694>
pub(crate) fn show_notification(title: &str, body: &str) -> Result<()> {
pub(crate) fn show_notification(_app: &AppHandle, title: &str, body: &str) -> Result<()> {
tracing::debug!(?title, ?body, "show_notification");
tauri_winrt_notification::Toast::new(BUNDLE_ID)

View File

@@ -10,7 +10,11 @@ use firezone_gui_client_common::{
compositor::{self, Image},
system_tray::{AppState, ConnlibState, Entry, Icon, IconBase, Item, Menu},
};
use tauri::{SystemTray, SystemTrayHandle};
use tauri::AppHandle;
type IsMenuItem = dyn tauri::menu::IsMenuItem<tauri::Wry>;
type TauriMenu = tauri::menu::Menu<tauri::Wry>;
type TauriSubmenu = tauri::menu::Submenu<tauri::Wry>;
// Figma is the source of truth for the tray icon layers
// <https://www.figma.com/design/THvQQ1QxKlsk47H9DZ2bhN/Core-Library?node-id=1250-772&t=nHBOzOnSY5Ol4asV-0>
@@ -22,23 +26,14 @@ const UPDATE_READY_LAYER: &[u8] = include_bytes!("../../../icons/tray/Update rea
const TOOLTIP: &str = "Firezone";
pub(crate) fn loading() -> SystemTray {
let state = AppState {
connlib: ConnlibState::Loading,
release: None,
};
SystemTray::new()
.with_icon(icon_to_tauri_icon(&Icon::default()))
.with_menu(build_app_state(state))
.with_tooltip(TOOLTIP)
}
pub(crate) struct Tray {
handle: SystemTrayHandle,
app: AppHandle,
handle: tauri::tray::TrayIcon,
last_icon_set: Icon,
last_menu_set: Option<Menu>,
}
fn icon_to_tauri_icon(that: &Icon) -> tauri::Icon {
pub(crate) fn icon_to_tauri_icon(that: &Icon) -> tauri::image::Image<'static> {
let layers = match that.base {
IconBase::Busy => &[LOGO_GREY_BASE, BUSY_LAYER][..],
IconBase::SignedIn => &[LOGO_BASE][..],
@@ -52,19 +47,17 @@ fn icon_to_tauri_icon(that: &Icon) -> tauri::Icon {
image_to_tauri_icon(composed)
}
fn image_to_tauri_icon(val: Image) -> tauri::Icon {
tauri::Icon::Rgba {
rgba: val.rgba,
width: val.width,
height: val.height,
}
fn image_to_tauri_icon(val: Image) -> tauri::image::Image<'static> {
tauri::image::Image::new_owned(val.rgba, val.width, val.height)
}
impl Tray {
pub(crate) fn new(handle: SystemTrayHandle) -> Self {
pub(crate) fn new(app: AppHandle, handle: tauri::tray::TrayIcon) -> Self {
Self {
app,
handle,
last_icon_set: Default::default(),
last_menu_set: None,
}
}
@@ -84,9 +77,24 @@ impl Tray {
update_ready: state.release.is_some(),
};
self.handle.set_tooltip(TOOLTIP)?;
self.handle.set_menu(build_app_state(state))?;
let menu = state.into_menu();
let menu_clone = menu.clone();
let app = self.app.clone();
let handle = self.handle.clone();
if Some(&menu) == self.last_menu_set.as_ref() {
tracing::debug!("Skipping redundant menu update");
} else {
self.app
.run_on_main_thread(move || {
if let Err(error) = update(handle, &app, &menu) {
tracing::error!(?error, "Error while updating tray icon");
}
})
.unwrap();
}
self.set_icon(new_icon)?;
self.last_menu_set = Some(menu_clone);
Ok(())
}
@@ -94,52 +102,82 @@ impl Tray {
// Only needed for the stress test
// Otherwise it would be inlined
pub(crate) fn set_icon(&mut self, icon: Icon) -> Result<()> {
if icon != self.last_icon_set {
// Don't call `set_icon` too often. On Linux it writes a PNG to `/run/user/$UID/tao/tray-icon-*.png` every single time.
// <https://github.com/tauri-apps/tao/blob/tao-v0.16.7/src/platform_impl/linux/system_tray.rs#L119>
// Yes, even if you use `Icon::File` and tell Tauri that the icon is already
// on disk.
self.handle.set_icon(icon_to_tauri_icon(&icon))?;
self.last_icon_set = icon;
if icon == self.last_icon_set {
return Ok(());
}
// Don't call `set_icon` too often. On Linux it writes a PNG to `/run/user/$UID/tao/tray-icon-*.png` every single time.
// <https://github.com/tauri-apps/tao/blob/tao-v0.16.7/src/platform_impl/linux/system_tray.rs#L119>
// Yes, even if you use `Icon::File` and tell Tauri that the icon is already
// on disk.
let handle = self.handle.clone();
self.last_icon_set = icon.clone();
self.app.run_on_main_thread(move || {
// These closures can't return any value for some reason
handle.set_icon(Some(icon_to_tauri_icon(&icon))).unwrap();
})?;
Ok(())
}
}
fn build_app_state(that: AppState) -> tauri::SystemTrayMenu {
build_menu(&that.into_menu())
fn update(handle: tauri::tray::TrayIcon, app: &AppHandle, menu: &Menu) -> Result<()> {
handle.set_tooltip(Some(TOOLTIP))?;
handle.set_menu(Some(build_app_state(app, menu)?))?;
Ok(())
}
pub(crate) fn build_app_state(app: &AppHandle, menu: &Menu) -> Result<TauriMenu> {
build_menu(app, menu)
}
/// Builds this abstract `Menu` into a real menu that we can use in Tauri.
///
/// This recurses but we never go deeper than 3 or 4 levels so it's fine.
pub(crate) fn build_menu(that: &Menu) -> tauri::SystemTrayMenu {
let mut menu = tauri::SystemTrayMenu::new();
///
/// Note that Menus and Submenus are different in Tauri. Using a Submenu as a Menu
/// may crash on Windows. <https://github.com/tauri-apps/tauri/issues/11363>
pub(crate) fn build_menu(app: &AppHandle, that: &Menu) -> Result<TauriMenu> {
let mut menu = tauri::menu::MenuBuilder::new(app);
for entry in &that.entries {
menu = match entry {
Entry::Item(item) => menu.add_item(build_item(item)),
Entry::Separator => menu.add_native_item(tauri::SystemTrayMenuItem::Separator),
Entry::Submenu { title, inner } => {
menu.add_submenu(tauri::SystemTraySubmenu::new(title, build_menu(inner)))
}
};
menu = menu.item(&*build_entry(app, entry)?);
}
menu
Ok(menu.build()?)
}
/// Builds this abstract `Item` into a real item that we can use in Tauri.
fn build_item(that: &Item) -> tauri::CustomMenuItem {
let mut item = tauri::CustomMenuItem::new(
serde_json::to_string(&that.event)
.expect("`serde_json` should always be able to serialize tray menu events"),
&that.title,
);
if that.event.is_none() {
item = item.disabled();
pub(crate) fn build_submenu(app: &AppHandle, title: &str, that: &Menu) -> Result<TauriSubmenu> {
let mut menu = tauri::menu::SubmenuBuilder::new(app, title);
for entry in &that.entries {
menu = menu.item(&*build_entry(app, entry)?);
}
if let Some(true) = that.checked {
item = item.selected();
}
item
Ok(menu.build()?)
}
fn build_entry(app: &AppHandle, entry: &Entry) -> Result<Box<IsMenuItem>> {
let entry = match entry {
Entry::Item(item) => build_item(app, item)?,
Entry::Separator => Box::new(tauri::menu::PredefinedMenuItem::separator(app)?),
Entry::Submenu { title, inner } => Box::new(build_submenu(app, title, inner)?),
};
Ok(entry)
}
fn build_item(app: &AppHandle, item: &Item) -> Result<Box<IsMenuItem>> {
let item: Box<IsMenuItem> = if let Some(checked) = item.checked {
let mut tauri_item = tauri::menu::CheckMenuItemBuilder::new(&item.title).checked(checked);
if let Some(event) = &item.event {
tauri_item = tauri_item.id(serde_json::to_string(event)?);
} else {
tauri_item = tauri_item.enabled(false);
}
Box::new(tauri_item.build(app)?)
} else {
let mut tauri_item = tauri::menu::MenuItemBuilder::new(&item.title);
if let Some(event) = &item.event {
tauri_item = tauri_item.id(serde_json::to_string(event)?);
} else {
tauri_item = tauri_item.enabled(false);
}
Box::new(tauri_item.build(app)?)
};
Ok(item)
}

View File

@@ -5,6 +5,7 @@ use firezone_gui_client_common::{
logging as common,
};
use std::path::PathBuf;
use tauri_plugin_dialog::DialogExt as _;
#[tauri::command]
pub(crate) async fn clear_logs(managed: tauri::State<'_, Managed>) -> Result<(), String> {
@@ -22,8 +23,11 @@ pub(crate) async fn clear_logs(managed: tauri::State<'_, Managed>) -> Result<(),
}
#[tauri::command]
pub(crate) async fn export_logs(managed: tauri::State<'_, Managed>) -> Result<(), String> {
show_export_dialog(managed.ctlr_tx.clone()).map_err(|e| e.to_string())
pub(crate) async fn export_logs(
app: tauri::AppHandle,
managed: tauri::State<'_, Managed>,
) -> Result<(), String> {
show_export_dialog(&app, managed.ctlr_tx.clone()).map_err(|e| e.to_string())
}
#[tauri::command]
@@ -32,7 +36,7 @@ pub(crate) async fn count_logs() -> Result<common::FileCount, String> {
}
/// Pops up the "Save File" dialog
fn show_export_dialog(ctlr_tx: CtlrTx) -> Result<()> {
fn show_export_dialog(app: &tauri::AppHandle, ctlr_tx: CtlrTx) -> Result<()> {
let now = chrono::Local::now();
let datetime_string = now.format("%Y_%m_%d-%H-%M");
let stem = PathBuf::from(format!("firezone_logs_{datetime_string}"));
@@ -41,12 +45,13 @@ fn show_export_dialog(ctlr_tx: CtlrTx) -> Result<()> {
bail!("zip filename isn't valid Unicode");
};
tauri::api::dialog::FileDialogBuilder::new()
tauri_plugin_dialog::FileDialogBuilder::new(app.dialog().clone())
.add_filter("Zip", &["zip"])
.set_file_name(filename)
.save_file(move |file_path| match file_path {
None => {}
Some(path) => {
let path = path.into_path().unwrap();
// blocking_send here because we're in a sync callback within Tauri somewhere
ctlr_tx
.blocking_send(ControllerRequest::ExportLogs { path, stem })

View File

@@ -6,5 +6,11 @@
mod client;
fn main() -> anyhow::Result<()> {
// Mitigates a bug in Ubuntu 22.04 - Under Wayland, some features of the window decorations like minimizing, closing the windows, etc., doesn't work unless you double-click the titlebar first.
// SAFETY: No other thread is running yet
unsafe {
std::env::set_var("GDK_BACKEND", "x11");
}
client::run()
}

View File

@@ -1,24 +1,13 @@
{
"build": {
"build": {
"beforeDevCommand": "",
"beforeBuildCommand": "",
"devPath": "../src",
"distDir": "../src",
"withGlobalTauri": true
"frontendDist": "../dist"
},
"package": {
"productName": "firezone-client-gui"
},
"tauri": {
"allowlist": {
"all": false,
"shell": {
"all": false,
"open": false
}
},
"bundle": {
"active": true,
"bundle": {
"active": true,
"shortDescription": "Firezone",
"linux": {
"deb": {
"files": {
"/usr/lib/systemd/system/firezone-client-ipc.service": "./deb_files/firezone-client-ipc.service",
@@ -30,40 +19,49 @@
"/usr/lib/systemd/system/firezone-client-ipc.service": "./deb_files/firezone-client-ipc.service",
"/usr/lib/sysusers.d/firezone-client-ipc.conf": "./deb_files/sysusers.conf"
}
},
"targets": ["deb", "msi", "rpm"],
"identifier": "dev.firezone.client",
"icon": [
"icons/128x128.png",
"icons/128x128@2x.png",
"icons/32x32.png",
"icons/icon.ico",
"icons/icon.png"
],
"publisher": "Firezone",
"shortDescription": "Firezone",
"windows": {
"wix": {
"bannerPath": "./win_files/banner.png",
"componentRefs": ["FirezoneClientIpcService"],
"dialogImagePath": "./win_files/install_dialog.png",
"fragmentPaths": ["./win_files/service.wxs"],
"template": "./win_files/main.wxs"
}
}
},
"targets": [
"deb",
"msi",
"rpm"
],
"windows": {
"wix": {
"bannerPath": "./win_files/banner.png",
"componentRefs": [
"FirezoneClientIpcService"
],
"dialogImagePath": "./win_files/install_dialog.png",
"fragmentPaths": [
"./win_files/service.wxs"
],
"template": "./win_files/main.wxs"
}
},
"icon": [
"icons/128x128.png",
"icons/128x128@2x.png",
"icons/32x32.png",
"icons/icon.ico",
"icons/icon.png"
],
"publisher": "Firezone"
},
"mainBinaryName": "firezone-client-gui",
"identifier": "dev.firezone.client",
"plugins": {},
"productName": "firezone-client-gui",
"app": {
"withGlobalTauri": true,
"security": {
"csp": null
},
"systemTray": {
"iconPath": "icons/tray/Busy layer.png",
"iconAsTemplate": true
},
"windows": [
{
"label": "about",
"title": "About Firezone",
"url": "about.html",
"url": "src/about.html",
"fullscreen": false,
"resizable": true,
"width": 640,
@@ -73,7 +71,7 @@
{
"label": "settings",
"title": "Settings",
"url": "settings.html",
"url": "src/settings.html",
"fullscreen": false,
"resizable": true,
"width": 640,
@@ -83,7 +81,7 @@
{
"label": "welcome",
"title": "Welcome",
"url": "welcome.html",
"url": "src/welcome.html",
"fullscreen": false,
"resizable": true,
"width": 640,

View File

@@ -2,7 +2,6 @@
"build": {
"beforeBundleCommand": "bash -c '../../scripts/build/sign.sh ../target/release/Firezone.exe ../target/release/firezone-client-ipc.exe'"
},
"package": {
"productName": "Firezone"
}
"mainBinaryName": "Firezone",
"productName": "Firezone"
}

View File

@@ -8,6 +8,9 @@ component.
<?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?>
@@ -33,6 +36,11 @@ component.
<!-- 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}}
@@ -60,6 +68,12 @@ component.
<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"/>
@@ -68,8 +82,7 @@ component.
<!-- launch app checkbox -->
<Property Id="WIXUI_EXITDIALOGOPTIONALCHECKBOXTEXT" Value="!(loc.LaunchApp)" />
<Property Id="WIXUI_EXITDIALOGOPTIONALCHECKBOX" Value="1"/>
<Property Id="WixShellExecTarget" Value="[!Path]" />
<CustomAction Id="LaunchApplication" BinaryKey="WixCA" DllEntry="WixShellExec" Impersonate="yes" />
<CustomAction Id="LaunchApplication" Impersonate="yes" FileKey="Path" ExeCommand="[LAUNCHAPPARGS]" Return="asyncNoWait" />
<UI>
<!-- launch app checkbox -->
@@ -115,9 +128,31 @@ component.
<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"/>
<File Id="Path" Source="{{main_binary_path}}" 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)">
@@ -310,6 +345,10 @@ component.
</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

@@ -6,8 +6,8 @@
<link rel="stylesheet" href="output.css" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>About Firezone</title>
<script src="./flowbite.min.js" defer></script>
<script type="module" src="about.js" defer></script>
<script type="module" src="./flowbite.min.js" defer></script>
<script type="module" src="about.ts" defer></script>
</head>
<body class="bg-neutral-100 text-neutral-900">

View File

@@ -1,6 +1,4 @@
import "./tauri_stub.js";
const invoke = window.__TAURI__.tauri.invoke;
import { invoke } from "@tauri-apps/api/core";
const cargoVersionSpan = <HTMLSpanElement>(
document.getElementById("cargo-version")
@@ -8,25 +6,27 @@ const cargoVersionSpan = <HTMLSpanElement>(
const gitVersionSpan = <HTMLSpanElement>document.getElementById("git-version");
function get_cargo_version() {
invoke("get_cargo_version")
.then((cargoVersion: string) => {
cargoVersionSpan.innerText = cargoVersion;
})
.catch((e: Error) => {
cargoVersionSpan.innerText = "Unknown";
console.error(e);
});
try {
invoke("get_cargo_version")
.then((cargoVersion: unknown) => {
cargoVersionSpan.innerText = cargoVersion as string;
});
} catch(e) {
cargoVersionSpan.innerText = "Unknown";
console.error(e);
}
}
function get_git_version() {
invoke("get_git_version")
.then((gitVersion: string) => {
gitVersionSpan.innerText = gitVersion;
})
.catch((e: Error) => {
gitVersionSpan.innerText = "Unknown";
console.error(e);
});
try {
invoke("get_git_version")
.then((gitVersion: unknown) => {
gitVersionSpan.innerText = gitVersion as string;
});
} catch(e) {
gitVersionSpan.innerText = "Unknown";
console.error(e);
};
}
document.addEventListener("DOMContentLoaded", () => {

View File

@@ -6,8 +6,8 @@
<link rel="stylesheet" href="output.css" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Settings</title>
<script src="./flowbite.min.js" defer></script>
<script type="module" src="settings.js" defer></script>
<script type="module" src="./flowbite.min.js" defer></script>
<script type="module" src="settings.ts" defer></script>
</head>
<body class="bg-neutral-100 text-neutral-900">

View File

@@ -1,8 +1,4 @@
// Purpose: TypeScript file for the settings page.
import "./tauri_stub.js";
const invoke = window.__TAURI__.tauri.invoke;
const listen = window.__TAURI__.event.listen;
import { invoke } from "@tauri-apps/api/core";
// Custom types
interface Settings {
@@ -81,95 +77,91 @@ async function applyAdvancedSettings() {
console.log("Applying advanced settings");
lockAdvancedSettingsForm();
invoke("apply_advanced_settings", {
settings: {
auth_base_url: authBaseUrlInput.value,
api_url: apiUrlInput.value,
log_filter: logFilterInput.value,
},
})
.catch((e: Error) => {
console.error(e);
})
.finally(() => {
unlockAdvancedSettingsForm();
try {
await invoke("apply_advanced_settings", {
settings: {
auth_base_url: authBaseUrlInput.value,
api_url: apiUrlInput.value,
log_filter: logFilterInput.value,
},
});
} catch(e) {
console.error(e);
} finally {
unlockAdvancedSettingsForm();
}
}
async function resetAdvancedSettings() {
console.log("Resetting advanced settings");
lockAdvancedSettingsForm();
invoke("reset_advanced_settings")
.then((settings: Settings) => {
authBaseUrlInput.value = settings.auth_base_url;
apiUrlInput.value = settings.api_url;
logFilterInput.value = settings.log_filter;
})
.catch((e: Error) => {
console.error(e);
})
.finally(() => {
unlockAdvancedSettingsForm();
});
try {
let settings = await invoke("reset_advanced_settings") as Settings;
authBaseUrlInput.value = settings.auth_base_url;
apiUrlInput.value = settings.api_url;
logFilterInput.value = settings.log_filter;
} catch(e) {
console.error(e);
} finally {
unlockAdvancedSettingsForm();
}
}
async function getAdvancedSettings() {
console.log("Getting advanced settings");
lockAdvancedSettingsForm();
invoke("get_advanced_settings")
.then((settings: Settings) => {
authBaseUrlInput.value = settings.auth_base_url;
apiUrlInput.value = settings.api_url;
logFilterInput.value = settings.log_filter;
})
.catch((e: Error) => {
console.error(e);
})
.finally(() => {
unlockAdvancedSettingsForm();
});
try {
let settings = await invoke("get_advanced_settings") as Settings;
authBaseUrlInput.value = settings.auth_base_url;
apiUrlInput.value = settings.api_url;
logFilterInput.value = settings.log_filter;
} catch(e) {
console.error(e);
} finally {
unlockAdvancedSettingsForm();
}
}
async function exportLogs() {
console.log("Exporting logs");
lockLogsForm();
invoke("export_logs")
.catch((e: Error) => {
try {
await invoke("export_logs");
} catch(e) {
console.error(e);
})
.finally(() => {
unlockLogsForm();
});
} finally {
unlockLogsForm();
}
}
async function clearLogs() {
console.log("Clearing logs");
lockLogsForm();
invoke("clear_logs")
.catch((e: Error) => {
console.error(e);
})
.finally(() => {
countLogs();
unlockLogsForm();
});
try {
await invoke("clear_logs");
} catch(e) {
console.error(e);
} finally {
countLogs();
unlockLogsForm();
};
}
async function countLogs() {
invoke("count_logs")
.then((fileCount) => {
console.log(fileCount);
const megabytes = Math.round(fileCount.bytes / 100000) / 10;
logCountOutput.innerText = `${fileCount.files} files, ${megabytes} MB`;
})
.catch((e: Error) => {
console.error(e);
logCountOutput.innerText = `Error counting logs: ${e.message}`;
});
try {
let fileCount = await invoke("count_logs") as FileCount;
console.log(fileCount);
const megabytes = Math.round(fileCount.bytes / 100000) / 10;
logCountOutput.innerText = `${fileCount.files} files, ${megabytes} MB`;
} catch(e) {
let error = e as Error;
console.error(e);
logCountOutput.innerText = `Error counting logs: ${error.message}`;
};
}
// Setup event listeners
@@ -177,16 +169,16 @@ form.addEventListener("submit", (e) => {
e.preventDefault();
applyAdvancedSettings();
});
resetAdvancedSettingsBtn.addEventListener("click", (e) => {
resetAdvancedSettingsBtn.addEventListener("click", (_e) => {
resetAdvancedSettings();
});
exportLogsBtn.addEventListener("click", (e) => {
exportLogsBtn.addEventListener("click", (_e) => {
exportLogs();
});
clearLogsBtn.addEventListener("click", (e) => {
clearLogsBtn.addEventListener("click", (_e) => {
clearLogs();
});
logsTabBtn.addEventListener("click", (e) => {
logsTabBtn.addEventListener("click", (_e) => {
countLogs();
});

View File

@@ -1,32 +0,0 @@
// Stub Tauri API for TypeScript. Helpful when developing without Tauri running.
interface TauriEvent {
type: string;
payload: any;
}
export {};
declare global {
interface Window {
__TAURI__: {
tauri: {
invoke: (cmd: string, args?: any) => Promise<any>;
};
event: {
listen: (cmd: string, callback: (event: TauriEvent) => void) => void;
};
};
}
}
window.__TAURI__ = window.__TAURI__ || {
tauri: {
invoke: (_cmd: string, _args?: any) => {
return Promise.reject("Tauri API not initialized");
},
},
event: {
listen: (_cmd: string, _callback: (event: TauriEvent) => void) => {
console.error("Tauri API not initialized");
},
},
};

View File

@@ -6,8 +6,8 @@
<link rel="stylesheet" href="output.css" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Welcome to Firezone</title>
<script src="./flowbite.min.js" defer></script>
<script type="module" src="welcome.js" defer></script>
<script type="module" src="./flowbite.min.js" defer></script>
<script type="module" src="welcome.ts" defer></script>
</head>
<body class="bg-neutral-100 text-neutral-900">

View File

@@ -1,6 +1,4 @@
import "./tauri_stub.js";
const invoke = window.__TAURI__.tauri.invoke;
import { invoke } from "@tauri-apps/api/core";
const signInBtn = <HTMLButtonElement>(
document.getElementById("sign-in")
@@ -15,4 +13,4 @@ async function sign_in() {
});
}
signInBtn.addEventListener("click", (e) => sign_in());
signInBtn.addEventListener("click", (_e) => sign_in());

View File

@@ -1,11 +1,23 @@
{
"compilerOptions": {
"target": "es6",
"module": "es6",
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"target": "ES2020",
"useDefineForClassFields": true,
"module": "ESNext",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
/* Linting */
"strict": true,
"moduleResolution": "node"
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": ["./src/**/*"],
"exclude": ["node_modules"]

View File

@@ -0,0 +1,40 @@
import { defineConfig } from "vite";
import { resolve } from 'path';
// @ts-expect-error process is a nodejs global
const host = process.env.TAURI_DEV_HOST;
// https://vitejs.dev/config/
export default defineConfig(async () => ({
build: {
rollupOptions: {
input: {
about: resolve(__dirname, "src/about.html"),
settings: resolve(__dirname, "src/settings.html"),
welcome: resolve(__dirname, "src/welcome.html"),
}
}
},
// Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build`
//
// 1. prevent vite from obscuring rust errors
clearScreen: false,
// 2. tauri expects a fixed port, fail if that port is not available
server: {
port: 1420,
strictPort: true,
host: host || false,
hmr: host
? {
protocol: "ws",
host,
port: 1421,
}
: undefined,
watch: {
// 3. tell vite to ignore watching `src-tauri`
ignored: ["**/src-tauri/**"],
},
},
}));

View File

@@ -1,4 +1,7 @@
#!/usr/bin/env bash
#
# Runs from `rust/gui-client` or `rust/tauri-client`
set -euox pipefail
SERVICE_NAME=firezone-client-ipc
@@ -9,21 +12,21 @@ function debug_exit() {
}
# For debugging
ls ../target/release ../target/release/bundle/deb
ls "$TARGET_DIR/release" "$TARGET_DIR/release/bundle/deb"
# Used for release artifact
# In release mode the name comes from tauri.conf.json
# Using a glob for the source, there will only be one exe and one deb anyway
cp ../target/release/firezone-client-gui "$BINARY_DEST_PATH"
cp ../target/release/firezone-gui-client.dwp "$BINARY_DEST_PATH.dwp"
cp ../target/release/bundle/deb/firezone-client-gui.deb "$BINARY_DEST_PATH.deb"
cp "$TARGET_DIR/release/firezone-client-gui" "$BINARY_DEST_PATH"
cp "$TARGET_DIR/release/firezone-gui-client.dwp" "$BINARY_DEST_PATH.dwp"
cp "$TARGET_DIR/release/bundle/deb/firezone-client-gui.deb" "$BINARY_DEST_PATH.deb"
# TODO: Debug symbols for Linux
function make_hash() {
sha256sum "$1" >"$1.sha256sum.txt"
}
# Windows uses x64, Debian amd64. Standardize on x86_64 naming here since that's
# Windows calls it `x64`, Debian `amd64`. Standardize on `x86_64` here since that's
# what Rust uses.
make_hash "$BINARY_DEST_PATH"
make_hash "$BINARY_DEST_PATH.dwp"
@@ -35,6 +38,8 @@ sudo apt-get install "$DEB_PATH"
# Debug-print the files. The icons and both binaries should be in here
dpkg --listfiles firezone-client-gui
# Print the deps
dpkg --info "$DEB_PATH"
# Confirm that both binaries and at least one icon were installed
which firezone-client-gui firezone-client-ipc

View File

@@ -2,13 +2,13 @@
set -euox pipefail
# For debugging
ls ../target/release ../target/release/bundle/msi
ls "$TARGET_DIR/release" "$TARGET_DIR/release/bundle/msi"
# Used for release artifact
# In release mode the name comes from tauri.conf.json
cp ../target/release/Firezone.exe "$BINARY_DEST_PATH.exe"
cp ../target/release/bundle/msi/*.msi "$BINARY_DEST_PATH.msi"
cp ../target/release/firezone_gui_client.pdb "$BINARY_DEST_PATH.pdb"
cp "$TARGET_DIR/release/Firezone.exe" "$BINARY_DEST_PATH.exe"
cp "$TARGET_DIR"/release/bundle/msi/*.msi "$BINARY_DEST_PATH.msi"
cp "$TARGET_DIR/release/firezone_gui_client.pdb" "$BINARY_DEST_PATH.pdb"
function make_hash() {
sha256sum "$1" >"$1.sha256sum.txt"

View File

@@ -22,6 +22,9 @@ export default function GUI({ title }: { title: string }) {
<ChangeItem pull="7123">
Reports the version to the Portal correctly.
</ChangeItem>
<ChangeItem pull="6996">
Supports Ubuntu 24.04, no longer supports Ubuntu 20.04.
</ChangeItem>
</Unreleased>
<Entry version="1.3.9" date={new Date("2024-10-09")}>
<ChangeItem enable={title === "Linux GUI"} pull="6987">