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
586 changes: 499 additions & 87 deletions apps/desktop-gpui/patches/zed-gpui.patch

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions apps/desktop-gpui/src/assets.rs
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,7 @@ const ICONS: &[(&str, &[u8])] = assets!("icons":
"sparkles.svg",
"timer.svg",
"volume-2.svg",
"volume-x.svg",
"diamond.svg",
"x-mark.svg",
"zap.svg",
Expand Down
24 changes: 11 additions & 13 deletions apps/desktop-gpui/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,7 @@ fn main() {
// A relaunch means "run the code I just built": take over from any
// previous instance still alive in the tray (see `single_instance`).
single_instance::acquire();
store::mark_handoff_session();
Comment thread
richiemcilroy marked this conversation as resolved.

platform::install_url_scheme_handler();
for argument in std::env::args().skip(1) {
Expand Down Expand Up @@ -254,7 +255,16 @@ fn main() {
move |window, cx| cx.new(|cx| MainWindow::new(session, window, cx))
},
)
.expect("failed to open the main window");
.inspect_err(|error| tracing::error!("failed to open the main window: {error:#}"));
let Ok(window_handle) = window_handle else {
cx.quit();
return;
};

cx.on_app_quit(|_| async {
crate::store::clear_handoff_marker();
})
.detach();

app_windows::init(window_handle, session, cx);
updates::schedule_startup_check(cx);
Expand Down Expand Up @@ -285,18 +295,6 @@ fn main() {
}
})
.detach();
// The other half of the Tauri app's hand-off protocol
// (`store::handoff_marker_path`): staying up this long is what proves
// to it that redirecting here was not a mistake.
cx.spawn(async move |cx| {
cx.background_executor()
.timer(std::time::Duration::from_secs(10))
.await;
cx.background_executor()
.spawn(async { crate::store::clear_handoff_marker() })
.await;
})
.detach();
// `CAP_GPUI_AUTO_TRAY` / `CAP_GPUI_TRAY_DUMP`: the tray's harness path.
tray::drive_from_env(cx);
// `CAP_GPUI_AUTO_CLOSE=settings|main:<secs>`: run the ⌘W body against
Expand Down
4 changes: 0 additions & 4 deletions apps/desktop-gpui/src/menus.rs
Original file line number Diff line number Diff line change
Expand Up @@ -287,10 +287,6 @@ pub fn close_window_by_handle(handle: gpui::AnyWindowHandle, cx: &mut App) {
/// quit waits for the session to come back to `Idle` (bounded, so a wedged
/// finalize cannot make the app unquittable).
pub fn quit(cx: &mut App) {
// Reaching a deliberate quit is proof enough that this build came up, even
// if it happens inside the marker's ten-second window.
crate::store::clear_handoff_marker();

let session = RecordingSession::global(cx);
if session.read(cx).phase == Phase::Idle {
cx.quit();
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop-gpui/src/onboarding_window.rs
Original file line number Diff line number Diff line change
Expand Up @@ -582,7 +582,7 @@ impl OnboardingWindow {
.radius(px(8.))
.icon("icons/rotate-ccw.svg")
.label("Relaunch Cap")
.on_click(cx.listener(|_, _, _, _| permissions::relaunch())),
.on_click(cx.listener(|_, _, _, cx| permissions::relaunch(cx))),
),
),
)
Expand Down
165 changes: 143 additions & 22 deletions apps/desktop-gpui/src/permissions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -263,36 +263,68 @@ pub fn open_permission_settings(permission: OSPermission) {

/// Relaunch the app -- the Tauri onboarding offers this after sending the
/// user to System Settings, because a fresh screen-recording or accessibility
/// grant often only takes effect in a new process. Spawns a detached shell
/// that reopens the bundle (or the bare binary in dev) after this process has
/// had a beat to exit, then quits.
pub fn relaunch() {
/// grant often only takes effect in a new process. The replacement must wait
/// for shutdown: Tauri otherwise sees this process alive and exits without
/// starting a replacement.
pub fn relaunch(cx: &mut gpui::App) {
static REQUESTED: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);

let Ok(exe) = std::env::current_exe() else {
tracing::error!("relaunch: current_exe unavailable");
return;
};

let mut command = relaunch_command(&exe, std::process::id());
if let Err(error) = spawn_relaunch(&mut command, &REQUESTED, || crate::menus::quit(cx)) {
tracing::error!("relaunch failed to spawn: {error}");
}
}

fn relaunch_command(exe: &std::path::Path, pid: u32) -> std::process::Command {
#[cfg(target_os = "macos")]
let command = {
let bundle = exe
.ancestors()
.find(|path| path.extension().is_some_and(|ext| ext == "app"))
.map(std::path::Path::to_path_buf);
match bundle {
Some(bundle) => format!("sleep 0.3; /usr/bin/open -n \"{}\"", bundle.display()),
None => format!("sleep 0.3; exec \"{}\"", exe.display()),
}
};
let bundle = exe
.ancestors()
.find(|path| path.extension().is_some_and(|ext| ext == "app"));
#[cfg(not(target_os = "macos"))]
let command = format!("sleep 0.3; exec \"{}\"", exe.display());
let bundle: Option<&std::path::Path> = None;

let (script, target) = match bundle {
Some(bundle) => (
r#"while kill -0 "$1" 2>/dev/null; do sleep 0.1; done; exec /usr/bin/open -n "$2""#,
bundle,
),
None => (
r#"while kill -0 "$1" 2>/dev/null; do sleep 0.1; done; exec "$2""#,
exe,
),
};
let mut command = std::process::Command::new("/bin/sh");
command
.args(["-c", script, "cap-relaunch"])
.arg(pid.to_string())
.arg(target);
command
}

match std::process::Command::new("/bin/sh")
.arg("-c")
.arg(command)
.spawn()
{
Ok(_) => std::process::exit(0),
Err(error) => tracing::error!("relaunch failed to spawn: {error}"),
fn spawn_relaunch(
command: &mut std::process::Command,
requested: &std::sync::atomic::AtomicBool,
quit: impl FnOnce(),
) -> std::io::Result<Option<std::process::Child>> {
use std::sync::atomic::Ordering;

if requested.swap(true, Ordering::AcqRel) {
return Ok(None);
}
match command.spawn() {
Ok(child) => {
quit();
Ok(Some(child))
}
Err(error) => {
requested.store(false, Ordering::Release);
Err(error)
}
}
}

Expand Down Expand Up @@ -401,6 +433,95 @@ mod macos {
mod tests {
use super::*;

#[test]
fn relaunch_requests_quit_once_after_spawn_and_allows_retry_after_failure() {
use std::{
cell::Cell,
sync::atomic::{AtomicBool, Ordering},
};

let requested = AtomicBool::new(false);
let quits = Cell::new(0);
assert!(
spawn_relaunch(&mut std::process::Command::new(""), &requested, || {
quits.set(quits.get() + 1);
})
.is_err()
);
assert!(!requested.load(Ordering::Acquire));
assert_eq!(quits.get(), 0);

let mut command = std::process::Command::new(std::env::current_exe().unwrap());
command.arg("--list").stdout(std::process::Stdio::null());
let mut child = spawn_relaunch(&mut command, &requested, || quits.set(quits.get() + 1))
.unwrap()
.unwrap();
assert!(child.wait().unwrap().success());
assert!(
spawn_relaunch(&mut command, &requested, || quits.set(quits.get() + 1))
.unwrap()
.is_none()
);
assert_eq!(quits.get(), 1);
}

#[cfg(target_os = "macos")]
#[test]
fn bundled_relaunch_passes_the_bundle_as_a_literal_argument() {
let bundle = std::path::Path::new("/Applications/Cap ' \" $ app.app");
let command = relaunch_command(&bundle.join("Contents/MacOS/cap-gpui"), 123);
let args = command.get_args().collect::<Vec<_>>();
assert_eq!(command.get_program(), "/bin/sh");
assert_eq!(args[3], "123");
assert_eq!(args[4], bundle.as_os_str());
assert!(
args[1]
.to_str()
.unwrap()
.ends_with(r#"exec /usr/bin/open -n "$2""#)
);
}

#[cfg(unix)]
#[test]
fn relaunch_waits_for_the_previous_process_and_handles_literal_paths() {
use std::os::unix::fs::PermissionsExt;

let nonce = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos();
let root =
std::env::temp_dir().join(format!("cap-gpui-relaunch-{}-{nonce}", std::process::id()));
std::fs::create_dir_all(&root).unwrap();
let executable = root.join("replacement ' \" $ program");
let output = root.join("relaunched");
std::fs::write(
&executable,
b"#!/bin/sh\nprintf relaunched > \"$CAP_GPUI_RELAUNCH_TEST_OUTPUT\"\n",
)
.unwrap();
std::fs::set_permissions(&executable, std::fs::Permissions::from_mode(0o700)).unwrap();

let mut previous = std::process::Command::new("/bin/sh")
.args(["-c", "read -r line"])
.stdin(std::process::Stdio::piped())
.spawn()
.unwrap();
let mut replacement = relaunch_command(&executable, previous.id())
.env("CAP_GPUI_RELAUNCH_TEST_OUTPUT", &output)
.spawn()
.unwrap();
std::thread::sleep(std::time::Duration::from_millis(400));
assert!(replacement.try_wait().unwrap().is_none());
assert!(!output.exists());
drop(previous.stdin.take());
previous.wait().unwrap();
assert!(replacement.wait().unwrap().success());
assert_eq!(std::fs::read(&output).unwrap(), b"relaunched");
std::fs::remove_dir_all(root).unwrap();
}

fn raw(
screen: bool,
ax: bool,
Expand Down
46 changes: 4 additions & 42 deletions apps/desktop-gpui/src/platform.rs
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,9 @@ pub struct PanelBehavior {
#[cfg(target_os = "macos")]
pub use mac::*;

#[cfg(target_os = "macos")]
mod macos_occlusion;

#[cfg(target_os = "macos")]
mod mac {
use gpui::Window;
Expand Down Expand Up @@ -291,48 +294,7 @@ mod mac {
/// (self-delegating) occlusion handler for windows that opened before the
/// state ever changed.
pub fn install_occlusion_shim() {
use objc2::ffi::{class_addMethod, objc_msgSendSuper, objc_super};

unsafe extern "C" fn occlusion_state_shim(this: *mut AnyObject, sel: Sel) -> usize {
unsafe {
let class = (*this).class();
let Some(superclass) = class.superclass() else {
return 0;
};
let mut sup = objc_super {
receiver: this.cast(),
super_class: (superclass as *const objc2::runtime::AnyClass).cast(),
};
let send: unsafe extern "C" fn(*mut objc_super, Sel) -> usize =
std::mem::transmute(objc_msgSendSuper as unsafe extern "C" fn());
let raw = send(&mut sup, sel);
if raw != 0 { raw | 0x2 } else { raw }
}
}

for name in ["GPUIWindow", "GPUIPanel"] {
let Some(class) = objc2::runtime::AnyClass::get(name) else {
// The class registers lazily with the first window; the caller
// runs before that only if nothing was opened -- harmless, the
// second call from `kick_display_link` retries.
continue;
};
let added = unsafe {
class_addMethod(
(class as *const objc2::runtime::AnyClass as *mut objc2::ffi::objc_class)
.cast(),
objc2::sel!(occlusionState).as_ptr(),
Some(std::mem::transmute::<
unsafe extern "C" fn(*mut AnyObject, Sel) -> usize,
unsafe extern "C" fn(),
>(occlusion_state_shim)),
c"Q@:".as_ptr(),
)
};
if objc2::runtime::Bool::from_raw(added).as_bool() {
tracing::info!("installed macOS 26 occlusion shim on {name}");
}
}
super::macos_occlusion::install();

// Same per-class, retried-from-the-same-spots lifecycle, so it rides
// along here.
Expand Down
Loading
Loading