packages feed

semaphore-compat 2.0.0 → 2.0.1

raw patch · 14 files changed

+775/−540 lines, 14 filesdep −exceptionsPVP: major bump suggested

API removals or changes: PVP suggests a major version bump

Dependencies removed: exceptions

API changes (from Hackage documentation)

- System.Semaphore.Internal.Common: SemaphoreAlreadyExists :: !SemaphoreIdentifier -> SemaphoreError
- System.Semaphore.Internal.Common: SemaphoreDoesNotExist :: !SemaphoreIdentifier -> SemaphoreError
- System.Semaphore.Internal.Common: SemaphoreIncompatibleVersion :: !SemaphoreProtocolVersion -> !SemaphoreProtocolVersion -> SemaphoreError
- System.Semaphore.Internal.Common: SemaphoreName :: !SemaphoreProtocolVersion -> !String -> SemaphoreName
- System.Semaphore.Internal.Common: SemaphoreOtherError :: !IOException -> SemaphoreError
- System.Semaphore.Internal.Common: SemaphoreProtocolVersion :: Int -> SemaphoreProtocolVersion
- System.Semaphore.Internal.Common: [getSemaphoreProtocolVersion] :: SemaphoreProtocolVersion -> Int
- System.Semaphore.Internal.Common: [semaphoreErrorActualVersion] :: SemaphoreError -> !SemaphoreProtocolVersion
- System.Semaphore.Internal.Common: [semaphoreErrorExpectedVersion] :: SemaphoreError -> !SemaphoreProtocolVersion
- System.Semaphore.Internal.Common: [semaphoreErrorIdentifier] :: SemaphoreError -> !SemaphoreIdentifier
- System.Semaphore.Internal.Common: [semaphoreOtherError] :: SemaphoreError -> !IOException
- System.Semaphore.Internal.Common: [semaphoreProtocolVersion] :: SemaphoreName -> !SemaphoreProtocolVersion
- System.Semaphore.Internal.Common: [unversionedSemaphoreNameString] :: SemaphoreName -> !String
- System.Semaphore.Internal.Common: data SemaphoreError
- System.Semaphore.Internal.Common: data SemaphoreName
- System.Semaphore.Internal.Common: getSemaphoreSocketPath :: SemaphoreName -> IO FilePath
- System.Semaphore.Internal.Common: iToBase62 :: Int -> String
- System.Semaphore.Internal.Common: instance GHC.Classes.Eq System.Semaphore.Internal.Common.SemaphoreError
- System.Semaphore.Internal.Common: instance GHC.Classes.Eq System.Semaphore.Internal.Common.SemaphoreName
- System.Semaphore.Internal.Common: instance GHC.Classes.Eq System.Semaphore.Internal.Common.SemaphoreProtocolVersion
- System.Semaphore.Internal.Common: instance GHC.Classes.Ord System.Semaphore.Internal.Common.SemaphoreProtocolVersion
- System.Semaphore.Internal.Common: instance GHC.Exception.Type.Exception System.Semaphore.Internal.Common.SemaphoreError
- System.Semaphore.Internal.Common: instance GHC.Show.Show System.Semaphore.Internal.Common.SemaphoreError
- System.Semaphore.Internal.Common: instance GHC.Show.Show System.Semaphore.Internal.Common.SemaphoreName
- System.Semaphore.Internal.Common: instance GHC.Show.Show System.Semaphore.Internal.Common.SemaphoreProtocolVersion
- System.Semaphore.Internal.Common: newtype SemaphoreProtocolVersion
- System.Semaphore.Internal.Common: parseSemaphoreIdentifier :: SemaphoreIdentifier -> Maybe SemaphoreName
- System.Semaphore.Internal.Common: semaphoreIdentifier :: SemaphoreName -> SemaphoreIdentifier
- System.Semaphore.Internal.Common: semaphoreVersion :: SemaphoreProtocolVersion
- System.Semaphore.Internal.Common: type SemaphoreIdentifier = String
- System.Semaphore.Internal.Posix: getTimeSeed :: IO Int
+ System.Semaphore.Internal: ClientSemaphore :: ClientSemaphore -> ClientSemaphore
+ System.Semaphore.Internal: SemaphoreToken :: SemaphoreToken -> SemaphoreToken
+ System.Semaphore.Internal: ServerSemaphore :: ServerSemaphore -> ServerSemaphore
+ System.Semaphore.Internal: clientSemaphoreName :: ClientSemaphore -> SemaphoreName
+ System.Semaphore.Internal: create_sem :: SemaphoreName -> Int -> IO (Either SemaphoreError ServerSemaphore)
+ System.Semaphore.Internal: destroyClientSemaphore :: ClientSemaphore -> IO ()
+ System.Semaphore.Internal: destroyServerSemaphore :: ServerSemaphore -> IO ()
+ System.Semaphore.Internal: getSemaphoreValue :: ServerSemaphore -> IO Int
+ System.Semaphore.Internal: get_time_seed :: IO Int
+ System.Semaphore.Internal: newtype ClientSemaphore
+ System.Semaphore.Internal: newtype SemaphoreToken
+ System.Semaphore.Internal: newtype ServerSemaphore
+ System.Semaphore.Internal: open_sem_raw :: SemaphoreName -> IO (Either SemaphoreError ClientSemaphore)
+ System.Semaphore.Internal: releaseSemaphoreToken :: HasCallStack => SemaphoreToken -> IO ()
+ System.Semaphore.Internal: serverClientSemaphore :: ServerSemaphore -> ClientSemaphore
+ System.Semaphore.Internal: tryWaitOnSemaphore :: HasCallStack => ClientSemaphore -> IO (Maybe SemaphoreToken)
+ System.Semaphore.Internal: waitOnSemaphore :: HasCallStack => ClientSemaphore -> IO SemaphoreToken
+ System.Semaphore.Internal.Posix: get_time_seed :: IO Int
+ System.Semaphore.Internal.Posix.Server: pattern CmdRelease :: Word8
+ System.Semaphore.Internal.Posix.Server: pattern CmdTryWait :: Word8
+ System.Semaphore.Internal.Posix.Server: pattern CmdWait :: Word8
+ System.Semaphore.Internal.Posix.Server: pattern RspFail :: Word8
+ System.Semaphore.Internal.Posix.Server: pattern RspOk :: Word8
+ System.Semaphore.Internal.Posix.Server: serverLoop :: TVar Int -> Fd -> Fd -> IO ()
+ System.Semaphore.Internal.Version: SemaphoreAlreadyExists :: !SemaphoreIdentifier -> SemaphoreError
+ System.Semaphore.Internal.Version: SemaphoreDoesNotExist :: !SemaphoreIdentifier -> SemaphoreError
+ System.Semaphore.Internal.Version: SemaphoreIncompatibleVersion :: !SemaphoreProtocolVersion -> !SemaphoreProtocolVersion -> SemaphoreError
+ System.Semaphore.Internal.Version: SemaphoreName :: !SemaphoreProtocolVersion -> !String -> SemaphoreName
+ System.Semaphore.Internal.Version: SemaphoreOtherError :: !IOException -> SemaphoreError
+ System.Semaphore.Internal.Version: SemaphoreProtocolVersion :: Int -> SemaphoreProtocolVersion
+ System.Semaphore.Internal.Version: [getSemaphoreProtocolVersion] :: SemaphoreProtocolVersion -> Int
+ System.Semaphore.Internal.Version: [semaphoreErrorActualVersion] :: SemaphoreError -> !SemaphoreProtocolVersion
+ System.Semaphore.Internal.Version: [semaphoreErrorExpectedVersion] :: SemaphoreError -> !SemaphoreProtocolVersion
+ System.Semaphore.Internal.Version: [semaphoreErrorIdentifier] :: SemaphoreError -> !SemaphoreIdentifier
+ System.Semaphore.Internal.Version: [semaphoreOtherError] :: SemaphoreError -> !IOException
+ System.Semaphore.Internal.Version: [semaphoreProtocolVersion] :: SemaphoreName -> !SemaphoreProtocolVersion
+ System.Semaphore.Internal.Version: [unversionedSemaphoreNameString] :: SemaphoreName -> !String
+ System.Semaphore.Internal.Version: data SemaphoreError
+ System.Semaphore.Internal.Version: data SemaphoreName
+ System.Semaphore.Internal.Version: getSemaphoreSocketPath :: SemaphoreName -> IO FilePath
+ System.Semaphore.Internal.Version: iToBase62 :: Int -> String
+ System.Semaphore.Internal.Version: instance GHC.Classes.Eq System.Semaphore.Internal.Version.SemaphoreError
+ System.Semaphore.Internal.Version: instance GHC.Classes.Eq System.Semaphore.Internal.Version.SemaphoreName
+ System.Semaphore.Internal.Version: instance GHC.Classes.Eq System.Semaphore.Internal.Version.SemaphoreProtocolVersion
+ System.Semaphore.Internal.Version: instance GHC.Classes.Ord System.Semaphore.Internal.Version.SemaphoreProtocolVersion
+ System.Semaphore.Internal.Version: instance GHC.Exception.Type.Exception System.Semaphore.Internal.Version.SemaphoreError
+ System.Semaphore.Internal.Version: instance GHC.Show.Show System.Semaphore.Internal.Version.SemaphoreError
+ System.Semaphore.Internal.Version: instance GHC.Show.Show System.Semaphore.Internal.Version.SemaphoreName
+ System.Semaphore.Internal.Version: instance GHC.Show.Show System.Semaphore.Internal.Version.SemaphoreProtocolVersion
+ System.Semaphore.Internal.Version: newtype SemaphoreProtocolVersion
+ System.Semaphore.Internal.Version: parseSemaphoreIdentifier :: SemaphoreIdentifier -> Maybe SemaphoreName
+ System.Semaphore.Internal.Version: semaphoreIdentifier :: SemaphoreName -> SemaphoreIdentifier
+ System.Semaphore.Internal.Version: semaphoreVersion :: SemaphoreProtocolVersion
+ System.Semaphore.Internal.Version: type SemaphoreIdentifier = String

Files

