diff --git a/build.ps1 b/build.ps1 new file mode 100644 index 00000000..81af3e37 --- /dev/null +++ b/build.ps1 @@ -0,0 +1,101 @@ +$ErrorActionPreference = "Stop" + +function Find-RequiredCommand([string]$Name, [string]$InstallHint) { + $Command = Get-Command $Name -CommandType Application -ErrorAction SilentlyContinue + if (-not $Command) { + throw "$Name was not found. $InstallHint" + } + return $Command.Source +} + +function Invoke-Native([string]$Description, [string]$FilePath, [string[]]$ArgumentList) { + & $FilePath @ArgumentList + if ($LASTEXITCODE -ne 0) { + throw "$Description failed with exit code $LASTEXITCODE." + } +} + +$RepoRoot = $PSScriptRoot +$CargoCommand = Get-Command cargo -CommandType Application -ErrorAction SilentlyContinue +if ($CargoCommand) { + $CargoPath = $CargoCommand.Source +} else { + $CargoPath = Join-Path $env:USERPROFILE ".cargo\bin\cargo.exe" + if (-not (Test-Path -LiteralPath $CargoPath)) { + throw "cargo was not found on PATH or at $CargoPath" + } +} +$RustcPath = Join-Path (Split-Path -Parent $CargoPath) "rustc.exe" +if (-not (Test-Path -LiteralPath $RustcPath)) { + throw "rustc was not found beside Cargo at $RustcPath" +} +$RustupPath = Find-RequiredCommand "rustup" "Install Rust through rustup." +$NodePath = Find-RequiredCommand "node" "Install Node.js 22 or newer." +$PnpmPath = Find-RequiredCommand "pnpm" "Install pnpm 10 (for example: npm install --global pnpm@10)." +$WasmPackPath = Find-RequiredCommand "wasm-pack" "Install wasm-pack (for example: cargo install wasm-pack)." +$InstalledRustTargets = & $RustupPath target list --installed +if ($LASTEXITCODE -ne 0) { + throw "Could not list installed Rust targets." +} +if ($InstalledRustTargets -notcontains "wasm32-unknown-unknown") { + throw "Rust target wasm32-unknown-unknown is required. Run: rustup target add wasm32-unknown-unknown" +} +$RustHost = (& $RustcPath -vV | Where-Object { $_ -like "host: *" }) -replace "^host: ", "" +$MsvcArch = switch ($RustHost) { + "aarch64-pc-windows-msvc" { "arm64" } + "x86_64-pc-windows-msvc" { "x64" } + "i686-pc-windows-msvc" { "x86" } + default { throw "unsupported MSVC Rust host target: $RustHost" } +} + +$VsWhere = Join-Path ${env:ProgramFiles(x86)} "Microsoft Visual Studio\Installer\vswhere.exe" +if (-not (Test-Path -LiteralPath $VsWhere)) { + throw "Visual Studio's vswhere.exe was not found at $VsWhere" +} +$VsInstall = & $VsWhere -latest -products * -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 -property installationPath +if (-not $VsInstall) { + throw "Visual Studio with the x64 MSVC build tools is required" +} +$VsDevCmd = Join-Path $VsInstall "Common7\Tools\VsDevCmd.bat" +if (-not (Test-Path -LiteralPath $VsDevCmd)) { + throw "Visual Studio developer command prompt was not found at $VsDevCmd" +} +$ClangDir = Join-Path $VsInstall "VC\Tools\Llvm\$MsvcArch\bin" +if (-not (Test-Path -LiteralPath (Join-Path $ClangDir "clang.exe"))) { + throw "Visual Studio's $MsvcArch clang.exe was not found at $ClangDir" +} + +# Import the matching MSVC environment into this PowerShell process so Cargo can +# find the linker and Windows SDK regardless of how this shell was opened. +$VsEnvironment = & cmd.exe /d /s /c "call `"$VsDevCmd`" -arch=$MsvcArch -host_arch=$MsvcArch >nul && set" +foreach ($Line in $VsEnvironment) { + if ($Line -match "^([^=]+)=(.*)$") { + Set-Item -LiteralPath "Env:$($matches[1])" -Value $matches[2] + } +} +$env:PATH = "$ClangDir;$env:PATH" +Set-Item -LiteralPath "Env:CC_$($RustHost -replace '-', '_')" -Value (Join-Path $ClangDir "clang.exe") + +Push-Location $RepoRoot +try { + Push-Location (Join-Path $RepoRoot "crates\browser") + try { + Invoke-Native "Build browser WebAssembly" $WasmPackPath @("build", "--target", "web", "--release", "--out-dir", "pkg") + } finally { + Pop-Location + } + + Push-Location (Join-Path $RepoRoot "js") + try { + Invoke-Native "Install JavaScript dependencies" $PnpmPath @("install", "--frozen-lockfile") + Invoke-Native "Build @blit-sh/core" $PnpmPath @("--filter", "@blit-sh/core", "run", "build") + Invoke-Native "Build @blit-sh/solid" $PnpmPath @("--filter", "@blit-sh/solid", "run", "build") + Invoke-Native "Build @blit-sh/ui" $PnpmPath @("--filter", "@blit-sh/ui", "run", "build") + } finally { + Pop-Location + } + + Invoke-Native "Build blit" $CargoPath @("build", "-p", "blit-cli", "--profile", "profiling") +} finally { + Pop-Location +} diff --git a/crates/cli/src/agent.rs b/crates/cli/src/agent.rs index 3fbf1f1a..b1ecc30e 100644 --- a/crates/cli/src/agent.rs +++ b/crates/cli/src/agent.rs @@ -165,6 +165,7 @@ impl AgentConn { } /// The reason this connection was kicked, once `recv` has seen one. + #[cfg(unix)] pub(crate) fn kicked_reason(&self) -> Option<&str> { self.kicked.as_deref() } diff --git a/crates/cli/src/transport.rs b/crates/cli/src/transport.rs index b68c3504..9a4897e0 100644 --- a/crates/cli/src/transport.rs +++ b/crates/cli/src/transport.rs @@ -156,11 +156,30 @@ pub async fn connect_ipc(path: &str) -> Result { #[cfg(windows)] { use tokio::net::windows::named_pipe::ClientOptions; - Ok(Transport::NamedPipe( - ClientOptions::new() - .open(path) - .map_err(|e| format!("cannot connect to {path}: {e}"))?, - )) + + // `CreateFile` opens an available pipe instance immediately. The + // server creates its next instance only after accepting that client, + // so a readiness probe followed immediately by the real connection + // can briefly see ERROR_PIPE_BUSY. Unlike a Unix socket, that does + // not mean the server is unavailable. + const ERROR_PIPE_BUSY: i32 = 231; + const PIPE_BUSY_RETRIES: usize = 100; + const PIPE_BUSY_RETRY_DELAY: std::time::Duration = std::time::Duration::from_millis(10); + + for attempt in 0..=PIPE_BUSY_RETRIES { + match ClientOptions::new().open(path) { + Ok(pipe) => return Ok(Transport::NamedPipe(pipe)), + Err(error) + if error.raw_os_error() == Some(ERROR_PIPE_BUSY) + && attempt < PIPE_BUSY_RETRIES => + { + tokio::time::sleep(PIPE_BUSY_RETRY_DELAY).await; + } + Err(error) => return Err(format!("cannot connect to {path}: {error}")), + } + } + + unreachable!("the final pipe-open attempt always returns") } } @@ -785,6 +804,35 @@ fn spawn_detached_server( mod tests { use super::*; + #[cfg(windows)] + #[tokio::test] + async fn connect_ipc_waits_for_next_pipe_instance() { + use tokio::net::windows::named_pipe::{ClientOptions, ServerOptions}; + + let pipe_name = format!(r"\\.\pipe\blit-test-connect-{}", std::process::id()); + let first_server = ServerOptions::new() + .first_pipe_instance(true) + .create(&pipe_name) + .unwrap(); + let first_client = ClientOptions::new().open(&pipe_name).unwrap(); + first_server.connect().await.unwrap(); + + // Keep the first instance occupied long enough that the second client + // must encounter ERROR_PIPE_BUSY before the listener creates another. + let next_name = pipe_name.clone(); + let next_instance = tokio::spawn(async move { + tokio::time::sleep(std::time::Duration::from_millis(50)).await; + let server = ServerOptions::new().create(&next_name).unwrap(); + server.connect().await.unwrap(); + }); + + let second_client = connect_ipc(&pipe_name).await.unwrap(); + drop(second_client); + drop(first_client); + drop(first_server); + next_instance.await.unwrap(); + } + #[tokio::test] async fn monitored_child_reports_early_exit() { #[cfg(unix)] diff --git a/crates/fssync/src/lib.rs b/crates/fssync/src/lib.rs index 4b8d6a7f..e8af49e5 100644 --- a/crates/fssync/src/lib.rs +++ b/crates/fssync/src/lib.rs @@ -1625,6 +1625,8 @@ fn write_atomic(target: &Path, bytes: &[u8], mode: u32, durable: bool) -> io::Re /// create-exclusive ("New File") precondition. fn create_exclusive(target: &Path, bytes: &[u8], mode: u32, durable: bool) -> io::Result<()> { use std::io::Write as _; + #[cfg(not(unix))] + let _ = mode; let mut opts = fs::OpenOptions::new(); opts.write(true).create_new(true); #[cfg(unix)] @@ -4234,7 +4236,9 @@ impl SyncEngine { }; let lock = path_write_lock(&target); let _guard = lock.lock().unwrap(); - let mut builder = fs::DirBuilder::new(); + let builder = fs::DirBuilder::new(); + #[cfg(unix)] + let mut builder = builder; #[cfg(unix)] if o.mode != 0 { use std::os::unix::fs::DirBuilderExt; diff --git a/crates/server/src/lib.rs b/crates/server/src/lib.rs index 7b1afc37..46ad8ea2 100644 --- a/crates/server/src/lib.rs +++ b/crates/server/src/lib.rs @@ -4,6 +4,8 @@ use blit_alacritty::{ use blit_compositor::{ CompositorCommand, CompositorEvent, CompositorHandle, TouchPhase, TouchPoint, }; +#[cfg(unix)] +use blit_remote::FEATURE_CREATE_EXEC; #[cfg(target_os = "linux")] use blit_remote::desktop::{ C2S_DESKTOP_SUBSCRIBE, C2S_NOTIFICATION_EVENT, C2S_TRAY_EVENT, DESKTOP_SUBSCRIBE_NOTIFICATIONS, @@ -36,19 +38,18 @@ use blit_remote::{ C2S_SURFACE_RESIZE, C2S_SURFACE_SUBSCRIBE, C2S_SURFACE_TEXT, C2S_SURFACE_TOUCH, C2S_SURFACE_UNSUBSCRIBE, C2S_TERM_CWD, C2S_UNSUBSCRIBE, CAPTURE_FORMAT_AVIF, CAPTURE_FORMAT_PNG, CLIENT_FEATURE_SURFACE_TIMESTAMP_SUB_US, CLIENT_LIST_WANT_ORIGIN, - FEATURE_CLIENT_CONTROL, FEATURE_CLIENT_ORIGIN, FEATURE_COPY_RANGE, FEATURE_CREATE_EXEC, - FEATURE_CREATE_NO_SUBSCRIBE, FEATURE_CREATE_NONCE, FEATURE_CREATE_STATUS, FEATURE_KILL_MODE, - FEATURE_PTY_DEADLINE, FEATURE_RESIZE_BATCH, FEATURE_RESTART, FEATURE_SCROLL_BY, FrameState, - KICK_REASON_MAX, KILL_LEADER_ONLY, READ_ANSI, READ_TAIL, REMOTE_INPUT_POINTER, - REMOTE_INPUT_TOUCH, S2C_CLOSED, S2C_CREATE_FAILED, S2C_CREATED, S2C_CREATED_N, S2C_LIST, - S2C_PING, S2C_QUIT, S2C_READY, S2C_SEARCH_RESULTS, S2C_SURFACE_CAPTURE, S2C_SURFACE_LIST, - S2C_TEXT, S2C_TITLE, STATUS_BUDGET, STATUS_CONFLICT, STATUS_INVALID, STATUS_NOT_FOUND, - STATUS_OK, STATUS_OTHER, STATUS_TOO_LARGE, SURFACE_FRAME_CODEC_H264, - SURFACE_FRAME_FLAG_KEYFRAME, SURFACE_POINTER_AXIS2_LEN, SURFACE_POINTER_DOWN, - SURFACE_POINTER_LEAVE, SURFACE_POINTER_MOVE, SURFACE_POINTER_UP, SURFACE_TOUCH_CANCEL, - SURFACE_TOUCH_DISABLE, SURFACE_TOUCH_DOWN, SURFACE_TOUCH_ENABLE, SURFACE_TOUCH_MOTION, - SURFACE_TOUCH_UP, build_update_msg, clamp_cursor_rect, msg_copy_failed, msg_hello, - msg_kick_result, msg_kicked, msg_s2c_client_list, msg_s2c_client_list2, + FEATURE_CLIENT_CONTROL, FEATURE_CLIENT_ORIGIN, FEATURE_COPY_RANGE, FEATURE_CREATE_NO_SUBSCRIBE, + FEATURE_CREATE_NONCE, FEATURE_CREATE_STATUS, FEATURE_KILL_MODE, FEATURE_PTY_DEADLINE, + FEATURE_RESIZE_BATCH, FEATURE_RESTART, FEATURE_SCROLL_BY, FrameState, KICK_REASON_MAX, + KILL_LEADER_ONLY, READ_ANSI, READ_TAIL, REMOTE_INPUT_POINTER, REMOTE_INPUT_TOUCH, S2C_CLOSED, + S2C_CREATE_FAILED, S2C_CREATED, S2C_CREATED_N, S2C_LIST, S2C_PING, S2C_QUIT, S2C_READY, + S2C_SEARCH_RESULTS, S2C_SURFACE_CAPTURE, S2C_SURFACE_LIST, S2C_TEXT, S2C_TITLE, STATUS_BUDGET, + STATUS_CONFLICT, STATUS_INVALID, STATUS_NOT_FOUND, STATUS_OK, STATUS_OTHER, STATUS_TOO_LARGE, + SURFACE_FRAME_CODEC_H264, SURFACE_FRAME_FLAG_KEYFRAME, SURFACE_POINTER_AXIS2_LEN, + SURFACE_POINTER_DOWN, SURFACE_POINTER_LEAVE, SURFACE_POINTER_MOVE, SURFACE_POINTER_UP, + SURFACE_TOUCH_CANCEL, SURFACE_TOUCH_DISABLE, SURFACE_TOUCH_DOWN, SURFACE_TOUCH_ENABLE, + SURFACE_TOUCH_MOTION, SURFACE_TOUCH_UP, build_update_msg, clamp_cursor_rect, msg_copy_failed, + msg_hello, msg_kick_result, msg_kicked, msg_s2c_client_list, msg_s2c_client_list2, msg_s2c_clipboard_content, msg_s2c_clipboard_list, msg_s2c_clipboard_owner, msg_s2c_scroll_offset, msg_s2c_surface_remote_input, msg_s2c_used_rows, msg_surface_activated, msg_surface_app_id, msg_surface_created, msg_surface_destroyed, msg_surface_encoder, @@ -19704,6 +19705,8 @@ async fn handle_client_registered() as u32; si.lpAttributeList = attr_list; @@ -661,7 +662,10 @@ pub fn respawn_child( 0, EXTENDED_STARTUPINFO_PRESENT | CREATE_SUSPENDED, std::ptr::null(), - std::ptr::null(), + dir_wide + .as_ref() + .map(|d| d.as_ptr()) + .unwrap_or(std::ptr::null()), &si.StartupInfo, &mut pi, )