#![cfg(target_os = "windows")]
use std::path::PathBuf;
use crate::config::DaemonConfig;
use crate::lifecycle::units::InstallOptions;
const SQRYD_VERSION: &str = env!("CARGO_PKG_VERSION");
fn resolve_exe_for_opts(opts: &InstallOptions) -> (PathBuf, bool) {
if let Some(ref p) = opts.exe_path {
return (p.clone(), true);
}
match std::env::current_exe() {
Ok(p) => {
let canonical = p.canonicalize().unwrap_or(p);
(canonical, true)
}
Err(_) => (PathBuf::from("sqryd.exe"), false),
}
}
#[must_use]
pub fn generate_sc_create(cfg: &DaemonConfig, opts: &InstallOptions) -> String {
let (exe, resolved) = resolve_exe_for_opts(opts);
let exe_str = exe.to_string_lossy();
let account = opts.user.as_deref().unwrap_or("LocalSystem");
let memory_mb = cfg.memory_limit_mb;
let mut out = String::with_capacity(2048);
out.push_str(&format!(
":: sqryd version {SQRYD_VERSION}\n\
:: Generated by: sqryd install-windows\n\
:: Run this script in an elevated (Administrator) PowerShell or cmd session.\n"
));
if !resolved {
out.push_str(
":: WARNING: could not resolve sqryd.exe path — \
substitute the correct absolute path below.\n",
);
}
out.push('\n');
out.push_str(&format!(
"sc.exe create sqryd \\\n\
\tbinPath= \"\\\"{exe_str}\\\" foreground\" \\\n\
\tstart= auto \\\n\
\ttype= own \\\n\
\tobj= \"{account}\"\n\n"
));
let builtin = matches!(
account,
"LocalSystem" | "NT AUTHORITY\\LocalService" | "NT AUTHORITY\\NetworkService"
);
if !builtin {
out.push_str(&format!(
":: NOTE: \"{account}\" is not a built-in virtual account.\n\
:: Domain and local user accounts require a password argument on sc.exe create.\n\
:: Re-run the create command and append: password= <your-plaintext-password>\n\n"
));
}
out.push_str(
"sc.exe config sqryd \\\n\
\tdisplayname= \"sqry Daemon (sqryd)\"\n\n",
);
out.push_str(&format!(
"sc.exe description sqryd \\\n\
\t\"sqry semantic code-search daemon — {memory_mb} MiB memory budget. \
See https://github.com/verivus-oss/sqry for documentation.\"\n\n"
));
out.push_str(
"sc.exe failure sqryd \\\n\
\treset= 86400 \\\n\
\tactions= restart/60000/restart/120000/\"\"/0\n\n",
);
out.push_str(
":: Verify registration:\n\
:: sc.exe qc sqryd\n\
:: Start the service:\n\
:: sc.exe start sqryd\n\
:: Enable auto-start at boot (if not already):\n\
:: sc.exe config sqryd start= auto\n",
);
out
}
#[must_use]
pub fn generate_task_xml(cfg: &DaemonConfig, opts: &InstallOptions) -> String {
let (exe, resolved) = resolve_exe_for_opts(opts);
let exe_str_raw = exe.to_string_lossy();
let exe_str = xml_escape(&exe_str_raw);
let memory_mb = cfg.memory_limit_mb;
let trigger_user_id_elem = opts
.user
.as_deref()
.map(|u| format!(" <UserId>{}</UserId>\n", xml_escape(u)))
.unwrap_or_default();
let principal_user_id_elem = opts
.user
.as_deref()
.map(|u| format!(" <UserId>{}</UserId>\n", xml_escape(u)))
.unwrap_or_default();
let warning_comment = if resolved {
String::new()
} else {
" <!-- WARNING: could not resolve sqryd.exe path — substitute the correct absolute path in <Command> below. -->\n".to_string()
};
format!(
r#"<?xml version="1.0" encoding="UTF-8"?>
<!-- sqryd version {SQRYD_VERSION} -->
<!-- Generated by: sqryd install-windows -->
<!-- Import with: schtasks.exe /Create /TN "\sqry\sqryd" /XML sqryd-task.xml /F -->
<!-- Memory budget: {memory_mb} MiB (from DaemonConfig.memory_limit_mb) -->
<Task version="1.4" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
<RegistrationInfo>
<Description>sqry semantic code-search daemon — persistent graph service ({memory_mb} MiB budget). See https://github.com/verivus-oss/sqry</Description>
<Author>sqry / verivus-oss</Author>
<Version>{SQRYD_VERSION}</Version>
</RegistrationInfo>
<Triggers>
<LogonTrigger>
<Enabled>true</Enabled>
{trigger_user_id_elem} </LogonTrigger>
</Triggers>
<Principals>
<Principal id="Author">
{principal_user_id_elem} <LogonType>InteractiveToken</LogonType>
<RunLevel>HighestAvailable</RunLevel>
</Principal>
</Principals>
<Settings>
<MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy>
<DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries>
<StopIfGoingOnBatteries>false</StopIfGoingOnBatteries>
<AllowHardTerminate>true</AllowHardTerminate>
<StartWhenAvailable>false</StartWhenAvailable>
<RunOnlyIfNetworkAvailable>false</RunOnlyIfNetworkAvailable>
<IdleSettings>
<StopOnIdleEnd>false</StopOnIdleEnd>
<RestartOnIdle>false</RestartOnIdle>
</IdleSettings>
<AllowStartOnDemand>true</AllowStartOnDemand>
<Enabled>true</Enabled>
<Hidden>false</Hidden>
<RunOnlyIfIdle>false</RunOnlyIfIdle>
<WakeToRun>false</WakeToRun>
<ExecutionTimeLimit>PT0S</ExecutionTimeLimit>
<Priority>7</Priority>
</Settings>
<Actions Context="Author">
{warning_comment} <Exec>
<Command>{exe_str}</Command>
<Arguments>foreground</Arguments>
</Exec>
</Actions>
</Task>
"#
)
}
fn xml_escape(s: &str) -> String {
let mut out = String::with_capacity(s.len() + 16);
for ch in s.chars() {
match ch {
'&' => out.push_str("&"),
'<' => out.push_str("<"),
'>' => out.push_str(">"),
'"' => out.push_str("""),
'\'' => out.push_str("'"),
other => out.push(other),
}
}
out
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::DaemonConfig;
use crate::lifecycle::units::InstallOptions;
fn fixture_cfg() -> DaemonConfig {
DaemonConfig::default()
}
#[test]
fn windows_install_sc_create_contains_binpath_and_start_auto() {
let cfg = fixture_cfg();
let opts = InstallOptions::default();
let output = generate_sc_create(&cfg, &opts);
assert!(
output.contains("sc.exe create sqryd"),
"expected 'sc.exe create sqryd' in output:\n{output}"
);
assert!(
output.contains("start= auto"),
"expected 'start= auto' in output:\n{output}"
);
assert!(
output.contains("type= own"),
"expected 'type= own' in output:\n{output}"
);
assert!(
output.contains("obj= \"LocalSystem\""),
"expected 'obj= \"LocalSystem\"' in output:\n{output}"
);
}
#[test]
fn windows_install_sc_create_contains_display_name_and_description() {
let cfg = fixture_cfg();
let opts = InstallOptions::default();
let output = generate_sc_create(&cfg, &opts);
assert!(
output.contains("sc.exe config sqryd"),
"expected 'sc.exe config sqryd' in output:\n{output}"
);
assert!(
output.contains("displayname=") || output.contains("displayname= "),
"expected displayname in output:\n{output}"
);
assert!(
output.contains("sc.exe description sqryd"),
"expected 'sc.exe description sqryd' in output:\n{output}"
);
let memory_mb = cfg.memory_limit_mb.to_string();
assert!(
output.contains(&memory_mb),
"expected memory budget '{memory_mb}' in description:\n{output}"
);
}
#[test]
fn windows_install_sc_create_contains_recovery_options() {
let cfg = fixture_cfg();
let opts = InstallOptions::default();
let output = generate_sc_create(&cfg, &opts);
assert!(
output.contains("sc.exe failure sqryd"),
"expected 'sc.exe failure sqryd' in output:\n{output}"
);
assert!(
output.contains("reset= 86400"),
"expected 'reset= 86400' in output:\n{output}"
);
assert!(
output.contains("restart/60000"),
"expected 'restart/60000' in output:\n{output}"
);
assert!(
output.contains("restart/120000"),
"expected 'restart/120000' in output:\n{output}"
);
}
#[test]
fn windows_install_sc_create_uses_custom_account_when_provided() {
let cfg = fixture_cfg();
let opts = InstallOptions {
user: Some("NT AUTHORITY\\NetworkService".to_string()),
exe_path: None,
};
let output = generate_sc_create(&cfg, &opts);
assert!(
output.contains("\"NT AUTHORITY\\NetworkService\""),
"expected quoted custom account in output:\n{output}"
);
assert!(
!output.contains("\"LocalSystem\""),
"default LocalSystem must not appear when custom account is set:\n{output}"
);
}
#[test]
fn windows_install_sc_create_contains_version_stamp() {
let cfg = fixture_cfg();
let opts = InstallOptions::default();
let output = generate_sc_create(&cfg, &opts);
assert!(
output.contains(SQRYD_VERSION),
"expected version stamp '{SQRYD_VERSION}' in output:\n{output}"
);
}
#[test]
fn windows_install_task_xml_is_valid_xml() {
let cfg = fixture_cfg();
let opts = InstallOptions::default();
let output = generate_task_xml(&cfg, &opts);
assert!(
output.starts_with(r#"<?xml version="1.0""#),
"expected XML declaration at start:\n{output}"
);
assert!(
output.contains("<Task "),
"expected <Task ...> root element:\n{output}"
);
assert!(
output.contains("</Task>"),
"expected </Task> closing tag:\n{output}"
);
assert!(
output.contains("http://schemas.microsoft.com/windows/2004/02/mit/task"),
"expected Task Scheduler 2.0 namespace:\n{output}"
);
}
#[test]
fn windows_install_task_xml_contains_logon_trigger() {
let cfg = fixture_cfg();
let opts = InstallOptions::default();
let output = generate_task_xml(&cfg, &opts);
assert!(
output.contains("<LogonTrigger>"),
"expected <LogonTrigger> in output:\n{output}"
);
assert!(
output.contains("</LogonTrigger>"),
"expected </LogonTrigger> in output:\n{output}"
);
assert!(
output.contains("<Enabled>true</Enabled>"),
"expected enabled trigger:\n{output}"
);
}
#[test]
fn windows_install_task_xml_omits_user_id_when_none() {
let cfg = fixture_cfg();
let opts = InstallOptions::default();
let output = generate_task_xml(&cfg, &opts);
assert!(
!output.contains("<UserId>"),
"expected no <UserId> element when opts.user is None:\n{output}"
);
assert!(
output.contains("<LogonTrigger>"),
"expected <LogonTrigger> present:\n{output}"
);
assert!(
output.contains("<Enabled>true</Enabled>"),
"expected trigger enabled:\n{output}"
);
}
#[test]
fn windows_install_task_xml_uses_custom_user_when_provided() {
let cfg = fixture_cfg();
let opts = InstallOptions {
user: Some("DOMAIN\\alice".to_string()),
exe_path: None,
};
let output = generate_task_xml(&cfg, &opts);
assert!(
output.contains("<UserId>DOMAIN\\alice</UserId>"),
"expected custom UserId in output:\n{output}"
);
assert!(
!output.contains("%USERNAME%"),
"%USERNAME% must never appear in generated XML:\n{output}"
);
}
#[test]
fn windows_install_task_xml_contains_exec_action_with_foreground() {
let cfg = fixture_cfg();
let opts = InstallOptions::default();
let output = generate_task_xml(&cfg, &opts);
assert!(
output.contains("<Exec>"),
"expected <Exec> action:\n{output}"
);
assert!(
output.contains("<Arguments>foreground</Arguments>"),
"expected <Arguments>foreground</Arguments>:\n{output}"
);
}
#[test]
fn windows_install_task_xml_contains_unlimited_execution_time_limit() {
let cfg = fixture_cfg();
let opts = InstallOptions::default();
let output = generate_task_xml(&cfg, &opts);
assert!(
output.contains("<ExecutionTimeLimit>PT0S</ExecutionTimeLimit>"),
"expected unlimited ExecutionTimeLimit PT0S:\n{output}"
);
}
#[test]
fn windows_install_task_xml_ignores_battery_state() {
let cfg = fixture_cfg();
let opts = InstallOptions::default();
let output = generate_task_xml(&cfg, &opts);
assert!(
output.contains("<DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries>"),
"expected DisallowStartIfOnBatteries=false:\n{output}"
);
assert!(
output.contains("<StopIfGoingOnBatteries>false</StopIfGoingOnBatteries>"),
"expected StopIfGoingOnBatteries=false:\n{output}"
);
}
#[test]
fn windows_install_task_xml_sets_ignore_new_multiple_instances_policy() {
let cfg = fixture_cfg();
let opts = InstallOptions::default();
let output = generate_task_xml(&cfg, &opts);
assert!(
output.contains("<MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy>"),
"expected IgnoreNew MultipleInstancesPolicy:\n{output}"
);
}
#[test]
fn windows_install_task_xml_contains_version_stamp() {
let cfg = fixture_cfg();
let opts = InstallOptions::default();
let output = generate_task_xml(&cfg, &opts);
assert!(
output.contains(SQRYD_VERSION),
"expected version stamp '{SQRYD_VERSION}' in output:\n{output}"
);
}
#[test]
fn windows_install_task_xml_contains_run_level_highest_available() {
let cfg = fixture_cfg();
let opts = InstallOptions::default();
let output = generate_task_xml(&cfg, &opts);
assert!(
output.contains("<RunLevel>HighestAvailable</RunLevel>"),
"expected RunLevel=HighestAvailable:\n{output}"
);
}
#[test]
fn windows_install_emits_sc_create_and_scheduler_xml() {
let cfg = fixture_cfg();
let opts = InstallOptions::default();
let sc = generate_sc_create(&cfg, &opts);
let xml = generate_task_xml(&cfg, &opts);
assert!(
!sc.trim_start().starts_with("<?xml"),
"sc_create output must not be XML:\n{sc}"
);
assert!(
!xml.contains("sc.exe"),
"task_xml output must not contain sc.exe commands:\n{xml}"
);
assert!(
sc.contains(SQRYD_VERSION),
"sc_create missing version:\n{sc}"
);
assert!(
xml.contains(SQRYD_VERSION),
"task_xml missing version:\n{xml}"
);
}
#[test]
fn windows_install_task_xml_is_utf8_not_utf16() {
let cfg = fixture_cfg();
let opts = InstallOptions::default();
let output = generate_task_xml(&cfg, &opts);
assert!(
output.contains(r#"encoding="UTF-8""#),
"expected UTF-8 encoding declaration:\n{output}"
);
assert!(
!output.contains("UTF-16"),
"must not claim UTF-16 encoding in a UTF-8 String:\n{output}"
);
}
#[test]
fn xml_escape_handles_special_chars() {
assert_eq!(xml_escape("a&b"), "a&b");
assert_eq!(xml_escape("a<b>c"), "a<b>c");
assert_eq!(xml_escape(r#"a"b'c"#), "a"b'c");
assert_eq!(xml_escape("normal"), "normal");
assert_eq!(
xml_escape("C:\\Program Files\\sqryd.exe"),
"C:\\Program Files\\sqryd.exe"
);
}
#[test]
fn windows_install_task_xml_escapes_special_chars_in_exe_path() {
let cfg = fixture_cfg();
let opts = InstallOptions {
user: None,
exe_path: Some(std::path::PathBuf::from("C:\\Prog&s\\sqry<d>.exe")),
};
let output = generate_task_xml(&cfg, &opts);
assert!(
!output.contains("Prog&s"),
"unescaped '&' must not appear in XML output:\n{output}"
);
assert!(
output.contains("Prog&s"),
"expected '&' escaped form in XML output:\n{output}"
);
}
#[test]
fn windows_install_task_xml_escapes_special_chars_in_user_id() {
let cfg = fixture_cfg();
let opts = InstallOptions {
user: Some("DOM&AIN\\ali<ce>".to_string()),
exe_path: None,
};
let output = generate_task_xml(&cfg, &opts);
assert!(
!output.contains("DOM&AIN"),
"unescaped '&' in user must not appear in XML:\n{output}"
);
assert!(
output.contains("DOM&AIN"),
"expected & in escaped UserId:\n{output}"
);
}
}