File: MonadServerImplementation.hs

package info (click to toggle)
haskell-lambdahack 0.11.0.1-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 4,064 kB
  • sloc: haskell: 45,636; makefile: 223
file content (191 lines) | stat: -rw-r--r-- 8,315 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
-- | The implementation of our custom game server monads. Just as any other
-- component of the library, this implementation can be substituted.
module Implementation.MonadServerImplementation
  ( executorSer
#ifdef EXPOSE_INTERNAL
    -- * Internal operations
  , SerState(..), SerImplementation(..)
#endif
  ) where

import Prelude ()

import Game.LambdaHack.Core.Prelude

import           Control.Concurrent
import           Control.Concurrent.Async
import qualified Control.Exception as Ex
import qualified Control.Monad.IO.Class as IO
import           Control.Monad.Trans.State.Strict hiding (State)
import qualified Data.EnumMap.Strict as EM
import qualified Data.Text.IO as T
import           Options.Applicative
  (defaultPrefs, execParserPure, handleParseResult)
import           System.Exit (ExitCode)
import           System.IO (hFlush, stdout)

import           Game.LambdaHack.Atomic
import           Game.LambdaHack.Client
import           Game.LambdaHack.Common.Kind
import           Game.LambdaHack.Common.MonadStateRead
import qualified Game.LambdaHack.Common.Save as Save
import           Game.LambdaHack.Common.State
import           Game.LambdaHack.Common.Thread
import           Game.LambdaHack.Server
import           Game.LambdaHack.Server.BroadcastAtomic
import           Game.LambdaHack.Server.HandleAtomicM
import           Game.LambdaHack.Server.MonadServer
import           Game.LambdaHack.Server.ProtocolM
import           Game.LambdaHack.Server.State

import Implementation.MonadClientImplementation (executorCli)

data SerState = SerState
  { serState  :: State             -- ^ current global state
  , serServer :: StateServer       -- ^ current server state
  , serDict   :: ConnServerDict    -- ^ client-server connection information
  , serToSave :: Save.ChanSave (State, StateServer)
                                       -- ^ connection to the save thread
  }

-- | Server state transformation monad.
newtype SerImplementation a =
    SerImplementation {runSerImplementation :: StateT SerState IO a}
  deriving (Monad, Functor, Applicative)

