Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 1 addition & 3 deletions apps/extension/vitest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,7 @@ export default defineConfig({
},
define: {
__BSK_EXT_VERSION__: JSON.stringify(pkg.version),
__BSK_DAEMON_WS_URL__: JSON.stringify(
process.env.BSK_DAEMON_WS_URL ?? "ws://127.0.0.1:52800",
),
__BSK_DAEMON_WS_URL__: JSON.stringify(process.env.BSK_DAEMON_WS_URL ?? "ws://127.0.0.1:52800"),
},
resolve: {
alias: {
Expand Down
114 changes: 96 additions & 18 deletions crates/bsk-cli/src/cli/update.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@ use std::io::{Cursor, Read, Write};
use std::path::{Path, PathBuf};
use std::time::{Duration, SystemTime};

#[cfg(windows)]
use std::os::windows::process::CommandExt;
#[cfg(windows)]
use std::process::{Command, Stdio};

use anyhow::{Context, Result, bail};
use clap::Args;
use flate2::read::GzDecoder;
Expand Down Expand Up @@ -246,20 +251,33 @@ fn run(args: UpdateArgs, format: Format) -> Result<()> {
InstallAction::Replaced => "replaced",
InstallAction::Staged => "staged",
};
let (status, message) = match action {
InstallAction::Replaced => (
"updated",
format!(
"updated bsk from {} to {}",
candidate.current, candidate.latest
),
),
InstallAction::Staged => (
"staged",
format!(
"staged bsk {} for replacement after this command exits",
candidate.latest
),
),
};

render_report(
format,
&UpdateReport {
status: "updated",
status,
current_version: candidate.current.to_string(),
latest_version: Some(candidate.latest.to_string()),
release_url: candidate.release_url,
asset_url: Some(candidate.asset.url),
install_action: Some(action_label),
message: format!(
"updated bsk from {} to {}",
candidate.current, candidate.latest
),
message,
},
)
}
Expand Down Expand Up @@ -335,7 +353,7 @@ fn install_candidate_with_client(
crate::daemon::start::run_stop().context("stop bsk daemon before update")?;
}

let action = replace_binary_at_path(&target, &binary)?;
let action = replace_binary_for_update(&target, &binary, daemon_was_running)?;

if daemon_was_running && matches!(action, InstallAction::Replaced) {
crate::daemon::start::run_start(StartArgs::default())
Expand Down Expand Up @@ -502,9 +520,7 @@ fn render_report(format: Format, report: &UpdateReport) -> Result<()> {
println!("release: {release_url}");
}
if matches!(report.install_action, Some("staged")) {
println!(
"restart your terminal and run `bsk --version` to verify the staged update"
);
println!("the detached update helper will apply the replacement after exit");
}
}
Format::Json => {
Expand All @@ -525,13 +541,22 @@ pub fn extract_bsk_binary(archive_bytes: &[u8], kind: ArchiveKind) -> Result<Vec
}

pub fn replace_binary_at_path(target: &Path, binary: &[u8]) -> Result<InstallAction> {
replace_binary_for_update(target, binary, false)
}

fn replace_binary_for_update(
target: &Path,
binary: &[u8],
restart_daemon: bool,
) -> Result<InstallAction> {
#[cfg(windows)]
{
stage_windows_replacement(target, binary)
stage_windows_replacement(target, binary, restart_daemon)
}

#[cfg(not(windows))]
{
let _ = restart_daemon;
replace_binary_atomically(target, binary)
}
}
Expand Down Expand Up @@ -574,15 +599,35 @@ fn replace_binary_atomically(target: &Path, binary: &[u8]) -> Result<InstallActi
}

#[cfg(windows)]
fn stage_windows_replacement(target: &Path, binary: &[u8]) -> Result<InstallAction> {
fn stage_windows_replacement(
target: &Path,
binary: &[u8],
restart_daemon: bool,
) -> Result<InstallAction> {
let paths = staged_replacement_paths(target, std::process::id())?;
std::fs::write(&paths.binary_path, binary)
.with_context(|| format!("write {}", paths.binary_path.display()))?;
std::fs::write(
&paths.script_path,
windows_replacement_script(target, &paths.binary_path),
windows_replacement_script(target, &paths.binary_path, restart_daemon),
)
.with_context(|| format!("write {}", paths.script_path.display()))?;

const CREATE_NO_WINDOW: u32 = 0x0800_0000;
let command = format!("\"{}\"", paths.script_path.display());
if let Err(err) = Command::new("cmd.exe")
.args(["/D", "/S", "/C"])
.arg(command)
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null())
.creation_flags(CREATE_NO_WINDOW)
.spawn()
{
let _ = std::fs::remove_file(&paths.binary_path);
let _ = std::fs::remove_file(&paths.script_path);
return Err(err).context("launch detached Windows update helper");
}
Ok(InstallAction::Staged)
}

Expand All @@ -599,19 +644,26 @@ pub fn staged_replacement_paths(target: &Path, pid: u32) -> Result<StagedReplace
})
}

