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 +22 -0
- package/SHA256SUMS +10 -0
- package/bin/linux-amd64/ingress-agent.xz +0 -0
- package/bin/linux-amd64/relay.xz +0 -0
- package/bin/linux-amd64/sing-box.xz +0 -0
- package/bin/linux-amd64/tunneld.xz +0 -0
- package/bin/linux-arm64/ingress-agent.xz +0 -0
- package/bin/linux-arm64/relay.xz +0 -0
- package/bin/linux-arm64/sing-box.xz +0 -0
- package/bin/linux-arm64/tunneld.xz +0 -0
- package/build-info.json +9 -0
- package/install-release.sh +610 -0
- package/package.json +21 -3
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
|
package/build-info.json
ADDED
|
@@ -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": "
|
|
4
|
-
"description": "Homeway server binaries (
|
|
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
|
}
|