File size: 7,117 Bytes
854994d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
//! Moves a legacy daemon to its dedicated package only during an explicit update.
//! Preparation leaves the legacy selection intact; publishing current is the cutover.

use std::path::Path;

use anyhow::Context;
use anyhow::Result;
use futures::FutureExt;

use super::InstallerHttp;
use super::InstallerMode;
use super::fetch_installer_script;
use super::run_installer_script;
use super::selected_release;
use crate::Daemon;
use crate::UpdateOutput;
use crate::UpdateStatus;
use crate::backend;
use crate::client;
use crate::install_lock::acquire_install_lock;
use crate::managed_install;

pub(super) async fn run(http: &impl InstallerHttp, legacy: &Daemon) -> Result<UpdateOutput> {
    let _operation_lock = legacy.acquire_operation_lock().await?;
    let previous = selected_release(legacy)?;
    anyhow::ensure!(
        previous.0.ends_with("standalone") && super::manual_update::supported(legacy)?,
        "daemon selection changed; retry the update"
    );
    let home = previous
        .0
        .parent()
        .and_then(Path::parent)
        .context("package root has no Codex home")?;
    let root = home.join("packages/app-server-daemon");
    let settings = legacy.load_settings().await?;
    let running = legacy.running_backend_instance(&settings).await?;
    if running.is_none() && client::probe(&legacy.socket_path).await.is_ok() {
        return super::manual_update::unsupported(legacy).await;
    }
    #[cfg(unix)]
    let mut terminate = tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate())?;
    let cancellation = async {
        #[cfg(unix)]
        tokio::select! {
            _ = terminate.recv() => {},
            _ = tokio::signal::ctrl_c() => {},
        }
        #[cfg(windows)]
        let _ = tokio::signal::ctrl_c().await;
        Some(())
    };
    let mut cancellation = std::pin::pin!(cancellation);

    let script = fetch_installer_script(http).await?;
    anyhow::ensure!(
        script
            .windows(b"CODEX_INSTALL_DEFER_SELECTION".len())
            .any(|window| window == b"CODEX_INSTALL_DEFER_SELECTION"),
        "the published installer does not support daemon migration yet; the legacy installation was left unchanged"
    );
    eprintln!("Preparing the daemon update in {}...", root.display());
    #[cfg(unix)]
    {
        if matches!(
            run_installer_script(
                &script,
                InstallerMode::Migration,
                &root,
                cancellation.as_mut()
            )
            .await?,
            super::UpdateLoopControl::Stop
        ) {
            return Err(std::io::Error::from(std::io::ErrorKind::Interrupted).into());
        }
    }
    #[cfg(windows)]
    tokio::select! {
        result = run_installer_script(&script, InstallerMode::Migration, &root) => { result?; },
        _ = &mut cancellation => return Err(std::io::Error::from(std::io::ErrorKind::Interrupted).into()),
    }

    let _install_lock = acquire_install_lock(&root).await?;
    let pending = root.join(".migration-current");
    let release = pending
        .canonicalize()
        .context("installer did not prepare a daemon package")?;
    let name = release
        .file_name()
        .context("prepared release has no name")?;
    anyhow::ensure!(
        release.parent() == Some(root.join("releases").canonicalize()?.as_path())
            && std::fs::read_to_string(root.join("auto-update-version"))? == name.to_string_lossy(),
        "installer did not prepare a latest-channel daemon package"
    );
    let entrypoint = if cfg!(windows) {
        "bin/codex.exe"
    } else {
        "bin/codex"
    };
    let binary = release.join(entrypoint);
    let version = managed_install::managed_codex_version(&binary).await?;
    anyhow::ensure!(
        name.to_string_lossy().starts_with(&format!("{version}-")),
        "prepared daemon version does not match its release"
    );
    // An older production updater understands only the legacy package/PID paths.
    anyhow::ensure!(
        managed_install::supports_daemon_command(
            &binary,
            &["pid-update-loop", "--check-package-ownership"]
        )
        .await,
        "the latest production release does not support daemon migration yet; the legacy daemon was left running"
    );
    #[cfg(windows)]
    backend::windows::ensure_detached_launch(&binary)?;
    anyhow::ensure!(
        selected_release(legacy)? == previous,
        "daemon selection changed; retry the update"
    );
    if cancellation.as_mut().now_or_never().is_some() {
        return Err(std::io::Error::from(std::io::ErrorKind::Interrupted).into());
    }
    let cutover = async {
        backend::pid_update_loop_backend(legacy.backend_paths(&settings))
            .stop()
            .await?;
        anyhow::ensure!(
            selected_release(legacy)? == previous,
            "daemon selection changed; retry the update"
        );
        if let Some(running) = &running {
            crate::thread_recovery::discard_pending(legacy)?;
            running
                .stop_with_grace(settings.shutdown_grace_seconds)
                .await?;
        }
        std::fs::rename(&pending, root.join("current"))
            .context("failed to select the new daemon package; retry the update")
    }
    .await;
    if let Err(error) = cutover {
        if let Err(restore_error) = async {
            legacy
                .current_installation()?
                .ensure_managed_updater(&settings)
                .await
        }
        .await
        {
            eprintln!(
                "warning: failed to restore the legacy updater after migration failed: {restore_error:#}"
            );
        }
        return Err(error);
    }
    let selected = Daemon {
        pid_file: legacy.pid_file.with_file_name(crate::DAEMON_PID_FILE_NAME),
        update_pid_file: legacy
            .update_pid_file
            .with_file_name(crate::DAEMON_UPDATE_PID_FILE_NAME),
        managed_codex_bin: root.join("current").join(entrypoint),
        ..legacy.clone()
    };
    let running_version = if running.is_some() {
        selected.start_managed_backend(&settings).await.context(
            "daemon migrated but could not start; retry with `codex app-server daemon start`",
        )?;
        Some(
            selected
                .wait_until_ready()
                .await
                .context(
                    "daemon migrated but is not ready; retry with `codex app-server daemon start`",
                )?
                .app_server_version,
        )
    } else {
        None
    };
    if let Err(error) = selected.ensure_managed_updater(&settings).await {
        eprintln!("warning: daemon migrated but its updater could not start: {error:#}");
    }
    Ok(UpdateOutput {
        status: UpdateStatus::Updated,
        installed_version: Some(version),
        running_version,
        managed_codex_path: selected.managed_codex_bin,
        message: "The daemon was updated and moved to its dedicated package. The legacy CLI package was left unchanged.".to_string(),
    })
}