diff --git a/src/Simplex/Chat/Remote.hs b/src/Simplex/Chat/Remote.hs index 6ff470cca..8dcf0de89 100644 --- a/src/Simplex/Chat/Remote.hs +++ b/src/Simplex/Chat/Remote.hs @@ -131,26 +131,38 @@ startRemoteHost' rh_ = do withRemoteHostSession_ rhKey $ maybe (Right ((), Just RHSessionStarting)) (\_ -> Left $ ChatErrorRemoteHost rhKey RHEBusy) ctrlAppInfo <- mkCtrlAppInfo (invitation, rchClient, vars) <- withAgent $ \a -> rcConnectHost a pairing (J.toJSON ctrlAppInfo) multicast - rhsWaitSession <- async $ waitForSession rhKey remoteHost_ rchClient vars + cmdOk <- newEmptyTMVarIO + rhsWaitSession <- async $ do + atomically $ takeTMVar cmdOk + cleanupOnError rchClient $ waitForSession remoteHost_ vars let rhs = RHPendingSession {rhKey, rchClient, rhsWaitSession, remoteHost_} withRemoteHostSession rhKey $ \case RHSessionStarting -> Right ((), RHSessionConnecting rhs) _ -> Left $ ChatErrorRemoteHost rhKey RHEBadState - pure (remoteHost_, invitation) + (remoteHost_, invitation) <$ atomically (putTMVar cmdOk ()) where mkCtrlAppInfo = do deviceName <- chatReadVar localDeviceName pure CtrlAppInfo {appVersionRange = ctrlAppVersionRange, deviceName} parseHostAppInfo :: RCHostHello -> ExceptT RemoteHostError IO HostAppInfo parseHostAppInfo RCHostHello {app = hostAppInfo} = do - hostInfo@HostAppInfo {deviceName, appVersion, encoding, encryptFiles} <- + hostInfo@HostAppInfo {appVersion, encoding} <- liftEitherWith (RHEProtocolError . RPEInvalidJSON) $ JT.parseEither J.parseJSON hostAppInfo unless (isAppCompatible appVersion ctrlAppVersionRange) $ throwError $ RHEBadVersion appVersion when (encoding == PEKotlin && localEncoding == PESwift) $ throwError $ RHEProtocolError RPEIncompatibleEncoding pure hostInfo - waitForSession :: ChatMonad m => RHKey -> Maybe RemoteHostInfo -> RCHostClient -> RCStepTMVar (ByteString, RCStepTMVar (RCHostSession, RCHostHello, RCHostPairing)) -> m () - waitForSession rhKey remoteHost_ _rchClient_kill_on_error vars = do - -- TODO handle errors + cleanupOnError :: ChatMonad m => RCHostClient -> (TMVar RHKey -> m ()) -> m () + cleanupOnError rchClient action = do + currentKey <- newEmptyTMVarIO + action currentKey `catchChatError` \err -> do + logError $ "startRemoteHost'.waitForSession crashed: " <> tshow err + sessions <- asks remoteHostSessions + atomically $ readTMVar currentKey >>= (`TM.delete` sessions) + liftIO $ cancelHostClient rchClient + waitForSession :: ChatMonad m => Maybe RemoteHostInfo -> RCStepTMVar (ByteString, RCStepTMVar (RCHostSession, RCHostHello, RCHostPairing)) -> TMVar RHKey -> m () + waitForSession remoteHost_ vars currentKey = do + let rhKey = maybe RHNew (\RemoteHostInfo {remoteHostId} -> RHId remoteHostId) remoteHost_ + atomically $ writeTMVar currentKey rhKey (sessId, vars') <- takeRCStep vars toView $ CRRemoteHostSessionCode {remoteHost_, sessionCode = verificationCode sessId} -- display confirmation code, wait for mobile to confirm (RCHostSession {tls, sessionKeys}, rhHello, pairing') <- takeRCStep vars' @@ -158,23 +170,24 @@ startRemoteHost' rh_ = do liftError (ChatErrorRemoteHost rhKey) $ parseHostAppInfo rhHello withRemoteHostSession rhKey $ \case RHSessionConnecting rhs' -> Right ((), RHSessionConfirmed rhs') -- TODO check it's the same session? - _ -> Left $ ChatErrorRemoteHost rhKey RHEBadState -- TODO kill client on error + _ -> Left $ ChatErrorRemoteHost rhKey RHEBadState -- update remoteHost with updated pairing rhi@RemoteHostInfo {remoteHostId, storePath} <- upsertRemoteHost pairing' remoteHost_ hostDeviceName - let rhKey' = RHId remoteHostId + let rhKey' = RHId remoteHostId -- rhKey may be invalid after upserting on RHNew + atomically $ writeTMVar currentKey rhKey' disconnected <- toIO $ onDisconnected remoteHostId - httpClient <- liftEitherError (httpError rhKey) $ attachRevHTTP2Client disconnected tls + httpClient <- liftEitherError (httpError rhKey') $ attachRevHTTP2Client disconnected tls let rhClient = mkRemoteHostClient httpClient sessionKeys storePath hostInfo pollAction <- async $ pollEvents remoteHostId rhClient withRemoteHostSession rhKey' $ \case RHSessionConfirmed RHPendingSession {} -> Right ((), RHSessionConnected {rhClient, pollAction, storePath}) - _ -> Left $ ChatErrorRemoteHost rhKey' RHEBadState -- TODO kill client on error + _ -> Left $ ChatErrorRemoteHost rhKey' RHEBadState chatWriteVar currentRemoteHost $ Just remoteHostId -- this is required for commands to be passed to remote host toView $ CRRemoteHostConnected rhi upsertRemoteHost :: ChatMonad m => RCHostPairing -> Maybe RemoteHostInfo -> Text -> m RemoteHostInfo - upsertRemoteHost pairing'@RCHostPairing {knownHost = kh_} rh_ hostDeviceName = do + upsertRemoteHost pairing'@RCHostPairing {knownHost = kh_} rhi_ hostDeviceName = do KnownHostPairing {hostDhPubKey = hostDhPubKey'} <- maybe (throwError . ChatError $ CEInternalError "KnownHost is known after verification") pure kh_ - case rh_ of + case rhi_ of Nothing -> do storePath <- liftIO randomStorePath rh@RemoteHost {remoteHostId} <- withStore $ \db -> insertRemoteHost db hostDeviceName storePath pairing' >>= getRemoteHost db @@ -307,11 +320,20 @@ connectRemoteCtrl inv@RCSignedInvitation {invitation = RCInvitation {ca, app}} = rc_ <- withStore' $ \db -> getRemoteCtrlByFingerprint db ca hostAppInfo <- getHostAppInfo v (rcsClient, vars) <- withAgent $ \a -> rcConnectCtrlURI a inv (ctrlPairing <$> rc_) (J.toJSON hostAppInfo) - rcsWaitSession <- async $ waitForSession rc_ ctrlDeviceName rcsClient vars - updateRemoteCtrlSession $ \case + cmdOk <- newEmptyTMVarIO + rcsWaitSession <- async $ do + atomically $ takeTMVar cmdOk + cleanupOnError rcsClient $ waitForSession rc_ ctrlDeviceName rcsClient vars + cleanupOnError rcsClient . updateRemoteCtrlSession $ \case RCSessionStarting -> Right RCSessionConnecting {rcsClient, rcsWaitSession} - _ -> Left $ ChatErrorRemoteCtrl RCEBadState -- TODO kill rcsClient + _ -> Left $ ChatErrorRemoteCtrl RCEBadState + atomically $ putTMVar cmdOk () where + cleanupOnError :: ChatMonad m => RCCtrlClient -> m () -> m () + cleanupOnError rcsClient action = action `catchChatError` \e -> do + logError $ "connectRemoteCtrl crashed with: " <> tshow e + chatWriteVar remoteCtrlSession Nothing -- XXX: can only wipe PendingConfirmation or RCSessionConnecting, which only have rcsClient to cancel + liftIO $ cancelCtrlClient rcsClient waitForSession :: ChatMonad m => Maybe RemoteCtrl -> Text -> RCCtrlClient -> RCStepTMVar (ByteString, RCStepTMVar (RCCtrlSession, RCCtrlPairing)) -> m () waitForSession rc_ ctrlName rcsClient vars = do (uniq, rcsWaitConfirmation) <- takeRCStep vars @@ -319,7 +341,7 @@ connectRemoteCtrl inv@RCSignedInvitation {invitation = RCInvitation {ca, app}} = toView CRRemoteCtrlSessionCode {remoteCtrl_ = (`remoteCtrlInfo` True) <$> rc_, sessionCode} updateRemoteCtrlSession $ \case RCSessionConnecting {rcsWaitSession} -> Right RCSessionPendingConfirmation {ctrlName, rcsClient, sessionCode, rcsWaitSession, rcsWaitConfirmation} - _ -> Left $ ChatErrorRemoteCtrl RCEBadState -- TODO kill rcsClient + _ -> Left $ ChatErrorRemoteCtrl RCEBadState parseCtrlAppInfo ctrlAppInfo = do CtrlAppInfo {deviceName, appVersionRange} <- liftEitherWith (const $ ChatErrorRemoteCtrl RCEBadInvitation) $ JT.parseEither J.parseJSON ctrlAppInfo @@ -449,7 +471,7 @@ confirmRemoteCtrl _rcId = do -- | Take a look at emoji of tlsunique, commit pairing, and start session server verifyRemoteCtrlSession :: ChatMonad m => (ByteString -> m ChatResponse) -> Text -> m RemoteCtrlInfo -verifyRemoteCtrlSession execChatCommand sessCode' = do +verifyRemoteCtrlSession execChatCommand sessCode' = cleanupOnError $ do (client, ctrlName, sessionCode, vars) <- getRemoteCtrlSession >>= \case RCSessionPendingConfirmation {rcsClient, ctrlName, sessionCode, rcsWaitConfirmation} -> pure (rcsClient, ctrlName, sessionCode, rcsWaitConfirmation) @@ -458,25 +480,35 @@ verifyRemoteCtrlSession execChatCommand sessCode' = do liftIO $ confirmCtrlSession client verified unless verified $ throwError $ ChatErrorRemoteCtrl RCEBadVerificationCode (rcsSession@RCCtrlSession {tls, sessionKeys}, rcCtrlPairing) <- takeRCStep vars - rc@RemoteCtrl {remoteCtrlId} <- withStore $ \db -> do - rc_ <- liftIO $ getRemoteCtrlByFingerprint db (ctrlFingerprint rcCtrlPairing) - case rc_ of - Nothing -> insertRemoteCtrl db ctrlName rcCtrlPairing >>= getRemoteCtrl db - Just rc@RemoteCtrl {remoteCtrlId} -> do - liftIO $ updateCtrlPairingKeys db remoteCtrlId (dhPrivKey rcCtrlPairing) - pure rc + rc@RemoteCtrl {remoteCtrlId} <- upsertRemoteCtrl ctrlName rcCtrlPairing remoteOutputQ <- asks (tbqSize . config) >>= newTBQueueIO http2Server <- async $ attachHTTP2Server tls $ handleRemoteCommand execChatCommand sessionKeys remoteOutputQ + void . forkIO $ monitor http2Server withRemoteCtrlSession $ \case RCSessionPendingConfirmation {} -> Right ((), RCSessionConnected {remoteCtrlId, rcsClient = client, rcsSession, http2Server, remoteOutputQ}) _ -> Left $ ChatErrorRemoteCtrl RCEBadState - void . forkIO $ do - waitCatch http2Server >>= \case - Left err | Just (BadThingHappen innerErr) <- fromException err -> logWarn $ "HTTP2 server crashed with internal " <> tshow innerErr - Left err | isNothing (fromException @AsyncCancelled err) -> logError $ "HTTP2 server crashed with " <> tshow err - _ -> logInfo "HTTP2 server stopped" - toView CRRemoteCtrlStopped pure $ remoteCtrlInfo rc True + where + upsertRemoteCtrl :: ChatMonad m => Text -> RCCtrlPairing -> m RemoteCtrl + upsertRemoteCtrl ctrlName rcCtrlPairing = withStore $ \db -> do + rc_ <- liftIO $ getRemoteCtrlByFingerprint db (ctrlFingerprint rcCtrlPairing) + case rc_ of + Nothing -> insertRemoteCtrl db ctrlName rcCtrlPairing >>= getRemoteCtrl db + Just rc@RemoteCtrl {remoteCtrlId} -> do + liftIO $ updateCtrlPairingKeys db remoteCtrlId (dhPrivKey rcCtrlPairing) + pure rc + cleanupOnError :: ChatMonad m => m a -> m a + cleanupOnError action = action `catchChatError` \e -> do + logError $ "verifyRemoteCtrlSession crashed with: " <> tshow e + withRemoteCtrlSession_ (\s -> pure (s, Nothing)) >>= mapM_ (liftIO . cancelRemoteCtrl) -- cancel session threads, if any + throwError e + monitor :: ChatMonad m => Async a -> m () + monitor server = do + waitCatch server >>= \case + Left err | Just (BadThingHappen innerErr) <- fromException err -> logWarn $ "HTTP2 server crashed with internal " <> tshow innerErr + Left err | isNothing (fromException @AsyncCancelled err) -> logError $ "HTTP2 server crashed with " <> tshow err + _ -> logInfo "HTTP2 server stopped" + toView CRRemoteCtrlStopped stopRemoteCtrl :: ChatMonad m => m () stopRemoteCtrl =