From 9778981191de35cea12b269e340dba00b68879ff Mon Sep 17 00:00:00 2001 From: Leon Date: Mon, 7 Sep 2026 13:15:18 +0800 Subject: [PATCH] Fix Windows scheduled startup failures --- autostart_runner.bat | 3 +++ docutranslate/app.py | 9 ++++++++- docutranslate/cli.py | 14 +++++++++++++- run.bat | 9 +++++++-- setup_autostart.ps1 | 3 +++ tests/test_service_startup.py | 28 ++++++++++++++++++++++++++++ 6 files changed, 62 insertions(+), 4 deletions(-) create mode 100644 tests/test_service_startup.py diff --git a/autostart_runner.bat b/autostart_runner.bat index 69fb0b4..99e4e45 100644 --- a/autostart_runner.bat +++ b/autostart_runner.bat @@ -6,6 +6,9 @@ cd /d "%~dp0" set "LOG_DIR=%~dp0logs" set "LOG_FILE=%LOG_DIR%\autostart.log" set "DOCUTRANSLATE_NONINTERACTIVE=1" +set "DOCUTRANSLATE_STRICT_PORT=1" +set "PYTHONUTF8=1" +set "PYTHONIOENCODING=utf-8" if not exist "%LOG_DIR%" mkdir "%LOG_DIR%" diff --git a/docutranslate/app.py b/docutranslate/app.py index 92fc904..3149cde 100644 --- a/docutranslate/app.py +++ b/docutranslate/app.py @@ -10,6 +10,7 @@ import os import re import shutil import socket +import sys import tempfile import time import uuid @@ -2654,6 +2655,11 @@ def run_app(host=None, port: int | None = None, enable_CORS=False, try: port_to_use = find_free_port(initial_port) if port_to_use != initial_port: + strict_port = os.environ.get( + "DOCUTRANSLATE_STRICT_PORT", "" + ).strip().lower() in {"1", "true", "yes", "on"} + if strict_port: + raise RuntimeError(f"端口 {initial_port} 已被占用") print(f"端口 {initial_port} 被占用,将使用端口 {port_to_use} 代替") print(f"正在启动 DocuTranslate WebUI 版本号:{__version__}") app.state.port_to_use = port_to_use @@ -2668,7 +2674,8 @@ def run_app(host=None, port: int | None = None, enable_CORS=False, ) uvicorn.run(app, host=host, port=port_to_use, workers=1) except Exception as e: - print(f"启动失败: {e}") + print(f"启动失败: {e}", file=sys.stderr) + raise if __name__ == "__main__": diff --git a/docutranslate/cli.py b/docutranslate/cli.py index a047fe2..64efd1c 100644 --- a/docutranslate/cli.py +++ b/docutranslate/cli.py @@ -4,7 +4,19 @@ import argparse import sys # 用于检查命令行参数数量 +def _configure_stdio() -> None: + """Keep redirected Windows service logs Unicode-safe.""" + for stream in (sys.stdout, sys.stderr): + reconfigure = getattr(stream, "reconfigure", None) + if callable(reconfigure): + try: + reconfigure(encoding="utf-8", errors="backslashreplace") + except (OSError, ValueError): + pass + + def main(): + _configure_stdio() parser = argparse.ArgumentParser( description="DocuTranslate: 一个文档翻译工具。", # 更新示例,展示如何使用 host 参数 @@ -88,4 +100,4 @@ def main(): if __name__ == "__main__": - main() \ No newline at end of file + main() diff --git a/run.bat b/run.bat index a3c4ce2..2cdfe67 100644 --- a/run.bat +++ b/run.bat @@ -4,6 +4,11 @@ setlocal REM 切换到脚本所在目录 cd /d "%~dp0" +REM 后台计划任务默认可能使用 cp1252,强制使用 UTF-8 输出日志 +chcp 65001 >nul +set "PYTHONUTF8=1" +set "PYTHONIOENCODING=utf-8" + REM 检查 .venv 是否存在 if not exist ".venv\Scripts\activate.bat" ( echo [ERROR] 未找到 .venv\Scripts\activate.bat @@ -16,9 +21,9 @@ call ".venv\Scripts\activate.bat" REM 如果传了参数,就用参数;否则默认用当前目录 if "%~1"=="" ( - python docutranslate/cli.py -i --host 0.0.0.0 + ".venv\Scripts\python.exe" -u docutranslate/cli.py -i --host 0.0.0.0 ) else ( - python docutranslate/cli.py -i --host 0.0.0.0 %* + ".venv\Scripts\python.exe" -u docutranslate/cli.py -i --host 0.0.0.0 %* ) set EXIT_CODE=%ERRORLEVEL% diff --git a/setup_autostart.ps1 b/setup_autostart.ps1 index e4d1e42..5e4f52f 100644 --- a/setup_autostart.ps1 +++ b/setup_autostart.ps1 @@ -28,6 +28,7 @@ try { -StartWhenAvailable ` -MultipleInstances IgnoreNew ` -ExecutionTimeLimit ([TimeSpan]::Zero) ` + -AllowStartIfOnBatteries ` -DontStopIfGoingOnBatteries ` -RestartCount 3 ` -RestartInterval (New-TimeSpan -Minutes 1) @@ -36,6 +37,7 @@ try { -StartWhenAvailable ` -MultipleInstances IgnoreNew ` -ExecutionTimeLimit ([TimeSpan]::Zero) ` + -AllowStartIfOnBatteries ` -DontStopIfGoingOnBatteries } @@ -57,6 +59,7 @@ $WatchdogSettings = New-ScheduledTaskSettingsSet ` -StartWhenAvailable ` -MultipleInstances IgnoreNew ` -ExecutionTimeLimit (New-TimeSpan -Minutes 2) ` + -AllowStartIfOnBatteries ` -DontStopIfGoingOnBatteries Register-ScheduledTask ` diff --git a/tests/test_service_startup.py b/tests/test_service_startup.py new file mode 100644 index 0000000..1aaaceb --- /dev/null +++ b/tests/test_service_startup.py @@ -0,0 +1,28 @@ +import os +import unittest +from unittest.mock import patch + +from docutranslate import app + + +class ServiceStartupTest(unittest.TestCase): + @patch.dict(os.environ, {"DOCUTRANSLATE_STRICT_PORT": "1"}, clear=False) + @patch.object(app, "find_free_port", return_value=8011) + def test_strict_port_rejects_occupied_configured_port(self, _find_free_port): + with self.assertRaisesRegex(RuntimeError, "端口 8010 已被占用"): + app.run_app(host="127.0.0.1", port=8010) + + @patch.dict(os.environ, {"DOCUTRANSLATE_STRICT_PORT": ""}, clear=False) + @patch.object(app.uvicorn, "run") + @patch.object(app, "find_free_port", return_value=8011) + def test_interactive_mode_can_still_fall_back_to_free_port( + self, _find_free_port, uvicorn_run + ): + app.run_app(host="127.0.0.1", port=8010) + uvicorn_run.assert_called_once_with( + app.app, host="127.0.0.1", port=8011, workers=1 + ) + + +if __name__ == "__main__": + unittest.main()