Skip to content

Commit c7bc0e1

Browse files
committed
feat(systemd-service-manager): install命令增强支持智能重启和状态检测
- install --start 在目标已运行时自动使用restart,让新配置立即生效 - 添加ssm_unit_activity_state函数读取unit活动状态 - 添加ssm_is_running_like_state函数判断运行状态 - 添加ssm_write_unit_file函数处理unit文件写入并返回状态(created/updated/unchanged) - 重构install命令逻辑,支持在配置更新时提示需要重启才能生效 - 增加相关测试用例验证unchanged状态和restart行为
1 parent cfb106d commit c7bc0e1

5 files changed

Lines changed: 168 additions & 15 deletions

File tree

scripts/bash/systemd-service-manager/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727

2828
- `start` / `stop` / `restart` / `status` / `logs` 这类命令依赖目标 unit 已经先执行过 `install`
2929
- `install` / `start` / `stop` / `restart` / `enable` / `disable` 这类 system scope 写操作在非 root 下会自动通过 `sudo` 重新执行脚本本身,因此不要求你手工把 `sudo` 写在命令前。
30+
- `install --start` 在目标已运行时会自动改用 `restart`,避免“配置文件已覆盖,但运行中的旧进程仍未生效”。
3031

3132
## Environment support
3233

scripts/bash/systemd-service-manager/commands/install.sh

