Skip to content

Commit af8dc64

Browse files
committed
feat(systemd-service-manager): 支持环境变量文件和用户组配置
- 添加了对 project.env、project.env.local、services/<name>.env、 services/<name>.env.local、timers/<name>.env、timers/<name>.env.local 等环境变量文件的支持 - 实现了环境变量的层级覆盖机制:<name>.env.local > <name>.env > project.env.local > project.env - 支持在 service 和 timer 配置中设置 USER 和 GROUP 字段来指定运行用户 - 更新了单元文件渲染逻辑,在 Environment= 中注入环境变量 - 为 fnm 使用场景提供了推荐的环境变量配置方式 BREAKING CHANGE: 移除了原有的全局环境变量加载方式, 改为按需合并特定服务的环境变量文件。
1 parent c5a4c28 commit af8dc64

8 files changed

Lines changed: 316 additions & 12 deletions

File tree

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

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,36 @@
2828
- `start` / `stop` / `restart` / `status` / `logs` 这类命令依赖目标 unit 已经先执行过 `install`
2929
- `install` / `start` / `stop` / `restart` / `enable` / `disable` 这类 system scope 写操作在非 root 下会自动通过 `sudo` 重新执行脚本本身,因此不要求你手工把 `sudo` 写在命令前。
3030

31+
## Environment support
32+
33+
- `project.env` / `project.env.local` 中的变量会渲染到所有 service / timer task unit 的 `Environment=`
34+
- `services/<name>.env` / `services/<name>.env.local` 会覆盖项目级变量,并只作用于对应 service。
35+
- `timers/<name>.env` / `timers/<name>.env.local` 会覆盖项目级变量,并只作用于对应 timer 生成的一次性 task service。
36+
- 优先级保持为:
37+
- `<name>.env.local`
38+
- `<name>.env`
39+
- `project.env.local`
40+
- `project.env`
41+
42+
## fnm 推荐写法
43+
44+
对于 `system` scope 的 Node / fnm 工具,推荐在 env 文件里提供稳定的 `PATH``HOME`,不要使用 `/run/user/.../fnm_multishells/...` 这类会话级临时路径。
45+
46+
推荐把下面内容写到 `project.env.local``services/<name>.env.local`
47+
48+
```dotenv
49+
HOME=/home/administrator
50+
PATH=/home/administrator/.local/share/fnm/node-versions/v24.11.0/installation/bin:/usr/local/bin:/usr/bin:/bin
51+
```
52+
53+
然后 `COMMAND` 可以直接写成:
54+
55+
```dotenv
56+
COMMAND=zread browse --host 0.0.0.0 --port 19681
57+
```
58+
59+
如果你更在意稳定性,也可以直接把 `COMMAND` 写成绝对路径,但一般优先推荐“稳定 PATH + 裸命令”这条线,可读性更好。
60+
3161
## Build
3262

