Skip to content
Merged
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
101 changes: 101 additions & 0 deletions build.ps1
Original file line number Diff line number Diff line change
@@ -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
}
1 change: 1 addition & 0 deletions crates/cli/src/agent.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
Expand Down
58 changes: 53 additions & 5 deletions crates/cli/src/transport.rs
Original file line number Diff line number Diff line change
Expand Up @@ -156,11 +156,30 @@ pub async fn connect_ipc(path: &str) -> Result<Transport, String> {
#[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")
}
}

Expand Down Expand Up @@ -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)]
Expand Down
6 changes: 5 additions & 1 deletion crates/fssync/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down Expand Up @@ -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;
Expand Down
29 changes: 16 additions & 13 deletions crates/server/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -19704,6 +19705,8 @@ async fn handle_client_registered<S: AsyncRead + AsyncWrite + Unpin + Send + 'st
if data[0] == blit_remote::C2S_APP_SOCKET {
if let Some((nonce, app_id, instance_id)) = blit_remote::parse_app_socket_request(&data)
{
#[cfg(not(target_os = "linux"))]
let _ = (app_id, instance_id);
#[cfg(target_os = "linux")]
let reply = {
// Names go into a filesystem path and an environment
Expand Down
11 changes: 8 additions & 3 deletions crates/server/src/process.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,10 @@ use tokio::task::AbortHandle;
#[cfg(unix)]
use crate::pty;

#[cfg(all(windows, test))]
use windows_sys::Win32::Foundation::WAIT_OBJECT_0;
#[cfg(windows)]
use windows_sys::Win32::Foundation::{CloseHandle, HANDLE, INVALID_HANDLE_VALUE, WAIT_OBJECT_0};
use windows_sys::Win32::Foundation::{CloseHandle, HANDLE, INVALID_HANDLE_VALUE};
#[cfg(windows)]
use windows_sys::Win32::Globalization::{CSTR_EQUAL, CompareStringOrdinal};
#[cfg(windows)]
Expand All @@ -50,8 +52,11 @@ use windows_sys::Win32::System::JobObjects::{
};
#[cfg(windows)]
use windows_sys::Win32::System::Threading::{
CREATE_NEW_PROCESS_GROUP, CREATE_SUSPENDED, OpenProcess, OpenThread, PROCESS_SYNCHRONIZE,
ResumeThread, THREAD_SUSPEND_RESUME, WaitForSingleObject,
CREATE_NEW_PROCESS_GROUP, CREATE_SUSPENDED, OpenThread, ResumeThread, THREAD_SUSPEND_RESUME,
};
#[cfg(all(windows, test))]
use windows_sys::Win32::System::Threading::{
OpenProcess, PROCESS_SYNCHRONIZE, WaitForSingleObject,
};

const DEFAULT_MAX_PER_CLIENT: usize = 16;
Expand Down
6 changes: 5 additions & 1 deletion crates/server/src/pty/pty_windows.rs
Original file line number Diff line number Diff line change
Expand Up @@ -646,6 +646,7 @@ pub fn respawn_child(
}

let mut cmd_line = build_command_line(shell, shell_flags, command);
let dir_wide = dir.map(to_wide);
let mut si: STARTUPINFOEXW = unsafe { std::mem::zeroed() };
si.StartupInfo.cb = std::mem::size_of::<STARTUPINFOEXW>() as u32;
si.lpAttributeList = attr_list;
Expand All @@ -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,
)
Expand Down
Loading