Lines changed: 44 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -30,22 +30,34 @@ ssm_cmd_install() {
3030
ssm_collect_env_entries_for_service "${project_dir}" "${SSM_RESOLVED_TARGET_NAME}"
3131
local env_block
3232
env_block="$(ssm_render_environment_lines)"
33-
local service_unit_file="${render_dir}/$(ssm_service_unit_name "${SSM_RESOLVED_TARGET_NAME}")"
33+
local service_unit_name
34+
service_unit_name="$(ssm_service_unit_name "${SSM_RESOLVED_TARGET_NAME}")"
35+
local service_unit_file="${render_dir}/${service_unit_name}"
36+
local destination_unit_file
37+
destination_unit_file="$(ssm_unit_dir_for_scope "${scope}")/${service_unit_name}"
38+
local previous_active_state
39+
previous_active_state="$(ssm_unit_activity_state "${scope}" "${service_unit_name}")"
3440
ssm_render_service_unit "${source_file}" "${env_block}" >"${service_unit_file}"
3541
ssm_verify_unit_file "${service_unit_file}" || ssm_die "systemd-analyze verify failed for ${service_unit_file}"
3642
if [[ "${SSM_CLI_DRY_RUN}" == "1" ]]; then
3743
printf '%s\n' "$(basename "${service_unit_file}")"
3844
return 0
3945
fi
40-
mkdir -p "$(ssm_unit_dir_for_scope "${scope}")"
41-
cp "${service_unit_file}" "$(ssm_unit_dir_for_scope "${scope}")/"
46+
ssm_write_unit_file "${service_unit_file}" "${destination_unit_file}"
4247
ssm_daemon_reload "${scope}"
43-
printf 'installed=%s\n' "$(ssm_service_unit_name "${SSM_RESOLVED_TARGET_NAME}")"
48+
printf '%s=%s\n' "${SSM_WRITE_STATUS}" "${service_unit_name}"
4449
if [[ "${SSM_CLI_START_AFTER_INSTALL}" == "1" ]]; then
45-
ssm_systemctl "${scope}" start "$(ssm_service_unit_name "${SSM_RESOLVED_TARGET_NAME}")"
46-
printf 'started=%s\n' "$(ssm_service_unit_name "${SSM_RESOLVED_TARGET_NAME}")"
50+
if ssm_is_running_like_state "${previous_active_state}"; then
51+
ssm_systemctl "${scope}" restart "${service_unit_name}"
52+
printf 'restarted=%s\n' "${service_unit_name}"
53+
else
54+
ssm_systemctl "${scope}" start "${service_unit_name}"
55+
printf 'started=%s\n' "${service_unit_name}"
56+
fi
57+
elif [[ "${SSM_WRITE_STATUS}" == "updated" ]] && ssm_is_running_like_state "${previous_active_state}"; then
58+
printf 'note=配置已更新,运行中的 unit 需要 restart 才会生效\n'
4759
fi
48-
ssm_print_unit_summary "${SSM_RESOLVED_TARGET_NAME}" "${scope}" "$(ssm_service_unit_name "${SSM_RESOLVED_TARGET_NAME}")"
60+
ssm_print_unit_summary "${SSM_RESOLVED_TARGET_NAME}" "${scope}" "${service_unit_name}"
4961
;;
5062
timer)
5163
ssm_parse_timer_config "${project_dir}" "${SSM_RESOLVED_TARGET_NAME}"
@@ -60,7 +72,15 @@ ssm_cmd_install() {
6072
local task_unit_name
6173
task_unit_name="$(ssm_timer_task_unit_name "${SSM_RESOLVED_TARGET_NAME}")"
6274
local task_unit_file="${render_dir}/${task_unit_name}"
63-
local timer_unit_file="${render_dir}/$(ssm_timer_unit_name "${SSM_RESOLVED_TARGET_NAME}")"
75+
local timer_unit_name
76+
timer_unit_name="$(ssm_timer_unit_name "${SSM_RESOLVED_TARGET_NAME}")"
77+
local timer_unit_file="${render_dir}/${timer_unit_name}"
78+
local destination_task_unit_file
79+
destination_task_unit_file="$(ssm_unit_dir_for_scope "${scope}")/${task_unit_name}"
80+
local destination_timer_unit_file
81+
destination_timer_unit_file="$(ssm_unit_dir_for_scope "${scope}")/${timer_unit_name}"
82+
local previous_active_state
83+
previous_active_state="$(ssm_unit_activity_state "${scope}" "${timer_unit_name}")"
6484
local task_exec_command=""
6585

6686
if [[ "${TARGET_TYPE}" == "service" ]]; then
@@ -86,16 +106,25 @@ ssm_cmd_install() {
86106
return 0
87107
fi
88108

89-
mkdir -p "$(ssm_unit_dir_for_scope "${scope}")"
90-
cp "${task_unit_file}" "$(ssm_unit_dir_for_scope "${scope}")/"
91-
cp "${timer_unit_file}" "$(ssm_unit_dir_for_scope "${scope}")/"
109+
ssm_write_unit_file "${task_unit_file}" "${destination_task_unit_file}"
110+
local task_write_status="${SSM_WRITE_STATUS}"
111+
ssm_write_unit_file "${timer_unit_file}" "${destination_timer_unit_file}"
112+
local timer_write_status="${SSM_WRITE_STATUS}"
92113
ssm_daemon_reload "${scope}"
93-
printf 'installed=%s\n' "$(ssm_timer_unit_name "${SSM_RESOLVED_TARGET_NAME}")"
114+
printf '%s=%s\n' "${timer_write_status}" "${timer_unit_name}"
115+
printf '%s=%s\n' "${task_write_status}" "${task_unit_name}"
94116
if [[ "${SSM_CLI_START_AFTER_INSTALL}" == "1" ]]; then
95-
ssm_systemctl "${scope}" start "$(ssm_timer_unit_name "${SSM_RESOLVED_TARGET_NAME}")"
96-
printf 'started=%s\n' "$(ssm_timer_unit_name "${SSM_RESOLVED_TARGET_NAME}")"
117+
if ssm_is_running_like_state "${previous_active_state}"; then
118+
ssm_systemctl "${scope}" restart "${timer_unit_name}"
119+
printf 'restarted=%s\n' "${timer_unit_name}"
120+
else
121+
ssm_systemctl "${scope}" start "${timer_unit_name}"
122+
printf 'started=%s\n' "${timer_unit_name}"
123+
fi
124+
elif [[ "${timer_write_status}" == "updated" ]] && ssm_is_running_like_state "${previous_active_state}"; then
125+
printf 'note=配置已更新,运行中的 unit 需要 restart 才会生效\n'
97126
fi
98-
ssm_print_unit_summary "${SSM_RESOLVED_TARGET_NAME}" "${scope}" "$(ssm_timer_unit_name "${SSM_RESOLVED_TARGET_NAME}")"
127+
ssm_print_unit_summary "${SSM_RESOLVED_TARGET_NAME}" "${scope}" "${timer_unit_name}"
99128
;;
100129
*)
101130
ssm_die "Unknown install target kind: ${target_kind}"

scripts/bash/systemd-service-manager/lib/cli.sh

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ Target syntax:
3737
Notes:
3838
start 前需要目标 unit 已经 install
3939
system scope 的写操作在非 root 下会自动通过 sudo 重新执行
40+
install --start 在目标已运行时会自动使用 restart,让新配置立即生效
4041
4142
Examples:
4243
systemd-service-manager init

scripts/bash/systemd-service-manager/lib/systemd.sh

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,46 @@ ssm_is_unit_installed() {
6767
[[ -f "$(ssm_unit_dir_for_scope "${scope}")/${unit_name}" ]] && printf 'true' || printf 'false'
6868
}
6969

70+
# 读取当前 unit 的原始 active 状态,供 install 决定 start 还是 restart。
71+
ssm_unit_activity_state() {
72+
local scope="$1"
73+
local unit_name="$2"
74+
ssm_systemctl "${scope}" is-active "${unit_name}" 2>/dev/null || true
75+
}
76+
77+
# 判断当前状态是否属于“已经在运行或正在启动”的集合。
78+
ssm_is_running_like_state() {
79+
local state="$1"
80+
case "${state}" in
81+
active | activating | reloading)
82+
return 0
83+
;;
84+
*)
85+
return 1
86+
;;
87+
esac
88+
}
89+
90+
# 把生成好的 unit 写入目标目录,并返回 created/updated/unchanged 状态。
91+
ssm_write_unit_file() {
92+
local source_file="$1"
93+
local destination_file="$2"
94+
mkdir -p "$(dirname "${destination_file}")"
95+
96+
if [[ -f "${destination_file}" ]]; then
97+
if cmp -s "${source_file}" "${destination_file}"; then
98+
SSM_WRITE_STATUS="unchanged"
99+
return 0
100+
fi
101+
cp "${source_file}" "${destination_file}"
102+
SSM_WRITE_STATUS="updated"
103+
return 0
104+
fi
105+
106+
cp "${source_file}" "${destination_file}"
107+
SSM_WRITE_STATUS="installed"
108+
}
109+
70110
# 把 service/timer 目标解析成 scope 与最终 unit 名,供 lifecycle 命令复用。
71111
ssm_resolve_target_spec() {
72112
local project_dir="$1"

scripts/bash/systemd-service-manager/tests/install.test.ts

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ describe('install command', () => {
9797
expect(
9898
fs.readFileSync(path.join(workspace.root, 'systemctl.log'), 'utf8'),
9999
).toContain('daemon-reload')
100+
expect(result.stdout).toContain('installed=myapp-cleanup.timer')
100101
})
101102

102103
it('renders merged environment and default user/group into service units', async () => {
@@ -158,6 +159,44 @@ describe('install command', () => {
158159
expect(unitText).toContain('Environment="APP_NAME=dataflow"')
159160
})
160161

162+
it('prints unchanged when the rendered unit content is identical', async () => {
163+
const workspace = createWorkspace()
164+
workspaces.push(workspace)
165+
166+
installMockCommand(
167+
workspace,
168+
'systemd-analyze',
169+
'#!/usr/bin/env bash\nexit 0\n',
170+
)
171+
installMockCommand(
172+
workspace,
173+
'systemctl',
174+
'#!/usr/bin/env bash\nexit 0\n',
175+
)
176+
177+
const projectRoot = path.join(
178+
workspace.managerHome,
179+
'tests',
180+
'fixtures',
181+
'project-basic',
182+
)
183+
184+
const first = await runSource(
185+
workspace,
186+
['install', 'api', '--project', projectRoot],
187+
{ SSM_TEST_EUID: '0' },
188+
)
189+
const second = await runSource(
190+
workspace,
191+
['install', 'api', '--project', projectRoot],
192+
{ SSM_TEST_EUID: '0' },
193+
)
194+
195+
expect(first.exitCode).toBe(0)
196+
expect(second.exitCode).toBe(0)
197+
expect(second.stdout).toContain('unchanged=myapp-api.service')
198+
})
199+
161200
it('renders merged environment into timer task units', async () => {
162201
const workspace = createWorkspace()
163202
workspaces.push(workspace)
@@ -279,6 +318,49 @@ describe('install command', () => {
279318
expect(fs.readFileSync(systemctlLog, 'utf8')).toContain('start myapp-api.service')
280319
})
281320

321+
it('restarts instead of start when install --start targets an already active unit', async () => {
322+
const workspace = createWorkspace()
323+
workspaces.push(workspace)
324+
325+
const systemctlLog = path.join(workspace.root, 'systemctl.log')
326+
installMockCommand(
327+
workspace,
328+
'systemd-analyze',
329+
'#!/usr/bin/env bash\nexit 0\n',
330+
)
331+
installMockCommand(
332+
workspace,
333+
'systemctl',
334+
[
335+
'#!/usr/bin/env bash',
336+
'printf "%s\\n" "$*" >>"${SSM_SYSTEMCTL_LOG}"',
337+
'if [[ "$1" == "is-active" ]]; then printf "active\\n"; fi',
338+
'if [[ "$1" == "is-enabled" ]]; then printf "enabled\\n"; fi',
339+
'exit 0',
340+
].join('\n'),
341+
)
342+
343+
const projectRoot = path.join(
344+
workspace.managerHome,
345+
'tests',
346+
'fixtures',
347+
'project-basic',
348+
)
349+
350+
const result = await runSource(
351+
workspace,
352+
['install', 'api', '--project', projectRoot, '--start'],
353+
{
354+
SSM_TEST_EUID: '0',
355+
SSM_SYSTEMCTL_LOG: systemctlLog,
356+
},
357+
)
358+
359+
expect(result.exitCode).toBe(0)
360+
expect(fs.readFileSync(systemctlLog, 'utf8')).toContain('restart myapp-api.service')
361+
expect(result.stdout).toContain('restarted=myapp-api.service')
362+
})
363+
282364
it('auto-elevates system-scope install when not root', async () => {
283365
const workspace = createWorkspace()
284366
workspaces.push(workspace)

0 commit comments

Comments
 (0)