自动化更新 Windows 服务的实践与思路

为团队提供一个共享的开发环境,能在代码更新时自动拉取最新代码、构建并启动服务,同时轻量化、易维护。本文记录了从需求到最终实现 Windows 上自动更新服务的全过程,包括问题分析、脚本优化与定时执行的思路。


目标

最初的目标是:

为团队提供一个共享的开发环境,能在代码更新时自动拉取最新代码、构建并启动服务,同时轻量化、易维护。


1. 技术选型思路

起初考虑使用轻量化容器:

  • 优点:隔离性好、可快速部署、便于版本管理
  • 缺点:宿主机无公网 IP,Webhook 调用难以实现

最终决定:

  • Windows 本地批处理 + 定时任务
  • 原因:
    • 不依赖公网
    • 可以在本地轻量化运行
    • 可以通过脚本检查 Git 更新,判断是否拉取和重启服务

2. 初始脚本设计

核心功能包括:

  1. Git 拉取最新代码
  2. Maven 构建
  3. 启动服务

示例核心脚本结构:

1
2
3
4
5
6
@echo off
set "JAR_NAME=xxx-admin.jar"
set "JAVA_OPTS=..."
call mvn clean
call mvn package -DskipTests
java -jar %JAVA_OPTS% "%JAR_NAME%"

3. 自动更新脚本(auto-update.bat)

核心思路

  1. 防止并发:使用锁文件 %TEMP%\auto_update.lock
  2. 检查代码更新:
    • git fetch origin dev
    • 对比 git rev-parse HEAD 与 git rev-parse origin/dev
    • 仅在有更新时继续执行
  3. 停止旧服务:
    • 获取服务 PID 并逐个关闭
    • 无论成功还是失败都写入日志,方便观察服务状态
  4. 拉取代码并启动服务:
    • git pull --rebase
    • 使用 start run-service.bat 独立启动服务,保证批处理自身可以结束并清理锁文件

4. 遇到的问题与解决思路

问题 核心原因 解决方案
多次运行提示“Another instance is running” 原脚本使用 call run-service.bat,批处理会等待服务运行结束,锁文件未释放 改用 start run-service.bat 独立启动服务,批处理自身可以结束并清理锁文件
停止旧服务时,观察不到具体结果 taskkill 原来没有区分成功/失败的日志 修改为 每个 PID 的操作都记录日志,成功和失败都输出,方便观察服务状态
Git 更新判断 需要避免无更新也构建 对比 HEADorigin/dev,仅在 commit 不同时执行后续操作

日志示例

1
2
[2025/10/31 10:00:00] Successfully killed PID 12345 for xxx-admin.jar
[2025/10/31 10:00:00] Failed to kill PID 67890 for xxx-admin.jar (may not exist)

5. 定时执行

  • 使用 Windows 任务计划程序(Task Scheduler)
  • 配置:
    1. 每 5 分钟执行一次 auto_update.bat
    2. 系统账户运行,保证不依赖登录用户
  • 命令行创建示例:
1
schtasks /create /tn "AutoUpdateZnder" /tr "C:\Projects\znder\auto_update.bat" /sc minute /mo 5 /ru SYSTEM /f

6. 总结

  • 从轻量化容器的想法,到完全本地化自动化解决方案
  • 核心思路:
    1. 判断代码是否有更新
    2. 有更新才停止旧服务
    3. 构建并启动新服务
    4. 日志完整记录,便于问题追踪
  • Windows 本地批处理 + 任务计划程序的组合,满足了轻量、稳定、可观察的需求
  • 通过锁文件机制和 start run-service.bat 解决了“批处理被锁住”的问题
  • taskkill 日志改进,保证每次服务停止操作都可追踪

整个过程体现了逐步拆解问题、测试验证、不断优化的思路,非常适合团队共享的开发环境自动化场景。


7.完整脚本示例

  1. run-service.bat
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
@echo off
setlocal enabledelayedexpansion
REM ========== All-in-one: git pull -> clean -> package -> run ==========
REM Usage: all-in-one.bat [mode]
REM mode: fast (default) = skip tests, parallel build
REM full = run tests, single-threaded

set "JAR_NAME=xxx-admin.jar"
set "JAVA_OPTS=-Xms256m -Xmx1024m -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=512m --add-opens java.base/java.lang=ALL-UNNAMED --add-opens java.base/java.lang.reflect=ALL-UNNAMED"
set "MAVEN_THREADS=4C"

set "MODE=%~1"
if "%MODE%"=="" set "MODE=fast"

