From 017d13d80c078ca6ba970c3f6ff46e39c19379c9 Mon Sep 17 00:00:00 2001 From: ndwarshuis Date: Thu, 29 Dec 2022 15:22:48 -0500 Subject: [PATCH] ENH clean up docs in shell --- lib/XMonad/Internal/Shell.hs | 113 ++++++++++++++++------------------- 1 file changed, 50 insertions(+), 63 deletions(-) diff --git a/lib/XMonad/Internal/Shell.hs b/lib/XMonad/Internal/Shell.hs index 73f2546..00264ee 100644 --- a/lib/XMonad/Internal/Shell.hs +++ b/lib/XMonad/Internal/Shell.hs @@ -1,55 +1,4 @@ -- | Functions for formatting and spawning shell commands --- --- TLDR: spawning a "command" in xmonad is complicated for weird reasons, and --- this solution is the most sane (for me) given the constraints of the xmonad --- codebase. --- --- A few facts about xmonad (and window managers in general): --- 1) It is single-threaded (since X is single threaded) --- 2) Because of (1), it ignores SIGCHLD, which means any subprocess started --- by xmonad will instantly be reaped after spawning. This guarantees the --- main thread running the WM will never be blocked. --- --- In general, this means that 'System.Process.waitForProcess' (and similar) --- will not work since these call wait() on the child process, which will fail --- because the child has already been cleared and thus there is nothing on which --- to wait. By extension this also means we don't have access to a child's exit --- code. --- --- XMonad and contrib use their own method of spawning subprocesses using the --- extremely low-level 'System.Process.Posix' API. See the code for --- 'XMonad.Core.spawn' or 'XMonad.Util.Run.safeSpawn'. Specifically, the --- sequence is (in terms of the low level Linux API): --- 1) call fork() --- 2) uninstall signal handlers --- 3) call setsid() --- 4) start new thing with exec() --- --- In practice, I'm guessing the main reason for 2 and 3 is so that child --- processes don't inherit the weird SIGCHLD behavior of xmonad itself. The --- setsid thing is one way to guarantee that killing the child thread will also --- kill its children (if any). Note that this obviously will not block since --- we are calling fork() without wait() (which would throw an error anyways). --- --- What if I actually want the exit code? --- --- The best solution (I can come up with), is to use bracket to uninstall --- handlers, run process (with wait), and then reinstall handlers. I can use --- this with a much higher-level interface which will make things easier. This --- obviously means that if the process is running in the main thread, it needs --- to be almost instantaneous (since it actually will be blocking). NOTE: I --- shouldn't use this to replace the existing functions in xmonad since --- 'spawning' a new process in a non-blocking manner with a higher-level API --- will produce lots of Haskell objects that need to be cleaned, and it will be --- hard (perhaps impossible) to keep track and deal with these after spawning. --- --- This works, albeit with the cost of using almost every process API in Haskell. --- --- Briefly: --- 1) 'System.Process.Posix' (where xmonad lives) --- 2) 'System.Process' (wraps 1) --- 2) 'System.Process.Typed' (wraps 2, which I prefer for getting exit codes) --- 3) 'RIO.Process' (wraps 3, which I prefer at the app level) {-# LANGUAGE OverloadedStrings #-} @@ -78,70 +27,108 @@ import qualified System.Process.Typed as P import qualified XMonad.Core as X import qualified XMonad.Util.Run as XR --------------------------------------------------------------------------------- --- | Opening subshell --- https://github.com/xmonad/xmonad/issues/113 +-- | Fork a new process and wait for its exit code. +-- +-- This function will work despite xmonad ignoring SIGCHLD. +-- +-- A few facts about xmonad (and window managers in general): +-- 1) It is single-threaded (since X is single threaded) +-- 2) Because of (1), it ignores SIGCHLD, which means any subprocess started +-- by xmonad will instantly be reaped after spawning. This guarantees the +-- main thread running the WM will never be blocked. +-- +-- In general, this means I can't wait for exit codes (since wait() doesn't +-- work) See https://github.com/xmonad/xmonad/issues/113. +-- +-- If I want an exit code, The best solution (I can come up with), is to use +-- bracket to uninstall handlers, run process (with wait), and then reinstall +-- handlers. I can use this with a much higher-level interface which will make +-- things easier. This obviously means that if the process is running in the +-- main thread, it needs to be almost instantaneous. Note if using a high-level +-- API for this, the process needs to spawn, finish, and be reaped by the +-- xmonad process all while the signal handlers are 'disabled' (which limits +-- the functions I can use to those that call waitForProcess). +-- +-- XMonad and contrib use their own method of spawning subprocesses using the +-- extremely low-level 'System.Process.Posix' API. See the code for +-- 'XMonad.Core.spawn' or 'XMonad.Util.Run.safeSpawn'. Specifically, the +-- sequence is (in terms of the low level Linux API): +-- 1) call fork() +-- 2) uninstall signal handlers (to allow wait() to work in subprocesses) +-- 3) call setsid() (so killing the child will kill its children, if any) +-- 4) start new thing with exec() +-- +-- In contrast with high-level APIs like 'System.Process', this will leave no +-- trailing data structures to clean up, at the cost of being gross to look at +-- and possibly more error-prone. +runProcess :: P.ProcessConfig a b c -> IO ExitCode +runProcess = withDefaultSignalHandlers . P.runProcess +-- | Run an action without xmonad's signal handlers. withDefaultSignalHandlers :: IO a -> IO a withDefaultSignalHandlers = bracket_ X.uninstallSignalHandlers X.installSignalHandlers +-- | Set a child process to create a new group and session addGroupSession :: P.ProcessConfig x y z -> P.ProcessConfig x y z addGroupSession = P.setCreateGroup True . P.setNewSession True -runProcess :: P.ProcessConfig a b c -> IO ExitCode -runProcess = withDefaultSignalHandlers . P.runProcess - +-- | Create a 'ProcessConfig' for a shell command shell :: T.Text -> P.ProcessConfig () () () shell = addGroupSession . P.shell . T.unpack +-- | Create a 'ProcessConfig' for a command with arguments proc :: FilePath -> [T.Text] -> P.ProcessConfig () () () proc cmd args = addGroupSession $ P.proc cmd (T.unpack <$> args) +-- | Run 'XMonad.Core.spawn' with 'Text' input. spawn :: MonadIO m => T.Text -> m () spawn = X.spawn . T.unpack --- spawnAt :: MonadIO m => FilePath -> T.Text -> m () --- spawnAt fp = liftIO . void . startProcess . P.setWorkingDir fp . shell - +-- | Run 'XMonad.Run.Utils.spawnPipe' with 'Text' input. spawnPipe :: MonadIO m => T.Text -> m Handle spawnPipe = XR.spawnPipe . T.unpack +-- | Run 'XMonad.Core.spawn' with a command and arguments spawnCmd :: MonadIO m => FilePath -> [T.Text] -> m () spawnCmd cmd = spawn . fmtCmd cmd --------------------------------------------------------------------------------- --- | Formatting commands - +-- | Format a command and list of arguments as 'Text' fmtCmd :: FilePath -> [T.Text] -> T.Text fmtCmd cmd args = T.unwords $ T.pack cmd : args op :: T.Text -> T.Text -> T.Text -> T.Text op a x b = T.unwords [a, x, b] +-- | Format two shell expressions separated by "&&" (#!&&) :: T.Text -> T.Text -> T.Text cmdA #!&& cmdB = op cmdA "&&" cmdB infixr 0 #!&& +-- | Format two shell expressions separated by "|" (#!|) :: T.Text -> T.Text -> T.Text cmdA #!| cmdB = op cmdA "|" cmdB infixr 0 #!| +-- | Format two shell expressions separated by "||" (#!||) :: T.Text -> T.Text -> T.Text cmdA #!|| cmdB = op cmdA "||" cmdB infixr 0 #!|| +-- | Format two shell expressions separated by ";" (#!>>) :: T.Text -> T.Text -> T.Text cmdA #!>> cmdB = op cmdA ";" cmdB infixr 0 #!>> +-- | Wrap input in double quotes doubleQuote :: T.Text -> T.Text doubleQuote s = T.concat ["\"", s, "\""] +-- | Wrap input in single quotes singleQuote :: T.Text -> T.Text singleQuote s = T.concat ["'", s, "'"]