#[cfg(windows)]
fn windows_replacement_script(target: &Path, staged_binary: &Path) -> String {
#[cfg(any(windows, test))]
fn windows_replacement_script(target: &Path, staged_binary: &Path, restart_daemon: bool) -> String {
let target = target.display().to_string().replace('%', "%%");
let staged_binary = staged_binary.display().to_string().replace('%', "%%");
let restart = if restart_daemon {
format!("start \"\" /B \"{target}\" daemon start >nul 2>nul\r\n")
} else {
String::new()
};
format!(
"@echo off\r\n\
setlocal\r\n\
:retry\r\n\
move /Y \"{}\" \"{}\" >nul 2>nul\r\n\
move /Y \"{staged_binary}\" \"{target}\" >nul 2>nul\r\n\
if errorlevel 1 (\r\n\
timeout /t 1 /nobreak >nul\r\n\
goto retry\r\n\
)\r\n",
staged_binary.display(),
target.display()
)\r\n\
{restart}\
del /F /Q \"%~f0\" >nul 2>nul\r\n"
)
}

Expand Down Expand Up @@ -798,6 +850,32 @@ mod tests {
);
}

#[test]
fn windows_replacement_script_retries_restarts_and_cleans_itself() {
let script = windows_replacement_script(
Path::new(r"C:\Program Files\bsk.exe"),
Path::new(r"C:\Program Files\bsk.exe.update-42"),
true,
);

assert!(script.contains(":retry"));
assert!(script.contains("move /Y"));
assert!(script.contains(r#"start "" /B "C:\Program Files\bsk.exe" daemon start"#));
assert!(script.contains(r#"del /F /Q "%~f0""#));
}

#[test]
fn windows_replacement_script_omits_restart_when_daemon_was_not_running() {
let script = windows_replacement_script(
Path::new(r"C:\bsk.exe"),
Path::new(r"C:\bsk.exe.update-42"),
false,
);

assert!(!script.contains("daemon start"));
assert!(script.contains(r#"del /F /Q "%~f0""#));
}

#[test]
fn cache_freshness_uses_epoch_seconds() {
let cache = UpdateCheckCache {
Expand Down
5 changes: 1 addition & 4 deletions crates/bsk-cli/tests/browser_list_ordering.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ use bsk_protocol::system::BrowserStatusEntry;
use bsk_protocol::{ErrorCode, Method};
use rand::Rng;
use serde::Deserialize;
use tokio::net::TcpListener;
use tokio::sync::mpsc;

fn tempfile_path(prefix: &str) -> PathBuf {
Expand All @@ -30,9 +29,7 @@ fn tempfile_path(prefix: &str) -> PathBuf {
}

async fn spawn_daemon() -> (daemon::DaemonHandle, PathBuf) {
let probe = TcpListener::bind("127.0.0.1:0").await.unwrap();
let port = probe.local_addr().unwrap().port();
drop(probe);
let port = 0;

let config = DaemonConfig::new(port);
let sock = tempfile_path("bsk-test-browser-list");
Expand Down
5 changes: 1 addition & 4 deletions crates/bsk-cli/tests/browser_wait.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ use bsk_protocol::system::{BrowserListParams, HandshakeParams, HandshakeResult,
use bsk_protocol::{BrowserPeerInfo, Method, RequestFrame, ResponseBody, ResponseFrame};
use futures_util::{SinkExt, StreamExt};
use rand::Rng;
use tokio::net::TcpListener;
use tokio_tungstenite::tungstenite::handshake::client::generate_key;
use tokio_tungstenite::tungstenite::http::Request;
use tokio_tungstenite::tungstenite::protocol::Message;
Expand Down Expand Up @@ -77,9 +76,7 @@ fn tempfile_path(prefix: &str) -> PathBuf {
}

async fn spawn_daemon() -> (daemon::DaemonHandle, PathBuf) {
let probe = TcpListener::bind("127.0.0.1:0").await.unwrap();
let port = probe.local_addr().unwrap().port();
drop(probe);
let port = 0;

let config = DaemonConfig::new(port);
let sock = tempfile_path("bsk-test-browser-wait");
Expand Down
5 changes: 1 addition & 4 deletions crates/bsk-cli/tests/cancel_forwarding.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ use bsk_protocol::{
use futures_util::{SinkExt, StreamExt};
use rand::Rng;
use serde_json::json;
use tokio::net::TcpListener;
use tokio_tungstenite::tungstenite::handshake::client::generate_key;
use tokio_tungstenite::tungstenite::http::Request;
use tokio_tungstenite::tungstenite::protocol::Message;
Expand All @@ -40,9 +39,7 @@ fn tempfile_path(prefix: &str) -> PathBuf {
}

async fn spawn_daemon() -> (daemon::DaemonHandle, PathBuf) {
let probe = TcpListener::bind("127.0.0.1:0").await.unwrap();
let port = probe.local_addr().unwrap().port();
drop(probe);
let port = 0;

let config = DaemonConfig::new(port);
let sock = tempfile_path("bsk-test-cancel");
Expand Down
5 changes: 1 addition & 4 deletions crates/bsk-cli/tests/handshake_compat.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ use bsk_protocol::system::{HandshakeParams, HandshakeResult, StatusResult};
use bsk_protocol::{BrowserPeerInfo, ErrorCode, Method, RequestFrame, ResponseBody, ResponseFrame};
use futures_util::{SinkExt, StreamExt};
use rand::Rng;
use tokio::net::TcpListener;
use tokio_tungstenite::tungstenite::handshake::client::generate_key;
use tokio_tungstenite::tungstenite::http::Request;
use tokio_tungstenite::tungstenite::protocol::Message;
Expand All @@ -28,9 +27,7 @@ fn tempfile_path(prefix: &str) -> PathBuf {
}

async fn spawn_daemon() -> (daemon::DaemonHandle, PathBuf) {
let probe = TcpListener::bind("127.0.0.1:0").await.unwrap();
let port = probe.local_addr().unwrap().port();
drop(probe);
let port = 0;

let config = DaemonConfig::new(port);
let sock = tempfile_path("bsk-test-handshake");
Expand Down
5 changes: 1 addition & 4 deletions crates/bsk-cli/tests/per_session_queue.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ use futures_util::{SinkExt, StreamExt};
use rand::Rng;
use serde::Deserialize;
use serde_json::json;
use tokio::net::TcpListener;
use tokio::sync::{Mutex, mpsc};
use tokio_tungstenite::tungstenite::handshake::client::generate_key;
use tokio_tungstenite::tungstenite::http::Request;
Expand All @@ -46,9 +45,7 @@ fn tempfile_path(prefix: &str) -> PathBuf {
}

async fn spawn_daemon() -> (daemon::DaemonHandle, PathBuf) {
let probe = TcpListener::bind("127.0.0.1:0").await.unwrap();
let port = probe.local_addr().unwrap().port();
drop(probe);
let port = 0;
let sock = tempfile_path("bsk-test-queue");
let handle = daemon::run(DaemonConfig::new(port), Some(sock.clone()))
.await
Expand Down
5 changes: 1 addition & 4 deletions crates/bsk-cli/tests/session_user_interrupt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ use bsk_protocol::{
use futures_util::{SinkExt, StreamExt};
use rand::Rng;
use serde_json::json;
use tokio::net::TcpListener;
use tokio_tungstenite::tungstenite::handshake::client::generate_key;
use tokio_tungstenite::tungstenite::http::Request;
use tokio_tungstenite::tungstenite::protocol::Message;
Expand All @@ -46,9 +45,7 @@ fn tempfile_path(prefix: &str) -> PathBuf {
}

async fn spawn_daemon() -> (daemon::DaemonHandle, PathBuf) {
let probe = TcpListener::bind("127.0.0.1:0").await.unwrap();
let port = probe.local_addr().unwrap().port();
drop(probe);
let port = 0;

let config = DaemonConfig::new(port);
let sock = tempfile_path("bsk-test-user-interrupt");
Expand Down
5 changes: 1 addition & 4 deletions crates/bsk-cli/tests/sessions_ipc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ use bsk_protocol::tools::{SessionStartParams, SessionStartResult, SessionStopPar
use bsk_protocol::{BrowserPeerInfo, Frame, Method, RequestFrame, ResponseBody, ResponseFrame};
use futures_util::{SinkExt, StreamExt};
use rand::Rng;
use tokio::net::TcpListener;
use tokio_tungstenite::tungstenite::handshake::client::generate_key;
use tokio_tungstenite::tungstenite::http::Request;
use tokio_tungstenite::tungstenite::protocol::Message;
Expand All @@ -39,9 +38,7 @@ async fn spawn_daemon() -> (daemon::DaemonHandle, PathBuf) {
}

async fn spawn_daemon_with_connect_wait(connect_wait: Duration) -> (daemon::DaemonHandle, PathBuf) {
let probe = TcpListener::bind("127.0.0.1:0").await.unwrap();
let port = probe.local_addr().unwrap().port();
drop(probe);
let port = 0;

let config = DaemonConfig::new(port).with_extension_connect_wait(connect_wait);
let sock = tempfile_path("bsk-test-ipc");
Expand Down
5 changes: 1 addition & 4 deletions crates/bsk-cli/tests/tools_ipc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ use futures_util::stream::{SplitSink, SplitStream};
use futures_util::{SinkExt, StreamExt};
use rand::Rng;
use serde_json::{Value, json};
use tokio::net::TcpListener;
use tokio::sync::Mutex;
use tokio_tungstenite::tungstenite::handshake::client::generate_key;
use tokio_tungstenite::tungstenite::http::Request;
Expand All @@ -40,9 +39,7 @@ fn tempfile_path(prefix: &str) -> PathBuf {
}

async fn spawn_daemon() -> (daemon::DaemonHandle, PathBuf) {
let probe = TcpListener::bind("127.0.0.1:0").await.unwrap();
let port = probe.local_addr().unwrap().port();
drop(probe);
let port = 0;
let sock = tempfile_path("bsk-test-tools");
let handle = daemon::run(DaemonConfig::new(port), Some(sock.clone()))
.await
Expand Down
5 changes: 1 addition & 4 deletions crates/bsk-cli/tests/tools_m7_ipc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ use futures_util::stream::{SplitSink, SplitStream};
use futures_util::{SinkExt, StreamExt};
use rand::Rng;
use serde_json::{Value, json};
use tokio::net::TcpListener;
use tokio::sync::Mutex;
use tokio_tungstenite::tungstenite::handshake::client::generate_key;
use tokio_tungstenite::tungstenite::http::Request;
Expand All @@ -44,9 +43,7 @@ fn tempfile_path(prefix: &str) -> PathBuf {
}

async fn spawn_daemon() -> (daemon::DaemonHandle, PathBuf) {
let probe = TcpListener::bind("127.0.0.1:0").await.unwrap();
let port = probe.local_addr().unwrap().port();
drop(probe);
let port = 0;
let sock = tempfile_path("bsk-test-tools-m7");
let handle = daemon::run(DaemonConfig::new(port), Some(sock.clone()))
.await
Expand Down
5 changes: 1 addition & 4 deletions crates/bsk-cli/tests/tools_m8_ipc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ use futures_util::stream::{SplitSink, SplitStream};
use futures_util::{SinkExt, StreamExt};
use rand::Rng;
use serde_json::Value;
use tokio::net::TcpListener;
use tokio::sync::Mutex;
use tokio_tungstenite::tungstenite::handshake::client::generate_key;
use tokio_tungstenite::tungstenite::http::Request;
Expand All @@ -43,9 +42,7 @@ fn tempfile_path(prefix: &str) -> PathBuf {
}

async fn spawn_daemon() -> (daemon::DaemonHandle, PathBuf) {
let probe = TcpListener::bind("127.0.0.1:0").await.unwrap();
let port = probe.local_addr().unwrap().port();
drop(probe);
let port = 0;
let sock = tempfile_path("bsk-test-tools-m8");
let handle = daemon::run(DaemonConfig::new(port), Some(sock.clone()))
.await
Expand Down
Loading
Loading