packages feed

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 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