homeway-server 0.0.1 → 2026.10.4-03b2e7d6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1 +1,23 @@
1
1
  # homeway-server
2
+
3
+ Homeway server binaries for linux-amd64 and linux-arm64, plus the host installer
4
+ `install-release.sh`. Binaries are xz-compressed; the installer decompresses only the ones a host runs.
5
+ It is not a library; there is nothing to `require()`.
6
+
7
+ | Path | Contents |
8
+ |---|---|
9
+ | `bin/linux-<arch>/tunneld.xz` | control plane |
10
+ | `bin/linux-<arch>/ingress-agent.xz` | ingress agent (runs sing-box) |
11
+ | `bin/linux-<arch>/sing-box.xz` | sing-box built from the pinned tag with Homeway's patches |
12
+ | `bin/linux-<arch>/relay.xz` | SNI / Host relay |
13
+ | `build-info.json` | version, commit, build time, sing-box version and commit |
14
+ | `SHA256SUMS` | checksums of every file above |
15
+
16
+ Every `main` commit that passes CI is tagged `vYYYY.M.D-<8-char-commit>` and
17
+ published here without the leading `v`, e.g. `homeway-server@2026.10.4-abcdef12`.
18
+ A host installs a version with `install-release.sh <version|latest>`, which
19
+ verifies the registry integrity and `SHA256SUMS`, stages only the binaries the
20
+ host needs, restarts its services and rolls back on its own if they do not come
21
+ up.
22
+
23
+ `0.0.1` is an empty placeholder that reserved the name.
package/SHA256SUMS ADDED
@@ -0,0 +1,10 @@
1
+ 504310bb0c5ff137a544b606e58394a1c6d3c0228d9b8fa8be72db437a9ceb38 bin/linux-amd64/ingress-agent.xz
2
+ d6adc9b44ea6fa4fb1e73b4dae04c6d7c6b1f4b9e81f71ab0fddf19ebc3f4985 bin/linux-amd64/relay.xz
3
+ 1139312b7d8c78a9d6fff2f0fb01a50a59c432dc5062b98d93f88d73df456d7e bin/linux-amd64/sing-box.xz
4
+ 27061b92d1c44efda1b2a94e63adb6bdbcdb2b9a418e89770f461623aca5d855 bin/linux-amd64/tunneld.xz
5
+ d61cdfcdca5935975ac2dfb235a8aa8e95635e918103a01367bf735e49d2a682 bin/linux-arm64/ingress-agent.xz
6
+ b7dc55cb19b89fbe41dfd881ac9b46417cc838513c4bb54fff57ac4d4de0dcac bin/linux-arm64/relay.xz
7
+ a7edec3b3f5a571bcd5594f1a74a5fa16332c65644b3f23fc528da63ce2de4c0 bin/linux-arm64/sing-box.xz
8
+ b0cf93a5d2d40f95820b389f6ff5f27f8d6478cc47f50b742d7e186177ed6c12 bin/linux-arm64/tunneld.xz
9
+ 38df2c9df98088eba3586a57a43725f8871e2da9ee22108a1ed3e6f7375750ca build-info.json
10
+ 6dc73b7783acb3edd159035d5d5d84d011a27985010db0adc42033f266fd79a5 install-release.sh
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
@@ -0,0 +1,9 @@
1
+ {
2
+ "version": "2026.10.4-03b2e7d6",
3
+ "commit": "03b2e7d627f1797246ecb113b2c3a2f149f16663",
4
+ "buildTime": "2026-10-04T11:38:46Z",
5
+ "singBox": {
6
+ "version": "v1.14.1",
7
+ "commit": "1ac1a339cb1223e9c70eae14c44411c75033c02d"
8
+ }
9
+ }
@@ -0,0 +1,610 @@
1
+ #!/bin/bash
2
+ # 把本机切到 homeway-server 的另一个发布 (或本机仍保留的旧发布),确认各组件起来了;没起来就自动回滚。
3
+ #
4
+ # install-release.sh <version|installed-release-name|latest>
5
+ # install-release.sh --adopt-legacy # 一次性迁移:把旧布局的二进制收为 releases/legacy 并指向 current
6
+ # install-release.sh --request [auto|<file>] # homeway-release.service 运行:执行组件写的发布请求 (管理后台发布)
7
+ #
8
+ # --request:组件 (tunneld、ingress-agent、relay) 以非特权用户运行,收到管理后台的发布目标后只在自己的状态目录写
9
+ # release-request.json;homeway-release.path 看到后以 root 启动 homeway-release.service 运行本脚本。请求来自非特权进程,
10
+ # 当作不可信输入:只认固定路径 (control /var/lib/homeway,ingress / relay 为 DynamicUser 的 /var/lib/private/homeway-*),
11
+ # 必须是普通文件 (不是链接)、属主为非 root 且与所在状态目录的属主相同 (control 另须为 homeway)、目录不可被组或他人写;
12
+ # 内容只取 .target 且必须是发布版本号 (不接受 latest 与已安装的其他名字)。先删除全部请求文件 (path 单元在文件存在时
13
+ # 一直触发),组件须在本机 COMPONENTS 里,之后与手动安装该版本相同。
14
+ #
15
+ # 本机跑哪些组件由 /etc/homeway/release.conf 的 COMPONENTS 决定 (control / ingress / relay,空格分隔):
16
+ # sg: COMPONENTS="control ingress" hk: COMPONENTS="ingress" jd1–jd4: COMPONENTS="relay"
17
+ #
18
+ # 版本从 npm registry (默认 npmmirror,国内主机也快) 下载 homeway-server 包,核对 registry 给的 sha512
19
+ # integrity、包内 SHA256SUMS 与 build-info.json 的版本,只把本机架构、本机组件需要的二进制放到
20
+ # /opt/homeway/releases/<version>/,先逐个执行 -version 证明能在本机运行,再原子切换 /opt/homeway/current。
21
+ # 本机已有的发布直接切换,不访问网络。
22
+ #
23
+ # 切换前,组件含 control 时用正在运行的 tunneld 备份数据库 (新二进制启动时会先迁移);切换后按
24
+ # control → ingress → relay 的顺序重启并逐个验证:
25
+ # control /readyz 就绪且 /version 的 commit 与包一致
26
+ # ingress 单元 active、主进程来自新发布、TCP 39843 在监听、与控制面同步成功 (日志里没有同步错误)
27
+ # relay 单元 active、主进程来自新发布、TCP 39880 在监听
28
+ # 任一失败则切回上一个发布并重启全部组件。未 enable 的单元 (还没有令牌的入口) 只切换不启动。
29
+ # 目标就是当前发布时,只重启还没跑在它上面的组件与 HOMEWAY_RESTART="control ingress" 点名的组件。
30
+ # 保留最新 3 个发布 (HOMEWAY_KEEP_RELEASES);current 与上一个 (回滚目标) 即使更旧也保留。
31
+ #
32
+ # 环境变量:HOMEWAY_NPM_REGISTRY (默认 release.conf 的 NPM_REGISTRY,再默认 https://registry.npmmirror.com)、
33
+ # HOMEWAY_RESTART、HOMEWAY_INGRESS_REQUIRE_SYNC=0 (控制面不可用时跳过入口的同步检查)、
34
+ # HOMEWAY_STATE_ROOT (请求文件所在的 /var/lib,测试用)。
35
+ #
36
+ # 以 root 运行。进度与最近一次的日志在 /var/lib/homeway-release/{status.json,job.log}。
37
+ # 依赖 curl、jq、openssl、flock、sha256sum、ss、systemd-run。
38
+ set -Eeuo pipefail
39
+ umask 022
40
+
41
+ root=${HOMEWAY_ROOT:-/opt/homeway}
42
+ conf_file=${HOMEWAY_RELEASE_CONF:-/etc/homeway/release.conf}
43
+ status_dir=${HOMEWAY_RELEASE_STATUS_DIR:-/var/lib/homeway-release}
44
+ keep_releases=${HOMEWAY_KEEP_RELEASES:-3}
45
+ package=homeway-server
46
+ releases_root=$root/releases
47
+ installed_script=$root/bin/install-release.sh
48
+ state_root=${HOMEWAY_STATE_ROOT:-/var/lib}
49
+
50
+ # 与 scripts/release-version.sh 一致
51
+ version_pattern='^[0-9]{4}\.([1-9]|1[0-2])\.([1-9]|[12][0-9]|3[01])-[0-9a-f]{8}$'
52
+ name_pattern='^[0-9A-Za-z][0-9A-Za-z._-]{0,79}$'
53
+
54
+ # 控制面 (tunneld) 的服务用户、环境与数据目录,与 deploy/control-plane 一致
55
+ tunneld_user=homeway
56
+ tunneld_env=/etc/homeway/tunneld.env
57
+ tunneld_home=/var/lib/homeway
58
+ ingress_port=39843
59
+ relay_port=39880
60
+
61
+ log() { printf '%s %s\n' "$(date -u +%H:%M:%S)" "$*"; }
62
+
63
+ # 配置文件里一个值。按行匹配,不 source。
64
+ conf_value() { # file name default
65
+ local value
66
+ value=$(sed -n "s/^[[:space:]]*$2=//p" "$1" 2>/dev/null | tail -n 1)
67
+ value=${value%\"}; value=${value#\"}; value=${value%\'}; value=${value#\'}
68
+ echo "${value:-$3}"
69
+ }
70
+
71
+ # ---- 组件 ----------------------------------------------------------------
72
+ unit_of() {
73
+ case $1 in
74
+ control) echo homeway-tunneld.service ;;
75
+ ingress) echo homeway-ingress.service ;;
76
+ relay) echo homeway-relay.service ;;
77
+ esac
78
+ }
79
+ binaries_of() {
80
+ case $1 in
81
+ control) echo tunneld ;;
82
+ ingress) echo ingress-agent sing-box ;;
83
+ relay) echo relay ;;
84
+ esac
85
+ }
86
+
87
+ # --- 任务状态 -------------------------------------------------------------
88
+ job_target="" job_release="" job_previous="" job_started=""
89
+ write_status() { # state message
90
+ [ -d "$status_dir" ] || return 0
91
+ local finished=""
92
+ case $1 in running) ;; *) finished=$(date -u +%Y-%m-%dT%H:%M:%SZ) ;; esac
93
+ jq -n --arg state "$1" --arg message "$2" --arg target "$job_target" \
94
+ --arg release "$job_release" --arg previous "$job_previous" \
95
+ --arg started "$job_started" --arg finished "$finished" \
96
+ '{state:$state,message:$message,target:$target,release:$release,previous:$previous}
97
+ + (if $started == "" then {} else {started_at:$started} end)
98
+ + (if $finished == "" then {} else {finished_at:$finished} end)' \
99
+ > "$status_dir/status.json.next"
100
+ chmod 0644 "$status_dir/status.json.next"
101
+ mv -f "$status_dir/status.json.next" "$status_dir/status.json"
102
+ }
103
+
104
+ die() {
105
+ trap - ERR
106
+ log "error: $*"
107
+ write_status failed "$*"
108
+ exit 1
109
+ }
110
+
111
+ switch_to() { # release-dir
112
+ ln -sfn "$1" "$root/current.next"
113
+ mv -Tf "$root/current.next" "$root/current"
114
+ }
115
+
116
+ # ---- 发布请求 (--request) ------------------------------------------------
117
+ # 组件的状态目录:control 为 homeway 用户的 StateDirectory;ingress / relay 为 DynamicUser,真实目录在 private/ 下
118
+ request_file_of() { # component
119
+ case $1 in
120
+ control) echo "$state_root/homeway/release-request.json" ;;
121
+ ingress) echo "$state_root/private/homeway-ingress/release-request.json" ;;
122
+ relay) echo "$state_root/private/homeway-relay/release-request.json" ;;
123
+ esac
124
+ }
125
+
126
+ # 校验一个请求文件并输出其中的版本号;不合格返回 1 (原因写标准错误,不回显文件内容)。
127
+ read_request() { # file component
128
+ local file=$1 dir uid mode value
129
+ dir=$(dirname "$file")
130
+ if [ -L "$file" ] || [ ! -f "$file" ]; then echo "$2: request is not a regular file" >&2; return 1; fi
131
+ if [ -L "$dir" ] || [ ! -d "$dir" ]; then echo "$2: request directory is a link" >&2; return 1; fi
132
+ uid=$(stat -c %u "$file")
133
+ if [ "$uid" = 0 ] || [ "$uid" != "$(stat -c %u "$dir")" ]; then
134
+ echo "$2: request is not owned by the service user that owns $dir" >&2
135
+ return 1
136
+ fi
137
+ if [ "$2" = control ] && [ "$(stat -c %U "$file")" != "$tunneld_user" ]; then
138
+ echo "$2: request is not owned by $tunneld_user" >&2
139
+ return 1
140
+ fi
141
+ mode=$(stat -c %a "$dir")
142
+ if (((8#$mode & 8#022) != 0)); then echo "$2: $dir is writable by others" >&2; return 1; fi
143
+ if [ "$(stat -c %h "$file")" != 1 ]; then echo "$2: request has other hard links" >&2; return 1; fi
144
+ value=$(head -c 4096 -- "$file" | jq -r 'if type == "object" and (.target | type) == "string" then .target else empty end' 2>/dev/null || true)
145
+ if ! [[ $value =~ $version_pattern ]]; then echo "$2: request does not name a release version" >&2; return 1; fi
146
+ echo "$value"
147
+ }
148
+
149
+ # 取走请求:全部请求文件先读后删 (不论是否合格,path 单元在文件存在时一直触发),按 control → ingress → relay
150
+ # 取第一个合格的。只给 file 时只认这个文件 (须是上面的固定路径之一)。输出 "<component> <version>"。
151
+ take_request() { # auto|file
152
+ local component file found="" value
153
+ for component in control ingress relay; do
154
+ file=$(request_file_of "$component")
155
+ [ -e "$file" ] || [ -L "$file" ] || continue
156
+ if [ "$1" != auto ] && [ "$file" != "$1" ]; then continue; fi
157
+ if value=$(read_request "$file" "$component") && [ -z "$found" ]; then
158
+ found="$component $value"
159
+ fi
160
+ # 链接只删链接本身;目录 (组件自己建的) 也删,否则 path 单元一直触发
161
+ rm -rf --one-file-system -- "$file"
162
+ done
163
+ [ -n "$found" ] || return 1
164
+ echo "$found"
165
+ }
166
+
167
+ # ---- 参数 ----------------------------------------------------------------
168
+ target=${1:-}
169
+ requested_by=${HOMEWAY_REQUESTED_BY:-${SUDO_USER:-$(id -un)}}
170
+ case $target in
171
+ -h | --help | "")
172
+ sed -n '2,37p' "$0"
173
+ exit 2
174
+ ;;
175
+ esac
176
+ [ "$(id -u)" = 0 ] || { echo "run as root" >&2; exit 1; }
177
+ request_component=""
178
+ if [ "$target" = --request ]; then
179
+ request=${2:-auto}
180
+ if ! taken=$(take_request "$request"); then
181
+ echo "no valid release request ($request)" >&2
182
+ exit 1
183
+ fi
184
+ request_component=${taken%% *}
185
+ target=${taken#* }
186
+ requested_by="console via $request_component"
187
+ fi
188
+ [ -f "$conf_file" ] || { echo "$conf_file is missing (COMPONENTS=\"control ingress relay\")" >&2; exit 1; }
189
+
190
+ read -r -a components <<< "$(conf_value "$conf_file" COMPONENTS "")"
191
+ [ "${#components[@]}" -gt 0 ] || { echo "COMPONENTS is empty in $conf_file" >&2; exit 1; }
192
+ for component in "${components[@]}"; do
193
+ case $component in
194
+ control | ingress | relay) ;;
195
+ *) echo "unknown component $component in $conf_file" >&2; exit 1 ;;
196
+ esac
197
+ done
198
+ if [ -n "$request_component" ] && [[ " ${components[*]} " != *" $request_component "* ]]; then
199
+ echo "ignoring a release request from $request_component, which does not run on this host" >&2
200
+ exit 1
201
+ fi
202
+ # 固定顺序:控制面先起,入口才同步得到
203
+ ordered=()
204
+ for component in control ingress relay; do
205
+ for c in "${components[@]}"; do
206
+ if [ "$c" = "$component" ]; then ordered+=("$component"); break; fi
207
+ done
208
+ done
209
+ needed=()
210
+ for component in "${ordered[@]}"; do
211
+ read -r -a bins <<< "$(binaries_of "$component")"
212
+ needed+=("${bins[@]}")
213
+ done
214
+
215
+ registry=${HOMEWAY_NPM_REGISTRY:-$(conf_value "$conf_file" NPM_REGISTRY https://registry.npmmirror.com)}
216
+ registry=${registry%/}
217
+
218
+ case $(uname -m) in
219
+ x86_64 | amd64) arch=amd64 ;;
220
+ aarch64 | arm64) arch=arm64 ;;
221
+ *) echo "unsupported architecture $(uname -m)" >&2; exit 1 ;;
222
+ esac
223
+
224
+ mkdir -p "$status_dir" "$releases_root" "$root/bin"
225
+ chmod 0755 "$status_dir"
226
+
227
+ # 同一时间只做一次切换
228
+ exec 9> /run/lock/homeway-release.lock
229
+ flock -w 30 9 || { echo "another release change is running" >&2; exit 1; }
230
+
231
+ # ---- 一次性迁移:旧布局 → releases/legacy -------------------------------
232
+ # 旧部署把二进制放在各自目录里。把它们复制成一个名为 legacy 的发布并让 current 指向它,
233
+ # 这样换成新单元 (ExecStart=/opt/homeway/current/…) 后,第一次安装失败也有可以回滚的发布。
234
+ if [ "$target" = --adopt-legacy ]; then
235
+ if [ -e "$root/current" ]; then
236
+ log "$root/current already exists ($(readlink -f "$root/current")); nothing to adopt"
237
+ exit 0
238
+ fi
239
+ legacy=$releases_root/legacy
240
+ rm -rf "$legacy.staging"
241
+ mkdir -p "$legacy.staging"
242
+ found=0
243
+ for pair in /opt/homeway-control/bin/tunneld:tunneld /opt/homeway-ingress/bin/ingress-agent:ingress-agent \
244
+ /opt/homeway-ingress/bin/sing-box:sing-box /opt/homeway/bin/relay:relay; do
245
+ if [ -x "${pair%%:*}" ]; then
246
+ install -m 0755 "${pair%%:*}" "$legacy.staging/${pair#*:}"
247
+ found=1
248
+ fi
249
+ done
250
+ if [ "$found" = 0 ]; then
251
+ rm -rf "$legacy.staging"
252
+ log "no legacy binaries found; a fresh host starts from its first release"
253
+ exit 0
254
+ fi
255
+ rm -rf "$legacy"
256
+ mv "$legacy.staging" "$legacy"
257
+ switch_to "$legacy"
258
+ log "adopted legacy binaries as $legacy: $(cd "$legacy" && echo *)"
259
+ exit 0
260
+ fi
261
+
262
+ # 输出同时写 job.log。自我刷新后的重新执行继承同一个 tee (调用方的终端照样看得到后续输出),不再另起。
263
+ # tee 不能继承锁,否则重新执行时它还拿着锁
264
+ if [ "${HOMEWAY_INSTALLER_REEXEC:-}" != 1 ]; then
265
+ : > "$status_dir/job.log"
266
+ chmod 0644 "$status_dir/job.log"
267
+ exec > >(exec 9>&-; tee -a "$status_dir/job.log") 2>&1
268
+ fi
269
+
270
+ job_target=$target
271
+ job_started=${HOMEWAY_JOB_STARTED:-$(date -u +%Y-%m-%dT%H:%M:%SZ)}
272
+ work=""
273
+ cleanup() { [ -z "$work" ] || rm -rf "$work"; }
274
+ failed() {
275
+ local code=$? command=$BASH_COMMAND
276
+ trap - ERR
277
+ log "error: '$command' exited with $code"
278
+ write_status failed "'$command' exited with $code; see the log"
279
+ exit "$code"
280
+ }
281
+ trap failed ERR
282
+ trap cleanup EXIT
283
+
284
+ [[ $target =~ $name_pattern ]] || die "invalid release target"
285
+ write_status running "started"
286
+ log "release change to $target (components: ${ordered[*]}, linux-$arch) requested by $requested_by"
287
+
288
+ previous_dir=$(readlink -f "$root/current" 2>/dev/null || true)
289
+ if [ ! -d "$previous_dir" ]; then previous_dir=""; fi
290
+ job_previous=${previous_dir:+$(basename "$previous_dir")}
291
+
292
+ # ---- 解析目标 ------------------------------------------------------------
293
+ fetch() { curl -fsSL --retry 3 --connect-timeout 10 --max-time 600 "$@"; }
294
+
295
+ if [ "$target" = latest ]; then
296
+ target=$(fetch "$registry/$package/latest" | jq -r '.version // empty')
297
+ [[ $target =~ $version_pattern ]] || die "the registry has no usable latest release"
298
+ log "latest is $target"
299
+ job_target=$target
300
+ fi
301
+
302
+ has_binaries() { # dir
303
+ local binary
304
+ for binary in "${needed[@]}"; do
305
+ [ -x "$1/$binary" ] || return 1
306
+ done
307
+ }
308
+
309
+ release_dir=$releases_root/$target
310
+ if has_binaries "$release_dir"; then
311
+ log "using installed release $target"
312
+ elif [[ $target =~ $version_pattern ]]; then
313
+ if [ -n "${HOMEWAY_INSTALLER_PACKAGE_DIR:-}" ]; then
314
+ package_dir=$HOMEWAY_INSTALLER_PACKAGE_DIR
315
+ work=$(dirname "$(dirname "$package_dir")")
316
+ else
317
+ work=$(mktemp -d /tmp/homeway-release.XXXXXX)
318
+ log "fetching $package@$target from $registry"
319
+ fetch "$registry/$package/$target" > "$work/manifest.json" ||
320
+ die "$package@$target could not be fetched from $registry (npmmirror not synced yet? https://npmmirror.com/sync/$package)"
321
+ integrity=$(jq -r '.dist.integrity // empty' "$work/manifest.json")
322
+ case $integrity in
323
+ sha512-*) ;;
324
+ *) die "the registry gave no sha512 integrity" ;;
325
+ esac
326
+ # registry 的固定布局,在这里拼出,不用 manifest 里的地址
327
+ fetch "$registry/$package/-/$package-$target.tgz" > "$work/package.tgz" ||
328
+ die "the $package@$target tarball could not be downloaded"
329
+ actual="sha512-$(openssl dgst -sha512 -binary "$work/package.tgz" | base64 -w0)"
330
+ [ "$actual" = "$integrity" ] || die "package integrity mismatch"
331
+ mkdir "$work/extract"
332
+ tar -xzf "$work/package.tgz" -C "$work/extract" --no-same-owner --no-same-permissions
333
+ package_dir=$work/extract/package
334
+ fi
335
+ (cd "$package_dir" && sha256sum --quiet -c SHA256SUMS) ||
336
+ die "package checksums do not match SHA256SUMS"
337
+ packaged=$(jq -r .version "$package_dir/build-info.json")
338
+ [ "$packaged" = "$target" ] || die "package build-info names $packaged, not $target"
339
+ # 先刷新本安装脚本再继续 (在检查包内容之前):需要新安装逻辑的发布不能被旧逻辑拒绝。先做语法检查,
340
+ # 坏掉的安装脚本没法修复自己。最多跳一次。
341
+ if [ -f "$package_dir/install-release.sh" ] && ! cmp -s "$package_dir/install-release.sh" "$installed_script"; then
342
+ bash -n "$package_dir/install-release.sh" || die "the packaged installer does not parse"
343
+ install -m 0755 "$package_dir/install-release.sh" "$installed_script.next"
344
+ mv -f "$installed_script.next" "$installed_script"
345
+ log "refreshed install-release.sh from $target"
346
+ if [ "${HOMEWAY_INSTALLER_REEXEC:-}" != 1 ]; then
347
+ log "continuing under the refreshed installer"
348
+ trap - ERR EXIT
349
+ HOMEWAY_INSTALLER_REEXEC=1 \
350
+ HOMEWAY_INSTALLER_PACKAGE_DIR=$package_dir \
351
+ HOMEWAY_JOB_STARTED=$job_started \
352
+ HOMEWAY_NPM_REGISTRY=$registry \
353
+ HOMEWAY_REQUESTED_BY=$requested_by \
354
+ exec 9>&- "$installed_script" "$target"
355
+ fi
356
+ fi
357
+
358
+ # 包里的二进制是 <名字>.xz (xz 压缩,包小约 30%) 或未压缩的 <名字>;只解出本机组件用到的
359
+ packaged_binary() { # binary
360
+ local base=$package_dir/bin/linux-$arch/$1
361
+ if [ -f "$base.xz" ]; then echo "$base.xz"; elif [ -f "$base" ]; then echo "$base"; fi
362
+ }
363
+ for binary in "${needed[@]}"; do
364
+ [ -n "$(packaged_binary "$binary")" ] || die "package is missing bin/linux-$arch/$binary"
365
+ done
366
+ command -v xz > /dev/null || [ -z "$(find "$package_dir/bin/linux-$arch" -name '*.xz' -print -quit)" ] ||
367
+ die "the package has xz-compressed binaries but xz is not installed (apt-get install -y xz-utils)"
368
+ # 放到 dest:.xz 先解压到旁边的临时文件,校验过的包内容 (SHA256SUMS) 加上 xz 自带的校验
369
+ place_binary() { # binary dest
370
+ local src
371
+ src=$(packaged_binary "$1")
372
+ case $src in
373
+ *.xz) xz -dc "$src" > "$2.tmp" ;;
374
+ *) cp "$src" "$2.tmp" ;;
375
+ esac
376
+ chmod 0755 "$2.tmp"
377
+ mv -f "$2.tmp" "$2"
378
+ }
379
+
380
+ if [ -d "$release_dir" ]; then
381
+ # 已有该发布但缺本机新增组件的二进制 (例如 sg 先迁移了 control,再迁移 ingress):只补缺的,
382
+ # 不整体替换目录 (它可能正是 current)
383
+ for binary in "${needed[@]}"; do
384
+ [ -x "$release_dir/$binary" ] && continue
385
+ place_binary "$binary" "$release_dir/$binary"
386
+ log "added $binary to $release_dir"
387
+ done
388
+ else
389
+ staging=$releases_root/.$target.staging
390
+ rm -rf "$staging"
391
+ mkdir -p "$staging"
392
+ for binary in "${needed[@]}"; do
393
+ place_binary "$binary" "$staging/$binary"
394
+ done
395
+ install -m 0644 "$package_dir/build-info.json" "$staging/build-info.json"
396
+ mv "$staging" "$release_dir"
397
+ log "staged $release_dir"
398
+ fi
399
+ else
400
+ die "$target is neither installed (with ${needed[*]}) nor a release version"
401
+ fi
402
+ job_release=$(basename "$release_dir")
403
+
404
+ # ---- 证明新二进制能在本机运行 -------------------------------------------
405
+ # 迁移时收进来的 legacy 没有 build-info.json,旧 relay 也不认 -version,跳过
406
+ expected_commit=""
407
+ if [ -f "$release_dir/build-info.json" ]; then
408
+ expected_commit=$(jq -r '.commit // empty' "$release_dir/build-info.json")
409
+ for binary in "${needed[@]}"; do
410
+ case $binary in
411
+ tunneld) out=$("$release_dir/tunneld" version 2>&1) ;;
412
+ sing-box) out=$("$release_dir/$binary" version 2>&1) ;;
413
+ *) out=$("$release_dir/$binary" -version 2>&1) ;;
414
+ esac || die "$release_dir/$binary does not run on this host: $out"
415
+ log "$binary: $(head -n 1 <<< "$out")"
416
+ if [ "$binary" != sing-box ] && [ -n "$expected_commit" ] &&
417
+ [ "$(jq -r '.commit // empty' <<< "$out" 2>/dev/null)" != "$expected_commit" ]; then
418
+ die "$binary reports a different commit than build-info.json"
419
+ fi
420
+ done
421
+ fi
422
+
423
+ http_address() {
424
+ local address
425
+ address=$(conf_value "$tunneld_env" HTTP_ADDR :8080)
426
+ case $address in :*) address=127.0.0.1$address ;; 0.0.0.0:*) address=127.0.0.1:${address#*:} ;; esac
427
+ echo "$address"
428
+ }
429
+
430
+ main_exe_in() { # unit release-dir
431
+ local pid
432
+ pid=$(systemctl show -p MainPID --value "$1")
433
+ [ -n "$pid" ] && [ "$pid" != 0 ] || return 1
434
+ case $(readlink -f "/proc/$pid/exe" 2>/dev/null) in
435
+ "$(readlink -f "$2")"/*) return 0 ;;
436
+ esac
437
+ return 1
438
+ }
439
+
440
+ listening() { # port
441
+ [ -n "$(ss -Hltn "sport = :$1" 2>/dev/null)" ]
442
+ }
443
+
444
+ verify_control() { # release-dir
445
+ local address expected="" served deadline=$((SECONDS + 90))
446
+ address=$(http_address)
447
+ if [ -f "$1/build-info.json" ]; then expected=$(jq -r '.commit // empty' "$1/build-info.json"); fi
448
+ while [ "$SECONDS" -lt "$deadline" ]; do
449
+ sleep 2
450
+ systemctl is-active --quiet homeway-tunneld.service || continue
451
+ curl -fsS --max-time 5 "http://$address/readyz" > /dev/null 2>&1 || continue
452
+ # 读不到 /version 的旧版本 (legacy) 只看 readyz
453
+ served=$(curl -fsS --max-time 5 "http://$address/version" 2>/dev/null | jq -r '.commit // empty' 2>/dev/null || true)
454
+ if [ -z "$expected" ] || [ "$served" = "$expected" ]; then
455
+ log "control: ready, /version commit ${served:-n/a}"
456
+ return 0
457
+ fi
458
+ done
459
+ return 1
460
+ }
461
+
462
+ # ingress-agent 启动后立即同步一次:失败记 {"msg":"sync",…},之后恢复记 "sync recovered";
463
+ # 有变化记 "synced",没变化 (304) 什么也不记。所以 15 s 内没有同步错误即算成功。
464
+ verify_ingress() { # release-dir since
465
+ local logs deadline=$((SECONDS + 60)) started=$SECONDS
466
+ while [ "$SECONDS" -lt "$deadline" ]; do
467
+ sleep 3
468
+ systemctl is-active --quiet homeway-ingress.service || continue
469
+ main_exe_in homeway-ingress.service "$1" || continue
470
+ listening "$ingress_port" || continue
471
+ [ "${HOMEWAY_INGRESS_REQUIRE_SYNC:-1}" = 1 ] || { log "ingress: active, :$ingress_port listening"; return 0; }
472
+ logs=$(journalctl -u homeway-ingress.service --since "$2" -o cat --no-pager 2>/dev/null || true)
473
+ if grep -Eq '"msg":"(synced|sync recovered)"' <<< "$logs"; then
474
+ log "ingress: active, :$ingress_port listening, synced with the control plane"
475
+ return 0
476
+ fi
477
+ if ! grep -q '"msg":"sync"' <<< "$logs" && [ $((SECONDS - started)) -ge 15 ]; then
478
+ log "ingress: active, :$ingress_port listening, no sync errors"
479
+ return 0
480
+ fi
481
+ done
482
+ return 1
483
+ }
484
+
485
+ verify_relay() { # release-dir
486
+ local deadline=$((SECONDS + 30))
487
+ while [ "$SECONDS" -lt "$deadline" ]; do
488
+ sleep 2
489
+ systemctl is-active --quiet homeway-relay.service || continue
490
+ main_exe_in homeway-relay.service "$1" || continue
491
+ listening "$relay_port" || continue
492
+ log "relay: active, :$relay_port listening"
493
+ return 0
494
+ done
495
+ return 1
496
+ }
497
+
498
+ unit_enabled() { # unit
499
+ systemctl is-enabled --quiet "$1" 2>/dev/null
500
+ }
501
+
502
+ # ---- 要重启哪些组件 ------------------------------------------------------
503
+ # 换了发布:全部重启。发布没变:只重启还没跑在它上面的组件 (例如 sg 迁移时后补的 ingress),
504
+ # 以及 HOMEWAY_RESTART 点名的组件 (部署脚本改了单元文件时传入,如 HOMEWAY_RESTART=control)。
505
+ # 未 enable 的单元 (例如还没有令牌的入口) 只切换,不启动。
506
+ restart_list=()
507
+ same_release=0
508
+ if [ "$(readlink -f "$release_dir")" = "$previous_dir" ]; then
509
+ same_release=1
510
+ for component in "${ordered[@]}"; do
511
+ unit=$(unit_of "$component")
512
+ unit_enabled "$unit" || continue
513
+ if [[ " ${HOMEWAY_RESTART:-} " == *" $component "* ]] || ! main_exe_in "$unit" "$release_dir"; then
514
+ restart_list+=("$component")
515
+ fi
516
+ done
517
+ if [ "${#restart_list[@]}" = 0 ]; then
518
+ write_status succeeded "$job_release is already current"
519
+ log "$job_release is already current"
520
+ exit 0
521
+ fi
522
+ log "$job_release is already current; restarting ${restart_list[*]}"
523
+ else
524
+ restart_list=("${ordered[@]}")
525
+ fi
526
+
527
+ # ---- 备份、切换、重启、验证 ---------------------------------------------
528
+ if [ "$same_release" = 0 ] && [[ " ${restart_list[*]} " == *" control "* ]] && [ -n "$previous_dir" ] &&
529
+ [ -x "$previous_dir/tunneld" ] && unit_enabled homeway-tunneld.service &&
530
+ [ -f "$(conf_value "$tunneld_env" DATABASE_PATH "$tunneld_home/tunneld.db")" ]; then
531
+ backup_dir=$tunneld_home/release-backups
532
+ install -d -o "$tunneld_user" -g "$tunneld_user" -m 0750 "$backup_dir"
533
+ backup_file=$backup_dir/pre-$job_release-$(date -u +%Y%m%d-%H%M%S).db
534
+ log "backing up the database with the running release to $backup_file"
535
+ # 与单元相同的用户、环境文件与工作目录,由 systemd 解析,和服务看到的一样
536
+ systemd-run --wait --pipe --quiet --collect \
537
+ -p User="$tunneld_user" -p Group="$tunneld_user" -p UMask=0077 \
538
+ -p EnvironmentFile="$tunneld_env" -p WorkingDirectory="$tunneld_home" \
539
+ "$previous_dir/tunneld" backup "$backup_file" 9>&- ||
540
+ die "the pre-switch database backup failed; nothing was changed"
541
+ # 只留最近 5 份
542
+ find "$backup_dir" -maxdepth 1 -type f -name 'pre-*.db' -printf '%T@ %p\n' | sort -rn |
543
+ tail -n +6 | cut -d' ' -f2- | while IFS= read -r old; do rm -f "$old"; done
544
+ fi
545
+
546
+ # 按顺序重启并验证,记下第一个失败的组件
547
+ restart_all() { # release-dir
548
+ local component unit since
549
+ for component in "${restart_list[@]}"; do
550
+ unit=$(unit_of "$component")
551
+ if ! unit_enabled "$unit"; then
552
+ log "$unit is not enabled; switched without starting it"
553
+ continue
554
+ fi
555
+ since=$(date '+%Y-%m-%d %H:%M:%S')
556
+ log "restarting $unit"
557
+ systemctl restart "$unit" 9>&- || true
558
+ if ! "verify_$component" "$1" "$since"; then
559
+ log "$component did not come up on $(basename "$1")"
560
+ journalctl -u "$unit" -n 40 --no-pager || true
561
+ failed_component=$component
562
+ return 1
563
+ fi
564
+ done
565
+ }
566
+
567
+ failed_component=""
568
+ if [ "$same_release" = 1 ]; then
569
+ restart_all "$release_dir" ||
570
+ die "$failed_component did not come up on $job_release (to go back: install-release.sh <an older release under $releases_root>)"
571
+ trap - ERR
572
+ write_status succeeded "$job_release is serving (restarted ${restart_list[*]})"
573
+ log "done: $job_release"
574
+ exit 0
575
+ fi
576
+
577
+ log "switching current to $job_release"
578
+ switch_to "$release_dir"
579
+ if ! restart_all "$release_dir"; then
580
+ if [ -n "$previous_dir" ]; then
581
+ log "rolling back to $job_previous"
582
+ switch_to "$previous_dir"
583
+ if restart_all "$previous_dir"; then
584
+ trap - ERR
585
+ write_status rolled_back "$job_release: $failed_component did not come up; $job_previous is serving again"
586
+ exit 1
587
+ fi
588
+ die "$job_release did not come up and $job_previous did not come back either ($failed_component)"
589
+ fi
590
+ die "$job_release: $failed_component did not come up and there is no previous release"
591
+ fi
592
+
593
+ # ---- 清理 ----------------------------------------------------------------
594
+ # 按安装时间保留最新的几个,current 与上一个 (回滚目标) 无论新旧都保留
595
+ seen=0
596
+ while IFS= read -r candidate; do
597
+ candidate=${candidate%/}
598
+ [ -d "$candidate" ] || continue
599
+ seen=$((seen + 1))
600
+ [ "$seen" -gt "$keep_releases" ] || continue
601
+ resolved=$(readlink -f "$candidate")
602
+ [ "$resolved" = "$(readlink -f "$release_dir")" ] && continue
603
+ [ "$resolved" = "$previous_dir" ] && continue
604
+ rm -rf "$candidate"
605
+ log "pruned $(basename "$candidate")"
606
+ done < <(find "$releases_root" -mindepth 1 -maxdepth 1 -type d ! -name '.*' -printf '%T@ %p\n' | sort -rn | cut -d' ' -f2-)
607
+
608
+ trap - ERR
609
+ write_status succeeded "$job_release is serving"
610
+ log "done: $job_release"
package/package.json CHANGED
@@ -1,6 +1,24 @@
1
1
  {
2
2
  "name": "homeway-server",
3
- "version": "0.0.1",
4
- "description": "Homeway server binaries (placeholder; real releases are published from CI).",
5
- "license": "UNLICENSED"
3
+ "version": "2026.10.4-03b2e7d6",
4
+ "description": "Homeway server binaries (Go + patched sing-box) for linux amd64/arm64 and the host installer. Published from CI for every main commit; installed on hosts by install-release.sh.",
5
+ "license": "UNLICENSED",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/homeway-tv/homeway.git"
9
+ },
10
+ "os": [
11
+ "linux"
12
+ ],
13
+ "cpu": [
14
+ "x64",
15
+ "arm64"
16
+ ],
17
+ "files": [
18
+ "bin/",
19
+ "install-release.sh",
20
+ "build-info.json",
21
+ "SHA256SUMS"
22
+ ],
23
+ "scripts": {}
6
24
  }