fix(update): retain binary installation path for self-restart

Resolve and retain the executable path at startup so updates, rollbacks and
restarts keep using the installation path after the running binary is renamed.
Document binary self-restart configuration and cover renamed executable restarts.

Refs #316

Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
Zyy
2026-09-28 13:06:04 +08:00
co-authored by Codex
parent e06ca56f93
commit bb859455f9
4 changed files with 97 additions and 3 deletions
@@ -241,9 +241,13 @@ pub struct ProcessSystemOperations {
impl ProcessSystemOperations {
#[must_use]
pub fn new(cancellation: CancellationToken, config: SystemUpdateConfig) -> Self {
// 组合根在接收请求前创建服务,记录本次启动的发行文件,后续替换不能改写这份事实。
let running_files = Arc::new(ReleaseFiles::installed(&config));
pub fn new(cancellation: CancellationToken, mut config: SystemUpdateConfig) -> Self {
// 组合根在接收请求前固定安装路径和发行文件;旧程序重命名后,
// current_exe 可能指向备份,后续更新、回滚和重启不能再反查运行文件。
let running_files = Arc::new(config.executable_path().and_then(|executable| {
config.executable_path = Some(executable);
ReleaseFiles::installed(&config)
}));
Self {
cancellation,
events: Arc::new(UpdateEvents::default()),
@@ -206,6 +206,73 @@ async fn restart_should_spawn_replacement_before_shutdown_outside_docker() {
.expect("replacement executed before fixture cleanup");
}
#[cfg(target_os = "linux")]
#[tokio::test]
async fn restart_should_use_startup_path_after_running_executable_is_renamed() {
const CHILD_ENV: &str = "CPR_TEST_RESTART_RENAMED_EXECUTABLE_CHILD";
let executable = std::env::current_exe().expect("test executable");
if std::env::var_os(CHILD_ENV).is_none() {
// 只重命名隔离副本,避免修改 Cargo 的测试程序或影响并行测试。
let directory = tempfile::tempdir().expect("isolated test directory");
let child = directory.path().join("restart-test");
fs::copy(&executable, &child).expect("copy test executable");
let output = std::process::Command::new(child)
.args([
"--exact",
"system_update::restart_should_use_startup_path_after_running_executable_is_renamed",
])
.env(CHILD_ENV, "1")
.env("CPR_RESTART_DELAY_MS", "1200")
.output()
.expect("isolated restart test");
assert!(
output.status.success(),
"stdout: {}\nstderr: {}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
return;
}
if executable.file_name() == Some(std::ffi::OsStr::new("codex-proxy-rs.backup")) {
// 误启动旧程序时留下可观察结果并退出,避免旧副本再次发起重启。
fs::write(
executable
.parent()
.expect("installation directory")
.join("replacement-version"),
"old",
)
.expect("old replacement marker");
return;
}
let fixture = Fixture::new();
fs::rename(executable, fixture.executable()).expect("move isolated running executable");
let mut config = fixture.config("http://127.0.0.1:1/repos");
config.executable_path = None;
config.self_restart_enabled = true;
let service = ProcessSystemOperations::new(CancellationToken::new(), config);
let backup = fixture.root.path().join("codex-proxy-rs.backup");
fs::rename(fixture.executable(), &backup).expect("back up running executable");
fixture.write_executable("#!/bin/sh\nprintf new > \"${0%/*}/replacement-version\"\n");
assert_eq!(std::env::current_exe().expect("renamed executable"), backup);
service.restart().await.expect("restart accepted");
let marker = fixture.root.path().join("replacement-version");
tokio::time::timeout(Duration::from_secs(5), async {
while !marker.is_file() {
tokio::time::sleep(Duration::from_millis(10)).await;
}
})
.await
.expect("replacement executed");
assert_eq!(
fs::read_to_string(marker).expect("replacement version"),
"new"
);
}
#[tokio::test]
async fn restart_should_conflict_while_another_system_operation_is_running() {
let fixture = Fixture::new();
+17
View File
@@ -526,6 +526,23 @@ alpha、beta、rc、exp 在 GitHub 标记为 Pre-release,不覆盖 GitHub Late
回滚入口只接受与记录指纹一致的完整备份;旧格式记录没有指纹,或备份被替换、丢失时,不再提供在线回滚,
但不会删除备份文件。此时需要按人工部署流程恢复已经核实的发行包。
直接运行二进制时,若未设置 `CPR_ENABLE_SELF_RESTART=true`,管理端自重启默认关闭。
在现有 `deploy/config.yaml` 的 `host` 下合并以下配置:
```yaml
host:
system_update:
deployment_mode: binary
self_restart_enabled: true
executable_path: '/opt/codex-proxy-rs/codex-proxy-rs'
```
`executable_path` 为可选项:未指定时,服务在启动时解析并固定当前程序路径;
显式配置时,将示例改为实际程序的绝对路径。`self_restart_enabled` 控制管理端的「立即重启」操作;
修改配置后需手动重启当前服务一次才会生效。
以上三个字段也可分别通过 `CPR_DEPLOYMENT_MODE`、`CPR_ENABLE_SELF_RESTART` 和 `CPR_UPDATE_EXE_PATH` 设置,
YAML 中显式配置的值优先于对应环境变量。
Compose 提供以下在线更新运行参数:
- `CPR_UPDATE_REPOSITORY`:只接受 `owner/repository`;默认 `zyycn/codex-proxy-rs`。
+6
View File
@@ -29,6 +29,12 @@ host:
directory: '../.runtime/logs'
retention_days: 7
max_file_size_mb: 20
# 二进制部署需要管理端自重启时,取消以下注释;可选路径改为实际程序绝对路径。
# Compose 已通过环境变量开启,保持本段注释即可;详情见 deploy/README.md。
# system_update:
# deployment_mode: 'binary'
# self_restart_enabled: true
# executable_path: '/opt/codex-proxy-rs/codex-proxy-rs' # 可选:覆盖启动时解析的程序路径。
drain_timeout_seconds: 30
worker_shutdown_timeout_seconds: 30