mirror of https://github.com/ospab/ostp.git
Compare commits
11 Commits
c5e703c144
...
1ab55fcdd0
| Author | SHA1 | Date |
|---|---|---|
|
|
1ab55fcdd0 | |
|
|
234497759b | |
|
|
a63c34669b | |
|
|
b18de0379c | |
|
|
96d6bb61d2 | |
|
|
8af4be9b0a | |
|
|
8b5c0a3a8c | |
|
|
cf14a4243c | |
|
|
66368c9d0f | |
|
|
bc61b47817 | |
|
|
5a33ed69c4 |
|
|
@ -414,9 +414,32 @@ jobs:
|
||||||
Copy-Item "ostp-gui/src-tauri/target/${{ matrix.target }}/release/ostp-gui.exe" $dir
|
Copy-Item "ostp-gui/src-tauri/target/${{ matrix.target }}/release/ostp-gui.exe" $dir
|
||||||
Copy-Item "target/${{ matrix.target }}/release/ostp-tun-helper.exe" $dir
|
Copy-Item "target/${{ matrix.target }}/release/ostp-tun-helper.exe" $dir
|
||||||
Copy-Item "target/${{ matrix.target }}/release/wintun.dll" $dir
|
Copy-Item "target/${{ matrix.target }}/release/wintun.dll" $dir
|
||||||
|
|
||||||
Compress-Archive -Path "$dir/*" -DestinationPath "ostp-windows-gui-${{ matrix.arch }}.zip" -Force
|
Compress-Archive -Path "$dir/*" -DestinationPath "ostp-windows-gui-${{ matrix.arch }}.zip" -Force
|
||||||
|
|
||||||
|
# The installer is what removes the per-connect consent prompt: it runs
|
||||||
|
# elevated, so its hook can register the helper's Scheduled Task once.
|
||||||
|
# The portable zip above cannot, and falls back to asking on first connect.
|
||||||
|
# The sidecar and its config are confined to this step: declaring
|
||||||
|
# externalBin in an auto-merged tauri.windows.conf.json would force every
|
||||||
|
# Windows build, down to a bare `cargo check`, to have the helper staged
|
||||||
|
# first, and fail the build script when it is not.
|
||||||
|
- name: Build NSIS Installer
|
||||||
|
working-directory: ostp-gui
|
||||||
|
# Chained, not two lines: pwsh does not abort a run block when a native
|
||||||
|
# command fails, so a staging failure would otherwise be reported far
|
||||||
|
# downstream as a missing sidecar rather than as itself.
|
||||||
|
run: node stage-sidecar.cjs --release --target ${{ matrix.target }} && npx tauri build --bundles nsis --target ${{ matrix.target }} --config src-tauri/tauri.installer.conf.json
|
||||||
|
|
||||||
|
- name: Collect installer
|
||||||
|
shell: pwsh
|
||||||
|
run: |
|
||||||
|
$nsis = Get-ChildItem -Path "ostp-gui/src-tauri/target/${{ matrix.target }}/release/bundle/nsis" -Filter *-setup.exe -ErrorAction SilentlyContinue |
|
||||||
|
Select-Object -First 1
|
||||||
|
if (-not $nsis) { Write-Error "NSIS installer was not produced"; exit 1 }
|
||||||
|
Copy-Item $nsis.FullName "ostp-windows-gui-${{ matrix.arch }}-setup.exe"
|
||||||
|
Write-Host "installer: $($nsis.Name) -> ostp-windows-gui-${{ matrix.arch }}-setup.exe"
|
||||||
|
|
||||||
- name: Upload to GitHub Release
|
- name: Upload to GitHub Release
|
||||||
uses: softprops/action-gh-release@v2
|
uses: softprops/action-gh-release@v2
|
||||||
with:
|
with:
|
||||||
|
|
@ -426,7 +449,9 @@ jobs:
|
||||||
# real stable release.
|
# real stable release.
|
||||||
tag_name: ${{ needs.resolve-channel.outputs.tag_name }}
|
tag_name: ${{ needs.resolve-channel.outputs.tag_name }}
|
||||||
prerelease: ${{ needs.resolve-channel.outputs.prerelease }}
|
prerelease: ${{ needs.resolve-channel.outputs.prerelease }}
|
||||||
files: ostp-windows-gui-${{ matrix.arch }}.zip
|
files: |
|
||||||
|
ostp-windows-gui-${{ matrix.arch }}.zip
|
||||||
|
ostp-windows-gui-${{ matrix.arch }}-setup.exe
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -57,3 +57,6 @@ ostp-control/
|
||||||
netstack-smoltcp/
|
netstack-smoltcp/
|
||||||
dnstt/
|
dnstt/
|
||||||
ostp-web/
|
ostp-web/
|
||||||
|
|
||||||
|
# Tauri sidecar staging area (copied from target/ at build time)
|
||||||
|
ostp-gui/src-tauri/binaries/
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"target_version": "0.4.3",
|
"target_version": "0.4.5",
|
||||||
"branch": "beta",
|
"branch": "beta",
|
||||||
"alpha_iteration": 0,
|
"alpha_iteration": 0,
|
||||||
"beta_iteration": 4
|
"beta_iteration": 3
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1386,7 +1386,7 @@ checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ostp"
|
name = "ostp"
|
||||||
version = "0.4.3"
|
version = "0.4.5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"base64",
|
"base64",
|
||||||
|
|
@ -1409,7 +1409,7 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ostp-client"
|
name = "ostp-client"
|
||||||
version = "0.4.3"
|
version = "0.4.5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"base64",
|
"base64",
|
||||||
|
|
@ -1440,7 +1440,7 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ostp-core"
|
name = "ostp-core"
|
||||||
version = "0.4.3"
|
version = "0.4.5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"bytes",
|
"bytes",
|
||||||
|
|
@ -1474,7 +1474,7 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ostp-server"
|
name = "ostp-server"
|
||||||
version = "0.4.3"
|
version = "0.4.5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"axum",
|
"axum",
|
||||||
|
|
@ -1507,7 +1507,7 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ostp-tun"
|
name = "ostp-tun"
|
||||||
version = "0.4.3"
|
version = "0.4.5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"libc",
|
"libc",
|
||||||
|
|
@ -1519,7 +1519,7 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ostp-tun-helper"
|
name = "ostp-tun-helper"
|
||||||
version = "0.4.3"
|
version = "0.4.5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"chrono",
|
"chrono",
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ resolver = "2"
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "AGPL-3.0"
|
license = "AGPL-3.0"
|
||||||
version = "0.4.3"
|
version = "0.4.5"
|
||||||
|
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
anyhow = "1.0"
|
anyhow = "1.0"
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
|
||||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||||
# In Windows, build-name is used as the major, minor, and patch parts
|
# In Windows, build-name is used as the major, minor, and patch parts
|
||||||
# of the product and file versions while build-number is used as the build suffix.
|
# of the product and file versions while build-number is used as the build suffix.
|
||||||
version: 0.4.3+29
|
version: 0.4.5+34
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ^3.11.4
|
sdk: ^3.11.4
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,15 @@
|
||||||
{
|
{
|
||||||
"name": "ostp-gui",
|
"name": "ostp-gui",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.4.3",
|
"version": "0.4.5",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"tauri": "tauri",
|
"tauri": "tauri",
|
||||||
"dev": "cargo build -p ostp-tun-helper && npx tauri dev",
|
"dev": "cargo build -p ostp-tun-helper && npx tauri dev",
|
||||||
"build": "cargo build -p ostp-tun-helper --release && npx tauri build --no-bundle",
|
"build": "cargo build -p ostp-tun-helper --release && npx tauri build --no-bundle",
|
||||||
"build:installer": "cargo build -p ostp-tun-helper --release && npx tauri build",
|
"build:installer": "cargo build -p ostp-tun-helper --release && node stage-sidecar.cjs --release && npx tauri build --bundles nsis --config src-tauri/tauri.installer.conf.json",
|
||||||
"build:dist": "npm run build && node build_dist.js"
|
"build:dist": "npm run build && node build_dist.js",
|
||||||
|
"sidecar": "node stage-sidecar.cjs"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tauri-apps/cli": "^2"
|
"@tauri-apps/cli": "^2"
|
||||||
|
|
|
||||||
|
|
@ -2665,7 +2665,7 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ostp-client"
|
name = "ostp-client"
|
||||||
version = "0.4.3"
|
version = "0.4.5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"base64 0.22.1",
|
"base64 0.22.1",
|
||||||
|
|
@ -2696,7 +2696,7 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ostp-core"
|
name = "ostp-core"
|
||||||
version = "0.4.3"
|
version = "0.4.5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"bytes",
|
"bytes",
|
||||||
|
|
@ -2713,7 +2713,7 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ostp-gui"
|
name = "ostp-gui"
|
||||||
version = "0.4.3"
|
version = "0.4.5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"json_comments",
|
"json_comments",
|
||||||
|
|
@ -2733,7 +2733,7 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ostp-tun"
|
name = "ostp-tun"
|
||||||
version = "0.4.3"
|
version = "0.4.5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"libc",
|
"libc",
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "ostp-gui"
|
name = "ostp-gui"
|
||||||
version = "0.4.3"
|
version = "0.4.5"
|
||||||
description = "OSTP desktop GUI"
|
description = "OSTP desktop GUI"
|
||||||
authors = ["ospab"]
|
authors = ["ospab"]
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
|
||||||
|
|
@ -134,16 +134,84 @@ struct AppState(Mutex<AppStateInner>);
|
||||||
|
|
||||||
// ── Config helpers ────────────────────────────────────────────────────────────
|
// ── Config helpers ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// Per-user config location, used whenever the config cannot live next to the
|
||||||
|
/// executable.
|
||||||
|
fn user_config_path() -> PathBuf {
|
||||||
|
let base = std::env::var_os(if cfg!(windows) { "APPDATA" } else { "HOME" })
|
||||||
|
.map(PathBuf::from)
|
||||||
|
.unwrap_or_else(std::env::temp_dir);
|
||||||
|
let dir = if cfg!(windows) { base.join("OSTP") } else { base.join(".config").join("ostp") };
|
||||||
|
dir.join("config.json")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Where the GUI reads and writes its configuration.
|
||||||
|
///
|
||||||
|
/// Portable installs keep the config beside the executable, which is what the
|
||||||
|
/// zip has always done, and that is preserved wherever the directory is
|
||||||
|
/// actually writable.
|
||||||
|
///
|
||||||
|
/// What it must never do again is fall back to a bare relative `config.json`.
|
||||||
|
/// That resolves against the process working directory, which for a Start Menu
|
||||||
|
/// shortcut is whatever Windows chose — often `C:\Windows\System32`. Reading
|
||||||
|
/// and saving settings then failed with "Access is denied" (os error 5), and on
|
||||||
|
/// a writable working directory it would have been worse still: settings would
|
||||||
|
/// silently persist somewhere unrelated and appear to vanish.
|
||||||
|
///
|
||||||
|
/// Writability is measured rather than inferred from the install location. An
|
||||||
|
/// installer can put the app anywhere — a per-machine install onto a data drive
|
||||||
|
/// may well be writable, while Program Files is not — so the location alone
|
||||||
|
/// says nothing.
|
||||||
fn get_config_path() -> PathBuf {
|
fn get_config_path() -> PathBuf {
|
||||||
if let Ok(exe_path) = std::env::current_exe() {
|
if let Ok(exe_path) = std::env::current_exe() {
|
||||||
if let Some(parent) = exe_path.parent() {
|
if let Some(parent) = exe_path.parent() {
|
||||||
let path = parent.join("config.json");
|
let portable = parent.join("config.json");
|
||||||
if path.exists() {
|
if portable.exists() {
|
||||||
return path;
|
if is_file_writable(&portable) {
|
||||||
|
return portable;
|
||||||
|
}
|
||||||
|
// Read-only beside the exe: unusable as the live file, but its
|
||||||
|
// contents are still worth carrying over once.
|
||||||
|
let user = user_config_path();
|
||||||
|
if !user.exists() {
|
||||||
|
if let Some(dir) = user.parent() {
|
||||||
|
let _ = std::fs::create_dir_all(dir);
|
||||||
|
}
|
||||||
|
let _ = std::fs::copy(&portable, &user);
|
||||||
|
}
|
||||||
|
} else if is_dir_writable(parent) {
|
||||||
|
// No config yet and the directory takes writes: a portable
|
||||||
|
// unzip, so keep the config travelling with the folder.
|
||||||
|
return portable;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
PathBuf::from("config.json")
|
|
||||||
|
let path = user_config_path();
|
||||||
|
if let Some(dir) = path.parent() {
|
||||||
|
let _ = std::fs::create_dir_all(dir);
|
||||||
|
}
|
||||||
|
path
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether an existing file can actually be written to.
|
||||||
|
///
|
||||||
|
/// Answered by opening it, not by reading permission bits: on Windows the
|
||||||
|
/// effective answer depends on the ACL and on virtualization, and `readonly()`
|
||||||
|
/// reflects neither.
|
||||||
|
fn is_file_writable(path: &std::path::Path) -> bool {
|
||||||
|
std::fs::OpenOptions::new().append(true).open(path).is_ok()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether new files can be created in a directory, tested by doing it.
|
||||||
|
fn is_dir_writable(dir: &std::path::Path) -> bool {
|
||||||
|
let probe = dir.join(format!(".ostp-write-test-{}", std::process::id()));
|
||||||
|
match std::fs::File::create(&probe) {
|
||||||
|
Ok(_) => {
|
||||||
|
let _ = std::fs::remove_file(&probe);
|
||||||
|
true
|
||||||
|
}
|
||||||
|
Err(_) => false,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn map_to_client_config(raw: &ClientConfigRaw, mode: &str) -> ostp_client::config::ClientConfig {
|
fn map_to_client_config(raw: &ClientConfigRaw, mode: &str) -> ostp_client::config::ClientConfig {
|
||||||
|
|
@ -204,19 +272,34 @@ fn get_wintun_install_path() -> String {
|
||||||
String::new()
|
String::new()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A `Command` for a console program, with the console window suppressed.
|
||||||
|
///
|
||||||
|
/// The GUI is a windowed-subsystem binary, so every console child it spawns
|
||||||
|
/// pops up a console window for as long as that child runs. With `reg`,
|
||||||
|
/// `tasklist` and `schtasks` all being invoked from here, that surfaced as
|
||||||
|
/// windows flashing on screen — worst while polling for the scheduled task,
|
||||||
|
/// which could spawn twenty of them in a row.
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
fn quiet_command(program: &str) -> std::process::Command {
|
||||||
|
use std::os::windows::process::CommandExt;
|
||||||
|
const CREATE_NO_WINDOW: u32 = 0x0800_0000;
|
||||||
|
let mut cmd = std::process::Command::new(program);
|
||||||
|
cmd.creation_flags(CREATE_NO_WINDOW);
|
||||||
|
cmd
|
||||||
|
}
|
||||||
|
|
||||||
/// Sets or removes the app from Windows startup (HKCU\...\Run).
|
/// Sets or removes the app from Windows startup (HKCU\...\Run).
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
fn set_autostart(enable: bool) -> Result<(), String> {
|
fn set_autostart(enable: bool) -> Result<(), String> {
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
{
|
{
|
||||||
use std::process::Command;
|
|
||||||
let key = r"HKCU\Software\Microsoft\Windows\CurrentVersion\Run";
|
let key = r"HKCU\Software\Microsoft\Windows\CurrentVersion\Run";
|
||||||
let app_name = "OSTP";
|
let app_name = "OSTP";
|
||||||
if enable {
|
if enable {
|
||||||
let exe = std::env::current_exe()
|
let exe = std::env::current_exe()
|
||||||
.map_err(|e| format!("Cannot get exe path: {}", e))?;
|
.map_err(|e| format!("Cannot get exe path: {}", e))?;
|
||||||
let exe_str = format!("\"{}\"", exe.to_string_lossy());
|
let exe_str = format!("\"{}\"", exe.to_string_lossy());
|
||||||
let out = Command::new("reg")
|
let out = quiet_command("reg")
|
||||||
.args(["add", key, "/v", app_name, "/t", "REG_SZ", "/d", &exe_str, "/f"])
|
.args(["add", key, "/v", app_name, "/t", "REG_SZ", "/d", &exe_str, "/f"])
|
||||||
.output()
|
.output()
|
||||||
.map_err(|e| format!("reg add failed: {}", e))?;
|
.map_err(|e| format!("reg add failed: {}", e))?;
|
||||||
|
|
@ -224,7 +307,7 @@ fn set_autostart(enable: bool) -> Result<(), String> {
|
||||||
return Err(String::from_utf8_lossy(&out.stderr).to_string());
|
return Err(String::from_utf8_lossy(&out.stderr).to_string());
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
let _ = Command::new("reg")
|
let _ = quiet_command("reg")
|
||||||
.args(["delete", key, "/v", app_name, "/f"])
|
.args(["delete", key, "/v", app_name, "/f"])
|
||||||
.output();
|
.output();
|
||||||
}
|
}
|
||||||
|
|
@ -275,9 +358,8 @@ fn linux_autostart_path() -> Option<PathBuf> {
|
||||||
fn get_autostart() -> bool {
|
fn get_autostart() -> bool {
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
{
|
{
|
||||||
use std::process::Command;
|
|
||||||
let key = r"HKCU\Software\Microsoft\Windows\CurrentVersion\Run";
|
let key = r"HKCU\Software\Microsoft\Windows\CurrentVersion\Run";
|
||||||
let out = Command::new("reg")
|
let out = quiet_command("reg")
|
||||||
.args(["query", key, "/v", "OSTP"])
|
.args(["query", key, "/v", "OSTP"])
|
||||||
.output();
|
.output();
|
||||||
if let Ok(o) = out {
|
if let Ok(o) = out {
|
||||||
|
|
@ -298,8 +380,7 @@ fn get_autostart() -> bool {
|
||||||
fn list_running_processes() -> Vec<String> {
|
fn list_running_processes() -> Vec<String> {
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
{
|
{
|
||||||
use std::process::Command;
|
if let Ok(out) = quiet_command("tasklist")
|
||||||
if let Ok(out) = Command::new("tasklist")
|
|
||||||
.args(["/FO", "CSV", "/NH"])
|
.args(["/FO", "CSV", "/NH"])
|
||||||
.output()
|
.output()
|
||||||
{
|
{
|
||||||
|
|
@ -823,120 +904,78 @@ fn helper_args_file() -> PathBuf {
|
||||||
base.join("OSTP").join("helper-args.json")
|
base.join("OSTP").join("helper-args.json")
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Minimal XML text escaping for the values interpolated into the task
|
/// Undoes XML entity escaping. `&` must be handled last, or `&lt;`
|
||||||
/// definition. Paths and usernames are attacker-irrelevant here but can easily
|
/// would come back as `<`.
|
||||||
/// contain `&`, which would otherwise produce invalid XML and a confusing
|
|
||||||
/// schtasks parse failure.
|
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
fn xml_escape(s: &str) -> String {
|
fn xml_unescape(s: &str) -> String {
|
||||||
s.replace('&', "&")
|
s.replace(""", "\"")
|
||||||
.replace('<', "<")
|
.replace("'", "'")
|
||||||
.replace('>', ">")
|
.replace("<", "<")
|
||||||
.replace('"', """)
|
.replace(">", ">")
|
||||||
.replace('\'', "'")
|
.replace("&", "&")
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Whether the elevated-launch Scheduled Task already exists.
|
/// The exe path currently baked into the registered task, if any.
|
||||||
#[cfg(target_os = "windows")]
|
|
||||||
fn helper_task_exists() -> bool {
|
|
||||||
use std::process::Command;
|
|
||||||
Command::new("schtasks")
|
|
||||||
.args(["/Query", "/TN", HELPER_TASK_NAME])
|
|
||||||
.output()
|
|
||||||
.map(|o| o.status.success())
|
|
||||||
.unwrap_or(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Register the Scheduled Task. This is the ONLY step that needs elevation, and
|
|
||||||
/// it happens once per machine; every later tunnel start reuses the task.
|
|
||||||
///
|
///
|
||||||
/// RunLevel=HIGHEST makes the task run elevated, and because a task launch is
|
/// Queried as XML rather than `/FO LIST /V`: the list format's field labels are
|
||||||
/// not an elevation request, Windows shows no consent dialog for it.
|
/// localized (on a Russian Windows "Task To Run" is "Задача для запуска"),
|
||||||
|
/// whereas XML tag names are fixed.
|
||||||
|
///
|
||||||
|
/// Encoding depends on where the output goes, which is measured rather than
|
||||||
|
/// assumed: to a console schtasks writes UTF-16LE with a BOM, but into a
|
||||||
|
/// redirected pipe — our case — it writes UTF-8 with no BOM. Both are handled,
|
||||||
|
/// keyed off the BOM, so this keeps working if that ever flips.
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
fn install_helper_task(exe: &std::path::Path) -> anyhow::Result<()> {
|
fn helper_task_command() -> Option<String> {
|
||||||
let args_file = helper_args_file();
|
let out = quiet_command("schtasks")
|
||||||
if let Some(dir) = args_file.parent() {
|
.args(["/Query", "/TN", HELPER_TASK_NAME, "/XML"])
|
||||||
std::fs::create_dir_all(dir)?;
|
.output()
|
||||||
|
.ok()?;
|
||||||
|
if !out.status.success() {
|
||||||
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Register from an XML definition rather than /TR. The command line would
|
let text = if out.stdout.starts_with(&[0xFF, 0xFE]) {
|
||||||
// otherwise need the exe path and the args path quoted INSIDE an already
|
let units: Vec<u16> = out.stdout[2..]
|
||||||
// quoted /TR value, escaped again through ShellExecuteW — a notoriously
|
.chunks_exact(2)
|
||||||
// brittle chain when either path contains a space, which both of these do
|
.map(|c| u16::from_le_bytes([c[0], c[1]]))
|
||||||
// by default (Program Files, and usernames with spaces). XML also lets the
|
.collect();
|
||||||
// battery and time-limit settings below be stated explicitly.
|
String::from_utf16_lossy(&units)
|
||||||
let user = format!(
|
} else {
|
||||||
"{}\\{}",
|
String::from_utf8_lossy(&out.stdout).into_owned()
|
||||||
std::env::var("USERDOMAIN").unwrap_or_else(|_| "%COMPUTERNAME%".into()),
|
};
|
||||||
std::env::var("USERNAME").unwrap_or_default()
|
|
||||||
);
|
|
||||||
let xml = format!(
|
|
||||||
r#"<?xml version="1.0" encoding="UTF-16"?>
|
|
||||||
<Task version="1.2" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
|
|
||||||
<RegistrationInfo>
|
|
||||||
<Description>Runs the OSTP TUN helper elevated so enabling the tunnel does not prompt for consent every time.</Description>
|
|
||||||
</RegistrationInfo>
|
|
||||||
<Principals>
|
|
||||||
<Principal id="Author">
|
|
||||||
<UserId>{user}</UserId>
|
|
||||||
<LogonType>InteractiveToken</LogonType>
|
|
||||||
<RunLevel>HighestAvailable</RunLevel>
|
|
||||||
</Principal>
|
|
||||||
</Principals>
|
|
||||||
<Settings>
|
|
||||||
<MultipleInstancesPolicy>Parallel</MultipleInstancesPolicy>
|
|
||||||
<DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries>
|
|
||||||
<StopIfGoingOnBatteries>false</StopIfGoingOnBatteries>
|
|
||||||
<StartWhenAvailable>false</StartWhenAvailable>
|
|
||||||
<RunOnlyIfNetworkAvailable>false</RunOnlyIfNetworkAvailable>
|
|
||||||
<ExecutionTimeLimit>PT0S</ExecutionTimeLimit>
|
|
||||||
<Enabled>true</Enabled>
|
|
||||||
<Hidden>false</Hidden>
|
|
||||||
<AllowHardTerminate>true</AllowHardTerminate>
|
|
||||||
</Settings>
|
|
||||||
<Actions Context="Author">
|
|
||||||
<Exec>
|
|
||||||
<Command>{exe}</Command>
|
|
||||||
<Arguments>--args-file "{args}"</Arguments>
|
|
||||||
</Exec>
|
|
||||||
</Actions>
|
|
||||||
</Task>
|
|
||||||
"#,
|
|
||||||
user = xml_escape(&user),
|
|
||||||
exe = xml_escape(&exe.display().to_string()),
|
|
||||||
args = xml_escape(&args_file.display().to_string()),
|
|
||||||
);
|
|
||||||
|
|
||||||
// schtasks /Create /XML expects UTF-16LE with a BOM.
|
let start = text.find("<Command>")? + "<Command>".len();
|
||||||
let xml_path = std::env::temp_dir().join(format!("ostp_task_{}.xml", rand::random::<u32>()));
|
let end = text[start..].find("</Command>")? + start;
|
||||||
let mut utf16: Vec<u8> = vec![0xFF, 0xFE];
|
Some(xml_unescape(text[start..end].trim()))
|
||||||
for unit in xml.encode_utf16() {
|
}
|
||||||
utf16.extend_from_slice(&unit.to_le_bytes());
|
|
||||||
|
/// Whether a task is registered AND still points at the exe we are about to run.
|
||||||
|
///
|
||||||
|
/// The path matters as much as the name. A task registered by a dev build (or
|
||||||
|
/// by an install that has since moved) keeps its original `<Command>`, and
|
||||||
|
/// `schtasks /Run` reports success merely for *accepting* the request — a task
|
||||||
|
/// whose exe no longer exists fails asynchronously and silently. Trusting the
|
||||||
|
/// name alone therefore bought a 60-second "Timeout connecting to helper" on
|
||||||
|
/// every single connect, permanently, until the task was deleted by hand.
|
||||||
|
/// Re-registering costs one consent prompt and fixes it for good.
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
fn helper_task_matches(exe: &std::path::Path) -> bool {
|
||||||
|
let Some(registered) = helper_task_command() else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
let registered = registered.trim().trim_matches('"');
|
||||||
|
|
||||||
|
// Canonicalize both sides when possible so `..`, short 8.3 names and
|
||||||
|
// casing differences do not read as a mismatch. A missing file cannot be
|
||||||
|
// canonicalized — which is itself a mismatch worth re-registering over.
|
||||||
|
match (
|
||||||
|
std::fs::canonicalize(registered),
|
||||||
|
std::fs::canonicalize(exe),
|
||||||
|
) {
|
||||||
|
(Ok(a), Ok(b)) => a == b,
|
||||||
|
_ => registered.eq_ignore_ascii_case(&exe.display().to_string()),
|
||||||
}
|
}
|
||||||
std::fs::write(&xml_path, &utf16)?;
|
|
||||||
|
|
||||||
// Registering a HighestAvailable task is itself privileged: this is the one
|
|
||||||
// prompt, and it happens once per machine.
|
|
||||||
let schtasks = std::path::PathBuf::from("schtasks.exe");
|
|
||||||
let params = format!(
|
|
||||||
"/Create /TN \"{}\" /XML \"{}\" /F",
|
|
||||||
HELPER_TASK_NAME,
|
|
||||||
xml_path.display()
|
|
||||||
);
|
|
||||||
let result = shell_execute_elevated(&schtasks, ¶ms);
|
|
||||||
// Best-effort cleanup; schtasks may still be reading it, so ignore errors.
|
|
||||||
let _ = std::fs::remove_file(&xml_path);
|
|
||||||
result?;
|
|
||||||
|
|
||||||
// schtasks runs asynchronously through ShellExecute; wait briefly for the
|
|
||||||
// task to appear rather than reporting success before it exists.
|
|
||||||
for _ in 0..20 {
|
|
||||||
if helper_task_exists() {
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
std::thread::sleep(std::time::Duration::from_millis(250));
|
|
||||||
}
|
|
||||||
anyhow::bail!("the scheduled task did not appear after the elevation prompt (it may have been declined)")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
|
|
@ -954,14 +993,14 @@ fn launch_as_admin(exe: &std::path::PathBuf, token: &str, port: u16) -> anyhow::
|
||||||
let wrote_args = std::fs::write(&args_file, payload.to_string()).is_ok();
|
let wrote_args = std::fs::write(&args_file, payload.to_string()).is_ok();
|
||||||
|
|
||||||
if wrote_args {
|
if wrote_args {
|
||||||
if !helper_task_exists() {
|
// Deliberately does NOT create the task when it is missing. Registering
|
||||||
if let Err(e) = install_helper_task(exe) {
|
// one is privileged, so the app could only do it by raising the very
|
||||||
eprintln!("[OSTP] could not register the helper task ({e}); falling back to a direct elevated launch");
|
// prompt this exists to avoid — and it would then charge the user two
|
||||||
}
|
// prompts for the privilege. Creating it belongs to the installer,
|
||||||
}
|
// which is already elevated. Without it we simply fall through to the
|
||||||
if helper_task_exists() {
|
// direct elevated launch, which prompts once per connect as before.
|
||||||
use std::process::Command;
|
if helper_task_matches(exe) {
|
||||||
let run = Command::new("schtasks")
|
let run = quiet_command("schtasks")
|
||||||
.args(["/Run", "/TN", HELPER_TASK_NAME])
|
.args(["/Run", "/TN", HELPER_TASK_NAME])
|
||||||
.output();
|
.output();
|
||||||
match run {
|
match run {
|
||||||
|
|
@ -1035,60 +1074,6 @@ fn launch_as_admin_direct(exe: &std::path::PathBuf, token: &str, port: u16) -> a
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Run `exe` elevated with `params`, raising the UAC prompt.
|
|
||||||
///
|
|
||||||
/// Shared by the fallback launch path and by the one-time task registration, so
|
|
||||||
/// both report a declined prompt the same way instead of ShellExecuteW's
|
|
||||||
/// pseudo-HINSTANCE being interpreted twice.
|
|
||||||
#[cfg(target_os = "windows")]
|
|
||||||
fn shell_execute_elevated(exe: &std::path::Path, params: &str) -> anyhow::Result<()> {
|
|
||||||
use std::ffi::OsStr;
|
|
||||||
use std::os::windows::ffi::OsStrExt;
|
|
||||||
use std::ptr::null_mut;
|
|
||||||
|
|
||||||
let exe_wstr: Vec<u16> = exe.as_os_str().encode_wide().chain(Some(0)).collect();
|
|
||||||
let verb_wstr: Vec<u16> = OsStr::new("runas").encode_wide().chain(Some(0)).collect();
|
|
||||||
let params_wstr: Vec<u16> = OsStr::new(params).encode_wide().chain(Some(0)).collect();
|
|
||||||
|
|
||||||
#[link(name = "shell32")]
|
|
||||||
extern "system" {
|
|
||||||
fn ShellExecuteW(h: *mut std::ffi::c_void, op: *const u16, f: *const u16, p: *const u16, d: *const u16, s: i32) -> isize;
|
|
||||||
}
|
|
||||||
#[link(name = "kernel32")]
|
|
||||||
extern "system" {
|
|
||||||
fn GetLastError() -> u32;
|
|
||||||
}
|
|
||||||
|
|
||||||
let cwd_path = std::env::current_exe().unwrap_or_else(|_| std::path::PathBuf::from("."));
|
|
||||||
let dir_wstr: Vec<u16> = cwd_path
|
|
||||||
.parent()
|
|
||||||
.unwrap_or(std::path::Path::new("."))
|
|
||||||
.as_os_str()
|
|
||||||
.encode_wide()
|
|
||||||
.chain(Some(0))
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
let ret = unsafe {
|
|
||||||
ShellExecuteW(null_mut(), verb_wstr.as_ptr(), exe_wstr.as_ptr(), params_wstr.as_ptr(), dir_wstr.as_ptr(), 1)
|
|
||||||
};
|
|
||||||
|
|
||||||
// 1223 is ERROR_CANCELLED, which lands in the ">32 means success" range —
|
|
||||||
// see the note in launch_as_admin_direct.
|
|
||||||
if ret == 1223 {
|
|
||||||
anyhow::bail!("UAC elevation was denied.");
|
|
||||||
}
|
|
||||||
if ret <= 32 {
|
|
||||||
let win_err = unsafe { GetLastError() };
|
|
||||||
anyhow::bail!(
|
|
||||||
"Failed to request UAC elevation (ShellExecuteW ret={}, GetLastError={}, path={})",
|
|
||||||
ret,
|
|
||||||
win_err,
|
|
||||||
exe.display()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
fn launch_as_admin(exe: &PathBuf, token: &str, port: u16) -> Result<()> {
|
fn launch_as_admin(exe: &PathBuf, token: &str, port: u16) -> Result<()> {
|
||||||
use std::os::unix::fs::PermissionsExt;
|
use std::os::unix::fs::PermissionsExt;
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"$schema": "https://schema.tauri.app/config/2",
|
"$schema": "https://schema.tauri.app/config/2",
|
||||||
"productName": "ostp-gui",
|
"productName": "ostp-gui",
|
||||||
"version": "0.4.3",
|
"version": "0.4.5",
|
||||||
"identifier": "com.ospab.ostp",
|
"identifier": "com.ospab.ostp",
|
||||||
"build": {
|
"build": {
|
||||||
"frontendDist": "../src"
|
"frontendDist": "../src"
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
{
|
||||||
|
"$schema": "https://schema.tauri.app/config/2",
|
||||||
|
"bundle": {
|
||||||
|
"externalBin": ["binaries/ostp-tun-helper"],
|
||||||
|
"resources": { "binaries/wintun.dll": "wintun.dll" },
|
||||||
|
"windows": {
|
||||||
|
"nsis": {
|
||||||
|
"installMode": "perMachine",
|
||||||
|
"installerHooks": "./windows/hooks.nsh"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,71 @@
|
||||||
|
; Registers the Scheduled Task that lets the GUI start the TUN helper elevated
|
||||||
|
; without a consent prompt.
|
||||||
|
;
|
||||||
|
; This belongs in the installer, not in the app. Registering a task that runs
|
||||||
|
; elevated is itself a privileged operation, so an unprivileged GUI could only
|
||||||
|
; obtain one by raising the very prompt we are trying to remove. The installer
|
||||||
|
; already runs elevated (installMode is perMachine), so here it costs nothing:
|
||||||
|
; the user consents once, to the install, and never again per connect.
|
||||||
|
;
|
||||||
|
; The task carries no trigger at all — it exists solely to be started on demand.
|
||||||
|
|
||||||
|
!macro NSIS_HOOK_POSTINSTALL
|
||||||
|
; Bundled resources land in $INSTDIR\resources, but the helper loads wintun
|
||||||
|
; with a plain LoadLibrary, which searches its own directory — so put a copy
|
||||||
|
; beside the executables. The destination is the directory, not a file path:
|
||||||
|
; CopyFiles takes a target directory, and naming the file made it fail.
|
||||||
|
${If} ${FileExists} "$INSTDIR\resources\wintun.dll"
|
||||||
|
DetailPrint "Placing wintun.dll next to the helper..."
|
||||||
|
CopyFiles /SILENT "$INSTDIR\resources\wintun.dll" "$INSTDIR"
|
||||||
|
${Else}
|
||||||
|
DetailPrint "WARNING: resources\wintun.dll is missing; TUN mode will not start."
|
||||||
|
${EndIf}
|
||||||
|
|
||||||
|
; Registered through PowerShell's ScheduledTasks module rather than
|
||||||
|
; `schtasks /XML`. Generating the XML from NSIS wrote a UTF-16 byte-order mark
|
||||||
|
; ahead of content whose encoding depended on whether makensis was built in
|
||||||
|
; Unicode mode, and schtasks rejected the result outright:
|
||||||
|
; "The task XML is malformed. (1,2)::ERROR: incorrect document syntax"
|
||||||
|
; The cmdlets take the same settings as arguments, so no file is written and
|
||||||
|
; there is no encoding to get wrong.
|
||||||
|
;
|
||||||
|
; The command is delimited with backticks, NSIS's third quote character, so
|
||||||
|
; that PowerShell's own single quotes and the shell's double quotes can both
|
||||||
|
; appear literally — inside a single-quoted NSIS string the first PowerShell
|
||||||
|
; quote would have terminated the argument early.
|
||||||
|
;
|
||||||
|
; $$ is an escaped literal dollar for PowerShell's variables; a bare $ would
|
||||||
|
; be read by NSIS as one of its own. The helper argument is assembled with
|
||||||
|
; [char]34 instead of nested quotes so that a username containing a space
|
||||||
|
; still yields a correctly quoted path, without three levels of escaping.
|
||||||
|
;
|
||||||
|
; The principal is the SID S-1-5-32-545 (BUILTIN\Users) rather than the
|
||||||
|
; installing user, so a per-machine install serves every account instead of
|
||||||
|
; only whoever ran the installer. The SID is used because the name is
|
||||||
|
; localized and would not resolve. %LOCALAPPDATA% is likewise left unexpanded
|
||||||
|
; for Task Scheduler to resolve per running user.
|
||||||
|
DetailPrint "Registering the OSTP TUN helper task..."
|
||||||
|
nsExec::ExecToLog `powershell -NoProfile -NonInteractive -ExecutionPolicy Bypass -Command "$$act = New-ScheduledTaskAction -Execute '$INSTDIR\ostp-tun-helper.exe' -Argument ('--args-file ' + [char]34 + '%LOCALAPPDATA%\OSTP\helper-args.json' + [char]34); $$prn = New-ScheduledTaskPrincipal -GroupId 'S-1-5-32-545' -RunLevel Highest; $$set = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -ExecutionTimeLimit ([TimeSpan]::Zero) -MultipleInstances Parallel; Register-ScheduledTask -TaskName 'OSTP TUN Helper' -Action $$act -Principal $$prn -Settings $$set -Force | Out-Null"`
|
||||||
|
Pop $R0
|
||||||
|
|
||||||
|
${If} $R0 == 0
|
||||||
|
DetailPrint "Helper task registered; connecting will not ask for consent."
|
||||||
|
${Else}
|
||||||
|
; Not fatal: the app still works, it just falls back to an elevated launch
|
||||||
|
; that asks for consent on each connect.
|
||||||
|
DetailPrint "Could not register the helper task (exit $R0)."
|
||||||
|
DetailPrint "OSTP will still work, but every connect will ask for consent."
|
||||||
|
${EndIf}
|
||||||
|
!macroend
|
||||||
|
|
||||||
|
!macro NSIS_HOOK_PREUNINSTALL
|
||||||
|
; Leaving the task behind would point it at a deleted executable, and
|
||||||
|
; `schtasks /Run` reports success for merely accepting such a request — the
|
||||||
|
; app would wait on a helper that never starts.
|
||||||
|
DetailPrint "Removing the OSTP TUN helper task..."
|
||||||
|
nsExec::ExecToLog 'schtasks.exe /Delete /TN "OSTP TUN Helper" /F'
|
||||||
|
Pop $R0
|
||||||
|
|
||||||
|
; Copied by the install hook, so the uninstaller has no record of it.
|
||||||
|
Delete "$INSTDIR\wintun.dll"
|
||||||
|
!macroend
|
||||||
|
|
@ -0,0 +1,73 @@
|
||||||
|
// Stages ostp-tun-helper where Tauri expects a sidecar.
|
||||||
|
//
|
||||||
|
// tauri.installer.conf.json declares `externalBin: ["binaries/ostp-tun-helper"]`,
|
||||||
|
// and Tauri resolves that to `binaries/ostp-tun-helper-<target-triple>.exe` at
|
||||||
|
// build time, failing the build outright when the file is absent. Cargo writes
|
||||||
|
// the plain name instead, so it has to be copied across first.
|
||||||
|
//
|
||||||
|
// Only the installer build needs this. That config is passed explicitly with
|
||||||
|
// --config rather than being named tauri.windows.conf.json, which Tauri would
|
||||||
|
// merge into every Windows build automatically — and then even a bare
|
||||||
|
// `cargo check` would fail on the missing sidecar.
|
||||||
|
//
|
||||||
|
// A no-op off Windows: the Linux and macOS GUI builds have no helper sidecar.
|
||||||
|
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const { execFileSync } = require('child_process');
|
||||||
|
|
||||||
|
if (process.platform !== 'win32') {
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --target may be passed through; fall back to the host triple rustc reports.
|
||||||
|
const targetFlag = process.argv.indexOf('--target');
|
||||||
|
const triple =
|
||||||
|
targetFlag !== -1 && process.argv[targetFlag + 1]
|
||||||
|
? process.argv[targetFlag + 1]
|
||||||
|
: execFileSync('rustc', ['-vV'], { encoding: 'utf8' })
|
||||||
|
.split('\n')
|
||||||
|
.find((l) => l.startsWith('host:'))
|
||||||
|
.slice('host:'.length)
|
||||||
|
.trim();
|
||||||
|
|
||||||
|
const profile = process.argv.includes('--release') ? 'release' : 'debug';
|
||||||
|
const repoRoot = path.resolve(__dirname, '..');
|
||||||
|
|
||||||
|
// Cargo drops a --target build under target/<triple>/, and a host build
|
||||||
|
// straight into target/. CI always passes --target; local builds usually do not.
|
||||||
|
const candidates = [
|
||||||
|
path.join(repoRoot, 'target', triple, profile, 'ostp-tun-helper.exe'),
|
||||||
|
path.join(repoRoot, 'target', profile, 'ostp-tun-helper.exe'),
|
||||||
|
];
|
||||||
|
const src = candidates.find((p) => fs.existsSync(p));
|
||||||
|
if (!src) {
|
||||||
|
console.error(
|
||||||
|
'stage-sidecar: ostp-tun-helper.exe not found. Looked in:\n ' +
|
||||||
|
candidates.join('\n ') +
|
||||||
|
`\nBuild it first: cargo build -p ostp-tun-helper${profile === 'release' ? ' --release' : ''}`
|
||||||
|
);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const destDir = path.join(__dirname, 'src-tauri', 'binaries');
|
||||||
|
fs.mkdirSync(destDir, { recursive: true });
|
||||||
|
const dest = path.join(destDir, `ostp-tun-helper-${triple}.exe`);
|
||||||
|
fs.copyFileSync(src, dest);
|
||||||
|
console.log(`stage-sidecar: ${path.relative(repoRoot, src)} -> ${path.relative(repoRoot, dest)}`);
|
||||||
|
|
||||||
|
// wintun.dll rides along as a bundled resource. It is only fetched by the
|
||||||
|
// release workflow, so a local build without it should warn rather than fail —
|
||||||
|
// the installer just ends up unable to bring a tunnel up.
|
||||||
|
const dllSrc = [
|
||||||
|
path.join(repoRoot, 'target', triple, profile, 'wintun.dll'),
|
||||||
|
path.join(repoRoot, 'target', profile, 'wintun.dll'),
|
||||||
|
].find((p) => fs.existsSync(p));
|
||||||
|
if (dllSrc) {
|
||||||
|
fs.copyFileSync(dllSrc, path.join(destDir, 'wintun.dll'));
|
||||||
|
console.log(`stage-sidecar: ${path.relative(repoRoot, dllSrc)} -> binaries/wintun.dll`);
|
||||||
|
} else if (fs.existsSync(path.join(destDir, 'wintun.dll'))) {
|
||||||
|
console.log('stage-sidecar: reusing the previously staged binaries/wintun.dll');
|
||||||
|
} else {
|
||||||
|
console.warn('stage-sidecar: WARNING wintun.dll not found; a bundle build will fail on the missing resource');
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue