From 250baccf5b7e3477ff9c23f12a716c71ebea4a1c Mon Sep 17 00:00:00 2001
From: Soulter <905617992@qq.com>
Date: Fri, 22 May 2026 15:37:38 +0800
Subject: [PATCH] feat(service): update Windows service command to use
PowerShell and enhance task XML configuration
---
astrbot/cli/commands/cmd_service.py | 48 +++++++++++++++++++++++------
tests/test_cli_service.py | 26 ++++++++++++----
2 files changed, 59 insertions(+), 15 deletions(-)
diff --git a/astrbot/cli/commands/cmd_service.py b/astrbot/cli/commands/cmd_service.py
index 28a13298d..91e28f2dd 100644
--- a/astrbot/cli/commands/cmd_service.py
+++ b/astrbot/cli/commands/cmd_service.py
@@ -1,3 +1,4 @@
+import base64
import copy
import getpass
import json
@@ -334,8 +335,8 @@ def _windows_service_log_paths(service_name: str) -> tuple[Path, Path]:
return log_dir / f"{service_name}.out.log", log_dir / f"{service_name}.err.log"
-def _windows_cmd_executable() -> str:
- return os.environ.get("COMSPEC") or "cmd.exe"
+def _windows_powershell_executable() -> str:
+ return "powershell.exe"
def _quote_windows_cmd_arg(value: Path | str) -> str:
@@ -343,14 +344,43 @@ def _quote_windows_cmd_arg(value: Path | str) -> str:
return f'"{escaped}"'
-def _build_windows_cmd_arguments(service_name: str, executable: Path) -> str:
+def _quote_powershell_literal(value: Path | str) -> str:
+ escaped = str(value).replace("'", "''")
+ return f"'{escaped}'"
+
+
+def _build_windows_cmd_line(service_name: str, executable: Path) -> str:
out_log, err_log = _windows_service_log_paths(service_name)
return (
- "/d /c "
- f'"{_quote_windows_cmd_arg(executable)} run '
+ f"{_quote_windows_cmd_arg(executable)} run "
f">> {_quote_windows_cmd_arg(out_log)} "
f"2>> {_quote_windows_cmd_arg(err_log)}"
- '"'
+ )
+
+
+def _build_windows_powershell_arguments(
+ service_name: str,
+ executable: Path,
+ workdir: Path,
+) -> str:
+ script = (
+ "$ErrorActionPreference = 'Stop'\n"
+ "$env:PYTHONUNBUFFERED = '1'\n"
+ "$cmdExe = if ($env:COMSPEC) { $env:COMSPEC } else { 'cmd.exe' }\n"
+ f"$astrbotCommand = {_quote_powershell_literal(_build_windows_cmd_line(service_name, executable))}\n"
+ "$process = Start-Process "
+ "-FilePath $cmdExe "
+ "-ArgumentList @('/d', '/c', $astrbotCommand) "
+ f"-WorkingDirectory {_quote_powershell_literal(workdir)} "
+ "-WindowStyle Hidden "
+ "-PassThru "
+ "-Wait\n"
+ "exit $process.ExitCode\n"
+ )
+ encoded_script = base64.b64encode(script.encode("utf-16le")).decode("ascii")
+ return (
+ "-NoLogo -NoProfile -NonInteractive -ExecutionPolicy Bypass "
+ f"-WindowStyle Hidden -EncodedCommand {encoded_script}"
)
@@ -386,7 +416,7 @@ def _build_windows_task_xml(
_task_element(settings, "RunOnlyIfNetworkAvailable", "false")
_task_element(settings, "AllowStartOnDemand", "true")
_task_element(settings, "Enabled", "true")
- _task_element(settings, "Hidden", "false")
+ _task_element(settings, "Hidden", "true")
_task_element(settings, "RunOnlyIfIdle", "false")
_task_element(settings, "WakeToRun", "false")
_task_element(settings, "ExecutionTimeLimit", "PT0S")
@@ -397,11 +427,11 @@ def _build_windows_task_xml(
actions = _task_element(task, "Actions", attrib={"Context": "Author"})
exec_action = _task_element(actions, "Exec")
- _task_element(exec_action, "Command", _windows_cmd_executable())
+ _task_element(exec_action, "Command", _windows_powershell_executable())
_task_element(
exec_action,
"Arguments",
- _build_windows_cmd_arguments(service_name, executable),
+ _build_windows_powershell_arguments(service_name, executable, workdir),
)
_task_element(exec_action, "WorkingDirectory", str(workdir))
diff --git a/tests/test_cli_service.py b/tests/test_cli_service.py
index 166becd26..a76d040ed 100644
--- a/tests/test_cli_service.py
+++ b/tests/test_cli_service.py
@@ -1,3 +1,4 @@
+import base64
import json
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path
@@ -22,6 +23,12 @@ from astrbot.cli.commands.cmd_service import (
)
+def _decode_windows_encoded_command(task_xml: str) -> str:
+ marker = "-EncodedCommand "
+ encoded_command = task_xml.split(marker, 1)[1].split("<", 1)[0]
+ return base64.b64decode(encoded_command).decode("utf-16le")
+
+
class _HealthyHandler(BaseHTTPRequestHandler):
def do_GET(self):
self.send_response(200)
@@ -134,13 +141,20 @@ def test_windows_task_xml_uses_astrbot_executable_and_working_directory():
Path("C:\\Users\\astrbot\\.local\\bin\\astrbot.exe"),
Path("C:\\Users\\astrbot\\AstrBot"),
).decode("utf-16")
+ powershell_script = _decode_windows_encoded_command(task_xml)
- assert "cmd.exe" in task_xml
- assert "C:\\Users\\astrbot\\.local\\bin\\astrbot.exe" in task_xml
- assert "run" in task_xml
- assert "astrbot.out.log" in task_xml
- assert "astrbot.err.log" in task_xml
- assert "C:\\Users\\astrbot\\AstrBot" in task_xml
+ assert "powershell.exe" in task_xml
+ assert "true" in task_xml
+ assert "-WindowStyle Hidden" in task_xml
+ assert "Start-Process" in powershell_script
+ assert "-WindowStyle Hidden" in powershell_script
+ assert "C:\\Users\\astrbot\\.local\\bin\\astrbot.exe" in powershell_script
+ assert "run" in powershell_script
+ assert "astrbot.out.log" in powershell_script
+ assert "astrbot.err.log" in powershell_script
+ assert (
+ "C:\\Users\\astrbot\\AstrBot" in task_xml
+ )
def test_load_dashboard_port_reads_cmd_config(tmp_path):