cbits/domain.c view
@@ -7,17 +7,29 @@ #include <sys/un.h> #include <unistd.h> +/* Some platforms may not have SOCK_CLOEXEC available as an option to socket(),+ * so we need to set it explicitly with fcntl()+ * */+static int set_cloexec(int fd) {+    int flags = fcntl(fd, F_GETFD, 0);+    if (flags == -1) return -1;+    return fcntl(fd, F_SETFD, flags | FD_CLOEXEC);+}+ int hs_listen_domain_socket(const char *path) {     if (strlen(path) >= sizeof(((struct sockaddr_un *)0)->sun_path)) {         errno = ENAMETOOLONG;         return -1;     } +    int listen_type = SOCK_STREAM; #ifdef SOCK_NONBLOCK-    int fd = socket(AF_UNIX, SOCK_STREAM | SOCK_NONBLOCK, 0);-#else-    int fd = socket(AF_UNIX, SOCK_STREAM, 0);+    listen_type |= SOCK_NONBLOCK; #endif+#ifdef SOCK_CLOEXEC+    listen_type |= SOCK_CLOEXEC;+#endif+    int fd = socket(AF_UNIX, listen_type, 0);     if (fd == -1) {         return -1;     }@@ -30,6 +42,14 @@         return -1;     } #endif+#ifndef SOCK_CLOEXEC+    if (set_cloexec(fd) == -1) {+        int saved_errno = errno;+        close(fd);+        errno = saved_errno;+        return -1;+    }+#endif      struct sockaddr_un server_addr;     memset(&server_addr, 0, sizeof(struct sockaddr_un));@@ -61,10 +81,22 @@         return -1;     } +#ifdef SOCK_CLOEXEC+    int fd = socket(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0);+#else     int fd = socket(AF_UNIX, SOCK_STREAM, 0);+#endif     if (fd == -1) {         return -1;     }+#ifndef SOCK_CLOEXEC+    if (set_cloexec(fd) == -1) {+        int saved_errno = errno;+        close(fd);+        errno = saved_errno;+        return -1;+    }+#endif      struct sockaddr_un server_addr;     memset(&server_addr, 0, sizeof(struct sockaddr_un));@@ -149,7 +181,13 @@         }         if (fds[0].revents & POLLIN) {             int afd = accept(listen_fd, NULL, NULL);-            if (afd >= 0) return afd;+            if (afd >= 0) {+                int fl = fcntl(afd, F_GETFL, 0);+                if (fl != -1)+                    fcntl(afd, F_SETFL, fl & ~O_NONBLOCK);+                set_cloexec(afd);+                return afd;+            }             /* Listen fd is O_NONBLOCK. an EAGAIN/EINTR here means the queued              * connection was droppped poll and accept. */             if (errno == EINTR || errno == EAGAIN || errno == EWOULDBLOCK)
changelog.md view
@@ -1,3 +1,13 @@+### 2.0.1 (June 30, 2026)++- Fix a bug on Darwin where accepted fds would inherit O_NONBLOCK from the listen fd,+  causing subsequent reads on the server to fail instantly.+- Fix fds opened by `semaphore-compat` leaking across `exec`.+- Avoid leaking fds from stderr handles in the testsuite.+- Improve documentation+- Drop dependency on `exceptions`+- `releaseSemaphoreToken` is now idempotent on Windows.+ ### 2.0.0 (May 22, 2026)  - Major release introducing protocol v2:
semaphore-compat.cabal view
@@ -3,7 +3,7 @@ name:     semaphore-compat version:-    2.0.0+    2.0.1 license:     BSD-3-Clause @@ -48,15 +48,14 @@      exposed-modules:         System.Semaphore-        System.Semaphore.Internal.Common+        System.Semaphore.Internal+        System.Semaphore.Internal.Version      build-depends:         base           >= 4.11 && < 4.24       , directory           >= 1.3  && < 1.4-      , exceptions-          >= 0.7  && < 0.11       , filepath           >= 1.4  && < 1.6 @@ -82,6 +81,7 @@       exposed-modules:           System.Semaphore.Internal.DomainSocket           System.Semaphore.Internal.Posix+          System.Semaphore.Internal.Posix.Server      default-language:         Haskell2010
src/System/Semaphore.hs view
@@ -1,17 +1,44 @@-{-# LANGUAGE CPP #-} +-- | This library provides a cross-platform implementation of semaphores.+--+-- Its main role is to provide a cross-platform notion of semaphore that can be+-- used to implement the jobserver\/jobclient model of+-- [GHC proposal #540](https://ghc-proposals.readthedocs.io/en/latest/proposals/0540-jsem.html).+--+-- Typical usage:+--+--  - The jobserver creates a 'ServerSemaphore', e.g. using 'createSemaphore'/'freshSemaphore'.+--  - The jobserver retrieves the serialisable 'SemaphoreIdentifier' of this+--    'ServerSemaphore' by using 'serverClientSemaphore', 'clientSemaphoreName'+--    and 'semaphoreIdentifier'.+--  - The jobserver passes the 'SemaphoreIdentifier' to the jobclient via IPC.+--  - The jobclient uses 'openSemaphore' to obtain the 'ClientSemaphore'+--    corresponding to the 'SemaphoreIdentifier' it has been given.+--  - The jobclient can then request resources from the semaphore using+--    'waitOnSemaphore' (and return them using 'releaseSemaphoreToken'), or by+--    using the 'withAbstractSem' bracket pattern.+--  - Once the jobclient is done, it uses 'destroyClientSemaphore' to ensure+--    all resources are released (including any remaining tokens).+--  - Once the jobserver is done, it cleans up the resources underlying the+--    semaphore using 'destroyServerSemaphore'.+--+-- NB: the above usage outline deliberately omits any mention of implicit+-- semaphore tokens: such a notion does not exist in this library, as they are+-- purely an abstraction layer used by the jobserver implementation.+-- See 'destroyClientSemaphore' for more details. module System.Semaphore   ( -- * System semaphores      -- $server-vs-client -    ClientSemaphore, ServerSemaphore, SemaphoreName(..)+    ClientSemaphore, ServerSemaphore+  , serverClientSemaphore -    -- | The name of a client semaphore+  , SemaphoreName(..)   , clientSemaphoreName++  , SemaphoreIdentifier   , semaphoreIdentifier-    -- | Retrieve the client semaphore corresponding to a server semaphore-  , serverClientSemaphore      -- ** Creating a semaphore   , createSemaphore, freshSemaphore@@ -19,7 +46,7 @@     -- ** Opening a semaphore   , SemaphoreToken   , openSemaphore-  , SemaphoreIdentifier, parseSemaphoreIdentifier+  , parseSemaphoreIdentifier   , SemaphoreProtocolVersion(..)   , semaphoreVersion   , versionsAreCompatible@@ -33,23 +60,33 @@     -- ** Releasing resources   , releaseSemaphoreToken -    -- $destroying   , destroyClientSemaphore, destroyServerSemaphore+    -- $implicit-tokens    -- * Abstract semaphores   , AbstractSem(..)   , withAbstractSem   ) where +-- base+import Control.Exception ( bracket, bracket_ )+import Data.List.NonEmpty ( NonEmpty(..) )++-- semaphore-compat+import System.Semaphore.Internal+import System.Semaphore.Internal.Version++--------------------------------------------------------------------------------+ {- $server-vs-client -Since version 2 of @semaphore-compat@, we distinguish between two kinds-of semaphores:+Since version 2 of @semaphore-compat@, the library distinguishes between two+kinds of semaphores:    - When a jobserver creates a semaphore via 'createSemaphore', it obtains     a 'ServerSemaphore'.-  - Jobclients (which open a pre-existing semaphore via 'openSemaphore')-    obtain a 'ClientSemaphore'.+  - Jobclients, which are passed the 'SemaphoreIdentifier' of a pre-existing+    semaphore, obtain a 'ClientSemaphore' via 'openSemaphore'.  When the jobserver wants to also act as a jobclient, it can use 'serverClientSemaphore' to obtain the 'ClientSemaphore' corresponding@@ -59,51 +96,42 @@ semaphore resources held by all clients. -} -{- $destroying--Destroying a semaphore releases the implicit token held by the-semaphore.--For example, when @cabal-install@ invokes @ghc@, the @ghc@ process-automatically has one semaphore token at the start (the implicit-token), and can request further tokens with 'waitOnSemaphore'. The-tokens acquired by 'waitOnSemaphore' are released by-'releaseSemaphoreToken', while the implicit token is released by-'destroyClientSemaphore'.--}+{- $implicit-tokens --- base-import Data.List.NonEmpty ( NonEmpty(..) )+The GHC jobserver protocol described in [GHC proposal #540](https://ghc-proposals.readthedocs.io/en/latest/proposals/0540-jsem.html)+specifies the notion of __implicit semaphore tokens__. --- exceptions-import qualified Control.Monad.Catch as MC+  1. To invoke a jobclient, the jobserver must be in possession of at least one+     semaphore token. We consider that the jobserver passes on ownership of this+     token to the jobclient, even though no interaction with the semaphore takes+     place. This is the __implicit token__.+  2. Each jobclient thus starts with one available token (the implicit token),+     and can request more tokens by waiting on the semaphore. -import System.Semaphore.Internal.Common+A jobclient calling 'destroyClientSemaphore' signals to the jobserver that the+client is done doing work. The jobserver thus regains control of the implicit+token, which it can now use to spawn other jobclients. -#if defined(mingw32_HOST_OS)-import System.Semaphore.Internal.Win32-#elif defined(wasm32_HOST_ARCH) || defined(javascript_HOST_ARCH)-import System.Semaphore.Internal.Unsupported-#else-import System.Semaphore.Internal.Posix-#endif+Implicit tokens are purely an accounting abstraction that we superimpose on top+of semaphore token ownership for the purposes of implementing the jobserver+protocol. They are not part of this library.+-}  --------------------------------------- -- Version compatibility  -- | Check whether two semaphore protocol versions are compatible.--- Only identical versions are compatible. versionsAreCompatible :: SemaphoreProtocolVersion -> SemaphoreProtocolVersion -> Bool-versionsAreCompatible a b = a == b+versionsAreCompatible a b =+  -- For now, only identical versions are compatible, but keeping this+  -- check abstract (via 'versionsAreCompatible') gives us more flexibility+  -- when evolving the library.+  a == b  --------------------------------------- -- System-specific semaphores  -- | Create a new semaphore with the given label and initial token count.------ On POSIX, crash recovery is automatic: disconnected clients' tokens--- are returned to the pool.  On Windows, tokens held by a crashed--- client are permanently lost. createSemaphore :: String -- ^ label                 -> Int    -- ^ number of tokens on the semaphore                 -> IO (Either SemaphoreError ServerSemaphore)@@ -121,7 +149,7 @@                -> Int    -- ^ number of tokens on the semaphore                -> IO (Either SemaphoreError ServerSemaphore) freshSemaphore prefix init_toks = do-  seed <- getTimeSeed+  seed <- get_time_seed   go 0 (seedStrings seed)   where     go :: Int -> NonEmpty String -> IO (Either SemaphoreError ServerSemaphore)@@ -141,13 +169,10 @@           | otherwise           -> return (Left err) --- | Open a semaphore from its 'SemaphoreIdentifier'.+-- | Open a pre-existing semaphore. ----- The identifier should normally begin with a version prefix @v\<N\>-@.--- An unversioned identifier is treated as v1 for backwards--- compatibility. Returns @Left SemaphoreIncompatibleVersion@ if the--- identifier's protocol version is not compatible with this build of--- @semaphore-compat@.+-- Returns @Left SemaphoreIncompatibleVersion@ if the semaphore protocol+-- version is not compatible with the current version of @semaphore-compat@. openSemaphore :: SemaphoreIdentifier -> IO (Either SemaphoreError ClientSemaphore) openSemaphore ident =   case parseSemaphoreIdentifier ident of@@ -168,7 +193,7 @@  -- | Acquire a token, run an action, then release the token. Exception safe. withSemaphoreToken :: ClientSemaphore -> (SemaphoreToken -> IO a) -> IO a-withSemaphoreToken sem = MC.bracket (waitOnSemaphore sem) releaseSemaphoreToken+withSemaphoreToken sem = bracket (waitOnSemaphore sem) releaseSemaphoreToken  seedStrings :: Int -> NonEmpty String seedStrings seed = fmap ( \ i -> iToBase62 (i + seed) ) (0 :| [1..])@@ -183,5 +208,10 @@     , releaseSem :: IO ()     } +-- | Acquire/release bracket pattern: acquire a token, perform an action,+-- and release the token.+--+-- Guarantees that the token is released in case that the inner action throws+-- an exception. withAbstractSem :: AbstractSem -> IO b -> IO b-withAbstractSem s = MC.bracket_ (acquireSem s) (releaseSem s)+withAbstractSem s = bracket_ (acquireSem s) (releaseSem s)
+ src/System/Semaphore/Internal.hs view
@@ -0,0 +1,136 @@+{-# LANGUAGE CPP #-}++-- This module selects the internal implementation (POSIX, Windows, wasm/js)+-- with CPP and wraps it with user-facing types and documentation.+--+-- Benefits:+--+--  - All the documentation lives in a single place, instead of being duplicated+--    across every implementation.+--  - We can document platform-dependent behaviour, as opposed to e.g. the+--    Hackage page only showing POSIX-specific documentation.++-- | Platform-independent internal API.+module System.Semaphore.Internal+  ( ClientSemaphore(..), ServerSemaphore(..), SemaphoreToken(..)+  , clientSemaphoreName, serverClientSemaphore++  , waitOnSemaphore, tryWaitOnSemaphore+  , releaseSemaphoreToken++  , destroyClientSemaphore, destroyServerSemaphore++  , getSemaphoreValue++    -- * Internals+  , create_sem, open_sem_raw+  , Impl.get_time_seed+  ) where++-- base+import Data.Coerce ( coerce )+import GHC.Stack ( HasCallStack )++-- semaphore-compat+import System.Semaphore.Internal.Version++#if defined(wasm32_HOST_ARCH) || defined(javascript_HOST_ARCH)+import qualified System.Semaphore.Internal.Unsupported as Impl+#elif defined(mingw32_HOST_OS)+import qualified System.Semaphore.Internal.Win32 as Impl+#else+import qualified System.Semaphore.Internal.Posix as Impl+#endif++---------------------------------------+-- User-facing API++-- | A client-side semaphore: the handle a jobclient uses to acquire and release+-- tokens from a semaphore created by a jobserver.+--+-- Retrieve the underlying name with 'clientSemaphoreName'.+newtype ClientSemaphore = ClientSemaphore Impl.ClientSemaphore++-- | A server-side semaphore: a 'ClientSemaphore' together with the book-keeping+-- needed to track resource usage by all clients.+--+-- Retrieve the corresponding client semaphore with 'serverClientSemaphore'.+newtype ServerSemaphore = ServerSemaphore Impl.ServerSemaphore++-- | A held semaphore token: evidence of one unit of resource acquired from a+-- semaphore.+--+-- Use 'releaseSemaphoreToken' or 'withSemaphoreToken' to ensure prompt release+-- of semaphore tokens.+--+-- Platform-dependent behaviour:+--+--  - On POSIX, if all references to a 'SemaphoreToken' are dropped without+--    being released, a finalizer returns the token to the semaphore.+--  - On Windows, tokens held by a crashed client are permanently lost.+newtype SemaphoreToken = SemaphoreToken Impl.SemaphoreToken++-- | Retrieve the underlying name of a client-side semaphore.+clientSemaphoreName :: ClientSemaphore -> SemaphoreName+clientSemaphoreName = coerce Impl.clientSemaphoreName++-- | Retrieve the client-side semaphore corresponding to a server-side semaphore.+serverClientSemaphore :: ServerSemaphore -> ClientSemaphore+serverClientSemaphore = coerce Impl.serverClientSemaphore++-- | Acquire a token from the semaphore, blocking until one is available.+--+-- The returned 'SemaphoreToken' must be released with 'releaseSemaphoreToken'.+-- For prompt and predictable release of resources, callers should use+-- 'System.Semaphore.withSemaphoreToken' or 'releaseSemaphoreToken'.+--+-- This operation is interruptible: it can be cancelled by+-- 'Control.Concurrent.throwTo', 'Control.Concurrent.killThread', etc.+-- If interrupted, any transiently acquired token is automatically returned.+waitOnSemaphore :: HasCallStack => ClientSemaphore -> IO SemaphoreToken+waitOnSemaphore = coerce Impl.waitOnSemaphore++-- | Try to acquire a token from the semaphore without blocking.+--+-- Returns @Just token@ if a token was available, @Nothing@ otherwise.+--+-- This is __not__ an interruptible operation, but that should not be a problem:+-- this function is not expected to block for long, as the server is supposed+-- to respond immediately.+tryWaitOnSemaphore :: HasCallStack => ClientSemaphore -> IO (Maybe SemaphoreToken)+tryWaitOnSemaphore = coerce Impl.tryWaitOnSemaphore++-- | Release a semaphore token, returning it to the pool.+--+-- Idempotent: a second call on the same token is a safe no-op.+releaseSemaphoreToken :: HasCallStack => SemaphoreToken -> IO ()+releaseSemaphoreToken = coerce Impl.releaseSemaphoreToken++-- | Destroy a client-side semaphore.+--+-- This is an idempotent operation: double calls to 'destroyClientSemaphore'+-- do not cause any problems.+destroyClientSemaphore :: ClientSemaphore -> IO ()+destroyClientSemaphore = coerce Impl.destroyClientSemaphore++-- | Destroy a server-side semaphore.+--+-- Idempotent. Not interruptible.+destroyServerSemaphore :: ServerSemaphore -> IO ()+destroyServerSemaphore = coerce Impl.destroyServerSemaphore++-- | Query the current semaphore value (how many tokens it has available).+--+-- This is mainly for debugging use, as it is easy to introduce race conditions+-- when nontrivial program logic depends on the value returned by this function.+getSemaphoreValue :: ServerSemaphore -> IO Int+getSemaphoreValue = coerce Impl.getSemaphoreValue++--------------------------------------------------------------------------------+-- Internals++create_sem :: SemaphoreName -> Int -> IO (Either SemaphoreError ServerSemaphore)+create_sem = coerce Impl.create_sem++open_sem_raw :: SemaphoreName -> IO (Either SemaphoreError ClientSemaphore)+open_sem_raw = coerce Impl.open_sem_raw
− src/System/Semaphore/Internal/Common.hs
@@ -1,142 +0,0 @@-{-# LANGUAGE BangPatterns #-}-{-# LANGUAGE CPP #-}-{-# LANGUAGE MagicHash #-}--module System.Semaphore.Internal.Common-  ( SemaphoreName(..)-  , SemaphoreIdentifier-  , SemaphoreProtocolVersion(..)-  , SemaphoreError(..)-  , semaphoreVersion-  , semaphoreIdentifier-  , parseSemaphoreIdentifier-  , getSemaphoreSocketPath-  , iToBase62-  ) where---- base-import Control.Exception ( Exception, IOException )-import GHC.Exts ( Char(..), Int(..), indexCharOffAddr# )---- directory-import System.Directory ( getTemporaryDirectory, createDirectoryIfMissing )---- filepath-import System.FilePath ( (</>) )-------------------------------------------- Types---- | The protocol version of a semaphore.-newtype SemaphoreProtocolVersion =-  SemaphoreProtocolVersion { getSemaphoreProtocolVersion :: Int }-  deriving (Eq, Ord, Show)---- | A semaphore name: a protocol version and an unversioned name string.-data SemaphoreName = SemaphoreName-  { semaphoreProtocolVersion :: !SemaphoreProtocolVersion-  , unversionedSemaphoreNameString :: !String-  } deriving (Eq, Show)---- | The identifier string of a semaphore, as serialised for transport--- between processes (e.g. on a command line via @-jsem@).------ For version 1 this is a bare name; for version @N@ (with @N >= 2@)--- this is @\"v\<N\>-\<name\>\"@.-type SemaphoreIdentifier = String---- | Errors that can occur when creating or opening a semaphore.-data SemaphoreError-  = SemaphoreAlreadyExists-    { semaphoreErrorIdentifier :: !SemaphoreIdentifier-    }-  | SemaphoreDoesNotExist-    { semaphoreErrorIdentifier :: !SemaphoreIdentifier-    }-  | SemaphoreIncompatibleVersion-    { semaphoreErrorActualVersion   :: !SemaphoreProtocolVersion-    , semaphoreErrorExpectedVersion :: !SemaphoreProtocolVersion-    }-  | SemaphoreOtherError-    { semaphoreOtherError :: !IOException-    }-  deriving (Show, Eq)--instance Exception SemaphoreError-------------------------------------------- Version negotiation---- | The protocol version on this platform.------ The version tracks the IPC mechanism, not the library version:------   * __POSIX:__ 2 (domain sockets, replacing v1 system semaphores).---   * __Windows:__ 1 (Win32 named semaphores, unchanged from v1).---   * __Unsupported platforms:__ 0 (no compatible IPC backend).------ Because the version is 1 on Windows, 'semaphoreIdentifier' produces--- a bare name (no @v\<N\>-@ prefix), matching the v1 format.-semaphoreVersion :: SemaphoreProtocolVersion-#if defined(mingw32_HOST_OS)-semaphoreVersion = SemaphoreProtocolVersion 1-#elif defined(wasm32_HOST_ARCH) || defined(javascript_HOST_ARCH)-semaphoreVersion = SemaphoreProtocolVersion 0-#else-semaphoreVersion = SemaphoreProtocolVersion 2-#endif----- | The serialised identifier of a 'SemaphoreName' for transport--- between processes.------ For version 1 this is the bare name; for version @N@ (@N >= 2@) the--- name is prefixed with @v\<N\>-@.-semaphoreIdentifier :: SemaphoreName -> SemaphoreIdentifier-semaphoreIdentifier sn-  | ver <= 1  = unversionedSemaphoreNameString sn-  | otherwise = "v" ++ show ver ++ "-" ++ unversionedSemaphoreNameString sn-  where-    ver = getSemaphoreProtocolVersion (semaphoreProtocolVersion sn)---- | Parse a 'SemaphoreIdentifier' into a 'SemaphoreName'.------ Returns @Nothing@ for unversioned strings (which should be treated as--- v1 by the caller's compatibility logic).-parseSemaphoreIdentifier :: SemaphoreIdentifier -> Maybe SemaphoreName-parseSemaphoreIdentifier ('v':rest) =-  case reads rest of-    [(n, '-':name)] -> Just (SemaphoreName (SemaphoreProtocolVersion n) name)-    _               -> Nothing-parseSemaphoreIdentifier _ = Nothing-------------------------------------------- Utilities---- | Convert an 'Int' to a base-62 string (digits @0@–@9@, @a@–@z@, @A@–@Z@).-iToBase62 :: Int -> String-iToBase62 m = go m' ""-  where-    m'-      | m == minBound = maxBound-      | otherwise     = abs m-    go n cs | n < 62-            = let !c = chooseChar62 n-              in c : cs-            | otherwise-            = let !(!q, r) = quotRem n 62-                  !c       = chooseChar62 r-              in go q (c : cs)--    chooseChar62 :: Int -> Char-    {-# INLINE chooseChar62 #-}-    chooseChar62 (I# n) = C# (indexCharOffAddr# chars62 n)-    chars62 = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"#---- | The socket file path for a semaphore.-getSemaphoreSocketPath :: SemaphoreName -> IO FilePath-getSemaphoreSocketPath sn = do-  tmp <- getTemporaryDirectory-  let dir = tmp </> "semaphore-compat"-  createDirectoryIfMissing True dir-  return (dir </> unversionedSemaphoreNameString sn)
src/System/Semaphore/Internal/DomainSocket.hs view
@@ -1,5 +1,6 @@ {-# LANGUAGE ForeignFunctionInterface #-} +-- | FFI glue code for Unix domain sockets. module System.Semaphore.Internal.DomainSocket   ( connectDomainSocket   , listenDomainSocket@@ -21,6 +22,8 @@  -- unix import System.Posix.Types ( Fd(..) )++--------------------------------------------------------------------------------  foreign import ccall safe "hs_connect_domain_socket"     c_connectDomainSocket :: CString -> IO CInt
src/System/Semaphore/Internal/Posix.hs view
@@ -1,18 +1,19 @@ {-# LANGUAGE PatternSynonyms #-}-{-# LANGUAGE RankNTypes #-} {-# LANGUAGE RecordWildCards #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TypeApplications #-} +-- | POSIX backend: semaphores implemented on top of Unix domain sockets. module System.Semaphore.Internal.Posix   ( ClientSemaphore(..), ServerSemaphore(..)   , SemaphoreToken(..)-  , create_sem, open_sem_raw   , waitOnSemaphore, tryWaitOnSemaphore   , releaseSemaphoreToken   , destroyClientSemaphore, destroyServerSemaphore   , getSemaphoreValue-  , getTimeSeed++  , create_sem, open_sem_raw+  , get_time_seed   ) where  -- base@@ -20,93 +21,92 @@   ( ThreadId, forkIOWithUnmask, killThread ) import Control.Concurrent.MVar   ( MVar, mkWeakMVar, newEmptyMVar, newMVar, putMVar-  , readMVar, takeMVar, tryTakeMVar )-import Control.Exception ( IOException )-import Control.Monad-import Data.Word ( Word8 )+  , takeMVar, tryTakeMVar )+import Control.Exception+  ( IOException, SomeException+  , finally, mask_, onException, throw, try, uninterruptibleMask_+  )+import Control.Monad ( void ) import Data.Bits ( xor )-import Foreign.C.Error ( Errno(Errno), eCONNABORTED ) import GHC.Clock ( getMonotonicTimeNSec )-import GHC.IO.Exception ( ioe_errno ) import GHC.Stack ( HasCallStack )-import System.IO.Error ( isFullError ) --- exceptions-import qualified Control.Monad.Catch as MC+-- directory+import System.Directory ( doesPathExist )  -- stm import Control.Concurrent.STM-  ( TVar, atomically, newTVarIO, readTVar, readTVarIO-  , modifyTVar', writeTVar, retry )---- directory-import System.Directory ( doesPathExist )+  ( TVar, newTVarIO, readTVarIO )  -- unix-import System.Posix.IO ( closeFd, createPipe )+import System.Posix.IO ( closeFd, createPipe, setFdOption, FdOption(CloseOnExec) ) import System.Posix.Files ( removeLink ) import System.Posix.Types ( Fd ) import System.Posix.Process ( getProcessID ) -import System.Semaphore.Internal.Common+-- semaphore-compat+import System.Semaphore.Internal.Version import System.Semaphore.Internal.DomainSocket   ( connectDomainSocket, listenDomainSocket-  , pollAcceptSocket, AcceptResult(..)   , fdReadByte, fdWriteByte   , fdShutdown )+import System.Semaphore.Internal.Posix.Server+  ( serverLoop+  , pattern CmdWait, pattern CmdTryWait, pattern CmdRelease+  , pattern RspOk, pattern RspFail+  ) --- | A semaphore identity (name + socket path).--- Each operation that needs a connection opens one internally.+--------------------------------------------------------------------------------++-- A client is just an identity (name + socket path); each operation that needs+-- a connection opens one internally. data ClientSemaphore =   ClientSemaphore     { clientSemaphoreName :: !SemaphoreName     , semSocketPath :: !FilePath     } --- | A held semaphore token, bound to one acquired resource.------ If all references to the 'SemaphoreToken' are dropped without being--- released, a finalizer closes the underlying connection and the server--- returns the token to the pool.  Use 'releaseSemaphoreToken' or--- 'System.Semaphore.withSemaphoreToken' for prompt release rather than--- relying on GC timing.------ The fd is held in an internal 'MVar' so 'releaseSemaphoreToken' takes--- ownership atomically: a second (erroneous) release is a safe no-op.+-- The fd is held in the 'MVar' so 'releaseSemaphoreToken' takes ownership+-- atomically: a second (erroneous) release is a safe no-op. newtype SemaphoreToken = SemaphoreToken   { tokenFdLock :: MVar Fd+    -- NB: the 'Fd' is held in an 'MVar' to allow 'releaseSemaphoreToken'+    -- to takes ownership of the 'Fd' atomically. This ensures that a+    -- second (erroneous) release is a safe no-op (avoiding Fd double-close bugs).   } --- | A server-side semaphore (owns the server thread, listen socket, and token pool). data ServerSemaphore = ServerSemaphore-  { serverClientSemaphore  :: !ClientSemaphore-  , serverThreadId   :: !ThreadId-  , serverPool       :: !(TVar Int)-  , serverState      :: !(MVar ServerState) -- ^ MVar is emptied when resources/fds are freed to prevent double close+  { serverClientSemaphore :: !ClientSemaphore+  , serverThreadId :: !ThreadId+  , serverPool     :: !(TVar Int)+  , serverState    :: !(MVar ServerState)+      -- NB: the MVar is emptied when resources/fds are freed to prevent double close   }  data ServerState = ServerState   { serverListenFd :: !Fd   , serverCancelFd :: !Fd-    -- ^ Write end of the cancel pipe.  Writing a byte signals the+    -- Write end of the cancel pipe.  Writing a byte signals the     -- server loop to exit its poll-accept.   }  create_sem :: SemaphoreName -> Int -> IO (Either SemaphoreError ServerSemaphore) create_sem sem_nm init_toks = do-  mb_res <- MC.try @_ @IOException $ MC.mask_ $ do+  mb_res <- try @IOException $ mask_ $ do     socketPath <- getSemaphoreSocketPath sem_nm     listenFd <- listenDomainSocket socketPath     -- ^^ creates the socket file, unlink it too on failure.     let cleanupListen = do-          void $ MC.try @_ @IOException $ closeFd listenFd-          void $ MC.try @_ @IOException $ removeLink socketPath-    flip MC.onException cleanupListen $ do+          void $ try @IOException $ closeFd listenFd+          void $ try @IOException $ removeLink socketPath+    flip onException cleanupListen $ do       pool <- newTVarIO init_toks       (cancelRd, cancelWr) <- createPipe+      setFdOption cancelRd CloseOnExec True+      setFdOption cancelWr CloseOnExec True       tid <- forkIOWithUnmask $ \unmask ->                unmask (serverLoop pool listenFd cancelRd)-                 `MC.finally` closeFd cancelRd+                 `finally` closeFd cancelRd       stateVar <- newMVar ServerState         { serverListenFd = listenFd         , serverCancelFd = cancelWr@@ -124,7 +124,7 @@  open_sem_raw :: SemaphoreName -> IO (Either SemaphoreError ClientSemaphore) open_sem_raw nm = do-  mb_res <- MC.try @_ @IOException $ do+  mb_res <- try @IOException $ do     socketPath <- getSemaphoreSocketPath nm     exists <- doesPathExist socketPath     return (socketPath, exists)@@ -137,65 +137,50 @@         , semSocketPath = socketPath         } --- | Acquire a token from the semaphore, blocking until one is available.------ This operation is interruptible: it can be cancelled by--- 'Control.Concurrent.throwTo', 'Control.Concurrent.killThread', etc. If--- interrupted, any transiently acquired token is automatically returned to the--- pool.------ For prompt and predictable release of resources, callers should use--- 'System.Semaphore.withSemaphoreToken' or `releaseSemaphoreToken'. waitOnSemaphore :: HasCallStack => ClientSemaphore -> IO SemaphoreToken waitOnSemaphore sem = do   resultVar <- newEmptyMVar   -- Mask exceptions until we get to the interruptible takeMVar-  MC.mask_ $ do+  mask_ $ do     fd <- connectDomainSocket (semSocketPath sem)     -- The read() runs in a forked thread     workerTid <- forkIOWithUnmask $ \_ -> do-      res <- MC.try @_ @MC.SomeException $ do+      res <- try @SomeException $ do         fdWriteByte fd CmdWait         fdReadByte fd       putMVar resultVar res     -- uninterruptibleMask_: killThread is interruptible, and a     -- second async between killThread and closeFd would leak fd.-    let cleanup = MC.uninterruptibleMask_ $ do+    let cleanup = uninterruptibleMask_ $ do           -- shutdown(SHUT_RDWR) causes the worker's read() to return EOF immediately-          void $ MC.try @_ @IOException $ fdShutdown fd+          void $ try @IOException $ fdShutdown fd           -- Wait for the worker to exit before closing the fd.           -- this prevents double close bugs           killThread workerTid           -- Finally close the fd-          void $ MC.try @_ @IOException $ closeFd fd+          void $ try @IOException $ closeFd fd     -- We achieve interruptiblity by relying on the interruptiblity of takeMVar     -- The worker thread is blocked on read(), fdShutdown in cleanup interrupts     -- it if we are interrupt by an async exception here.-    res <- takeMVar resultVar `MC.onException` cleanup+    res <- takeMVar resultVar `onException` cleanup     case res of       Right resp         | resp == RspOk -> mkToken fd-        | otherwise     -> do void $ MC.try @_ @IOException $ closeFd fd+        | otherwise     -> do void $ try @IOException $ closeFd fd                               fail $ "semaphore-compat: unexpected response in waitOnSemaphore: " ++ show resp-      Left e            -> do void $ MC.try @_ @IOException $ closeFd fd-                              MC.throwM e+      Left e            -> do void $ try @IOException $ closeFd fd+                              throw e --- | Try to acquire a token from the semaphore without blocking.------ Returns @Just token@ if a token was available, @Nothing@ otherwise.------ Not interruptible, but this shouldn't block for long as the server is--- supposed to respond immediately. tryWaitOnSemaphore :: HasCallStack => ClientSemaphore -> IO (Maybe SemaphoreToken) tryWaitOnSemaphore sem =-  MC.mask_ $ do+  mask_ $ do     fd <- connectDomainSocket (semSocketPath sem)-    resp <- flip MC.onException (closeFd fd) $ do+    resp <- flip onException (closeFd fd) $ do       fdWriteByte fd CmdTryWait       fdReadByte fd     case resp of       RspOk -> Just <$> mkToken fd-      _     -> do void $ MC.try @_ @IOException $ closeFd fd+      _     -> do void $ try @IOException $ closeFd fd                   return Nothing  mkToken :: Fd -> IO SemaphoreToken@@ -207,25 +192,19 @@       Nothing  -> return ()       -- already released       -- Closing the fd triggers the server's disconnect handling,       -- which returns the token to the pool.-      Just fd' -> void $ MC.try @_ @IOException $ closeFd fd'+      Just fd' -> void $ try @IOException $ closeFd fd'   return (SemaphoreToken fdVar) --- | Release a semaphore token, returning it to the pool.------ Sends a release command on the token's connection, then closes it.--- Idempotent: a second call on the same token is a safe no-op.------ Not interruptible; only returns when the release has succeeded. releaseSemaphoreToken :: HasCallStack => SemaphoreToken -> IO () releaseSemaphoreToken (SemaphoreToken fdVar) =-  MC.mask_ $ do+  mask_ $ do     mb <- tryTakeMVar fdVar     case mb of       Nothing -> return ()  -- already released       Just fd -> do         resp <- (do fdWriteByte fd CmdRelease                     fdReadByte fd-                ) `MC.finally` (void $ MC.try @_ @IOException $ closeFd fd)+                ) `finally` (void $ try @IOException $ closeFd fd)         case resp of           RspOk   -> return ()           -- myCount <= 0 on the server means the token is effectively@@ -233,19 +212,12 @@           RspFail -> return ()           _       -> fail "semaphore-compat: unexpected response in releaseSemaphoreToken" --- | Destroy a client-side semaphore.--- -- On POSIX this is a no-op: 'ClientSemaphore' holds no live connection. destroyClientSemaphore :: ClientSemaphore -> IO () destroyClientSemaphore _ = return () --- | Destroy a server-side semaphore.------ Idempotent. Subsequent calls after the first one are no-ops--- Not interruptible. Only returns when the server and all resources--- have been cleaned up destroyServerSemaphore :: ServerSemaphore -> IO ()-destroyServerSemaphore server = MC.uninterruptibleMask_ $ do+destroyServerSemaphore server = uninterruptibleMask_ $ do   -- we justify the uninterruptibleMask_ here by analysis of the server, which must exit   -- in a bounded amount of time once it receives the cancel signal on serverCancelFd   -- It has a bounded number of child threads it needs to cleanup before returning,@@ -259,189 +231,17 @@     Just ServerState{..} -> do       let path = semSocketPath $ serverClientSemaphore server       -- Signal the server loop to exit pollAcceptSocket, then wait for it.-      void $ MC.try @_ @IOException $ fdWriteByte serverCancelFd 0-      void $ MC.try @_ @IOException $ closeFd serverCancelFd+      void $ try @IOException $ fdWriteByte serverCancelFd 0+      void $ try @IOException $ closeFd serverCancelFd       killThread (serverThreadId server)-      void $ MC.try @_ @IOException $ closeFd serverListenFd-      void $ MC.try @_ @IOException $ removeLink path+      void $ try @IOException $ closeFd serverListenFd+      void $ try @IOException $ removeLink path --- | Query the current semaphore value (how many tokens it has available).------ This is mainly for debugging use, as it is easy to introduce race conditions--- when nontrivial program logic depends on the value returned by this function. getSemaphoreValue :: ServerSemaphore -> IO Int getSemaphoreValue server = readTVarIO (serverPool server) -getTimeSeed :: IO Int-getTimeSeed = do+get_time_seed :: IO Int+get_time_seed = do   ns <- getMonotonicTimeNSec   pid <- getProcessID   return $ fromIntegral ns `xor` fromIntegral pid-------------------------------------------- Server (Unix domain socket)------ The server manages a shared token pool (TVar Int) and accepts multiple--- client connections, each served on its own thread.------ Protocol (SOCK_STREAM, one byte per command):------   "-"  Wait (blocking acquire).  Decrements semaphore; replies ".".---   "?"  Try-wait.  Decrements if positive and replies "."; otherwise replies "!".---   "+"  Release.  Increments pool; replies ".".  Rejected with "!" if---        myCount <= 0 (client has not acquired any tokens on this connection).------ Unrecognised bytes are rejected with RspFail.------ Per-connection token tracking: the server counts tokens held by each--- connection (myCount).  On disconnect (EOF / ResourceVanished) held--- tokens are returned to the pool, so a crashing client cannot leak--- tokens.------ Connections are tied to SemaphoreToken, so each token (obtained by waitOnSemaphore)--- has its own connection---------------------------------------------- Protocol byte constants--pattern CmdWait, CmdTryWait, CmdRelease :: Word8-pattern CmdWait    = 0x2D -- '-'-pattern CmdTryWait = 0x3F -- '?'-pattern CmdRelease = 0x2B -- '+'--pattern RspOk, RspFail :: Word8-pattern RspOk   = 0x2E -- '.'-pattern RspFail = 0x21 -- '!'---- | Children of the server, to be cleaned up--- childFdLock is full when the 'Fd' is still known to be valid,--- and subject to being shutdown or closed-data Child = Child-  { childThread :: !(MVar ThreadId)-  , childFdLock :: !(MVar Fd)-  }--serverLoop :: TVar Int -> Fd -> Fd -> IO ()-serverLoop pool listenFd cancelFd = do-    children <- newTVarIO ([] :: [Child])-    loop children `MC.finally` killChildren children-  where-    loop children = do-      continueLoop <- MC.mask_ $ do-        r <- acceptWithRetry-        case r of-          AcceptCancelled     -> return False-          AcceptedFd clientFd -> do-            forkServeChild children clientFd-            return True-      when continueLoop $ loop children--    -- pollAcceptSocket only returns if the accept succeeded, or cancellation-    -- was signalled via the cancel pipe.-    acceptWithRetry :: IO AcceptResult-    acceptWithRetry = pollAcceptSocket listenFd cancelFd `MC.catch` handleIOError--    -- Retry accept on transient errors.-    handleIOError :: IOException -> IO AcceptResult-    handleIOError e-      -- 'isFullError' catches ResourceExhausted (EMFILE/ENFILE/ENOBUFS/ENOMEM and more that accept doesn't produce but are harmless to retry).-      | isFullError e                             = acceptWithRetry-      -- ECONNABORTED is also transient but categorised as OtherError, we additionaly match that.-      | Just err <- ioe_errno e-      , Errno err == eCONNABORTED                 = acceptWithRetry-      -- EINTR is absorbed in hs_poll_accept.-      -- everything else (EBADF, EINVAL, ENOTSOCK, ...) is rethrown.-      | otherwise                                 = MC.throwM e--    forkServeChild children clientFd = do-      fdLock <- newMVar clientFd-      tidVar <- newEmptyMVar-      let child = Child tidVar fdLock-      atomically $ modifyTVar' children (child :)-      childTid <- forkIOWithUnmask $ \unmask ->-        serve unmask pool children clientFd child-      putMVar tidVar childTid--    -- Interrupt all children blocked on a read from the FD, then kill them.-    killChildren :: TVar [Child] -> IO ()-    killChildren children = do-      kids <- readTVarIO children-      forM_ kids $ \child -> do-        -- If the child is in a read(), interrupt it-        -- by calling 'fdShutdown'.-        mb <- tryTakeMVar (childFdLock child)-        case mb of-          Just cfd -> do-            void $ MC.try @_ @IOException $ fdShutdown cfd-            putMVar (childFdLock child) cfd-          Nothing  -> return ()  -- serve thread already closing-      -- No children blocked on read: terminate them all.-      -- childThread was filled by the parent before mask exit; readMVar of-      -- a definitely-full MVar is non-interruptible.-      forM_ kids $ \child -> do-        tid <- readMVar (childThread child)-        killThread tid---- | Per-connection server loop.-serve :: (forall a. IO a -> IO a)-      -> TVar Int -> TVar [Child] -> Fd -> Child-      -> IO ()-serve restore pool children fd (Child _ fdLock) = do-    myCount <- newTVarIO (0 :: Int)-    let loop = forever $ MC.mask $ \restoreInner -> do-            -- fdReadByte is a safe-FFI read(2), interrupted by-            -- fdShutdown from killChildren, not by throwTo.-            msg <- fdReadByte fd-            case msg of-              CmdWait -> do-                -- Block until a token is available.-                -- restoreInner keeps retry interruptible under mask.-                restoreInner $ atomically $ do-                    n <- readTVar pool-                    when (n <= 0) retry-                    writeTVar pool (n - 1)-                    modifyTVar' myCount (+ 1)-                fdWriteByte fd RspOk--              CmdRelease -> do-                ok <- atomically $ do-                    mc <- readTVar myCount-                    if mc > 0-                      then do-                        modifyTVar' pool (+ 1)-                        modifyTVar' myCount (subtract 1)-                        return True-                      else return False-                fdWriteByte fd (if ok then RspOk else RspFail)--              CmdTryWait -> do-                acquired <- atomically $ do-                    n <- readTVar pool-                    if n > 0-                      then do-                        writeTVar pool (n - 1)-                        modifyTVar' myCount (+ 1)-                        return True-                      else return False-                if acquired-                  then fdWriteByte fd RspOk-                  else fdWriteByte fd RspFail--              -- Unknown command: reply so the client doesn't hang in read().-              _ -> fdWriteByte fd RspFail--        cleanup = do-          -- Return tokens to the pool and remove from children list.-          atomically $ do-            n <- readTVar myCount-            when (n > 0) $ modifyTVar' pool (+ n)-            modifyTVar' children (filter (\c -> childFdLock c /= fdLock))-          -- Take fd ownership and close.-          -- prevents killChildren from double closing fd-          void $ takeMVar fdLock-          void $ MC.try @_ @IOException $ closeFd fd--    -- restore so thread can be killed in between loop iterations-    -- Catch IOException (EOF/disconnect) silently.-    (restore loop `MC.catch` \(_ :: IOException) -> return ())-      `MC.finally` cleanup
+ src/System/Semaphore/Internal/Posix/Server.hs view
@@ -0,0 +1,214 @@+{-# LANGUAGE PatternSynonyms #-}+{-# LANGUAGE RankNTypes #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE TypeApplications #-}++-- | The Unix domain socket server backing the POSIX semaphore implementation.+--+-- The server manages a shared token pool and accepts multiple client+-- connections, each served on its own thread.+--+-- Protocol (SOCK_STREAM, one byte per command):+--+--   "-"  Wait (blocking acquire).  Decrements semaphore; replies ".".+--   "?"  Try-wait.  Decrements if positive and replies "."; otherwise replies "!".+--   "+"  Release.  Increments pool; replies ".".  Rejected with "!" if+--        myCount <= 0 (client has not acquired any tokens on this connection).+--+-- Unrecognised bytes are rejected with 'RspFail'.+--+-- Per-connection token tracking: the server counts tokens held by each+-- connection (myCount).  On disconnect (EOF / ResourceVanished) held+-- tokens are returned to the pool, so a crashing client cannot leak+-- tokens.+--+-- Connections are tied to a token, so each token (obtained by+-- @waitOnSemaphore@) has its own connection.+module System.Semaphore.Internal.Posix.Server+  ( serverLoop+  , pattern CmdWait, pattern CmdTryWait, pattern CmdRelease+  , pattern RspOk, pattern RspFail+  ) where++-- base+import Control.Concurrent+  ( ThreadId, forkIOWithUnmask, killThread )+import Control.Concurrent.MVar+  ( MVar, newEmptyMVar, newMVar, putMVar+  , readMVar, takeMVar, tryTakeMVar )+import Control.Exception+  ( IOException, SomeException+  , catch, finally, mask, mask_+  , onException, throw, try, uninterruptibleMask_+  )+import Control.Monad+  ( forM_, forever, void, when )+import Data.Word ( Word8 )+import Foreign.C.Error ( Errno(Errno), eCONNABORTED )+import GHC.IO.Exception ( ioe_errno )+import System.IO.Error ( isFullError )++-- stm+import Control.Concurrent.STM+  ( TVar, atomically, newTVarIO, readTVar, readTVarIO+  , modifyTVar', writeTVar, retry )++-- unix+import System.Posix.IO ( closeFd )+import System.Posix.Types ( Fd )++import System.Semaphore.Internal.DomainSocket+  ( pollAcceptSocket, AcceptResult(..)+  , fdReadByte, fdWriteByte+  , fdShutdown )++---------------------------------------+-- Protocol byte constants++pattern CmdWait, CmdTryWait, CmdRelease :: Word8+pattern CmdWait    = 0x2D -- '-'+pattern CmdTryWait = 0x3F -- '?'+pattern CmdRelease = 0x2B -- '+'++pattern RspOk, RspFail :: Word8+pattern RspOk   = 0x2E -- '.'+pattern RspFail = 0x21 -- '!'++-- | Children of the server, to be cleaned up+-- childFdLock is full when the 'Fd' is still known to be valid,+-- and subject to being shutdown or closed+data Child = Child+  { childThread :: !(MVar ThreadId)+  , childFdLock :: !(MVar Fd)+  }++-- | Run the server accept loop on a listening socket until cancellation is+-- signalled via the cancel pipe.  All accepted connections are served on+-- child threads, which are cleaned up before this returns.+serverLoop :: TVar Int -- ^ shared token pool+           -> Fd        -- ^ listening socket+           -> Fd        -- ^ read end of the cancel pipe+           -> IO ()+serverLoop pool listenFd cancelFd = do+    children <- newTVarIO ([] :: [Child])+    loop children `finally` killChildren children+  where+    loop children = do+      continueLoop <- mask_ $ do+        r <- acceptWithRetry+        case r of+          AcceptCancelled     -> return False+          AcceptedFd clientFd -> do+            forkServeChild children clientFd+            return True+      when continueLoop $ loop children++    -- pollAcceptSocket only returns if the accept succeeded, or cancellation+    -- was signalled via the cancel pipe.+    acceptWithRetry :: IO AcceptResult+    acceptWithRetry = pollAcceptSocket listenFd cancelFd `catch` handleIOError++    -- Retry accept on transient errors.+    handleIOError :: IOException -> IO AcceptResult+    handleIOError e+      -- 'isFullError' catches ResourceExhausted (EMFILE/ENFILE/ENOBUFS/ENOMEM and more that accept doesn't produce but are harmless to retry).+      | isFullError e                             = acceptWithRetry+      -- ECONNABORTED is also transient but categorised as OtherError, we additionaly match that.+      | Just err <- ioe_errno e+      , Errno err == eCONNABORTED                 = acceptWithRetry+      -- EINTR is absorbed in hs_poll_accept.+      -- everything else (EBADF, EINVAL, ENOTSOCK, ...) is rethrown.+      | otherwise                                 = throw e++    forkServeChild children clientFd = do+      fdLock <- newMVar clientFd+      tidVar <- newEmptyMVar+      let child = Child tidVar fdLock+      atomically $ modifyTVar' children (child :)+      childTid <- forkIOWithUnmask $ \unmask ->+        serve unmask pool children clientFd child+      putMVar tidVar childTid++    -- Interrupt all children blocked on a read from the FD, then kill them.+    killChildren :: TVar [Child] -> IO ()+    killChildren children = do+      kids <- readTVarIO children+      forM_ kids $ \child -> do+        -- If the child is in a read(), interrupt it+        -- by calling 'fdShutdown'.+        mb <- tryTakeMVar (childFdLock child)+        case mb of+          Just cfd -> do+            void $ try @IOException $ fdShutdown cfd+            putMVar (childFdLock child) cfd+          Nothing  -> return ()  -- serve thread already closing+      -- No children blocked on read: terminate them all.+      -- childThread was filled by the parent before mask exit; readMVar of+      -- a definitely-full MVar is non-interruptible.+      forM_ kids $ \child -> do+        tid <- readMVar (childThread child)+        killThread tid++-- | Per-connection server loop.+serve :: (forall a. IO a -> IO a)+      -> TVar Int -> TVar [Child] -> Fd -> Child+      -> IO ()+serve restore pool children fd (Child _ fdLock) = do+    myCount <- newTVarIO (0 :: Int)+    let loop = forever $ mask $ \restoreInner -> do+            -- fdReadByte is a safe-FFI read(2), interrupted by+            -- fdShutdown from killChildren, not by throwTo.+            msg <- fdReadByte fd+            case msg of+              CmdWait -> do+                -- Block until a token is available.+                -- restoreInner keeps retry interruptible under mask.+                restoreInner $ atomically $ do+                    n <- readTVar pool+                    when (n <= 0) retry+                    writeTVar pool (n - 1)+                    modifyTVar' myCount (+ 1)+                fdWriteByte fd RspOk++              CmdRelease -> do+                ok <- atomically $ do+                    mc <- readTVar myCount+                    if mc > 0+                      then do+                        modifyTVar' pool (+ 1)+                        modifyTVar' myCount (subtract 1)+                        return True+                      else return False+                fdWriteByte fd (if ok then RspOk else RspFail)++              CmdTryWait -> do+                acquired <- atomically $ do+                    n <- readTVar pool+                    if n > 0+                      then do+                        writeTVar pool (n - 1)+                        modifyTVar' myCount (+ 1)+                        return True+                      else return False+                if acquired+                  then fdWriteByte fd RspOk+                  else fdWriteByte fd RspFail++              -- Unknown command: reply so the client doesn't hang in read().+              _ -> fdWriteByte fd RspFail++        cleanup = do+          -- Return tokens to the pool and remove from children list.+          atomically $ do+            n <- readTVar myCount+            when (n > 0) $ modifyTVar' pool (+ n)+            modifyTVar' children (filter (\c -> childFdLock c /= fdLock))+          -- Take fd ownership and close.+          -- prevents killChildren from double closing fd+          void $ takeMVar fdLock+          void $ try @IOException $ closeFd fd++    -- restore so thread can be killed in between loop iterations+    -- Catch IOException (EOF/disconnect) silently.+    (restore loop `catch` \(_ :: IOException) -> return ())+      `finally` cleanup
src/System/Semaphore/Internal/Unsupported.hs view
@@ -5,22 +5,27 @@ -- | Stub backend for wasm and targets without POSIX/Win32 sockets/semaphores. module System.Semaphore.Internal.Unsupported   ( ClientSemaphore, ServerSemaphore-  , clientSemaphoreName, serverClientSemaphore   , SemaphoreToken-  , create_sem, open_sem_raw+  , clientSemaphoreName, serverClientSemaphore   , waitOnSemaphore, tryWaitOnSemaphore   , releaseSemaphoreToken   , destroyClientSemaphore, destroyServerSemaphore   , getSemaphoreValue-  , getTimeSeed++  , create_sem, open_sem_raw+  , get_time_seed   ) where +-- base import Control.Exception ( IOException ) import GHC.IO.Exception ( unsupportedOperation ) import System.IO.Error ( ioeSetLocation ) -import System.Semaphore.Internal.Common+-- semaphore-compat+import System.Semaphore.Internal.Version +--------------------------------------------------------------------------------+ data ClientSemaphore  data ServerSemaphore@@ -62,6 +67,5 @@ getSemaphoreValue :: ServerSemaphore -> IO Int getSemaphoreValue = \case {} -getTimeSeed :: IO Int-getTimeSeed =-  ioError $ unsupported "getTimeSeed"+get_time_seed :: IO Int+get_time_seed = ioError $ unsupported "get_time_seed"
+ src/System/Semaphore/Internal/Version.hs view
@@ -0,0 +1,153 @@+{-# LANGUAGE BangPatterns #-}+{-# LANGUAGE CPP #-}+{-# LANGUAGE MagicHash #-}++-- | Semaphore protocol version and versioned semaphore identifiers.+module System.Semaphore.Internal.Version+  ( SemaphoreName(..)+  , SemaphoreIdentifier+  , SemaphoreProtocolVersion(..)+  , SemaphoreError(..)+  , semaphoreVersion+  , semaphoreIdentifier+  , parseSemaphoreIdentifier+  , getSemaphoreSocketPath+  , iToBase62+  ) where++-- base+import Control.Exception ( Exception, IOException )+import GHC.Exts ( Char(..), Int(..), indexCharOffAddr# )++-- directory+import System.Directory ( getTemporaryDirectory, createDirectoryIfMissing )++-- filepath+import System.FilePath ( (</>) )++---------------------------------------+-- Types++-- | The semaphore protocol version currently being used.+--+-- The version tracks the IPC mechanism, not the library version:+--+--   * __POSIX:__ 2 (domain sockets, replacing v1 system semaphores).+--   * __Windows:__ 1 (Win32 named semaphores, unchanged from v1).+--   * __Unsupported platforms:__ 0 (no compatible IPC backend).+newtype SemaphoreProtocolVersion =+  SemaphoreProtocolVersion { getSemaphoreProtocolVersion :: Int }+  deriving (Eq, Ord, Show)++-- | A semaphore name: a protocol version and an unversioned name string.+data SemaphoreName = SemaphoreName+  { semaphoreProtocolVersion :: !SemaphoreProtocolVersion+  , unversionedSemaphoreNameString :: !String+  } deriving (Eq, Show)++-- | The identifier string of a semaphore: a serialised 'SemaphoreName' used+-- for inter-process communication (e.g. as a command line argument).+--+-- The specific format of this name string depends on the __protocol__ version:+--+--   - For version 1, this is the unadorned semaphore name.+--   - For version @N >= 2@, the semaphore name is prefixed with @v\<N\>-@.+type SemaphoreIdentifier = String++-- | Errors that can occur when creating or opening a semaphore.+data SemaphoreError+  -- | Can't create a semaphore: a semaphore with this name already exists.+  = SemaphoreAlreadyExists+    { semaphoreErrorIdentifier :: !SemaphoreIdentifier+    }+  -- | Can't open a semaphore: no semaphore with this name exists.+  | SemaphoreDoesNotExist+    { semaphoreErrorIdentifier :: !SemaphoreIdentifier+    }+  -- | Protocol version incompatibility: the semaphore identifier uses a+  -- different protocol version than the library is currently using.+  | SemaphoreIncompatibleVersion+    { semaphoreErrorActualVersion   :: !SemaphoreProtocolVersion+    , semaphoreErrorExpectedVersion :: !SemaphoreProtocolVersion+    }+  -- | An 'IOException' was raised when creating or opening the semaphore.+  | SemaphoreOtherError+    { semaphoreOtherError :: !IOException+    }+  deriving (Show, Eq)++instance Exception SemaphoreError++---------------------------------------+-- Version negotiation++-- | The protocol version used on this host platform with this version of+-- @semaphore-compat@.+--+-- See 'SemaphoreProtocolVersion'.+semaphoreVersion :: SemaphoreProtocolVersion+#if defined(mingw32_HOST_OS)+semaphoreVersion = SemaphoreProtocolVersion 1+#elif defined(wasm32_HOST_ARCH) || defined(javascript_HOST_ARCH)+semaphoreVersion = SemaphoreProtocolVersion 0+#else+semaphoreVersion = SemaphoreProtocolVersion 2+#endif+++-- | The serialised identifier of a 'SemaphoreName' for inter-process+-- communication.+--+-- See 'SemaphoreIdentifier' for more information.+semaphoreIdentifier :: SemaphoreName -> SemaphoreIdentifier+semaphoreIdentifier sn+  | ver <= 1  = unversionedSemaphoreNameString sn+  | otherwise = "v" ++ show ver ++ "-" ++ unversionedSemaphoreNameString sn+  where+    ver = getSemaphoreProtocolVersion (semaphoreProtocolVersion sn)++++-- TODO: I think 'parseSemaphoreIdentifier' is unused (neither Cabal nor GHC use it).++-- | Parse a 'SemaphoreIdentifier' into a 'SemaphoreName'.+--+-- Returns @Nothing@ for unversioned strings (which should be treated as+-- v1 by the caller's compatibility logic).+parseSemaphoreIdentifier :: SemaphoreIdentifier -> Maybe SemaphoreName+parseSemaphoreIdentifier ('v':rest) =+  case reads rest of+    [(n, '-':name)] -> Just (SemaphoreName (SemaphoreProtocolVersion n) name)+    _               -> Nothing+parseSemaphoreIdentifier _ = Nothing++---------------------------------------+-- Utilities++-- | Convert an 'Int' to a base-62 string (digits @0@–@9@, @a@–@z@, @A@–@Z@).+iToBase62 :: Int -> String+iToBase62 m = go m' ""+  where+    m'+      | m == minBound = maxBound+      | otherwise     = abs m+    go n cs | n < 62+            = let !c = chooseChar62 n+              in c : cs+            | otherwise+            = let !(!q, r) = quotRem n 62+                  !c       = chooseChar62 r+              in go q (c : cs)++    chooseChar62 :: Int -> Char+    {-# INLINE chooseChar62 #-}+    chooseChar62 (I# n) = C# (indexCharOffAddr# chars62 n)+    chars62 = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"#++-- | The socket file path for a semaphore.+getSemaphoreSocketPath :: SemaphoreName -> IO FilePath+getSemaphoreSocketPath sn = do+  tmp <- getTemporaryDirectory+  let dir = tmp </> "semaphore-compat"+  createDirectoryIfMissing True dir+  return (dir </> unversionedSemaphoreNameString sn)
src/System/Semaphore/Internal/Win32.hs view
@@ -1,26 +1,31 @@ {-# LANGUAGE TypeApplications #-} +-- | Windows backend: semaphores implemented on top of Win32 named semaphores. module System.Semaphore.Internal.Win32   ( ClientSemaphore(..), ServerSemaphore(..)   , SemaphoreToken(..)-  , create_sem, open_sem_raw   , waitOnSemaphore, tryWaitOnSemaphore   , releaseSemaphoreToken   , destroyClientSemaphore, destroyServerSemaphore   , getSemaphoreValue-  , getTimeSeed++  , create_sem, open_sem_raw+  , get_time_seed   ) where +-- base import Control.Concurrent   ( forkIO, killThread ) import Control.Concurrent.MVar   ( MVar, newEmptyMVar, newMVar, putMVar, takeMVar, tryReadMVar, tryTakeMVar )-import Control.Exception ( IOException, uninterruptibleMask_ )+import Control.Exception+  ( IOException, SomeException+  , finally, mask_, onException, throw, try, uninterruptibleMask_+  ) import Control.Monad import System.IO.Error ( isDoesNotExistError ) -import qualified Control.Monad.Catch as MC-+-- win32 import qualified System.Win32.Event     as Win32   ( createEvent, setEvent   , waitForSingleObject, waitForMultipleObjects@@ -35,33 +40,31 @@ import qualified System.Win32.Time      as Win32   ( FILETIME(..), getSystemTimeAsFileTime ) -import System.Semaphore.Internal.Common+-- semaphore-compat+import System.Semaphore.Internal.Version --- | A client-side semaphore (semaphore for a jobclient).+--------------------------------------------------------------------------------+ data ClientSemaphore =   ClientSemaphore     { clientSemaphoreName :: !SemaphoreName     , semaphore           :: !(MVar Win32.Semaphore)-      -- ^ MVar is emptied when the handle is closed to prevent double close.+      -- NB: the MVar is emptied when the handle is closed to prevent double close.     } --- | A held semaphore token. newtype SemaphoreToken = SemaphoreToken-  { tokenSemaphore :: Win32.Semaphore+  { tokenSemaphore :: MVar Win32.Semaphore+    -- NB: the MVar is emptied when the token is released to prevent double release.   } --- | A server-side semaphore: a 'ClientSemaphore' with additional--- book-keeping to keep track of resource usage by all clients.------ Use 'serverClientSemaphore' to retrieve the client-side semaphore. newtype ServerSemaphore = ServerSemaphore   { serverClientSemaphore :: ClientSemaphore   }  create_sem :: SemaphoreName -> Int -> IO (Either SemaphoreError ServerSemaphore)-create_sem sem_nm init_toks = MC.mask_ $ do+create_sem sem_nm init_toks = mask_ $ do   let toks = fromIntegral init_toks-  mb_sem <- MC.try @_ @IOException $+  mb_sem <- try @IOException $     Win32.createSemaphore Nothing toks 0x7FFFFFFF (Just (semaphoreIdentifier sem_nm))   case mb_sem of     Left err ->@@ -69,7 +72,7 @@     Right (sem, True) -> do       -- Semaphore already existed; createSemaphore still returned a valid       -- handle to it, which we must close to avoid leaking it.-      void $ MC.try @_ @IOException $ Win32.closeHandle (Win32.semaphoreHandle sem)+      void $ try @IOException $ Win32.closeHandle (Win32.semaphoreHandle sem)       return $ Left $ SemaphoreAlreadyExists (semaphoreIdentifier sem_nm)     Right (sem, False) -> do       semVar <- newMVar sem@@ -78,12 +81,13 @@           { serverClientSemaphore =               ClientSemaphore                 { clientSemaphoreName = sem_nm-                , semaphore     = semVar }+                , semaphore           = semVar+                }           }  open_sem_raw :: SemaphoreName -> IO (Either SemaphoreError ClientSemaphore)-open_sem_raw nm = MC.mask_ $ do-  mb_res <- MC.try @_ @IOException $+open_sem_raw nm = mask_ $ do+  mb_res <- try @IOException $     Win32.openSemaphore Win32.sEMAPHORE_ALL_ACCESS True (semaphoreIdentifier nm)   case mb_res of     Left  err@@ -96,9 +100,10 @@       return $ Right $         ClientSemaphore           { clientSemaphoreName = nm-          , semaphore     = semVar }+          , semaphore           = semVar+          } --- | Read the kernel handle from a 'ClientSemaphore'.  Throws if the+-- Read the kernel handle from a 'ClientSemaphore'.  Throws if the -- client has been destroyed. readClientHandle :: ClientSemaphore -> String -> IO Win32.Semaphore readClientHandle client opName = do@@ -108,20 +113,14 @@     Nothing  -> ioError $ userError $       "semaphore-compat." ++ opName ++ ": semaphore has been destroyed" --- | Acquire a token from the semaphore, blocking until one is available.------ This operation is interruptible. If interrupted after the token was acquired,--- the token is automatically released back to the pool.------ Returns a 'SemaphoreToken' that must be released with 'releaseSemaphoreToken'. waitOnSemaphore :: ClientSemaphore -> IO SemaphoreToken waitOnSemaphore client = do   resultVar <- newEmptyMVar-  MC.mask_ $ do+  mask_ $ do     sem <- readClientHandle client "waitOnSemaphore"     cancelEvent <- Win32.createEvent Nothing True False ""-    workerTid <- (forkIO $ MC.mask_ $ do-      res <- MC.try @_ @MC.SomeException $ do+    workerTid <- (forkIO $ mask_ $ do+      res <- try @SomeException $ do         wait_res <-           Win32.waitForMultipleObjects             [ Win32.semaphoreHandle sem@@ -129,13 +128,13 @@             False -- WaitAny             Win32.iNFINITE         if wait_res == Win32.wAIT_OBJECT_0-          then return (SemaphoreToken sem)+          then SemaphoreToken <$> newMVar sem           else ioError (userError "semaphore wait cancelled")       putMVar resultVar res-      ) `MC.onException` Win32.closeHandle cancelEvent+      ) `onException` Win32.closeHandle cancelEvent     let cleanup =           -- Signal the cancel event and wait for the worker to exit.-          (do ok <- MC.try @_ @IOException $ Win32.setEvent cancelEvent+          (do ok <- try @IOException $ Win32.setEvent cancelEvent               case ok of                 Right () ->                   -- Cancel event signaled. Worker's WaitForMultipleObjects@@ -146,43 +145,39 @@                   -- The worker is in a safe ffi call so killThread                   -- will just block forever.                   return ()-          ) `MC.finally` (do+          ) `finally` (do               -- Recover any transiently acquired token.  If the worker               -- acquired the semaphore before seeing the cancel event,               -- release it back to the pool.               mbRes <- tryTakeMVar resultVar               case mbRes of                 Just (Right tok) ->-                  void $ MC.try @_ @MC.SomeException $ releaseSemaphoreToken tok+                  void $ try @SomeException $ releaseSemaphoreToken tok                 _ -> return ()               Win32.closeHandle cancelEvent)-    res <- takeMVar resultVar `MC.onException` cleanup+    res <- takeMVar resultVar `onException` cleanup     Win32.closeHandle cancelEvent     case res of       Right tok -> return tok-      Left e    -> MC.throwM e+      Left e    -> throw e --- | Try to acquire a token from the semaphore without blocking.------ Returns @Just token@ if a token was available, @Nothing@ otherwise. tryWaitOnSemaphore :: ClientSemaphore -> IO (Maybe SemaphoreToken) tryWaitOnSemaphore client =-  MC.mask_ $ do+  mask_ $ do     sem <- readClientHandle client "tryWaitOnSemaphore"     wait_res <- Win32.waitForSingleObject (Win32.semaphoreHandle sem) 0     if wait_res == Win32.wAIT_OBJECT_0-      then return (Just (SemaphoreToken sem))+      then Just . SemaphoreToken <$> newMVar sem       else return Nothing --- | Release a semaphore token, returning it to the pool. releaseSemaphoreToken :: SemaphoreToken -> IO ()-releaseSemaphoreToken (SemaphoreToken sem) =-  MC.mask_ $ do-    void $ Win32.releaseSemaphore sem 1+releaseSemaphoreToken (SemaphoreToken v) =+  mask_ $ do+    mb <- tryTakeMVar v+    case mb of+      Nothing  -> return ()+      Just sem -> void $ Win32.releaseSemaphore sem 1 --- | Destroy a client-side semaphore.------ Idempotent: subsequent calls after the first are no-ops. destroyClientSemaphore :: ClientSemaphore -> IO () destroyClientSemaphore client = uninterruptibleMask_ $ do   mb <- tryTakeMVar (semaphore client)@@ -190,20 +185,13 @@     Nothing  -> return ()  -- already destroyed     Just sem -> Win32.closeHandle (Win32.semaphoreHandle sem) --- | Destroy a server-side semaphore.------ Idempotent: subsequent calls after the first are no-ops. destroyServerSemaphore :: ServerSemaphore -> IO () destroyServerSemaphore server =   destroyClientSemaphore (serverClientSemaphore server) --- | Query the current semaphore value (how many tokens it has available).------ This is mainly for debugging use, as it is easy to introduce race conditions--- when nontrivial program logic depends on the value returned by this function. getSemaphoreValue :: ServerSemaphore -> IO Int getSemaphoreValue server =-  MC.mask_ $ do+  mask_ $ do     sem <- readClientHandle (serverClientSemaphore server) "getSemaphoreValue"     wait_res <- Win32.waitForSingleObject (Win32.semaphoreHandle sem) 0     if wait_res == Win32.wAIT_OBJECT_0@@ -212,7 +200,7 @@     else       return 0 -getTimeSeed :: IO Int-getTimeSeed = do+get_time_seed :: IO Int+get_time_seed = do   Win32.FILETIME t <- Win32.getSystemTimeAsFileTime   return (fromIntegral t)
test-common/System/Semaphore/TestHelper.hs view
@@ -210,19 +210,20 @@ data HelperHandle = HelperHandle     { hStdin   :: Handle     , hStdout  :: Handle+    , hStderr  :: Handle     , hProcess :: ProcessHandle     }  startHelper :: IO HelperHandle startHelper = do-    (Just hin, Just hout, _, ph) <- createProcess (proc "semaphore-helper" [])+    (Just hin, Just hout, Just herr, ph) <- createProcess (proc "semaphore-helper" [])         { std_in  = CreatePipe         , std_out = CreatePipe         , std_err = CreatePipe         }     hSetBuffering hin LineBuffering     hSetBuffering hout LineBuffering-    return (HelperHandle hin hout ph)+    return (HelperHandle hin hout herr ph)  cleanupHelper :: HelperHandle -> IO () cleanupHelper h = do@@ -233,6 +234,7 @@         return ()     _ <- try (hClose (hStdin h)) :: IO (Either SomeException ())     _ <- try (hClose (hStdout h)) :: IO (Either SomeException ())+    _ <- try (hClose (hStderr h)) :: IO (Either SomeException ())     return ()  withHelper :: (HelperHandle -> IO a) -> IO a
test/Main.hs view
@@ -25,17 +25,16 @@ import Test.Tasty.HUnit  import System.Semaphore-import qualified System.Semaphore.Internal.Common as Internal+import qualified System.Semaphore.Internal.Version as Internal import System.Semaphore.TestHelper +import System.Semaphore.Internal ( SemaphoreToken(..) ) #if !defined(mingw32_HOST_OS) import Control.Concurrent.MVar ( tryTakeMVar )-import System.Semaphore.Internal.Posix ( SemaphoreToken(tokenFdLock) )+import System.Semaphore.Internal.Posix ( tokenFdLock ) import System.Semaphore.Internal.DomainSocket   ( connectDomainSocket, fdReadByte, fdWriteByte ) import System.Posix.IO ( closeFd )-#else-import System.Semaphore.Internal.Win32 ( SemaphoreToken(..) ) #endif  -- | Simulate a crash: close the token's underlying connection without@@ -43,7 +42,7 @@ -- and return the token to the pool. crashToken :: SemaphoreToken -> IO () #if !defined(mingw32_HOST_OS)-crashToken tok = do+crashToken (SemaphoreToken tok) = do   mb <- tryTakeMVar (tokenFdLock tok)   case mb of     Just fd -> closeFd fd