Skip to content

Commit 05460dc

Browse files
committed
Add login auto-start option for Windows, macOS and Linux
Register the binary at the OS user-level login hook (Windows Run key, macOS LaunchAgent, Linux XDG autostart) so users can opt in to automatically launching acceleration after sign-in. The GUI exposes the toggle in the details page, and a hidden --autostart flag triggers the Start action right after the GUI loads.
1 parent 0d2a7b1 commit 05460dc

5 files changed

Lines changed: 425 additions & 4 deletions

File tree

src/autostart.rs

Lines changed: 304 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,304 @@
1+
use std::path::Path;
2+
#[cfg(any(target_os = "macos", target_os = "linux"))]
3+
use std::path::PathBuf;
4+
5+
use anyhow::{Context, Result, bail};
6+
7+
#[cfg(target_os = "macos")]
8+
const AUTOSTART_LABEL: &str = "io.linuxdo.accelerator";
9+
#[cfg(any(target_os = "windows", target_os = "linux"))]
10+
const AUTOSTART_DISPLAY_NAME: &str = "Linux.do Accelerator";
11+
#[cfg(any(target_os = "windows", target_os = "macos", target_os = "linux"))]
12+
const AUTOSTART_FLAG: &str = "--autostart";
13+
14+
pub fn enable(config_path: &Path) -> Result<()> {
15+
let exe = std::env::current_exe().context("failed to locate current executable")?;
16+
enable_for_exe(&exe, config_path)
17+
}
18+
19+
pub fn disable() -> Result<()> {
20+
platform_disable()
21+
}
22+
23+
pub fn is_enabled() -> bool {
24+
platform_is_enabled().unwrap_or(false)
25+
}
26+
27+
fn enable_for_exe(exe: &Path, config_path: &Path) -> Result<()> {
28+
platform_enable(exe, config_path)
29+
}
30+
31+
#[cfg(target_os = "windows")]
32+
fn platform_enable(exe: &Path, config_path: &Path) -> Result<()> {
33+
use std::process::Command;
34+
35+
let exe = exe
36+
.canonicalize()
37+
.unwrap_or_else(|_| exe.to_path_buf())
38+
.to_string_lossy()
39+
.into_owned();
40+
let config = config_path
41+
.canonicalize()
42+
.unwrap_or_else(|_| config_path.to_path_buf())
43+
.to_string_lossy()
44+
.into_owned();
45+
let value = format!(
46+
"\"{}\" --config \"{}\" {} gui",
47+
exe, config, AUTOSTART_FLAG
48+
);
49+
50+
let mut command = Command::new("reg");
51+
command.args([
52+
"add",
53+
"HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run",
54+
"/v",
55+
AUTOSTART_DISPLAY_NAME,
56+
"/t",
57+
"REG_SZ",
58+
"/d",
59+
&value,
60+
"/f",
61+
]);
62+
hide_windows_window(&mut command);
63+
let output = command.output().context("failed to invoke reg.exe")?;
64+
if !output.status.success() {
65+
bail!(
66+
"reg add failed: {}",
67+
String::from_utf8_lossy(&output.stderr).trim()
68+
);
69+
}
70+
Ok(())
71+
}
72+
73+
#[cfg(target_os = "windows")]
74+
fn platform_disable() -> Result<()> {
75+
use std::process::Command;
76+
77+
let mut command = Command::new("reg");
78+
command.args([
79+
"delete",
80+
"HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run",
81+
"/v",
82+
AUTOSTART_DISPLAY_NAME,
83+
"/f",
84+
]);
85+
hide_windows_window(&mut command);
86+
let output = command.output().context("failed to invoke reg.exe")?;
87+
if !output.status.success() {
88+
let stderr = String::from_utf8_lossy(&output.stderr).to_lowercase();
89+
if stderr.contains("unable to find") || stderr.contains("找不到") {
90+
return Ok(());
91+
}
92+
bail!(
93+
"reg delete failed: {}",
94+
String::from_utf8_lossy(&output.stderr).trim()
95+
);
96+
}
97+
Ok(())
98+
}
99+
100+
#[cfg(target_os = "windows")]
101+
fn platform_is_enabled() -> Result<bool> {
102+
use std::process::Command;
103+
104+
let mut command = Command::new("reg");
105+
command.args([
106+
"query",
107+
"HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run",
108+
"/v",
109+
AUTOSTART_DISPLAY_NAME,
110+
]);
111+
hide_windows_window(&mut command);
112+
let output = command.output().context("failed to invoke reg.exe")?;
113+
Ok(output.status.success())
114+
}
115+
116+
#[cfg(target_os = "windows")]
117+
fn hide_windows_window(command: &mut std::process::Command) {
118+
use std::os::windows::process::CommandExt;
119+
const CREATE_NO_WINDOW: u32 = 0x0800_0000;
120+
command.creation_flags(CREATE_NO_WINDOW);
121+
}
122+
123+
#[cfg(target_os = "macos")]
124+
fn platform_enable(exe: &Path, config_path: &Path) -> Result<()> {
125+
use std::fs;
126+
127+
let plist_path = macos_plist_path()?;
128+
if let Some(parent) = plist_path.parent() {
129+
fs::create_dir_all(parent)
130+
.with_context(|| format!("failed to create {}", parent.display()))?;
131+
}
132+
133+
let exe = exe
134+
.canonicalize()
135+
.unwrap_or_else(|_| exe.to_path_buf())
136+
.to_string_lossy()
137+
.into_owned();
138+
let config = config_path
139+
.canonicalize()
140+
.unwrap_or_else(|_| config_path.to_path_buf())
141+
.to_string_lossy()
142+
.into_owned();
143+
let plist = format!(
144+
r#"<?xml version="1.0" encoding="UTF-8"?>
145+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
146+
<plist version="1.0">
147+
<dict>
148+
<key>Label</key>
149+
<string>{label}</string>
150+
<key>ProgramArguments</key>
151+
<array>
152+
<string>{exe}</string>
153+
<string>--config</string>
154+
<string>{config}</string>
155+
<string>{flag}</string>
156+
<string>gui</string>
157+
</array>
158+
<key>RunAtLoad</key>
159+
<true/>
160+
<key>ProcessType</key>
161+
<string>Interactive</string>
162+
</dict>
163+
</plist>
164+
"#,
165+
label = AUTOSTART_LABEL,
166+
exe = xml_escape(&exe),
167+
config = xml_escape(&config),
168+
flag = AUTOSTART_FLAG,
169+
);
170+
171+
fs::write(&plist_path, plist)
172+
.with_context(|| format!("failed to write {}", plist_path.display()))?;
173+
174+
let _ = std::process::Command::new("launchctl")
175+
.args(["unload", &plist_path.to_string_lossy()])
176+
.output();
177+
let _ = std::process::Command::new("launchctl")
178+
.args(["load", &plist_path.to_string_lossy()])
179+
.output();
180+
Ok(())
181+
}
182+
183+
#[cfg(target_os = "macos")]
184+
fn platform_disable() -> Result<()> {
185+
use std::fs;
186+
187+
let plist_path = macos_plist_path()?;
188+
if plist_path.exists() {
189+
let _ = std::process::Command::new("launchctl")
190+
.args(["unload", &plist_path.to_string_lossy()])
191+
.output();
192+
fs::remove_file(&plist_path)
193+
.with_context(|| format!("failed to remove {}", plist_path.display()))?;
194+
}
195+
Ok(())
196+
}
197+
198+
#[cfg(target_os = "macos")]
199+
fn platform_is_enabled() -> Result<bool> {
200+
Ok(macos_plist_path()
201+
.map(|path| path.exists())
202+
.unwrap_or(false))
203+
}
204+
205+
#[cfg(target_os = "macos")]
206+
fn macos_plist_path() -> Result<PathBuf> {
207+
let home = home_dir().context("failed to resolve user home directory")?;
208+
Ok(home
209+
.join("Library")
210+
.join("LaunchAgents")
211+
.join(format!("{AUTOSTART_LABEL}.plist")))
212+
}
213+
214+
#[cfg(target_os = "macos")]
215+
fn xml_escape(value: &str) -> String {
216+
value
217+
.replace('&', "&amp;")
218+
.replace('<', "&lt;")
219+
.replace('>', "&gt;")
220+
}
221+
222+
#[cfg(target_os = "linux")]
223+
fn platform_enable(exe: &Path, config_path: &Path) -> Result<()> {
224+
use std::fs;
225+
226+
let desktop_path = linux_desktop_path()?;
227+
if let Some(parent) = desktop_path.parent() {
228+
fs::create_dir_all(parent)
229+
.with_context(|| format!("failed to create {}", parent.display()))?;
230+
}
231+
232+
let exec = format!(
233+
"{} --config {} {} gui",
234+
desktop_quote(&exe.to_string_lossy()),
235+
desktop_quote(&config_path.to_string_lossy()),
236+
AUTOSTART_FLAG,
237+
);
238+
239+
let contents = format!(
240+
"[Desktop Entry]\nType=Application\nName={name}\nExec={exec}\nIcon=linuxdo-accelerator\nTerminal=false\nStartupNotify=false\nX-GNOME-Autostart-enabled=true\n",
241+
name = AUTOSTART_DISPLAY_NAME,
242+
exec = exec,
243+
);
244+
245+
fs::write(&desktop_path, contents)
246+
.with_context(|| format!("failed to write {}", desktop_path.display()))?;
247+
Ok(())
248+
}
249+
250+
#[cfg(target_os = "linux")]
251+
fn platform_disable() -> Result<()> {
252+
use std::fs;
253+
254+
let desktop_path = linux_desktop_path()?;
255+
if desktop_path.exists() {
256+
fs::remove_file(&desktop_path)
257+
.with_context(|| format!("failed to remove {}", desktop_path.display()))?;
258+
}
259+
Ok(())
260+
}
261+
262+
#[cfg(target_os = "linux")]
263+
fn platform_is_enabled() -> Result<bool> {
264+
Ok(linux_desktop_path()
265+
.map(|path| path.exists())
266+
.unwrap_or(false))
267+
}
268+
269+
#[cfg(target_os = "linux")]
270+
fn linux_desktop_path() -> Result<PathBuf> {
271+
let config_home = std::env::var_os("XDG_CONFIG_HOME")
272+
.map(PathBuf::from)
273+
.or_else(|| home_dir().map(|home| home.join(".config")))
274+
.context("failed to resolve user config directory")?;
275+
Ok(config_home
276+
.join("autostart")
277+
.join("linuxdo-accelerator.desktop"))
278+
}
279+
280+
#[cfg(target_os = "linux")]
281+
fn desktop_quote(value: &str) -> String {
282+
let escaped = value.replace('\\', "\\\\").replace('"', "\\\"");
283+
format!("\"{escaped}\"")
284+
}
285+
286+
#[cfg(any(target_os = "macos", target_os = "linux"))]
287+
fn home_dir() -> Option<PathBuf> {
288+
std::env::var_os("HOME").map(PathBuf::from)
289+
}
290+
291+
#[cfg(not(any(target_os = "windows", target_os = "macos", target_os = "linux")))]
292+
fn platform_enable(_exe: &Path, _config_path: &Path) -> Result<()> {
293+
bail!("autostart is not supported on this platform")
294+
}
295+
296+
#[cfg(not(any(target_os = "windows", target_os = "macos", target_os = "linux")))]
297+
fn platform_disable() -> Result<()> {
298+
bail!("autostart is not supported on this platform")
299+
}
300+
301+
#[cfg(not(any(target_os = "windows", target_os = "macos", target_os = "linux")))]
302+
fn platform_is_enabled() -> Result<bool> {
303+
Ok(false)
304+
}

