diff --git a/api/lua/client.lua b/api/lua/client.lua index 3120d5c..146331b 100644 --- a/api/lua/client.lua +++ b/api/lua/client.lua @@ -99,6 +99,7 @@ function client.get_window(identifier) local response = ReadMsg() local props = response.RequestResponse.response.Window.window + ---@type Window local win = { id = props.id, diff --git a/api/lua/example_config.lua b/api/lua/example_config.lua index 878e387..5fe15cc 100644 --- a/api/lua/example_config.lua +++ b/api/lua/example_config.lua @@ -11,8 +11,8 @@ pcall(require, "luarocks.loader") -- Neovim users be like: require("pinnacle").setup(function(pinnacle) - local input = pinnacle.input --Key and mouse binds - local client = pinnacle.client --Window management + local input = pinnacle.input -- Key and mouse binds + local client = pinnacle.client -- Window management local process = pinnacle.process -- Process spawning -- Every key supported by xkbcommon. diff --git a/api/lua/process.lua b/api/lua/process.lua index 7b65cfd..2222b39 100644 --- a/api/lua/process.lua +++ b/api/lua/process.lua @@ -4,11 +4,19 @@ -- -- SPDX-License-Identifier: MPL-2.0 +---@diagnostic disable: redefined-local + local process = {} ---Spawn a process with an optional callback for its stdout and stderr. +--- +---`callback` has the following parameters: +--- - `stdout`: The process's stdout printed this line. +--- - `stderr`: The process's stderr printed this line. +--- - `exit_code`: The process exited with this code. +--- - `exit_msg`: The process exited with this message. ---@param command string|string[] The command as one whole string or a table of each of its arguments ----@param callback fun(stdout: string?, stderr: string?, exit_code: integer?, exit_msg: string?)? A callback to do something whenever the process's stdout or stderr print a line. Only one will be non-nil at a time. +---@param callback fun(stdout: string?, stderr: string?, exit_code: integer?, exit_msg: string?)? A callback to do something whenever the process's stdout or stderr print a line, or when the process exits. function process.spawn(command, callback) ---@type integer|nil local callback_id = nil diff --git a/src/state.rs b/src/state.rs index 8d272ee..35b4213 100644 --- a/src/state.rs +++ b/src/state.rs @@ -6,19 +6,16 @@ use std::{ error::Error, - ffi::OsString, + ffi::{CString, OsString}, io::{BufRead, BufReader}, os::{fd::AsRawFd, unix::net::UnixStream}, process::Stdio, - sync::{ - atomic::{AtomicU32, Ordering}, - Arc, Mutex, - }, + sync::{Arc, Mutex}, }; use crate::{ api::{ - msg::{Args, Msg, OutgoingMsg, Request, RequestResponse}, + msg::{Args, CallbackId, Msg, OutgoingMsg, Request, RequestResponse}, PinnacleSocketSource, }, focus::FocusState, @@ -94,7 +91,7 @@ pub struct State { } impl State { - /// Create the main [State]. + /// Create the main [`State`]. /// /// This will set the WAYLAND_DISPLAY environment variable, insert Wayland necessary sources /// into the event loop, and run an implementation of the config API (currently Lua). @@ -109,6 +106,19 @@ impl State { std::env::set_var("WAYLAND_DISPLAY", socket_name.clone()); + // Opening a new process will use up a few file descriptors, around 10 for Alacritty, for + // example. Because of this, opening up only around 100 processes would exhaust the file + // descriptor limit on my system (Arch btw) and cause a "Too many open files" crash. + // + // To fix this, I just set the limit to be higher. As Pinnacle is the whole graphical + // environment, I *think* this is ok. + smithay::reexports::nix::sys::resource::setrlimit( + smithay::reexports::nix::sys::resource::Resource::RLIMIT_NOFILE, + 65536, + 65536 * 2, + ) + .unwrap(); + loop_handle.insert_source(socket, |stream, _metadata, data| { data.display .handle() @@ -133,6 +143,7 @@ impl State { Event::Msg(msg) => { // TODO: move this into its own function // TODO: no like seriously this is getting a bit unwieldy + // TODO: no like rustfmt literally refuses to format the code below match msg { Msg::SetKeybind { key, @@ -164,139 +175,7 @@ impl State { command, callback_id, } => { - let mut command = command.into_iter().peekable(); - if command.peek().is_none() { - // TODO: notify that command was nothing - return; - } - - // TODO: may need to set env for WAYLAND_DISPLAY - let mut child = - std::process::Command::new(OsString::from(command.next().unwrap())) - .env("WAYLAND_DISPLAY", data.state.socket_name.clone()) - .stdin(Stdio::null()) - .stdout(Stdio::null()) - .stderr(Stdio::null()) - .stdin(if callback_id.is_some() { - Stdio::piped() - } else { - // piping to null because foot won't open without a callback_id - // otherwise - Stdio::null() - }) - .stdout(if callback_id.is_some() { - Stdio::piped() - } else { - Stdio::null() - }) - .stderr(if callback_id.is_some() { - Stdio::piped() - } else { - Stdio::null() - }) - .args(command) - .spawn() - .unwrap(); // TODO: handle unwrap - - // TODO: find a way to make this hellish code look better, deal with unwraps - if let Some(callback_id) = callback_id { - let stdout = child.stdout.take(); - let stderr = child.stderr.take(); - let stream_out = data.state.api_state.stream.as_ref().unwrap().clone(); - let stream_err = stream_out.clone(); - let stream_exit = stream_out.clone(); - - // TODO: make this not use 3 whole threads per process - if let Some(stdout) = stdout { - std::thread::spawn(move || { - let mut reader = BufReader::new(stdout); - loop { - let mut buf = String::new(); - match reader.read_line(&mut buf) { - Ok(0) => break, // stream closed - Ok(_) => { - let mut stream = stream_out.lock().unwrap(); - crate::api::send_to_client( - &mut stream, - &OutgoingMsg::CallCallback { - callback_id, - args: Some(Args::Spawn { - stdout: Some( - buf.trim_end_matches('\n') - .to_string(), - ), - stderr: None, - exit_code: None, - exit_msg: None, - }), - }, - ) - .unwrap(); - } - Err(err) => { - tracing::error!("child read err: {err}"); - break; - } - } - } - }); - } - if let Some(stderr) = stderr { - std::thread::spawn(move || { - let mut reader = BufReader::new(stderr); - loop { - let mut buf = String::new(); - match reader.read_line(&mut buf) { - Ok(0) => break, // stream closed - Ok(_) => { - let mut stream = stream_err.lock().unwrap(); - crate::api::send_to_client( - &mut stream, - &OutgoingMsg::CallCallback { - callback_id, - args: Some(Args::Spawn { - stdout: None, - stderr: Some( - buf.trim_end_matches('\n') - .to_string(), - ), - exit_code: None, - exit_msg: None, - }), - }, - ) - .unwrap(); - } - Err(err) => { - tracing::error!("child read err: {err}"); - break; - } - } - } - }); - } - std::thread::spawn(move || match child.wait() { - Ok(exit_status) => { - let mut stream = stream_exit.lock().unwrap(); - crate::api::send_to_client( - &mut stream, - &OutgoingMsg::CallCallback { - callback_id, - args: Some(Args::Spawn { - stdout: None, - stderr: None, - exit_code: exit_status.code(), - exit_msg: Some(exit_status.to_string()), - }), - }, - ) - .unwrap() - } - Err(err) => { - tracing::warn!("child wait() err: {err}"); - } - }); - } + data.state.handle_spawn(command, callback_id); } Msg::SpawnShell { shell, @@ -490,6 +369,136 @@ impl State { popup_manager: PopupManager::default(), }) } + + pub fn handle_spawn(&self, command: Vec, callback_id: Option) { + let mut command = command.into_iter().peekable(); + if command.peek().is_none() { + // TODO: notify that command was nothing + return; + } + + let mut child = std::process::Command::new(OsString::from(command.next().unwrap())) + .env("WAYLAND_DISPLAY", self.socket_name.clone()) + .stdin(Stdio::null()) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .stdin(if callback_id.is_some() { + Stdio::piped() + } else { + // piping to null because foot won't open without a callback_id + // otherwise + Stdio::null() + }) + .stdout(if callback_id.is_some() { + Stdio::piped() + } else { + Stdio::null() + }) + .stderr(if callback_id.is_some() { + Stdio::piped() + } else { + Stdio::null() + }) + .args(command) + .spawn() + .unwrap(); // TODO: handle unwrap + + // TODO: find a way to make this hellish code look better, deal with unwraps + if let Some(callback_id) = callback_id { + let stdout = child.stdout.take(); + let stderr = child.stderr.take(); + let stream_out = self.api_state.stream.as_ref().unwrap().clone(); + let stream_err = stream_out.clone(); + let stream_exit = stream_out.clone(); + + // TODO: make this not use 3 whole threads per process + if let Some(stdout) = stdout { + std::thread::spawn(move || { + let mut reader = BufReader::new(stdout); + loop { + let mut buf = String::new(); + match reader.read_line(&mut buf) { + Ok(0) => break, // stream closed + Ok(_) => { + let mut stream = stream_out.lock().unwrap(); + crate::api::send_to_client( + &mut stream, + &OutgoingMsg::CallCallback { + callback_id, + args: Some(Args::Spawn { + stdout: Some(buf.trim_end_matches('\n').to_string()), + stderr: None, + exit_code: None, + exit_msg: None, + }), + }, + ) + .unwrap(); + } + Err(err) => { + tracing::error!("child read err: {err}"); + break; + } + } + } + }); + } + if let Some(stderr) = stderr { + std::thread::spawn(move || { + let mut reader = BufReader::new(stderr); + loop { + let mut buf = String::new(); + match reader.read_line(&mut buf) { + Ok(0) => break, // stream closed + Ok(_) => { + let mut stream = stream_err.lock().unwrap(); + crate::api::send_to_client( + &mut stream, + &OutgoingMsg::CallCallback { + callback_id, + args: Some(Args::Spawn { + stdout: None, + stderr: Some(buf.trim_end_matches('\n').to_string()), + exit_code: None, + exit_msg: None, + }), + }, + ) + .unwrap(); + } + Err(err) => { + tracing::error!("child read err: {err}"); + break; + } + } + } + }); + } + std::thread::spawn(move || match child.wait() { + Ok(exit_status) => { + let mut stream = stream_exit.lock().unwrap(); + crate::api::send_to_client( + &mut stream, + &OutgoingMsg::CallCallback { + callback_id, + args: Some(Args::Spawn { + stdout: None, + stderr: None, + exit_code: exit_status.code(), + exit_msg: Some(exit_status.to_string()), + }), + }, + ) + .unwrap() + } + Err(err) => { + tracing::warn!("child wait() err: {err}"); + } + }); + } else { + std::thread::spawn(move || child.wait()); + } + } } pub struct CalloopData {