atelier-core 0.1.0.0 → 0.2.0.0
raw patch · 6 files changed
+155/−49 lines, 6 filesdep −hs-opentelemetry-exporter-otlpdep ~atelier-coredep ~basedep ~bytestringPVP ok
version bump matches the API change (PVP)
Dependencies removed: hs-opentelemetry-exporter-otlp
Dependency ranges changed: atelier-core, base, bytestring, containers, effectful-plugin, filepath, time
API changes (from Hackage documentation)
- Atelier.Effects.Process: setCreateGroup :: Bool -> ProcessConfig stdin stdout stderr -> ProcessConfig stdin stdout stderr
- Atelier.Effects.Process: startProcess :: forall i o e (es :: [Effect]). (HasCallStack, Process :> es) => ProcessConfig i o e -> Eff es (Process i o e)
- Atelier.Effects.Process: stopProcess :: forall i o e (es :: [Effect]). (HasCallStack, Process :> es) => Process i o e -> Eff es ()
- Atelier.Effects.Process: type RunningProcess = Process
+ Atelier.Effects.File: AppendMode :: IOMode
+ Atelier.Effects.File: ReadMode :: IOMode
+ Atelier.Effects.File: ReadWriteMode :: IOMode
+ Atelier.Effects.File: WriteMode :: IOMode
+ Atelier.Effects.File: data IOMode
+ Atelier.Effects.Process: data RunningProcess i o e
+ Atelier.Effects.Process: terminateProcessGroup :: forall (es :: [Effect]) i o e. Process :> es => RunningProcess i o e -> Eff es ()
+ Atelier.Effects.Process: withProcessGroup :: forall (es :: [Effect]) i o e a. Process :> es => ProcessConfig i o e -> (RunningProcess i o e -> Eff es a) -> Eff es a
+ Atelier.Effects.Process.Internal: RunningProcess :: Process i o e -> RunningProcess i o e
+ Atelier.Effects.Process.Internal: newtype RunningProcess i o e
- Atelier.Effects.Process: getStderr :: Process stdin stdout stderr -> stderr
+ Atelier.Effects.Process: getStderr :: RunningProcess i o e -> e
- Atelier.Effects.Process: getStdin :: Process stdin stdout stderr -> stdin
+ Atelier.Effects.Process: getStdin :: RunningProcess i o e -> i
- Atelier.Effects.Process: getStdout :: Process stdin stdout stderr -> stdout
+ Atelier.Effects.Process: getStdout :: RunningProcess i o e -> o
- Atelier.Effects.Process: interruptProcessGroup :: forall i o e (es :: [Effect]). (HasCallStack, Process :> es) => Process i o e -> Eff es ()
+ Atelier.Effects.Process: interruptProcessGroup :: forall i o e (es :: [Effect]). (HasCallStack, Process :> es) => RunningProcess i o e -> Eff es ()
- Atelier.Effects.Process: waitExitCode :: forall i o e (es :: [Effect]). (HasCallStack, Process :> es) => Process i o e -> Eff es ExitCode
+ Atelier.Effects.Process: waitExitCode :: forall i o e (es :: [Effect]). (HasCallStack, Process :> es) => RunningProcess i o e -> Eff es ExitCode
Files
- CHANGELOG.md +27/−1
- atelier-core.cabal +26/−17
- src/Atelier/Effects/File.hs +1/−0
- src/Atelier/Effects/Process.hs +84/−31
- src/Atelier/Effects/Process/Internal.hs +15/−0
- src/Atelier/Exception.hs +2/−0
CHANGELOG.md view
@@ -5,7 +5,33 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to the [PVP](https://pvp.haskell.org/). -## [Unreleased]+## [0.2.0.0] - 2026-06-26++### Added++- `Atelier.Effects.File` now re-exports `IOMode (..)`.+- `Atelier.Effects.Process`: `withProcessGroup` runs a process in its own+ process group and guarantees the whole group is torn down when the body+ returns or throws; `terminateProcessGroup` aborts a running group from+ another thread (for children that trap `SIGINT`, where interrupting the+ group is not enough).+- New module `Atelier.Effects.Process.Internal`, exposing the `RunningProcess`+ constructor for tests that fabricate a handle. Production code should not+ import it.++### Changed++- **Breaking:** `Atelier.Effects.Process.RunningProcess` is now a distinct+ `newtype` wrapping `System.Process.Typed.Process` (previously a type+ synonym). `getStdin`/`getStdout`/`getStderr` operate on the new type.++### Removed++- **Breaking:** `Atelier.Effects.Process` no longer exports `setCreateGroup`,+ `startProcess`, or `stopProcess`. Process lifecycle is now managed through+ `withProcessGroup`.++## [0.1.0.0] - 2026-06-05 ### Added
atelier-core.cabal view
@@ -1,11 +1,11 @@ cabal-version: 2.0 --- This file has been generated from package.yaml by hpack version 0.38.2.+-- This file has been generated from package.yaml by hpack version 0.38.3. -- -- see: https://github.com/sol/hpack name: atelier-core-version: 0.1.0.0+version: 0.2.0.0 synopsis: Foundational Effectful-based effects and utilities description: Core effects and utilities for effect-based applications, built on Effectful — part of the atelier toolkit. category: Control@@ -16,6 +16,11 @@ license: MIT license-file: LICENSE build-type: Simple+tested-with:+ GHC == 9.10.3+ , GHC == 9.6.7+ , GHC == 9.8.4+ , GHC == 9.12.4 extra-doc-files: CHANGELOG.md README.md@@ -57,6 +62,7 @@ Atelier.Effects.Posix.Daemons Atelier.Effects.Posix.IO Atelier.Effects.Process+ Atelier.Effects.Process.Internal Atelier.Effects.Publishing Atelier.Effects.Tally Atelier.Effects.Timeout@@ -94,27 +100,26 @@ StrictData TemplateHaskell TypeFamilies- ghc-options: -Weverything -Wno-unsafe -Wno-missing-safe-haskell-mode -Wno-monomorphism-restriction -Wno-missing-kind-signatures -Wno-missing-poly-kind-signatures -Wno-missing-role-annotations -Wno-missing-local-signatures -Wno-missing-import-lists -Wno-implicit-prelude -Wno-unticked-promoted-constructors -Wno-unused-packages -Wno-all-missed-specialisations -Wno-missed-specialisations -fplugin=Effectful.Plugin -threaded+ ghc-options: -Weverything -Wno-unsafe -Wno-missing-safe-haskell-mode -Wno-monomorphism-restriction -Wno-missing-kind-signatures -Wno-missing-local-signatures -Wno-missing-import-lists -Wno-implicit-prelude -Wno-unticked-promoted-constructors -Wno-unused-packages -Wno-all-missed-specialisations -Wno-missed-specialisations -fplugin=Effectful.Plugin -threaded build-depends: aeson ==2.2.* , atelier-prelude ==0.1.*- , base ==4.20.*+ , base >=4.18 && <4.23 , base64-bytestring ==1.2.*- , bytestring ==0.12.*+ , bytestring >=0.11 && <0.13 , casing ==0.1.*- , containers ==0.7.*+ , containers >=0.6 && <0.9 , contra-tracer ==0.2.* , daemons ==0.4.* , data-default ==0.8.* , directory ==1.3.* , effectful ==2.6.* , effectful-core ==2.6.*- , effectful-plugin ==2.0.*+ , effectful-plugin >=2.0 && <2.2 , effectful-th ==1.0.*- , filepath ==1.5.*+ , filepath >=1.4 && <1.6 , fsnotify ==0.4.* , hs-opentelemetry-api ==0.3.*- , hs-opentelemetry-exporter-otlp ==0.1.* , hs-opentelemetry-sdk ==0.1.* , http-api-data ==0.7.* , http-types ==0.12.*@@ -127,7 +132,7 @@ , stm ==2.5.* , stm-containers ==1.2.* , text ==2.1.*- , time ==1.12.*+ , time >=1.12 && <1.16 , time-units ==1.0.* , typed-process ==0.2.* , unagi-chan ==0.4.*@@ -139,6 +144,8 @@ mixins: base hiding (Prelude) default-language: GHC2021+ if impl(GHC >= 9.8)+ ghc-options: -Wno-missing-poly-kind-signatures -Wno-missing-role-annotations test-suite atelier-test type: exitcode-stdio-1.0@@ -186,21 +193,21 @@ StrictData TemplateHaskell TypeFamilies- ghc-options: -Weverything -Wno-unsafe -Wno-missing-safe-haskell-mode -Wno-monomorphism-restriction -Wno-missing-kind-signatures -Wno-missing-poly-kind-signatures -Wno-missing-role-annotations -Wno-missing-local-signatures -Wno-missing-import-lists -Wno-implicit-prelude -Wno-unticked-promoted-constructors -Wno-unused-packages -Wno-all-missed-specialisations -Wno-missed-specialisations -fplugin=Effectful.Plugin -threaded -Wno-prepositive-qualified-module+ ghc-options: -Weverything -Wno-unsafe -Wno-missing-safe-haskell-mode -Wno-monomorphism-restriction -Wno-missing-kind-signatures -Wno-missing-local-signatures -Wno-missing-import-lists -Wno-implicit-prelude -Wno-unticked-promoted-constructors -Wno-unused-packages -Wno-all-missed-specialisations -Wno-missed-specialisations -fplugin=Effectful.Plugin -threaded -Wno-prepositive-qualified-module build-tool-depends: tasty-discover:tasty-discover build-depends: aeson ==2.2.* , async ==2.2.*- , atelier-core ==0.1.*+ , atelier-core ==0.2.* , atelier-prelude ==0.1.*- , base ==4.20.*- , bytestring ==0.12.*- , containers ==0.7.*+ , base >=4.18 && <4.23+ , bytestring >=0.11 && <0.13+ , containers >=0.6 && <0.9 , data-default ==0.8.* , effectful ==2.6.* , effectful-core ==2.6.*- , effectful-plugin ==2.0.*+ , effectful-plugin >=2.0 && <2.2 , hedgehog ==1.7.* , hs-opentelemetry-api ==0.3.* , hspec ==2.11.*@@ -209,7 +216,9 @@ , stm-containers ==1.2.* , tasty ==1.5.* , tasty-hspec ==1.2.*- , time ==1.12.*+ , time >=1.12 && <1.16 mixins: base hiding (Prelude) default-language: GHC2021+ if impl(GHC >= 9.8)+ ghc-options: -Wno-missing-poly-kind-signatures -Wno-missing-role-annotations
src/Atelier/Effects/File.hs view
@@ -10,6 +10,7 @@ ( File , Handle , BufferMode (..)+ , IOMode (..) , withFile , hClose , hFlush
src/Atelier/Effects/Process.hs view
@@ -1,12 +1,13 @@ -- | Effect for spawning and interacting with external processes. ----- Unifies @typed-process@ (configuration, lifecycle and stream capture) and--- the one operation only @process@ provides (process-group interruption)--- behind a single effect, so callers never depend on either package directly.+-- Unifies @typed-process@ (configuration, lifecycle and stream capture) and the+-- process-group signalling that @process@ provides behind a single effect, so+-- callers never depend on either package directly. ----- Build a 'ProcessConfig' with the re-exported @typed-process@ DSL--- ('proc', 'shell', 'setStdin', …), then run it with one of the operations--- below.+-- Build a 'ProcessConfig' with the re-exported @typed-process@ DSL ('proc',+-- 'shell', 'setStdin', …), then run it with 'withProcessGroup', which runs the+-- process in its own group and guarantees the /whole group/ is torn down on+-- exit. Callers never manage pids or process groups themselves. module Atelier.Effects.Process ( -- * Effect Process@@ -20,7 +21,6 @@ , setStdout , setStderr , setWorkingDir- , setCreateGroup , createPipe , getStdin , getStdout@@ -29,27 +29,26 @@ -- * Operations , readProcessStdout , readProcessSafe- , startProcess- , stopProcess- , waitExitCode+ , withProcessGroup+ , terminateProcessGroup , interruptProcessGroup+ , waitExitCode -- * Interpreters , runProcessIO ) where +import Control.Exception (IOException, catch) import Effectful (Effect, IOE) import Effectful.Dispatch.Dynamic (interpret_)-import Effectful.Exception (trySync)+import Effectful.Exception (bracket, trySync) import Effectful.TH (makeEffect) import System.Exit (ExitCode (..))-import System.Process (interruptProcessGroupOf)+import System.Posix.Signals (sigTERM, signalProcessGroup)+import System.Process (Pid, getPid, interruptProcessGroupOf) import System.Process.Typed ( ProcessConfig , createPipe- , getStderr- , getStdin- , getStdout , proc , setCreateGroup , setStderr@@ -61,30 +60,81 @@ import System.Process.Typed qualified as TP +import Atelier.Effects.Process.Internal (RunningProcess (..)) --- | @typed-process@'s started-process type, re-exported under a non-clashing--- name (the effect itself is called 'Process'). Parameterised by its stdin,--- stdout and stderr stream types.-type RunningProcess = TP.Process +-- | The process's stdin stream (its type set by the 'ProcessConfig').+getStdin :: RunningProcess i o e -> i+getStdin (RunningProcess p) = TP.getStdin p ++-- | The process's stdout stream.+getStdout :: RunningProcess i o e -> o+getStdout (RunningProcess p) = TP.getStdout p+++-- | The process's stderr stream.+getStderr :: RunningProcess i o e -> e+getStderr (RunningProcess p) = TP.getStderr p++ data Process :: Effect where -- | Run a process to completion, returning its exit code and captured stdout. ReadProcessStdout :: ProcessConfig i o e -> Process m (ExitCode, LByteString)- -- | Spawn a process and return its handle for further interaction.- StartProcess :: ProcessConfig i o e -> Process m (TP.Process i o e)- -- | Stop a process: close its streams, terminate it, and wait for it to exit.- StopProcess :: TP.Process i o e -> Process m ()+ -- | Spawn a process and return its handle. Internal; callers use 'withProcessGroup'.+ StartProcess :: ProcessConfig i o e -> Process m (RunningProcess i o e)+ -- | Terminate the leader and close its streams. Internal; does not reach the+ -- rest of the group.+ StopProcess :: RunningProcess i o e -> Process m () -- | Block until the process exits and return its exit code.- WaitExitCode :: TP.Process i o e -> Process m ExitCode- -- | Send an interrupt (SIGINT) to the process's group. Requires the process- -- to have been started with @'setCreateGroup' True@.- InterruptProcessGroup :: TP.Process i o e -> Process m ()+ WaitExitCode :: RunningProcess i o e -> Process m ExitCode+ -- | Send SIGINT to the process's group. Requires @'setCreateGroup' True@.+ InterruptProcessGroup :: RunningProcess i o e -> Process m ()+ -- | Look up the OS process id, or 'Nothing' if it has already exited. Internal.+ GetProcessId :: RunningProcess i o e -> Process m (Maybe Pid)+ -- | Send SIGTERM to the given process group. Internal.+ SignalProcessGroupTerm :: Pid -> Process m () makeEffect ''Process +-- | Run a process in its own process group, terminating the /whole group/ — the+-- process and every descendant it spawned — when the body returns or throws.+--+-- Cleanup is guaranteed even if the process has already exited. The body may+-- share the handle so another thread can 'terminateProcessGroup' it early.+withProcessGroup+ :: (Process :> es)+ => ProcessConfig i o e+ -> (RunningProcess i o e -> Eff es a)+ -> Eff es a+withProcessGroup cfg body =+ bracket acquire release (\(p, _) -> body p)+ where+ acquire = do+ p <- startProcess (setCreateGroup True cfg)+ pgid <- getProcessId p+ pure (p, pgid)+ release (p, pgid) = tearDownGroup pgid p+++-- | Terminate a running process and its /whole group/ immediately. Use this to+-- abort a process started with 'withProcessGroup' from another thread — for+-- children that trap SIGINT, where 'interruptProcessGroup' is not enough.+terminateProcessGroup :: (Process :> es) => RunningProcess i o e -> Eff es ()+terminateProcessGroup p = do+ pgid <- getProcessId p+ tearDownGroup pgid p+++-- | SIGTERM the group (if its id is known), then best-effort reap the leader.+tearDownGroup :: (Process :> es) => Maybe Pid -> RunningProcess i o e -> Eff es ()+tearDownGroup pgid p = do+ maybe (pure ()) signalProcessGroupTerm pgid+ void $ trySync $ stopProcess p++ -- | Run @cmd@ with @args@ and return its stdout as 'Text', or 'Nothing' on any -- error (non-zero exit, the executable not being found, etc.). readProcessSafe :: (Process :> es) => FilePath -> [String] -> Eff es (Maybe Text)@@ -98,7 +148,10 @@ runProcessIO :: (IOE :> es) => Eff (Process : es) a -> Eff es a runProcessIO = interpret_ \case ReadProcessStdout cfg -> liftIO $ TP.readProcessStdout cfg- StartProcess cfg -> liftIO $ TP.startProcess cfg- StopProcess p -> liftIO $ TP.stopProcess p- WaitExitCode p -> liftIO $ TP.waitExitCode p- InterruptProcessGroup p -> liftIO $ interruptProcessGroupOf (TP.unsafeProcessHandle p)+ StartProcess cfg -> liftIO $ RunningProcess <$> TP.startProcess cfg+ StopProcess (RunningProcess p) -> liftIO $ TP.stopProcess p+ WaitExitCode (RunningProcess p) -> liftIO $ TP.waitExitCode p+ InterruptProcessGroup (RunningProcess p) -> liftIO $ interruptProcessGroupOf (TP.unsafeProcessHandle p)+ GetProcessId (RunningProcess p) -> liftIO $ getPid (TP.unsafeProcessHandle p)+ SignalProcessGroupTerm pid ->+ liftIO $ signalProcessGroup sigTERM pid `catch` \(_ :: IOException) -> pure ()
+ src/Atelier/Effects/Process/Internal.hs view
@@ -0,0 +1,15 @@+-- | The 'RunningProcess' constructor, for tests that fabricate a handle.+--+-- Constructing one directly bypasses 'Atelier.Effects.Process.withProcessGroup'+-- and its guarantee that the process heads its own group, so the group-signalling+-- operations may target the wrong group. Production code should not import this.+module Atelier.Effects.Process.Internal+ ( RunningProcess (..)+ ) where++import System.Process.Typed qualified as TP+++-- | A process run in its own group by 'Atelier.Effects.Process.withProcessGroup'.+-- Parameterised by its stdin, stdout and stderr stream types.+newtype RunningProcess i o e = RunningProcess (TP.Process i o e)
src/Atelier/Exception.hs view
@@ -1,3 +1,5 @@+{-# OPTIONS_GHC -Wno-redundant-constraints #-}+ -- | Helpers for telling synchronous exceptions apart from asynchronous ones. -- -- Synchronous exceptions are genuine failures worth catching, logging, or