src/cli.rs

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ use std::path::PathBuf;
33
use anyhow::Result;
44
use clap::{Parser, Subcommand};
55

6+
use crate::autostart;
67
use crate::config::AppConfig;
78
#[cfg(any(windows, target_os = "linux", target_os = "macos"))]
89
use crate::gui;
@@ -15,6 +16,10 @@ pub struct Cli {
1516
#[arg(long)]
1617
pub config: Option<PathBuf>,
1718

19+
/// Launch the GUI and immediately start acceleration (used by OS auto-start entries)
20+
#[arg(long)]
21+
pub autostart: bool,
22+
1823
#[command(subcommand)]
1924
pub command: Option<Command>,
2025
}
@@ -34,6 +39,10 @@ pub enum Command {
3439
RestoreHosts,
3540
UninstallCert,
3641
Cleanup,
42+
/// Register this binary to launch automatically on user login
43+
EnableAutostart,
44+
/// Remove the auto-start entry for this binary
45+
DisableAutostart,
3746
#[command(hide = true)]
3847
ConfigJson,
3948
#[command(hide = true)]
@@ -47,15 +56,17 @@ pub enum Command {
4756
}
4857

4958
pub fn run(cli: Cli) -> Result<()> {
59+
let auto_start = cli.autostart;
5060
match cli.command {
5161
None | Some(Command::Gui) => {
5262
#[cfg(any(windows, target_os = "linux", target_os = "macos"))]
5363
{
5464
let config_path = service::init_config(cli.config.clone())?;
55-
gui::run(config_path)?;
65+
gui::run(config_path, auto_start)?;
5666
}
5767
#[cfg(target_os = "android")]
5868
{
69+
let _ = auto_start;
5970
anyhow::bail!("GUI is not supported on Android yet; use CLI subcommands");
6071
}
6172
}
@@ -105,6 +116,15 @@ pub fn run(cli: Cli) -> Result<()> {
105116
service::cleanup(cli.config)?;
106117
println!("cleanup complete");
107118
}
119+
Some(Command::EnableAutostart) => {
120+
let config_path = service::init_config(cli.config)?;
121+
autostart::enable(&config_path)?;
122+
println!("autostart enabled");
123+
}
124+
Some(Command::DisableAutostart) => {
125+
autostart::disable()?;
126+
println!("autostart disabled");
127+
}
108128
Some(Command::ConfigJson) => {
109129
let paths = service::resolve_paths(cli.config)?;
110130
let config = AppConfig::load_or_create(&paths.config_path)?;

src/config.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ pub struct AppConfig {
3939
pub ca_common_name: String,
4040
#[serde(default = "default_server_common_name")]
4141
pub server_common_name: String,
42+
#[serde(default)]
43+
pub autostart: bool,
4244
}
4345

4446
#[derive(Debug, Clone, Serialize, Deserialize)]
@@ -181,6 +183,7 @@ impl AppConfig {
181183
.unwrap_or_else(default_certificate_domains),
182184
ca_common_name: legacy.ca_common_name,
183185
server_common_name: legacy.server_common_name,
186+
autostart: false,
184187
};
185188

186189
let serialized =

0 commit comments

Comments
 (0)