Skip to content

Commit 98daa75

Browse files
Desktop: Add support for opening files through the already-running instance via a local socket (#4123)
* Desktop: Forward file-open args from a second launch to the running instance Also adds socket infrastructure that can be used in the future to allow dispatching actions from another process * Use socket instead of ipc terminologie * Fix * Fix * Better pipe name on windows
1 parent a28b943 commit 98daa75

10 files changed

Lines changed: 261 additions & 84 deletions

File tree

Cargo.lock

Lines changed: 32 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

about.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
accepted = [
2+
"0BSD", # Keep this list in sync with those in `/deny.toml`
23
"Apache-2.0 WITH LLVM-exception", # Keep this list in sync with those in `/deny.toml`
34
"Apache-2.0", # Keep this list in sync with those in `/deny.toml`
45
"BSD-2-Clause", # Keep this list in sync with those in `/deny.toml`

deny.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ ignore = [
6363
# [possible values: any SPDX 3.11 short identifier (+ optional exception)].
6464
#
6565
allow = [
66+
"0BSD", # Keep this list in sync with those in `/about.toml`
6667
"Apache-2.0 WITH LLVM-exception", # Keep this list in sync with those in `/about.toml`
6768
"Apache-2.0", # Keep this list in sync with those in `/about.toml`
6869
"BSD-2-Clause", # Keep this list in sync with those in `/about.toml`

desktop/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ lzma-rust2 = { workspace = true }
4848
serde = { workspace = true }
4949
rand = { workspace = true, features = ["thread_rng"] }
5050
clap = { workspace = true, features = ["derive"] }
51+
interprocess = "2.4.2"
5152
fd-lock = "4.0.4"
5253
ctrlc = "3.5.1"
5354
window_clipboard = "0.5"

desktop/src/app.rs

Lines changed: 20 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,6 @@ pub(crate) struct App {
4545
start_render_sender: SyncSender<()>,
4646
web_communication_initialized: bool,
4747
web_communication_startup_buffer: Vec<Vec<u8>>,
48-
#[cfg_attr(not(target_os = "macos"), expect(unused))]
4948
preferences: Preferences,
5049
launch_documents: Option<Vec<PathBuf>>,
5150
startup_time: Option<Instant>,
@@ -320,7 +319,7 @@ impl App {
320319
tracing::error!("OpenLaunchDocuments should only be sent once");
321320
return;
322321
};
323-
self.open_files(launch_documents);
322+
self.app_event_scheduler.schedule(AppEvent::OpenFiles(launch_documents));
324323
}
325324
DesktopFrontendMessage::UpdateMenu { entries } => {
326325
if let Some(window) = &self.window {
@@ -478,38 +477,35 @@ impl App {
478477
tracing::info!("Exiting main event loop");
479478
event_loop.exit();
480479
}
481-
#[cfg(target_os = "macos")]
482-
AppEvent::AddLaunchDocuments(paths) => {
480+
AppEvent::OpenFiles(paths) => {
481+
// Accumulate launch documents until OpenLaunchDocuments message is received
483482
if let Some(launch_documents) = &mut self.launch_documents {
484483
launch_documents.extend(paths);
485-
} else {
486-
self.open_files(paths);
484+
return;
485+
}
486+
487+
if paths.is_empty() {
488+
return;
487489
}
490+
let app_event_scheduler = self.app_event_scheduler.clone();
491+
let _ = thread::spawn(move || {
492+
for path in paths {
493+
tracing::info!("Opening file: {}", path.display());
494+
if let Ok(content) = fs::read(&path) {
495+
let message = DesktopWrapperMessage::OpenFile { path, content };
496+
app_event_scheduler.schedule(AppEvent::DesktopWrapperMessage(message));
497+
} else {
498+
tracing::error!("Failed to read file: {}", path.display());
499+
}
500+
}
501+
});
488502
}
489503
#[cfg(target_os = "macos")]
490504
AppEvent::MenuEvent { id } => {
491505
self.dispatch_desktop_wrapper_message(DesktopWrapperMessage::MenuEvent { id });
492506
}
493507
}
494508
}
495-
496-
fn open_files(&mut self, paths: Vec<PathBuf>) {
497-
if paths.is_empty() {
498-
return;
499-
}
500-
let app_event_scheduler = self.app_event_scheduler.clone();
501-
let _ = thread::spawn(move || {
502-
for path in paths {
503-
tracing::info!("Opening file: {}", path.display());
504-
if let Ok(content) = fs::read(&path) {
505-
let message = DesktopWrapperMessage::OpenFile { path, content };
506-
app_event_scheduler.schedule(AppEvent::DesktopWrapperMessage(message));
507-
} else {
508-
tracing::error!("Failed to read file: {}", path.display());
509-
}
510-
}
511-
});
512-
}
513509
}
514510
impl ApplicationHandler for App {
515511
fn can_create_surfaces(&mut self, event_loop: &dyn ActiveEventLoop) {

desktop/src/consts.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ pub(crate) const APP_DIRECTORY_NAME: &str = "graphite";
77
#[cfg(not(target_os = "linux"))]
88
pub(crate) const APP_DIRECTORY_NAME: &str = "Graphite";
99
pub(crate) const APP_LOCK_FILE_NAME: &str = "instance.lock";
10+
pub(crate) const APP_SOCKET_FILE_NAME: &str = "instance.sock";
1011
pub(crate) const APP_STATE_FILE_NAME: &str = "state.ron";
1112
pub(crate) const APP_PREFERENCES_FILE_NAME: &str = "preferences.ron";
1213
pub(crate) const APP_DOCUMENTS_DIRECTORY_NAME: &str = "documents";

desktop/src/event.rs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,7 @@ pub(crate) enum AppEvent {
99
DesktopWrapperMessage(DesktopWrapperMessage),
1010
NodeGraphExecutionResult(NodeGraphExecutionResult),
1111
Exit,
12-
#[cfg(target_os = "macos")]
13-
AddLaunchDocuments(Vec<std::path::PathBuf>),
12+
OpenFiles(Vec<std::path::PathBuf>),
1413
#[cfg(target_os = "macos")]
1514
MenuEvent {
1615
id: String,

desktop/src/lib.rs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ mod gpu_context;
1919
mod persist;
2020
mod preferences;
2121
mod render;
22+
mod socket;
2223
mod window;
2324

2425
pub(crate) mod consts;
@@ -58,7 +59,13 @@ pub fn start() {
5859
}
5960
Err(_) => {
6061
tracing::error!("Another instance is already running, Exiting.");
61-
std::process::exit(1);
62+
if !cli.files.is_empty()
63+
&& let Err(error) = socket::send(socket::Message::OpenFiles(cli.files))
64+
{
65+
tracing::error!("Failed to send socket message to running instance: {}", error);
66+
std::process::exit(1);
67+
}
68+
return;
6269
}
6370
};
6471

@@ -78,6 +85,8 @@ pub fn start() {
7885
let (app_event_sender, app_event_receiver) = std::sync::mpsc::channel();
7986
let app_event_scheduler = event_loop.create_app_event_scheduler(app_event_sender);
8087

88+
let _socket_handle = socket::start(app_event_scheduler.clone());
89+
8190
let (cef_view_info_sender, cef_view_info_receiver) = std::sync::mpsc::channel();
8291

8392
if cli.disable_ui_acceleration {

desktop/src/socket.rs

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
use interprocess::local_socket::{GenericFilePath, GenericNamespaced, ListenerNonblockingMode, ListenerOptions, Name, prelude::*};
2+
use std::io::{ErrorKind, Read, Write};
3+
use std::sync::mpsc;
4+
use std::thread;
5+
use std::time::Duration;
6+
7+
use crate::consts::APP_SOCKET_FILE_NAME;
8+
use crate::event::{AppEvent, AppEventScheduler};
9+
10+
// TODO: Needs to be integrated/replaced with the action system.
11+
// TODO: At that point this should just wrap the action, meaning all actions bindable by the user can also be accessed via the socket.
12+
#[derive(serde::Serialize, serde::Deserialize)]
13+
pub(crate) enum Message {
14+
OpenFiles(Vec<std::path::PathBuf>),
15+
}
16+
17+
fn handle_message(message: Message, app_event_scheduler: &AppEventScheduler) {
18+
match message {
19+
Message::OpenFiles(paths) => {
20+
app_event_scheduler.schedule(AppEvent::OpenFiles(paths));
21+
}
22+
}
23+
}
24+
25+
pub(crate) fn send(message: Message) -> std::io::Result<()> {
26+
let data = ron::ser::to_string(&message).map_err(|error| std::io::Error::new(std::io::ErrorKind::InvalidData, error))?;
27+
let mut connection = interprocess::local_socket::Stream::connect(socket_name())?;
28+
connection.write_all(data.as_bytes())
29+
}
30+
31+
pub(crate) struct SocketHandle {
32+
thread: Option<thread::JoinHandle<()>>,
33+
shutdown_sender: mpsc::Sender<()>,
34+
}
35+
impl Drop for SocketHandle {
36+
fn drop(&mut self) {
37+
let _ = self.shutdown_sender.send(());
38+
let _ = self.thread.take().expect("SocketHandle can only be dropped once").join();
39+
}
40+
}
41+
42+
pub(crate) fn start(app_event_scheduler: AppEventScheduler) -> SocketHandle {
43+
let (shutdown_sender, shutdown_receiver) = mpsc::channel();
44+
45+
let thread = thread::Builder::new()
46+
.name("socket".to_string())
47+
.spawn(move || run(app_event_scheduler, shutdown_receiver))
48+
.expect("Failed to spawn socket thread");
49+
50+
SocketHandle {
51+
shutdown_sender,
52+
thread: Some(thread),
53+
}
54+
}
55+
56+
fn run(app_event_scheduler: AppEventScheduler, shutdown_receiver: mpsc::Receiver<()>) {
57+
let listener = match ListenerOptions::new()
58+
.name(socket_name())
59+
.nonblocking(ListenerNonblockingMode::Accept)
60+
.try_overwrite(true)
61+
.max_spin_time(Duration::from_millis(100))
62+
.create_sync()
63+
{
64+
Ok(listener) => listener,
65+
Err(error) => {
66+
tracing::error!("Failed to bind socket: {}", error);
67+
return;
68+
}
69+
};
70+
71+
let max_backoff = Duration::from_millis(100);
72+
let mut backoff = Duration::ZERO;
73+
74+
loop {
75+
if backoff.is_zero() {
76+
match shutdown_receiver.try_recv() {
77+
Ok(()) | Err(mpsc::TryRecvError::Disconnected) => break,
78+
Err(mpsc::TryRecvError::Empty) => {}
79+
}
80+
backoff = Duration::from_nanos(1);
81+
} else {
82+
match shutdown_receiver.recv_timeout(backoff) {
83+
Ok(()) | Err(mpsc::RecvTimeoutError::Disconnected) => break,
84+
Err(mpsc::RecvTimeoutError::Timeout) => {}
85+
}
86+
backoff = (backoff * 2).min(max_backoff);
87+
}
88+
89+
match listener.accept() {
90+
Ok(mut connection) => {
91+
backoff = Duration::ZERO;
92+
93+
let app_event_scheduler = app_event_scheduler.clone();
94+
let spawn_result = thread::Builder::new().name("socket-connection".to_string()).spawn(move || {
95+
let mut data = String::new();
96+
if let Err(error) = connection.read_to_string(&mut data) {
97+
tracing::error!("Failed to read socket message: {}", error);
98+
return;
99+
}
100+
101+
match ron::de::from_str(&data) {
102+
Ok(message) => handle_message(message, &app_event_scheduler),
103+
Err(error) => tracing::error!("Failed to deserialize socket message: {}", error),
104+
}
105+
});
106+
if let Err(error) = spawn_result {
107+
tracing::error!("Failed to spawn socket connection thread: {}", error);
108+
}
109+
}
110+
Err(error) if matches!(error.kind(), ErrorKind::WouldBlock | ErrorKind::Interrupted) => {}
111+
Err(error) => {
112+
tracing::error!("Failed to accept socket connection: {}", error);
113+
}
114+
}
115+
}
116+
}
117+
118+
fn socket_name() -> Name<'static> {
119+
if cfg!(target_os = "windows") {
120+
let user = std::env::var("USERNAME").unwrap_or_default();
121+
let name = format!("{user}-{app}-{APP_SOCKET_FILE_NAME}", app = crate::consts::APP_NAME);
122+
name.to_ns_name::<GenericNamespaced>().expect("valid named pipe name")
123+
} else {
124+
crate::dirs::app_data_dir().join(APP_SOCKET_FILE_NAME).to_fs_name::<GenericFilePath>().expect("valid socket path")
125+
}
126+
}

0 commit comments

Comments
 (0)