packages feed

push-notify-apn 0.2.0.0 → 0.2.0.1

raw patch · 4 files changed

+89/−84 lines, 4 filesdep +lifted-basedep +mtldep +resource-poolPVP: major bump suggested

API removals or changes: PVP suggests a major version bump

Dependencies added: lifted-base, mtl, resource-pool

API changes (from Hackage documentation)

- Network.PushNotify.APN: instance Network.PushNotify.APN.SpecifyError Network.PushNotify.APN.ApnMessageResult
+ Network.PushNotify.APN: ApnMessageResultClientError :: ClientError -> ApnMessageResult
- Network.PushNotify.APN: newSession :: FilePath -> FilePath -> FilePath -> Bool -> Int -> ByteString -> IO ApnSession
+ Network.PushNotify.APN: newSession :: FilePath -> FilePath -> FilePath -> Bool -> Int -> Int -> ByteString -> IO ApnSession

Files

app/Main.hs view
@@ -70,7 +70,8 @@  send :: ApnOptions -> IO () send o = do-    let mkSession = newSession (keypath o) (certpath o) (capath o) (sandbox o) 10 (B8.pack $ topic o)+    let mkSession =+            newSession (keypath o) (certpath o) (capath o) (sandbox o) 10 1 (B8.pack $ topic o)     session <- mkSession     if interactive o     then
changelog.md view
@@ -1,3 +1,14 @@+0.2.0.1+=======++- Use Data.Pool++0.2.0.0+=======++- Threading bugfix+- Migrate to http2-client-0.9.0.0 and lts-14+ 0.1.1.1 ======= 
push-notify-apn.cabal view
@@ -1,5 +1,5 @@ name:                push-notify-apn-version:             0.2.0.0+version:             0.2.0.1 synopsis:            Send push notifications to mobile iOS devices description:     push-notify-apn is a library and command line utility that can be used to send@@ -12,7 +12,7 @@ license-file:        LICENSE author:              Hans-Christian Esperer maintainer:          hc@hcesperer.org-copyright:           2017-2018 Memrange UG+copyright:           2017-2020 Memrange UG category:            Network, Cloud, Mobile build-type:          Simple extra-source-files:  README.md@@ -31,7 +31,10 @@                      , data-default                      , http2                      , http2-client+                     , lifted-base+                     , mtl                      , random+                     , resource-pool                      , semigroups                      , text                      , time
src/Network/PushNotify/APN.hs view
@@ -10,6 +10,7 @@ {-# LANGUAGE DeriveGeneric     #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE RecordWildCards   #-}+{-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TupleSections     #-}  module Network.PushNotify.APN@@ -48,8 +49,9 @@  import           Control.Concurrent import           Control.Concurrent.QSem-import           Control.Exception+import           Control.Exception.Lifted (Exception, try, bracket_, throw, throwIO) import           Control.Monad+import           Control.Monad.Except import           Data.Aeson import           Data.Aeson.Types import           Data.ByteString                      (ByteString)@@ -60,8 +62,10 @@ import           Data.IORef import           Data.Map.Strict                      (Map) import           Data.Maybe+import           Data.Pool import           Data.Semigroup                       ((<>)) import           Data.Text                            (Text)+import           Data.Time.Clock import           Data.Time.Clock.POSIX import           Data.Typeable                        (Typeable) import           Data.X509@@ -91,10 +95,9 @@  -- | A session that manages connections to Apple's push notification service data ApnSession = ApnSession-    { apnSessionPool              :: !(IORef [ApnConnection])-    , apnSessionConnectionInfo    :: !ApnConnectionInfo-    , apnSessionConnectionManager :: !ThreadId-    , apnSessionOpen              :: !(IORef Bool)}+    { apnSessionPool :: !(Pool ApnConnection)+    , apnSessionOpen :: !(IORef Bool)+    }  -- | Information about an APN connection data ApnConnectionInfo = ApnConnectionInfo@@ -110,7 +113,6 @@     { apnConnectionConnection        :: !Http2Client     , apnConnectionInfo              :: !ApnConnectionInfo     , apnConnectionWorkerPool        :: !QSem-    , apnConnectionLastUsed          :: !Int64     , apnConnectionFlowControlWorker :: !ThreadId     , apnConnectionOpen              :: !(IORef Bool)} @@ -151,11 +153,9 @@                       | ApnMessageResultFatalError ApnFatalError                       | ApnMessageResultTemporaryError ApnTemporaryError                       | ApnMessageResultIOError IOError+                      | ApnMessageResultClientError ClientError     deriving (Eq, Show) -instance SpecifyError ApnMessageResult where-    isAnError = ApnMessageResultIOError- -- | The specification of a push notification's message body data JsonApsAlert = JsonApsAlert     { jaaTitle :: !(Maybe Text)@@ -377,21 +377,31 @@     -- ^ True if the apn development servers should be used, False to use the production servers     -> Int     -- ^ How many messages will be sent in parallel? This corresponds to the number of http2 streams open in parallel; 100 seems to be a default value.+    -> Int+    -- ^ How many connections to be opened at maximum.     -> ByteString     -- ^ Topic (bundle name of the app)     -> IO ApnSession     -- ^ The newly created session-newSession certKey certPath caPath dev maxparallel topic = do+newSession certKey certPath caPath dev maxparallel maxConnectionCount topic = do     let hostname = if dev             then "api.development.push.apple.com"             else "api.push.apple.com"         connInfo = ApnConnectionInfo certPath certKey caPath hostname maxparallel topic     certsOk <- checkCertificates connInfo     unless certsOk $ error "Unable to load certificates and/or the private key"-    connections <- newIORef []-    connectionManager <- forkIO $ manage 600 connections     isOpen <- newIORef True-    let session = ApnSession connections connInfo connectionManager isOpen++    let connectionUnusedTimeout :: NominalDiffTime+        connectionUnusedTimeout = 600+    pool <-+        createPool+            (newConnection connInfo) closeApnConnection 1 connectionUnusedTimeout maxConnectionCount+    let session =+            ApnSession+            { apnSessionPool = pool+            , apnSessionOpen = isOpen+            }     addFinalizer session $         closeSession session     return session@@ -406,41 +416,25 @@ closeSession s = do     isOpen <- atomicModifyIORef' (apnSessionOpen s) (False,)     unless isOpen $ error "Session is already closed"-    killThread (apnSessionConnectionManager s)-    let ioref = apnSessionPool s-    openConnections <- atomicModifyIORef' ioref ([],)-    mapM_ closeApnConnection openConnections+    destroyAllResources (apnSessionPool s)  -- | Check whether a session is open or has been closed -- by a call to closeSession isOpen :: ApnSession -> IO Bool isOpen = readIORef . apnSessionOpen -withConnection :: ApnSession -> (ApnConnection -> IO a) -> IO a+withConnection :: ApnSession -> (ApnConnection -> ClientIO a) -> ClientIO a withConnection s action = do-    ensureOpen s-    let pool = apnSessionPool s-    connections <- readIORef pool-    let len = length connections-    if len == 0-    then do-        conn <- newConnection s-        res <- action conn-        atomicModifyIORef' pool (\a -> (conn:a, ()))-        return res-    else do-        num <- randomRIO (0, len - 1)-        currtime <- round <$> getPOSIXTime :: IO Int64-        let conn = connections !! num-            conn1 = conn { apnConnectionLastUsed=currtime }-        atomicModifyIORef' pool (\a -> (removeNth num a, ()))-        isOpen <- readIORef (apnConnectionOpen conn)-        if isOpen-        then do-            res <- action conn1-            atomicModifyIORef' pool (\a -> (conn1:a, ()))-            return res-        else withConnection s action+    lift $ ensureOpen s+    ExceptT . try $+        withResource (apnSessionPool s) $ \conn -> do+        res <- runClientIO (action conn)+        case res of+          Left clientError ->+              -- When there is a clientError, we think that the connetion is broken.+              -- Throwing an exception is the way we inform the resource pool.+              throw clientError+          Right res -> return res  checkCertificates :: ApnConnectionInfo -> IO Bool checkCertificates aci = do@@ -448,26 +442,8 @@     credential <- credentialLoadX509 (aciCertPath aci) (aciCertKey aci)     return $ isJust castore && isRight credential -replaceNth n newVal (x:xs)-    | n == 0 = newVal:xs-    | otherwise = x:replaceNth (n-1) newVal xs--removeNth n (x:xs)-    | n == 0 = xs-    | otherwise = x:removeNth (n-1) xs--manage :: Int64 -> IORef [ApnConnection] -> IO ()-manage timeout ioref = forever $ do-    currtime <- round <$> getPOSIXTime :: IO Int64-    let minTime = currtime - timeout-    expiredOnes <- atomicModifyIORef' ioref-        (foldl ( \(a,b) i -> if apnConnectionLastUsed i < minTime then (a, i:b ) else ( i:a ,b)) ([],[]))-    mapM_ closeApnConnection expiredOnes-    threadDelay 60000000--newConnection :: ApnSession -> IO ApnConnection-newConnection apnSession = do-    let aci = apnSessionConnectionInfo apnSession+newConnection :: ApnConnectionInfo -> IO ApnConnection+newConnection aci = do     Just castore <- readCertificateStore $ aciCaPath aci     Right credential <- credentialLoadX509 (aciCertPath aci) (aciCertKey aci)     let credentials = Credentials [credential]@@ -496,27 +472,32 @@                ]          hostname = aciHostname aci-    httpFrameConnection <- newHttp2FrameConnection (T.unpack hostname) 443 (Just clip)     isOpen <- newIORef True     let handleGoAway rsgaf = do-            writeIORef isOpen False+            lift $ writeIORef isOpen False             return ()-    client <- newHttp2Client httpFrameConnection 4096 4096 conf handleGoAway ignoreFallbackHandler-    linkAsyncs client+    client <-+        fmap (either throw id) . runClientIO $ do+        httpFrameConnection <- newHttp2FrameConnection (T.unpack hostname) 443 (Just clip)+        client <-+            newHttp2Client httpFrameConnection 4096 4096 conf handleGoAway ignoreFallbackHandler+        linkAsyncs client+        return client     flowWorker <- forkIO $ forever $ do-        updated <- _updateWindow $ _incomingFlowControl client+        updated <- runClientIO $ _updateWindow $ _incomingFlowControl client         threadDelay 1000000-     workersem <- newQSem maxConcurrentStreams-    currtime <- round <$> getPOSIXTime :: IO Int64-    return $ ApnConnection client aci workersem currtime flowWorker isOpen+    return $ ApnConnection client aci workersem flowWorker isOpen   closeApnConnection :: ApnConnection -> IO ()-closeApnConnection connection = do-    writeIORef (apnConnectionOpen connection) False+closeApnConnection connection =+    -- Ignoring ClientErrors in this place. We want to close our session, so we do not need to+    -- fail on this kind of errors.+    void $ runClientIO $ do+    lift $ writeIORef (apnConnectionOpen connection) False     let flowWorker = apnConnectionFlowControlWorker connection-    killThread flowWorker+    lift $ killThread flowWorker     _gtfo (apnConnectionConnection connection) HTTP2.NoError ""     _close (apnConnectionConnection connection) @@ -531,7 +512,7 @@     -- ^ The message to send     -> IO ApnMessageResult     -- ^ The response from the APN server-sendRawMessage s token payload = catchIOErrors $+sendRawMessage s token payload = catchErrors $     withConnection s $ \c ->         sendApnRaw c token payload @@ -545,7 +526,7 @@     -- ^ The message to send     -> IO ApnMessageResult     -- ^ The response from the APN server-sendMessage s token payload = catchIOErrors $+sendMessage s token payload = catchErrors $     withConnection s $ \c ->         sendApnRaw c token message   where message = L.toStrict $ encode payload@@ -558,7 +539,7 @@     -- ^ Device to send the message to     -> IO ApnMessageResult     -- ^ The response from the APN server-sendSilentMessage s token = catchIOErrors $+sendSilentMessage s token = catchErrors $     withConnection s $ \c ->         sendApnRaw c token message   where message = "{\"aps\":{\"content-available\":1}}"@@ -576,10 +557,10 @@     -- ^ Device to send the message to     -> ByteString     -- ^ The message to send-    -> IO ApnMessageResult+    -> ClientIO ApnMessageResult sendApnRaw connection token message = bracket_-  (waitQSem (apnConnectionWorkerPool connection))-  (signalQSem (apnConnectionWorkerPool connection)) $ do+  (lift $ waitQSem (apnConnectionWorkerPool connection))+  (lift $ signalQSem (apnConnectionWorkerPool connection)) $ do     let requestHeaders = [ ( ":method", "POST" )                   , ( ":scheme", "https" )                   , ( ":authority", TE.encodeUtf8 hostname )@@ -597,7 +578,7 @@                 -- sendData client stream (HTTP2.setEndStream) message                 upload message (HTTP2.setEndHeader . HTTP2.setEndStream) client (_outgoingFlowControl client) stream osfc                 let pph hStreamId hStream hHeaders hIfc hOfc =-                        print hHeaders+                        lift $ print hHeaders                 response <- waitStream stream isfc pph                 let (errOrHeaders, frameResponses, _) = response                 case errOrHeaders of@@ -632,8 +613,17 @@         getHeaderEx :: HTTP2.HeaderName -> [HTTP2.Header] -> HTTP2.HeaderValue         getHeaderEx name headers = fromMaybe (throw $ ApnExceptionMissingHeader name) (DL.lookup name headers) -catchIOErrors :: SpecifyError a => IO a -> IO a-catchIOErrors = flip catchIOError (return . isAnError)++catchErrors :: ClientIO ApnMessageResult -> IO ApnMessageResult+catchErrors = catchIOErrors . catchClientErrors+    where+        catchIOErrors :: IO ApnMessageResult -> IO ApnMessageResult+        catchIOErrors = flip catchIOError (return . ApnMessageResultIOError)++        catchClientErrors :: ClientIO ApnMessageResult -> IO ApnMessageResult+        catchClientErrors act =+            either ApnMessageResultClientError id <$> runClientIO act+  -- The type of permanent error indicated by APNS -- See https://apple.co/2RDCdWC table 8-6 for the meaning of each value.