instance MonadStateRead SerImplementation where
  {-# INLINE getsState #-}
  getsState f = SerImplementation $ gets $ f . serState

instance MonadStateWrite SerImplementation where
  {-# INLINE modifyState #-}
  modifyState f = SerImplementation $ state $ \serS ->
    let !newSerS = serS {serState = f $ serState serS}
    in ((), newSerS)
  {-# INLINE putState #-}
  putState newSerState = SerImplementation $ state $ \serS ->
    let !newSerS = serS {serState = newSerState}
    in ((), newSerS)

instance MonadServer SerImplementation where
  {-# INLINE getsServer #-}
  getsServer   f = SerImplementation $ gets $ f . serServer
  {-# INLINE modifyServer #-}
  modifyServer f = SerImplementation $ state $ \serS ->
    let !newSerS = serS {serServer = f $ serServer serS}
    in ((), newSerS)
  chanSaveServer = SerImplementation $ gets serToSave
  liftIO         = SerImplementation . IO.liftIO

instance MonadServerComm SerImplementation where
  {-# INLINE getsDict #-}
  getsDict f = SerImplementation $ gets $ f . serDict
  {-# INLINE putDict #-}
  putDict newSerDict = SerImplementation $ state $ \serS ->
    let !newSerS = serS {serDict = newSerDict}
    in ((), newSerS)
  liftIO = SerImplementation . IO.liftIO

instance MonadServerAtomic SerImplementation where
  execUpdAtomic cmd = do
    oldState <- getState
    (ps, atomicBroken, executedOnServer) <- handleCmdAtomicServer cmd
    when executedOnServer $ cmdAtomicSemSer oldState cmd
    handleAndBroadcast ps atomicBroken (UpdAtomic cmd)
  execUpdAtomicSer cmd = SerImplementation $ StateT $ \cliS -> do
    cliSNewOrE <- Ex.try
                  $ execStateT (runSerImplementation $ handleUpdAtomic cmd)
                               cliS
    case cliSNewOrE of
      Left AtomicFail{} -> return (False, cliS)
      Right !cliSNew ->
        -- We know @cliSNew@ differs only in @serState@.
        return (True, cliSNew)
  execUpdAtomicFid fid cmd = SerImplementation $ StateT $ \cliS -> do
    -- Don't catch anything; assume exceptions impossible.
    let sFid = sclientStates (serServer cliS) EM.! fid
    cliSNew <- execStateT (runSerImplementation $ handleUpdAtomic cmd)
                          cliS {serState = sFid}
    -- We know @cliSNew@ differs only in @serState@.
    let serServerNew = (serServer cliS)
          {sclientStates = EM.insert fid (serState cliSNew)
                           $ sclientStates $ serServer cliS}
        !newCliS = cliS {serServer = serServerNew}
    return ((), newCliS)
  execUpdAtomicFidCatch fid cmd = SerImplementation $ StateT $ \cliS -> do
    let sFid = sclientStates (serServer cliS) EM.! fid
    cliSNewOrE <- Ex.try
                  $ execStateT (runSerImplementation $ handleUpdAtomic cmd)
                               cliS {serState = sFid}
    case cliSNewOrE of
      Left AtomicFail{} -> return (False, cliS)
      Right cliSNew -> do
        -- We know @cliSNew@ differs only in @serState@.
        let serServerNew = (serServer cliS)
              {sclientStates = EM.insert fid (serState cliSNew)
                               $ sclientStates $ serServer cliS}
            !newCliS = cliS {serServer = serServerNew}
        return (True, newCliS)
  execSfxAtomic sfx = do
    ps <- posSfxAtomic sfx
    handleAndBroadcast ps [] (SfxAtomic sfx)
  execSendPer = sendPer

-- Don't inline this, to keep GHC hard work inside the library
-- for easy access of code analysis tools.
-- | Run the main server loop, with the given arguments and empty
-- initial states, in the @IO@ monad.
executorSer :: COps -> CCUI -> ServerOptions -> UIOptions -> IO ()
executorSer cops@COps{corule} ccui soptionsNxtCmdline sUIOptions = do
  soptionsNxtRaw <- case uOverrideCmdline sUIOptions of
    []   -> return soptionsNxtCmdline
    args -> handleParseResult $ execParserPure defaultPrefs serverOptionsPI args
  -- Options for the clients modified with the configuration file.
  let clientOptions = applyUIOptions cops sUIOptions
                      $ sclientOptions soptionsNxtRaw
      soptionsNxt = soptionsNxtRaw {sclientOptions = clientOptions}
      -- Partially applied main loop of the clients.
      executorClient startsNewGame =
        executorCli ccui sUIOptions clientOptions startsNewGame cops
  -- Wire together game content, the main loop of game clients
  -- and the game server loop.
  let stateToFileName (_, ser) =
        ssavePrefixSer (soptions ser) <> Save.saveNameSer corule
      totalState serToSave = SerState
        { serState = updateCOpsAndCachedData (const cops) emptyState
            -- state is empty, so the cached data is left empty and untouched
        , serServer = emptyStateServer
        , serDict = EM.empty
        , serToSave
        }
      m = loopSer soptionsNxt executorClient
      exe = evalStateT (runSerImplementation m) . totalState
      exeWithSaves = Save.wrapInSaves cops stateToFileName exe
      unwrapEx e = case Ex.fromException e of
        Just (ExceptionInLinkedThread _ ex) -> unwrapEx ex
        _ -> e
  -- Wait for clients to exit even in case of server crash
  -- (or server and client crash), which gives them time to save
  -- and report their own inconsistencies, if any.
  Ex.handle (\ex -> case Ex.fromException (unwrapEx ex) :: Maybe ExitCode of
               Just{} ->
                 -- User-forced shutdown, not crash, so the intention is
                 -- to keep old saves and also clients may be not ready to save.
                 Ex.throwIO ex
               _ -> do
                 Ex.uninterruptibleMask_ $ threadDelay 1000000
                   -- let clients report their errors and save
                 moveAside <- Save.bkpAllSaves corule clientOptions
                 when moveAside $
                   T.hPutStrLn stdout
                               "The game crashed, so savefiles are moved aside."
                 hFlush stdout
                 Ex.throwIO ex  -- crash eventually, which kills clients
            )
            exeWithSaves
--  T.hPutStrLn stdout "Server exiting, waiting for clients."
--  hFlush stdout
  waitForChildren childrenServer  -- no crash, wait for clients indefinitely
--  T.hPutStrLn stdout "Server exiting now."
--  hFlush stdout