3363
```bash

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

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,11 @@ ssm_cmd_install() {
2727
ssm_require_safe_name "UNIT_PREFIX" "${UNIT_PREFIX}"
2828
source_file="$(ssm_service_config_path "${project_dir}" "${SSM_RESOLVED_TARGET_NAME}")"
2929
scope="${SSM_SERVICE_SCOPE}"
30+
ssm_collect_env_entries_for_service "${project_dir}" "${SSM_RESOLVED_TARGET_NAME}"
31+
local env_block
32+
env_block="$(ssm_render_environment_lines)"
3033
local service_unit_file="${render_dir}/$(ssm_service_unit_name "${SSM_RESOLVED_TARGET_NAME}")"
31-
ssm_render_service_unit "${source_file}" >"${service_unit_file}"
34+
ssm_render_service_unit "${source_file}" "${env_block}" >"${service_unit_file}"
3235
ssm_verify_unit_file "${service_unit_file}" || ssm_die "systemd-analyze verify failed for ${service_unit_file}"
3336
if [[ "${SSM_CLI_DRY_RUN}" == "1" ]]; then
3437
printf '%s\n' "$(basename "${service_unit_file}")"
@@ -49,6 +52,9 @@ ssm_cmd_install() {
4952
ssm_require_safe_name "UNIT_PREFIX" "${UNIT_PREFIX}"
5053
source_file="$(ssm_timer_config_path "${project_dir}" "${SSM_RESOLVED_TARGET_NAME}")"
5154
scope="${SSM_TIMER_SCOPE}"
55+
ssm_collect_env_entries_for_timer "${project_dir}" "${SSM_RESOLVED_TARGET_NAME}"
56+
local env_block
57+
env_block="$(ssm_render_environment_lines)"
5258
local schedule_block
5359
schedule_block="$(ssm_resolve_schedule "${SCHEDULE}")"
5460
local task_unit_name
@@ -69,7 +75,7 @@ ssm_cmd_install() {
6975
task_exec_command="${COMMAND}"
7076
fi
7177

72-
ssm_render_task_service_unit "${source_file}" "${task_exec_command}" >"${task_unit_file}"
78+
ssm_render_task_service_unit "${source_file}" "${task_exec_command}" "${env_block}" >"${task_unit_file}"
7379
ssm_render_timer_unit "${source_file}" "${task_unit_name}" "${schedule_block}" >"${timer_unit_file}"
7480
ssm_verify_unit_file "${task_unit_file}" || ssm_die "systemd-analyze verify failed for ${task_unit_file}"
7581
ssm_verify_unit_file "${timer_unit_file}" || ssm_die "systemd-analyze verify failed for ${timer_unit_file}"

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,12 @@ ssm_cmd_list() {
1515
if [[ "${SSM_DEBUG_DUMP_CONFIG:-}" == "1" ]]; then
1616
if [[ -f "$(ssm_service_config_path "${project_dir}" "api")" ]]; then
1717
ssm_parse_service_config "${project_dir}" "api"
18+
ssm_collect_env_entries_for_service "${project_dir}" "api"
1819
fi
1920
printf 'project=%s\n' "${SSM_PROJECT_NAME}"
2021
printf 'scope=%s\n' "${SSM_SERVICE_SCOPE:-${DEFAULT_SCOPE:-system}}"
21-
printf 'APP_PORT=%s\n' "${APP_PORT:-}"
22-
printf 'APP_NAME=%s\n' "${APP_NAME:-}"
22+
printf 'APP_PORT=%s\n' "$(ssm_get_env_entry_value "APP_PORT")"
23+
printf 'APP_NAME=%s\n' "$(ssm_get_env_entry_value "APP_NAME")"
2324
return 0
2425
fi
2526

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

Lines changed: 136 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,8 +67,6 @@ ssm_load_project_config() {
6767
config_root="$(ssm_config_root "${project_dir}")"
6868

6969
ssm_load_key_value_file "${config_root}/project.conf"
70-
ssm_load_key_value_file "${config_root}/project.env"
71-
ssm_load_key_value_file "${config_root}/project.env.local"
7270

7371
SSM_PROJECT_NAME="${PROJECT_NAME:-}"
7472
UNIT_PREFIX="${UNIT_PREFIX:-${PROJECT_NAME:-project}}"
@@ -96,3 +94,139 @@ ssm_load_timer_env() {
9694
ssm_load_key_value_file "${config_root}/timers/${timer_name}.env"
9795
ssm_load_key_value_file "${config_root}/timers/${timer_name}.env.local"
9896
}
97+
98+
# 从指定 key=value 文件中读取某个 key 的最后一个值,用于解析与环境变量同名的配置键。
99+
ssm_read_key_value_from_file() {
100+
local file_path="$1"
101+
local target_key="$2"
102+
[[ -f "${file_path}" ]] || return 0
103+
104+
local line=""
105+
local key=""
106+
local value=""
107+
local found=""
108+
109+
while IFS= read -r line || [[ -n "${line}" ]]; do
110+
line="$(ssm_trim "${line}")"
111+
[[ -z "${line}" || "${line}" == \#* ]] && continue
112+
[[ "${line}" == *=* ]] || continue
113+
114+
key="$(ssm_trim "${line%%=*}")"
115+
value="$(ssm_normalize_value "${line#*=}")"
116+
if [[ "${key}" == "${target_key}" ]]; then
117+
found="${value}"
118+
fi
119+
done < "${file_path}"
120+
121+
printf '%s' "${found}"
122+
}
123+
124+
# 清空当前待渲染的环境变量集合。
125+
ssm_reset_env_entries() {
126+
SSM_ENV_KEYS=()
127+
SSM_ENV_VALUES=()
128+
}
129+
130+
# 合并同名环境变量,后出现的值覆盖先前的值。
131+
ssm_upsert_env_entry() {
132+
local key="$1"
133+
local value="$2"
134+
local index=0
135+
136+
while [[ "${index}" -lt "${#SSM_ENV_KEYS[@]}" ]]; do
137+
if [[ "${SSM_ENV_KEYS[${index}]}" == "${key}" ]]; then
138+
SSM_ENV_VALUES[${index}]="${value}"
139+
return 0
140+
fi
141+
index=$((index + 1))
142+
done
143+
144+
SSM_ENV_KEYS+=("${key}")
145+
SSM_ENV_VALUES+=("${value}")
146+
}
147+
148+
# 把 key=value 文件合并到待渲染的环境变量集合里。
149+
ssm_merge_env_file_into_entries() {
150+
local file_path="$1"
151+
[[ -f "${file_path}" ]] || return 0
152+
153+
local line=""
154+
local key=""
155+
local value=""
156+
157+
while IFS= read -r line || [[ -n "${line}" ]]; do
158+
line="$(ssm_trim "${line}")"
159+
[[ -z "${line}" || "${line}" == \#* ]] && continue
160+
[[ "${line}" == *=* ]] || ssm_die "Invalid key-value line in ${file_path}: ${line}"
161+
162+
key="$(ssm_trim "${line%%=*}")"
163+
value="$(ssm_normalize_value "${line#*=}")"
164+
165+
if [[ ! "${key}" =~ ^[A-Za-z_][A-Za-z0-9_]*$ ]]; then
166+
ssm_die "Invalid config key in ${file_path}: ${key}"
167+
fi
168+
169+
ssm_upsert_env_entry "${key}" "${value}"
170+
done < "${file_path}"
171+
}
172+
173+
# 按项目级 -> 项目 local -> service 级 -> service local 顺序收集 service 环境变量。
174+
ssm_collect_env_entries_for_service() {
175+
local project_dir="$1"
176+
local service_name="$2"
177+
local config_root
178+
config_root="$(ssm_config_root "${project_dir}")"
179+
180+
ssm_reset_env_entries
181+
ssm_merge_env_file_into_entries "${config_root}/project.env"
182+
ssm_merge_env_file_into_entries "${config_root}/project.env.local"
183+
ssm_merge_env_file_into_entries "${config_root}/services/${service_name}.env"
184+
ssm_merge_env_file_into_entries "${config_root}/services/${service_name}.env.local"
185+
}
186+
187+
# 按项目级 -> 项目 local -> timer 级 -> timer local 顺序收集 timer 环境变量。
188+
ssm_collect_env_entries_for_timer() {
189+
local project_dir="$1"
190+
local timer_name="$2"
191+
local config_root
192+
config_root="$(ssm_config_root "${project_dir}")"
193+
194+
ssm_reset_env_entries
195+
ssm_merge_env_file_into_entries "${config_root}/project.env"
196+
ssm_merge_env_file_into_entries "${config_root}/project.env.local"
197+
ssm_merge_env_file_into_entries "${config_root}/timers/${timer_name}.env"
198+
ssm_merge_env_file_into_entries "${config_root}/timers/${timer_name}.env.local"
199+
}
200+
201+
# 从当前已收集的环境变量集合里读取某个 key 的值,便于调试输出。
202+
ssm_get_env_entry_value() {
203+
local target_key="$1"
204+
local index=0
205+
206+
while [[ "${index}" -lt "${#SSM_ENV_KEYS[@]}" ]]; do
207+
if [[ "${SSM_ENV_KEYS[${index}]}" == "${target_key}" ]]; then
208+
printf '%s' "${SSM_ENV_VALUES[${index}]}"
209+
return 0
210+
fi
211+
index=$((index + 1))
212+
done
213+
}
214+
215+
# 对 Environment= 值做最小转义,避免双引号和反斜杠破坏 unit 语法。
216+
ssm_escape_unit_env_value() {
217+
local value="$1"
218+
value="${value//\\/\\\\}"
219+
value="${value//\"/\\\"}"
220+
printf '%s' "${value}"
221+
}
222+
223+
# 把当前环境变量集合渲染成 systemd Environment= 行。
224+
ssm_render_environment_lines() {
225+
local index=0
226+
while [[ "${index}" -lt "${#SSM_ENV_KEYS[@]}" ]]; do
227+
printf 'Environment="%s=%s"\n' \
228+
"${SSM_ENV_KEYS[${index}]}" \
229+
"$(ssm_escape_unit_env_value "${SSM_ENV_VALUES[${index}]}")"
230+
index=$((index + 1))
231+
done
232+
}

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

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,19 @@ ssm_parse_service_config() {
1616

1717
ssm_load_project_config "${project_dir}"
1818
ssm_load_key_value_file "${service_file}"
19-
ssm_load_service_env "${project_dir}" "${service_name}"
2019

2120
[[ -n "${COMMAND:-}" ]] || ssm_die "Missing COMMAND in ${service_file}"
2221

2322
SSM_SERVICE_NAME="${service_name}"
2423
SSM_SERVICE_SCOPE="${SCOPE:-${DEFAULT_SCOPE:-system}}"
24+
SSM_SERVICE_RUN_USER="$(ssm_read_key_value_from_file "${service_file}" "USER")"
25+
SSM_SERVICE_RUN_GROUP="$(ssm_read_key_value_from_file "${service_file}" "GROUP")"
26+
27+
if [[ -z "${SSM_SERVICE_RUN_USER}" ]]; then
28+
SSM_SERVICE_RUN_USER="${DEFAULT_USER:-}"
29+
fi
30+
31+
if [[ -z "${SSM_SERVICE_RUN_GROUP}" ]]; then
32+
SSM_SERVICE_RUN_GROUP="${DEFAULT_GROUP:-}"
33+
fi
2534
}

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

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ ssm_parse_timer_config() {
1616

1717
ssm_load_project_config "${project_dir}"
1818
ssm_load_key_value_file "${timer_file}"
19-
ssm_load_timer_env "${project_dir}" "${timer_name}"
2019

2120
[[ -n "${TARGET_TYPE:-}" ]] || ssm_die "Missing TARGET_TYPE in ${timer_file}"
2221
[[ -n "${SCHEDULE:-}" ]] || ssm_die "Missing SCHEDULE in ${timer_file}"
@@ -34,4 +33,14 @@ ssm_parse_timer_config() {
3433

3534
SSM_TIMER_NAME="${timer_name}"
3635
SSM_TIMER_SCOPE="${SCOPE:-${DEFAULT_SCOPE:-system}}"
36+
SSM_TIMER_RUN_USER="$(ssm_read_key_value_from_file "${timer_file}" "USER")"
37+
SSM_TIMER_RUN_GROUP="$(ssm_read_key_value_from_file "${timer_file}" "GROUP")"
38+
39+
if [[ -z "${SSM_TIMER_RUN_USER}" ]]; then
40+
SSM_TIMER_RUN_USER="${DEFAULT_USER:-}"
41+
fi
42+
43+
if [[ -z "${SSM_TIMER_RUN_GROUP}" ]]; then
44+
SSM_TIMER_RUN_GROUP="${DEFAULT_GROUP:-}"
45+
fi
3746
}

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

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ SSM_RENDER_SERVICE_LOADED=1
88
# 渲染常驻 service unit,保持最小字段集合和统一 managed header。
99
ssm_render_service_unit() {
1010
local source_file="$1"
11+
local env_block="${2:-}"
1112
cat <<EOF
1213
# Managed by systemd-service-manager
1314
# Source: ${source_file}
@@ -19,9 +20,10 @@ ${WANTS:+Wants=${WANTS}}
1920
[Service]
2021
Type=simple
2122
WorkingDirectory=${WORKDIR:-${DEFAULT_WORKDIR:-/tmp}}
23+
${env_block}
2224
ExecStart=${COMMAND}
23-
${USER:+User=${USER}}
24-
${GROUP:+Group=${GROUP}}
25+
${SSM_SERVICE_RUN_USER:+User=${SSM_SERVICE_RUN_USER}}
26+
${SSM_SERVICE_RUN_GROUP:+Group=${SSM_SERVICE_RUN_GROUP}}
2527
Restart=${RESTART:-on-failure}
2628
RestartSec=${RESTART_SEC:-5s}
2729
@@ -34,6 +36,7 @@ EOF
3436
ssm_render_task_service_unit() {
3537
local source_file="$1"
3638
local exec_command="$2"
39+
local env_block="${3:-}"
3740
cat <<EOF
3841
# Managed by systemd-service-manager
3942
# Source: ${source_file}
@@ -43,8 +46,9 @@ Description=${DESCRIPTION:-${SSM_TIMER_NAME}}
4346
[Service]
4447
Type=oneshot
4548
WorkingDirectory=${WORKDIR:-${DEFAULT_WORKDIR:-/tmp}}
49+
${env_block}
4650
ExecStart=${exec_command}
47-
${USER:+User=${USER}}
48-
${GROUP:+Group=${GROUP}}
51+
${SSM_TIMER_RUN_USER:+User=${SSM_TIMER_RUN_USER}}
52+
${SSM_TIMER_RUN_GROUP:+Group=${SSM_TIMER_RUN_GROUP}}
4953
EOF
5054
}

0 commit comments

Comments
 (0)