echo ==================================================
echo Starting all-in-one script. Mode: %MODE%
echo %DATE% %TIME%
echo ==================================================

pushd "%~dp0"

echo [1/4] Git pull...
git branch
git pull --rebase

echo [2/4] Maven clean...
cd ..
call mvn clean

echo [3/4] Maven package...
if /I "%MODE%"=="fast" (
call mvn package -DskipTests -T %MAVEN_THREADS%
) else (
call mvn package
)

echo [4/4] Running application in admin target directory...
cd /d "%~dp0../xxx-admin/target"
if errorlevel 1 (
echo ERROR: Failed to cd into ../xxx-admin/target
goto end
)

if not exist "%JAR_NAME%" (
echo ERROR: %JAR_NAME% not found in %CD%
goto end
)

echo Running %JAR_NAME% ...
java -jar %JAVA_OPTS% "%JAR_NAME%"
if errorlevel 1 (
echo Application exited with error code %errorlevel%.
goto end
)

:end
popd
echo.
echo Script finished. Press any key to exit...
pause >nul
  1. auto-update.bat
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
@echo off
setlocal enabledelayedexpansion

REM ================== 配置区 ==================
set "BASE_DIR=%~dp0"
set "LOCK_FILE=%TEMP%\auto_update.lock"
set "LOG_FILE=%BASE_DIR%auto_update.log"
set "JAR_NAME=xxx-admin.jar"
set "RUN_SCRIPT=%BASE_DIR%run-service.bat"
set "BRANCH=dev"

echo ================================================== >> "%LOG_FILE%"
echo [%DATE% %TIME%] Auto update started >> "%LOG_FILE%"

REM ================== 防止并发执行 ==================
if exist "%LOCK_FILE%" (
echo [%DATE% %TIME%] Another instance is running, exiting. >> "%LOG_FILE%"
exit /b
)
echo > "%LOCK_FILE%"

pushd "%BASE_DIR%" >nul

REM ================== 检查更新 ==================
echo [%DATE% %TIME%] Checking for updates... >> "%LOG_FILE%"
git fetch origin %BRANCH% >> "%LOG_FILE%" 2>&1

for /f %%i in ('git rev-parse HEAD') do set LOCAL_COMMIT=%%i
for /f %%i in ('git rev-parse origin/%BRANCH%') do set REMOTE_COMMIT=%%i

echo [%DATE% %TIME%] Local commit : !LOCAL_COMMIT! >> "%LOG_FILE%"
echo [%DATE% %TIME%] Remote commit: !REMOTE_COMMIT! >> "%LOG_FILE%"

if "!LOCAL_COMMIT!"=="!REMOTE_COMMIT!" (
echo [%DATE% %TIME%] No updates found. Exiting. >> "%LOG_FILE%"
del "%LOCK_FILE%" >nul 2>&1
popd
endlocal
exit /b
)

echo [%DATE% %TIME%] Updates found. Proceeding... >> "%LOG_FILE%"

REM ================== 停止旧服务 ==================
echo [%DATE% %TIME%] Stopping old service (!JAR_NAME!)... >> "%LOG_FILE%"

for /f "skip=1 tokens=1" %%i in ('wmic process where "commandline like '%%!JAR_NAME!%%'" get ProcessId') do (
if not "%%i"=="" (
echo [%DATE% %TIME%] Killing process PID %%i for !JAR_NAME!... >> "%LOG_FILE%"
taskkill /PID %%i /F >> "%LOG_FILE%" 2>&1
if !errorlevel! equ 0 (
echo [%DATE% %TIME%] Successfully killed PID %%i >> "%LOG_FILE%"
) else (
echo [%DATE% %TIME%] Failed to kill PID %%i >> "%LOG_FILE%"
)
)
)

REM ================== 拉取最新代码 ==================
echo [%DATE% %TIME%] Pulling latest code... >> "%LOG_FILE%"
git pull --rebase origin %BRANCH% >> "%LOG_FILE%" 2>&1

REM ================== 启动新服务(非阻塞) ==================
if exist "%RUN_SCRIPT%" (
echo [%DATE% %TIME%] Starting new service via %RUN_SCRIPT%... >> "%LOG_FILE%"
start "" "%RUN_SCRIPT%"
) else (
echo [%DATE% %TIME%] ERROR: %RUN_SCRIPT% not found! >> "%LOG_FILE%"
)

REM ================== 清理锁文件 ==================
del "%LOCK_FILE%" >nul 2>&1

popd >nul
echo [%DATE% %TIME%] Auto update finished >> "%LOG_FILE%"
echo. >> "%LOG_FILE%"
endlocal