mirror of https://github.com/ospab/ostp.git
Compare commits
6 Commits
v0.4.3-bet
...
master
| Author | SHA1 | Date |
|---|---|---|
|
|
cf14a4243c | |
|
|
66368c9d0f | |
|
|
bc61b47817 | |
|
|
5a33ed69c4 | |
|
|
c5e703c144 | |
|
|
05f25155bd |
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"target_version": "0.4.3",
|
||||
"branch": "beta",
|
||||
"target_version": "0.4.4",
|
||||
"branch": "master",
|
||||
"alpha_iteration": 0,
|
||||
"beta_iteration": 3
|
||||
"beta_iteration": 0
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1386,7 +1386,7 @@ checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381"
|
|||
|
||||
[[package]]
|
||||
name = "ostp"
|
||||
version = "0.4.3"
|
||||
version = "0.4.4"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"base64",
|
||||
|
|
@ -1409,7 +1409,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "ostp-client"
|
||||
version = "0.4.3"
|
||||
version = "0.4.4"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"base64",
|
||||
|
|
@ -1440,7 +1440,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "ostp-core"
|
||||
version = "0.4.3"
|
||||
version = "0.4.4"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bytes",
|
||||
|
|
@ -1474,7 +1474,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "ostp-server"
|
||||
version = "0.4.3"
|
||||
version = "0.4.4"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum",
|
||||
|
|
@ -1507,7 +1507,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "ostp-tun"
|
||||
version = "0.4.3"
|
||||
version = "0.4.4"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"libc",
|
||||
|
|
@ -1519,7 +1519,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "ostp-tun-helper"
|
||||
version = "0.4.3"
|
||||
version = "0.4.4"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"chrono",
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ resolver = "2"
|
|||
[workspace.package]
|
||||
edition = "2021"
|
||||
license = "AGPL-3.0"
|
||||
version = "0.4.3"
|
||||
version = "0.4.4"
|
||||
|
||||
[workspace.dependencies]
|
||||
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
|
||||
# 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.
|
||||
version: 0.4.3+28
|
||||
version: 0.4.4+31
|
||||
|
||||
environment:
|
||||
sdk: ^3.11.4
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "ostp-gui",
|
||||
"private": true,
|
||||
"version": "0.4.3",
|
||||
"version": "0.4.4",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"tauri": "tauri",
|
||||
|
|
|
|||
|
|
@ -2665,7 +2665,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "ostp-client"
|
||||
version = "0.4.3"
|
||||
version = "0.4.4"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"base64 0.22.1",
|
||||
|
|
@ -2696,7 +2696,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "ostp-core"
|
||||
version = "0.4.3"
|
||||
version = "0.4.4"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bytes",
|
||||
|
|
@ -2713,7 +2713,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "ostp-gui"
|
||||
version = "0.4.3"
|
||||
version = "0.4.4"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"json_comments",
|
||||
|
|
@ -2733,7 +2733,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "ostp-tun"
|
||||
version = "0.4.3"
|
||||
version = "0.4.4"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"libc",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "ostp-gui"
|
||||
version = "0.4.3"
|
||||
version = "0.4.4"
|
||||
description = "OSTP desktop GUI"
|
||||
authors = ["ospab"]
|
||||
edition = "2021"
|
||||
|
|
|
|||
|
|
@ -204,19 +204,34 @@ fn get_wintun_install_path() -> String {
|
|||
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).
|
||||
#[tauri::command]
|
||||
fn set_autostart(enable: bool) -> Result<(), String> {
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
use std::process::Command;
|
||||
let key = r"HKCU\Software\Microsoft\Windows\CurrentVersion\Run";
|
||||
let app_name = "OSTP";
|
||||
if enable {
|
||||
let exe = std::env::current_exe()
|
||||
.map_err(|e| format!("Cannot get exe path: {}", e))?;
|
||||
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"])
|
||||
.output()
|
||||
.map_err(|e| format!("reg add failed: {}", e))?;
|
||||
|
|
@ -224,7 +239,7 @@ fn set_autostart(enable: bool) -> Result<(), String> {
|
|||
return Err(String::from_utf8_lossy(&out.stderr).to_string());
|
||||
}
|
||||
} else {
|
||||
let _ = Command::new("reg")
|
||||
let _ = quiet_command("reg")
|
||||
.args(["delete", key, "/v", app_name, "/f"])
|
||||
.output();
|
||||
}
|
||||
|
|
@ -275,9 +290,8 @@ fn linux_autostart_path() -> Option<PathBuf> {
|
|||
fn get_autostart() -> bool {
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
use std::process::Command;
|
||||
let key = r"HKCU\Software\Microsoft\Windows\CurrentVersion\Run";
|
||||
let out = Command::new("reg")
|
||||
let out = quiet_command("reg")
|
||||
.args(["query", key, "/v", "OSTP"])
|
||||
.output();
|
||||
if let Ok(o) = out {
|
||||
|
|
@ -298,8 +312,7 @@ fn get_autostart() -> bool {
|
|||
fn list_running_processes() -> Vec<String> {
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
use std::process::Command;
|
||||
if let Ok(out) = Command::new("tasklist")
|
||||
if let Ok(out) = quiet_command("tasklist")
|
||||
.args(["/FO", "CSV", "/NH"])
|
||||
.output()
|
||||
{
|
||||
|
|
@ -803,8 +816,272 @@ fn find_helper_exe() -> Option<PathBuf> {
|
|||
None
|
||||
}
|
||||
|
||||
/// Name of the Scheduled Task that runs the helper elevated without a prompt.
|
||||
#[cfg(target_os = "windows")]
|
||||
const HELPER_TASK_NAME: &str = "OSTP TUN Helper";
|
||||
|
||||
/// Fixed path the GUI writes launch parameters to, and the task's command line
|
||||
/// reads them from.
|
||||
///
|
||||
/// A Scheduled Task stores a FIXED command line, so the per-launch port and
|
||||
/// token cannot travel as arguments. The file lives under the user's own
|
||||
/// LOCALAPPDATA: the helper runs elevated but as the SAME user, so this keeps
|
||||
/// the token inside the trust boundary it already had — no other user can read
|
||||
/// it, which would not be true of a shared location.
|
||||
#[cfg(target_os = "windows")]
|
||||
fn helper_args_file() -> PathBuf {
|
||||
let base = std::env::var_os("LOCALAPPDATA")
|
||||
.map(PathBuf::from)
|
||||
.unwrap_or_else(std::env::temp_dir);
|
||||
base.join("OSTP").join("helper-args.json")
|
||||
}
|
||||
|
||||
/// Minimal XML text escaping for the values interpolated into the task
|
||||
/// definition. Paths and usernames are attacker-irrelevant here but can easily
|
||||
/// contain `&`, which would otherwise produce invalid XML and a confusing
|
||||
/// schtasks parse failure.
|
||||
#[cfg(target_os = "windows")]
|
||||
fn xml_escape(s: &str) -> String {
|
||||
s.replace('&', "&")
|
||||
.replace('<', "<")
|
||||
.replace('>', ">")
|
||||
.replace('"', """)
|
||||
.replace('\'', "'")
|
||||
}
|
||||
|
||||
/// Reverse of [`xml_escape`]. `&` must be undone last or `&lt;` would
|
||||
/// come back as `<`.
|
||||
#[cfg(target_os = "windows")]
|
||||
fn xml_unescape(s: &str) -> String {
|
||||
s.replace(""", "\"")
|
||||
.replace("'", "'")
|
||||
.replace("<", "<")
|
||||
.replace(">", ">")
|
||||
.replace("&", "&")
|
||||
}
|
||||
|
||||
/// The exe path currently baked into the registered task, if any.
|
||||
///
|
||||
/// Queried as XML rather than `/FO LIST /V`: the list format's field labels are
|
||||
/// localized (on a Russian Windows "Task To Run" is "Задача для запуска"),
|
||||
/// whereas XML tag names are fixed. schtasks writes UTF-16LE with a BOM here,
|
||||
/// but tolerate UTF-8 in case that ever changes.
|
||||
#[cfg(target_os = "windows")]
|
||||
fn helper_task_command() -> Option<String> {
|
||||
let out = quiet_command("schtasks")
|
||||
.args(["/Query", "/TN", HELPER_TASK_NAME, "/XML"])
|
||||
.output()
|
||||
.ok()?;
|
||||
if !out.status.success() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let text = if out.stdout.starts_with(&[0xFF, 0xFE]) {
|
||||
let units: Vec<u16> = out.stdout[2..]
|
||||
.chunks_exact(2)
|
||||
.map(|c| u16::from_le_bytes([c[0], c[1]]))
|
||||
.collect();
|
||||
String::from_utf16_lossy(&units)
|
||||
} else {
|
||||
String::from_utf8_lossy(&out.stdout).into_owned()
|
||||
};
|
||||
|
||||
let start = text.find("<Command>")? + "<Command>".len();
|
||||
let end = text[start..].find("</Command>")? + start;
|
||||
Some(xml_unescape(text[start..end].trim()))
|
||||
}
|
||||
|
||||
/// 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()),
|
||||
}
|
||||
}
|
||||
|
||||
/// 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
|
||||
/// not an elevation request, Windows shows no consent dialog for it.
|
||||
#[cfg(target_os = "windows")]
|
||||
fn install_helper_task(exe: &std::path::Path) -> anyhow::Result<()> {
|
||||
let args_file = helper_args_file();
|
||||
if let Some(dir) = args_file.parent() {
|
||||
std::fs::create_dir_all(dir)?;
|
||||
}
|
||||
|
||||
// Register from an XML definition rather than /TR. The command line would
|
||||
// otherwise need the exe path and the args path quoted INSIDE an already
|
||||
// quoted /TR value, escaped again through ShellExecuteW — a notoriously
|
||||
// brittle chain when either path contains a space, which both of these do
|
||||
// by default (Program Files, and usernames with spaces). XML also lets the
|
||||
// battery and time-limit settings below be stated explicitly.
|
||||
let user = format!(
|
||||
"{}\\{}",
|
||||
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 xml_path = std::env::temp_dir().join(format!("ostp_task_{}.xml", rand::random::<u32>()));
|
||||
let mut utf16: Vec<u8> = vec![0xFF, 0xFE];
|
||||
for unit in xml.encode_utf16() {
|
||||
utf16.extend_from_slice(&unit.to_le_bytes());
|
||||
}
|
||||
std::fs::write(&xml_path, &utf16)?;
|
||||
|
||||
// Registering a HighestAvailable task is itself privileged: this is the one
|
||||
// prompt, and it happens once per machine.
|
||||
//
|
||||
// Elevate through PowerShell's Start-Process -Wait rather than
|
||||
// ShellExecuteW. ShellExecuteW returns as soon as the elevated process is
|
||||
// LAUNCHED, so the XML below was being deleted while schtasks was still
|
||||
// starting up — registration then failed, leaving the user with a consent
|
||||
// prompt that accomplished nothing, followed by a second prompt from the
|
||||
// fallback path. -Wait makes the deletion safe and lets the exit code be
|
||||
// checked instead of guessed at by polling.
|
||||
//
|
||||
// ArgumentList takes an array, so the task name and XML path never need
|
||||
// quoting or escaping through a command line, only PowerShell's own
|
||||
// single-quote doubling.
|
||||
let ps = format!(
|
||||
"$p = Start-Process -FilePath 'schtasks.exe' -Verb RunAs -Wait -PassThru \
|
||||
-WindowStyle Hidden -ArgumentList @('/Create','/TN','{}','/XML','{}','/F'); \
|
||||
exit $p.ExitCode",
|
||||
ps_quote(HELPER_TASK_NAME),
|
||||
ps_quote(&xml_path.display().to_string()),
|
||||
);
|
||||
|
||||
let status = quiet_command("powershell")
|
||||
.args(["-NoProfile", "-NonInteractive", "-WindowStyle", "Hidden", "-Command", &ps])
|
||||
.status();
|
||||
|
||||
// schtasks has exited by now, so this is safe.
|
||||
let _ = std::fs::remove_file(&xml_path);
|
||||
|
||||
match status {
|
||||
Ok(s) if s.success() => {}
|
||||
Ok(s) => anyhow::bail!(
|
||||
"registering the scheduled task failed (exit code {:?}). A declined consent prompt \
|
||||
reports 1223.",
|
||||
s.code()
|
||||
),
|
||||
Err(e) => anyhow::bail!("could not run powershell to register the task: {e}"),
|
||||
}
|
||||
|
||||
if helper_task_matches(exe) {
|
||||
Ok(())
|
||||
} else {
|
||||
anyhow::bail!("schtasks reported success but the task does not point at {}", exe.display())
|
||||
}
|
||||
}
|
||||
|
||||
/// Escape a value for embedding in a PowerShell single-quoted string.
|
||||
#[cfg(target_os = "windows")]
|
||||
fn ps_quote(s: &str) -> String {
|
||||
s.replace('\'', "''")
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
fn launch_as_admin(exe: &std::path::PathBuf, token: &str, port: u16) -> anyhow::Result<()> {
|
||||
// Preferred path: hand the parameters over in a file and trigger the
|
||||
// pre-registered task, which runs elevated with no prompt. Falls back to a
|
||||
// direct elevated launch when the task is absent (first ever run, or the
|
||||
// user removed it) — and that first run is also where the task gets created,
|
||||
// so the prompt appears once rather than on every connect.
|
||||
let args_file = helper_args_file();
|
||||
if let Some(dir) = args_file.parent() {
|
||||
let _ = std::fs::create_dir_all(dir);
|
||||
}
|
||||
let payload = serde_json::json!({ "port": port, "token": token });
|
||||
let wrote_args = std::fs::write(&args_file, payload.to_string()).is_ok();
|
||||
|
||||
if wrote_args {
|
||||
if !helper_task_matches(exe) {
|
||||
if let Err(e) = install_helper_task(exe) {
|
||||
eprintln!("[OSTP] could not register the helper task ({e}); falling back to a direct elevated launch");
|
||||
}
|
||||
}
|
||||
if helper_task_matches(exe) {
|
||||
let run = quiet_command("schtasks")
|
||||
.args(["/Run", "/TN", HELPER_TASK_NAME])
|
||||
.output();
|
||||
match run {
|
||||
Ok(o) if o.status.success() => return Ok(()),
|
||||
Ok(o) => eprintln!(
|
||||
"[OSTP] schtasks /Run failed: {}",
|
||||
String::from_utf8_lossy(&o.stderr).trim()
|
||||
),
|
||||
Err(e) => eprintln!("[OSTP] schtasks /Run could not start: {e}"),
|
||||
}
|
||||
}
|
||||
// Falling through: remove the file so a stale token is not left behind.
|
||||
let _ = std::fs::remove_file(&args_file);
|
||||
}
|
||||
|
||||
launch_as_admin_direct(exe, token, port)
|
||||
}
|
||||
|
||||
/// The original one-prompt-per-launch path, kept as the fallback.
|
||||
#[cfg(target_os = "windows")]
|
||||
fn launch_as_admin_direct(exe: &std::path::PathBuf, token: &str, port: u16) -> anyhow::Result<()> {
|
||||
use std::ffi::OsStr;
|
||||
use std::os::windows::ffi::OsStrExt;
|
||||
use std::ptr::null_mut;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"productName": "ostp-gui",
|
||||
"version": "0.4.3",
|
||||
"version": "0.4.4",
|
||||
"identifier": "com.ospab.ostp",
|
||||
"build": {
|
||||
"frontendDist": "../src"
|
||||
|
|
|
|||
|
|
@ -24,6 +24,14 @@ fn log_to_file(msg: &str) {
|
|||
|
||||
|
||||
|
||||
/// Launch parameters handed over in a file rather than on the command line.
|
||||
/// See the `--args-file` handling in `main` for why.
|
||||
#[derive(Deserialize)]
|
||||
struct HelperArgs {
|
||||
port: u16,
|
||||
token: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(tag = "cmd", rename_all = "lowercase")]
|
||||
enum GuiCmd {
|
||||
|
|
@ -76,6 +84,28 @@ async fn main() -> Result<()> {
|
|||
let _ = std::fs::remove_file(path); // securely delete after reading
|
||||
}
|
||||
}
|
||||
// Both port and token from one file. A Scheduled Task stores a FIXED
|
||||
// command line, so anything that varies per launch cannot be passed as
|
||||
// an argument — the GUI writes this file immediately before triggering
|
||||
// the task instead. That indirection is what lets the task be created
|
||||
// once (a single UAC prompt) and reused for every later connect without
|
||||
// prompting again.
|
||||
if args[i] == "--args-file" && i + 1 < args.len() {
|
||||
let path = &args[i + 1];
|
||||
match std::fs::read_to_string(path) {
|
||||
Ok(content) => {
|
||||
let _ = std::fs::remove_file(path); // single use
|
||||
match serde_json::from_str::<HelperArgs>(&content) {
|
||||
Ok(parsed) => {
|
||||
port = parsed.port;
|
||||
expected_token = parsed.token;
|
||||
}
|
||||
Err(e) => log_to_file(&format!("Failed to parse --args-file: {e}")),
|
||||
}
|
||||
}
|
||||
Err(e) => log_to_file(&format!("Failed to read --args-file {path}: {e}")),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
log_to_file("Helper started (TCP mode)");
|
||||
|
|
|
|||
Loading…
Reference in New Issue