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
4 changes: 3 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "local-dev-proxy"
version = "0.0.17"
version = "0.0.18"
edition = "2024"
description = "Local development process orchestration with a built-in reverse proxy"
authors = ["Andrew Chen <hello@andrewtheguy.com>"]
Expand Down Expand Up @@ -37,6 +37,8 @@ windows-sys = { version = "0.61", features = ["Win32_Foundation", "Win32_System_
[target.'cfg(target_os = "macos")'.dependencies]
objc2 = "0.6.4"
objc2-app-kit = { version = "0.3.2", default-features = false, features = ["std", "NSApplication", "NSResponder"] }
objc2-foundation = { version = "0.3.2", default-features = false, features = ["std", "block2", "NSNotification", "NSOperation", "NSString"] }
block2 = "0.6.2"

[dev-dependencies]
futures-util = { version = "0.3.34", default-features = false, features = ["sink", "std"] }
Expand Down
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -164,8 +164,9 @@ similar locations resolve even when the binary is started outside a terminal.
it from the tray icon's **Open Manager** (or by launching the application again). On
macOS the Dock icon is shown only while the window is open. Without a tray icon,
closing the window quits.
- **Quit** (the window's button, the tray menu, or Ctrl/Cmd-Q) → stops the proxy, stops
every managed service together with its child processes, and exits.
- **Quit** (the window's button, the tray menu, or Ctrl/Cmd-Q; on macOS also the app
menu, the Dock, and logging out) → stops the proxy, stops every managed service
together with its child processes, and exits.
- **SIGTERM, SIGINT (Ctrl-C), or SIGHUP** (Ctrl-C, Ctrl-Break, or closing the console
window on Windows) → the same as Quit.
- **A managed service exits on its own** → it is reported as `crashed` with its exit
Expand Down
65 changes: 63 additions & 2 deletions src/desktop.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,10 +53,16 @@ pub struct Desktop;
impl Frontend for Desktop {
fn run(self: Box<Self>, manager: Manager, events: Receiver<AppEvent>) -> ExitCode {
let manager = Arc::new(Mutex::new(manager));
// AppKit's own quit paths (the app menu's Quit and its Cmd-Q, the
// Dock's Quit, logging out) exit the process from inside the event
// loop, so this never regains control after them.
let _terminate = app_exit::on_terminate({
let manager = Arc::clone(&manager);
move || shut_down(&manager)
});
let result = run_event_loop(Arc::clone(&manager), events);
// However the event loop ended, the window is gone; stop everything.
tracing::info!("Shutting down");
lock(&manager).stop();
shut_down(&manager);
match result {
Ok(()) => ExitCode::SUCCESS,
Err(err) => {
Expand Down Expand Up @@ -115,6 +121,11 @@ fn forward_app_events(
Ok(())
}

fn shut_down(manager: &Mutex<Manager>) {
tracing::info!("Shutting down");
lock(manager).stop();
}

fn lock(manager: &Mutex<Manager>) -> MutexGuard<'_, Manager> {
manager.lock().unwrap_or_else(PoisonError::into_inner)
}
Expand Down Expand Up @@ -770,6 +781,56 @@ mod dock {
pub fn set_icon_visible(_visible: bool) {}
}

mod app_exit {
/// Keeps the terminate handler registered while alive.
pub struct Registration {
#[cfg(target_os = "macos")]
token: objc2::rc::Retained<
objc2::runtime::ProtocolObject<dyn objc2::runtime::NSObjectProtocol>,
>,
}

/// Run `handler` when AppKit is about to exit the process (`terminate:`),
/// which it does without returning from the event loop.
#[cfg(target_os = "macos")]
pub fn on_terminate(handler: impl Fn() + Send + 'static) -> Registration {
use std::ptr::NonNull;

use block2::RcBlock;
use objc2_app_kit::NSApplicationWillTerminateNotification;
use objc2_foundation::{NSNotification, NSNotificationCenter};

let block = RcBlock::new(move |_: NonNull<NSNotification>| handler());
let center = NSNotificationCenter::defaultCenter();
// SAFETY: the name is an AppKit constant; with no queue the block
// runs synchronously on the posting (main) thread, and it is `Send`.
let token = unsafe {
center.addObserverForName_object_queue_usingBlock(
Some(NSApplicationWillTerminateNotification),
None,
None,
&block,
)
};
Registration { token }
}

#[cfg(not(target_os = "macos"))]
pub fn on_terminate(_handler: impl Fn() + Send + 'static) -> Registration {
Registration {}
}

#[cfg(target_os = "macos")]
impl Drop for Registration {
fn drop(&mut self) {
use objc2_foundation::NSNotificationCenter;

// SAFETY: `token` came from this center's `addObserverForName`.
unsafe { NSNotificationCenter::defaultCenter().removeObserver(self.token.as_ref()) };
}
}
}

fn install_toml_syntax(window: &ManagerWindow) {
let syntax = window.global::<TomlSyntax>();
syntax.on_tokenize(|text| {
Expand Down
Loading