Fix Windows scheduled startup failures

This commit is contained in:
2026-09-07 13:15:18 +08:00
parent 44ce5e655d
commit 9778981191
6 changed files with 62 additions and 4 deletions
+3
View File
@@ -6,6 +6,9 @@ cd /d "%~dp0"
set "LOG_DIR=%~dp0logs" set "LOG_DIR=%~dp0logs"
set "LOG_FILE=%LOG_DIR%\autostart.log" set "LOG_FILE=%LOG_DIR%\autostart.log"
set "DOCUTRANSLATE_NONINTERACTIVE=1" set "DOCUTRANSLATE_NONINTERACTIVE=1"
set "DOCUTRANSLATE_STRICT_PORT=1"
set "PYTHONUTF8=1"
set "PYTHONIOENCODING=utf-8"
if not exist "%LOG_DIR%" mkdir "%LOG_DIR%" if not exist "%LOG_DIR%" mkdir "%LOG_DIR%"
+8 -1
View File
@@ -10,6 +10,7 @@ import os
import re import re
import shutil import shutil
import socket import socket
import sys
import tempfile import tempfile
import time import time
import uuid import uuid
@@ -2654,6 +2655,11 @@ def run_app(host=None, port: int | None = None, enable_CORS=False,
try: try:
port_to_use = find_free_port(initial_port) port_to_use = find_free_port(initial_port)
if port_to_use != 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"端口 {initial_port} 被占用,将使用端口 {port_to_use} 代替")
print(f"正在启动 DocuTranslate WebUI 版本号:{__version__}") print(f"正在启动 DocuTranslate WebUI 版本号:{__version__}")
app.state.port_to_use = port_to_use 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) uvicorn.run(app, host=host, port=port_to_use, workers=1)
except Exception as e: except Exception as e:
print(f"启动失败: {e}") print(f"启动失败: {e}", file=sys.stderr)
raise
if __name__ == "__main__": if __name__ == "__main__":
+13 -1
View File
@@ -4,7 +4,19 @@ import argparse
import sys # 用于检查命令行参数数量 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(): def main():
_configure_stdio()
parser = argparse.ArgumentParser( parser = argparse.ArgumentParser(
description="DocuTranslate: 一个文档翻译工具。", description="DocuTranslate: 一个文档翻译工具。",
# 更新示例,展示如何使用 host 参数 # 更新示例,展示如何使用 host 参数
@@ -88,4 +100,4 @@ def main():
if __name__ == "__main__": if __name__ == "__main__":
main() main()
+7 -2
View File
@@ -4,6 +4,11 @@ setlocal
REM 切换到脚本所在目录 REM 切换到脚本所在目录
cd /d "%~dp0" cd /d "%~dp0"
REM 后台计划任务默认可能使用 cp1252,强制使用 UTF-8 输出日志
chcp 65001 >nul
set "PYTHONUTF8=1"
set "PYTHONIOENCODING=utf-8"
REM 检查 .venv 是否存在 REM 检查 .venv 是否存在
if not exist ".venv\Scripts\activate.bat" ( if not exist ".venv\Scripts\activate.bat" (
echo [ERROR] 未找到 .venv\Scripts\activate.bat echo [ERROR] 未找到 .venv\Scripts\activate.bat
@@ -16,9 +21,9 @@ call ".venv\Scripts\activate.bat"
REM 如果传了参数,就用参数;否则默认用当前目录 REM 如果传了参数,就用参数;否则默认用当前目录
if "%~1"=="" ( 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 ( ) 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% set EXIT_CODE=%ERRORLEVEL%
+3
View File
@@ -28,6 +28,7 @@ try {
-StartWhenAvailable ` -StartWhenAvailable `
-MultipleInstances IgnoreNew ` -MultipleInstances IgnoreNew `
-ExecutionTimeLimit ([TimeSpan]::Zero) ` -ExecutionTimeLimit ([TimeSpan]::Zero) `
-AllowStartIfOnBatteries `
-DontStopIfGoingOnBatteries ` -DontStopIfGoingOnBatteries `
-RestartCount 3 ` -RestartCount 3 `
-RestartInterval (New-TimeSpan -Minutes 1) -RestartInterval (New-TimeSpan -Minutes 1)
@@ -36,6 +37,7 @@ try {
-StartWhenAvailable ` -StartWhenAvailable `
-MultipleInstances IgnoreNew ` -MultipleInstances IgnoreNew `
-ExecutionTimeLimit ([TimeSpan]::Zero) ` -ExecutionTimeLimit ([TimeSpan]::Zero) `
-AllowStartIfOnBatteries `
-DontStopIfGoingOnBatteries -DontStopIfGoingOnBatteries
} }
@@ -57,6 +59,7 @@ $WatchdogSettings = New-ScheduledTaskSettingsSet `
-StartWhenAvailable ` -StartWhenAvailable `
-MultipleInstances IgnoreNew ` -MultipleInstances IgnoreNew `
-ExecutionTimeLimit (New-TimeSpan -Minutes 2) ` -ExecutionTimeLimit (New-TimeSpan -Minutes 2) `
-AllowStartIfOnBatteries `
-DontStopIfGoingOnBatteries -DontStopIfGoingOnBatteries
Register-ScheduledTask ` Register-ScheduledTask `
+28
View File
@@ -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()