Skip to content
Open
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
32 changes: 32 additions & 0 deletions Cargo.lock

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

1 change: 1 addition & 0 deletions about.toml
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
accepted = [
"0BSD", # Keep this list in sync with those in `/deny.toml`
"Apache-2.0 WITH LLVM-exception", # Keep this list in sync with those in `/deny.toml`
"Apache-2.0", # Keep this list in sync with those in `/deny.toml`
"BSD-2-Clause", # Keep this list in sync with those in `/deny.toml`
Expand Down
1 change: 1 addition & 0 deletions deny.toml
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ ignore = [
# [possible values: any SPDX 3.11 short identifier (+ optional exception)].
#
allow = [
"0BSD", # Keep this list in sync with those in `/about.toml`
"Apache-2.0 WITH LLVM-exception", # Keep this list in sync with those in `/about.toml`
"Apache-2.0", # Keep this list in sync with those in `/about.toml`
"BSD-2-Clause", # Keep this list in sync with those in `/about.toml`
Expand Down
1 change: 1 addition & 0 deletions desktop/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ lzma-rust2 = { workspace = true }
serde = { workspace = true }
rand = { workspace = true, features = ["thread_rng"] }
clap = { workspace = true, features = ["derive"] }
interprocess = "2.4.2"
fd-lock = "4.0.4"
ctrlc = "3.5.1"
window_clipboard = "0.5"
Expand Down
44 changes: 20 additions & 24 deletions desktop/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,6 @@ pub(crate) struct App {
start_render_sender: SyncSender<()>,
web_communication_initialized: bool,
web_communication_startup_buffer: Vec<Vec<u8>>,
#[cfg_attr(not(target_os = "macos"), expect(unused))]
preferences: Preferences,
launch_documents: Option<Vec<PathBuf>>,
startup_time: Option<Instant>,
Expand Down Expand Up @@ -312,7 +311,7 @@ impl App {
tracing::error!("OpenLaunchDocuments should only be sent once");
return;
};
self.open_files(launch_documents);
self.app_event_scheduler.schedule(AppEvent::OpenFiles(launch_documents));
}
DesktopFrontendMessage::UpdateMenu { entries } => {
if let Some(window) = &self.window {
Expand Down Expand Up @@ -465,38 +464,35 @@ impl App {
tracing::info!("Exiting main event loop");
event_loop.exit();
}
#[cfg(target_os = "macos")]
AppEvent::AddLaunchDocuments(paths) => {
AppEvent::OpenFiles(paths) => {
// Accumulate launch documents until OpenLaunchDocuments message is received
if let Some(launch_documents) = &mut self.launch_documents {
launch_documents.extend(paths);
} else {
self.open_files(paths);
return;
}

if paths.is_empty() {
return;
}
let app_event_scheduler = self.app_event_scheduler.clone();
let _ = thread::spawn(move || {
for path in paths {
tracing::info!("Opening file: {}", path.display());
if let Ok(content) = fs::read(&path) {
let message = DesktopWrapperMessage::OpenFile { path, content };
app_event_scheduler.schedule(AppEvent::DesktopWrapperMessage(message));
} else {
tracing::error!("Failed to read file: {}", path.display());
}
}
});
}
#[cfg(target_os = "macos")]
AppEvent::MenuEvent { id } => {
self.dispatch_desktop_wrapper_message(DesktopWrapperMessage::MenuEvent { id });
}
}
}

fn open_files(&mut self, paths: Vec<PathBuf>) {
if paths.is_empty() {
return;
}
let app_event_scheduler = self.app_event_scheduler.clone();
let _ = thread::spawn(move || {
for path in paths {
tracing::info!("Opening file: {}", path.display());
if let Ok(content) = fs::read(&path) {
let message = DesktopWrapperMessage::OpenFile { path, content };
app_event_scheduler.schedule(AppEvent::DesktopWrapperMessage(message));
} else {
tracing::error!("Failed to read file: {}", path.display());
}
}
});
}
}
impl ApplicationHandler for App {
fn can_create_surfaces(&mut self, event_loop: &dyn ActiveEventLoop) {
Expand Down
1 change: 1 addition & 0 deletions desktop/src/consts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ pub(crate) const APP_DIRECTORY_NAME: &str = "graphite";
#[cfg(not(target_os = "linux"))]
pub(crate) const APP_DIRECTORY_NAME: &str = "Graphite";
pub(crate) const APP_LOCK_FILE_NAME: &str = "instance.lock";
pub(crate) const APP_SOCKET_FILE_NAME: &str = "instance.sock";
pub(crate) const APP_STATE_FILE_NAME: &str = "state.ron";
pub(crate) const APP_PREFERENCES_FILE_NAME: &str = "preferences.ron";
pub(crate) const APP_DOCUMENTS_DIRECTORY_NAME: &str = "documents";
Expand Down
3 changes: 1 addition & 2 deletions desktop/src/event.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,7 @@ pub(crate) enum AppEvent {
DesktopWrapperMessage(DesktopWrapperMessage),
NodeGraphExecutionResult(NodeGraphExecutionResult),
Exit,
#[cfg(target_os = "macos")]
AddLaunchDocuments(Vec<std::path::PathBuf>),
OpenFiles(Vec<std::path::PathBuf>),
#[cfg(target_os = "macos")]
MenuEvent {
id: String,
Expand Down
11 changes: 10 additions & 1 deletion desktop/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ mod gpu_context;
mod persist;
mod preferences;
mod render;
mod socket;
mod window;

pub(crate) mod consts;
Expand Down Expand Up @@ -58,7 +59,13 @@ pub fn start() {
}
Err(_) => {
tracing::error!("Another instance is already running, Exiting.");
std::process::exit(1);
if !cli.files.is_empty()
&& let Err(error) = socket::send(socket::Message::OpenFiles(cli.files))
{
tracing::error!("Failed to send socket message to running instance: {}", error);
std::process::exit(1);
}
return;
}
};

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

let _socket_handle = socket::start(app_event_scheduler.clone());

let (cef_view_info_sender, cef_view_info_receiver) = std::sync::mpsc::channel();

if cli.disable_ui_acceleration {
Expand Down
126 changes: 126 additions & 0 deletions desktop/src/socket.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
use interprocess::local_socket::{GenericFilePath, GenericNamespaced, ListenerNonblockingMode, ListenerOptions, Name, prelude::*};
use std::io::{ErrorKind, Read, Write};
use std::sync::mpsc;
use std::thread;
use std::time::Duration;

use crate::consts::APP_SOCKET_FILE_NAME;
use crate::event::{AppEvent, AppEventScheduler};

// TODO: Needs to be integrated/replaced with the action system.
// TODO: At that point this should just wrap the action, meaning all actions bindable by the user can also be accessed via the socket.
#[derive(serde::Serialize, serde::Deserialize)]
pub(crate) enum Message {
OpenFiles(Vec<std::path::PathBuf>),
}

fn handle_message(message: Message, app_event_scheduler: &AppEventScheduler) {
match message {
Message::OpenFiles(paths) => {
app_event_scheduler.schedule(AppEvent::OpenFiles(paths));
}
}
}

pub(crate) fn send(message: Message) -> std::io::Result<()> {
let data = ron::ser::to_string(&message).map_err(|error| std::io::Error::new(std::io::ErrorKind::InvalidData, error))?;
let mut connection = interprocess::local_socket::Stream::connect(socket_name())?;
connection.write_all(data.as_bytes())
}

pub(crate) struct SocketHandle {
thread: Option<thread::JoinHandle<()>>,
shutdown_sender: mpsc::Sender<()>,
}
impl Drop for SocketHandle {
fn drop(&mut self) {
let _ = self.shutdown_sender.send(());
let _ = self.thread.take().expect("SocketHandle can only be dropped once").join();
}
}

pub(crate) fn start(app_event_scheduler: AppEventScheduler) -> SocketHandle {
let (shutdown_sender, shutdown_receiver) = mpsc::channel();

let thread = thread::Builder::new()
.name("socket".to_string())
.spawn(move || run(app_event_scheduler, shutdown_receiver))
.expect("Failed to spawn socket thread");

SocketHandle {
shutdown_sender,
thread: Some(thread),
}
}

fn run(app_event_scheduler: AppEventScheduler, shutdown_receiver: mpsc::Receiver<()>) {
let listener = match ListenerOptions::new()
.name(socket_name())
.nonblocking(ListenerNonblockingMode::Accept)
.try_overwrite(true)
.max_spin_time(Duration::from_millis(100))
.create_sync()
{
Ok(listener) => listener,
Err(error) => {
tracing::error!("Failed to bind socket: {}", error);
return;
}
};

let max_backoff = Duration::from_millis(100);
let mut backoff = Duration::ZERO;

loop {
if backoff.is_zero() {
match shutdown_receiver.try_recv() {
Ok(()) | Err(mpsc::TryRecvError::Disconnected) => break,
Err(mpsc::TryRecvError::Empty) => {}
}
backoff = Duration::from_nanos(1);
} else {
match shutdown_receiver.recv_timeout(backoff) {
Ok(()) | Err(mpsc::RecvTimeoutError::Disconnected) => break,
Err(mpsc::RecvTimeoutError::Timeout) => {}
}
backoff = (backoff * 2).min(max_backoff);
}

match listener.accept() {
Ok(mut connection) => {
backoff = Duration::ZERO;

let app_event_scheduler = app_event_scheduler.clone();
let spawn_result = thread::Builder::new().name("socket-connection".to_string()).spawn(move || {
let mut data = String::new();
if let Err(error) = connection.read_to_string(&mut data) {
tracing::error!("Failed to read socket message: {}", error);
return;
}

match ron::de::from_str(&data) {
Ok(message) => handle_message(message, &app_event_scheduler),
Err(error) => tracing::error!("Failed to deserialize socket message: {}", error),
}
});
Comment thread
timon-schelling marked this conversation as resolved.
if let Err(error) = spawn_result {
tracing::error!("Failed to spawn socket connection thread: {}", error);
}
}
Err(error) if matches!(error.kind(), ErrorKind::WouldBlock | ErrorKind::Interrupted) => {}
Err(error) => {
tracing::error!("Failed to accept socket connection: {}", error);
}
}
}
}

fn socket_name() -> Name<'static> {
if cfg!(target_os = "windows") {
let dir = crate::dirs::app_data_dir().to_string_lossy().replace('\\', "-");
let name = format!("graphite-{dir}-{APP_SOCKET_FILE_NAME}");
name.to_ns_name::<GenericNamespaced>().expect("should be avalid named pipe name")
} else {
crate::dirs::app_data_dir().join(APP_SOCKET_FILE_NAME).to_fs_name::<GenericFilePath>().expect("valid socket path")
}
}
Loading
Loading