module HsDev.Server.Commands (
ServerCommand(..), ServerOpts(..), ClientOpts(..),
Request(..),
sendCommand, runServerCommand,
findPath,
processRequest, processClient, processClientSocket,
module HsDev.Server.Types
) where
import Control.Concurrent
import Control.Concurrent.Async
import Control.Exception
import Control.Monad
import Control.Monad.Except
import Data.Aeson hiding (Result, Error)
import Data.Aeson.Encode.Pretty
import qualified Data.ByteString.Char8 as BS
import Data.ByteString.Lazy.Char8 (ByteString)
import qualified Data.ByteString.Lazy.Char8 as L
import qualified Data.Map as M
import Data.Maybe
import qualified Data.Text as T (pack)
import Data.Traversable (for)
import Network.Socket hiding (connect)
import qualified Network.Socket as Net hiding (send)
import qualified Network.Socket.ByteString as Net (send)
import qualified Network.Socket.ByteString.Lazy as Net (getContents)
import System.Directory
import System.Exit
import System.FilePath
import System.IO
import qualified System.Log.Simple.Base as Log
import Control.Concurrent.Util
import qualified Control.Concurrent.FiniteChan as F
import Text.Format ((~~))
import System.Directory.Paths
import qualified HsDev.Client.Commands as Client
import qualified HsDev.Database.Async as DB
import HsDev.Server.Base
import HsDev.Server.Types
import HsDev.Util
import HsDev.Version
#if mingw32_HOST_OS
import Data.Aeson.Types hiding (Result, Error)
import Data.Char
import Data.List
import System.Environment
import System.Process
import System.Win32.FileMapping.Memory (withMapFile, readMapFile)
import System.Win32.FileMapping.NamePool
import System.Win32.PowerShell (escape, quote, quoteDouble)
#else
import System.Posix.Process
import System.Posix.IO
#endif
sendCommand :: ClientOpts -> Bool -> Command -> (Notification -> IO a) -> IO Result
sendCommand copts noFile c onNotification = do
asyncAct <- async sendReceive
res <- waitCatch asyncAct
case res of
Left e -> return $ Error (show e) $ M.fromList []
Right r -> return r
where
encodeValue :: ToJSON a => a -> L.ByteString
encodeValue
| clientPretty copts = encodePretty
| otherwise = encode
sendReceive = do
curDir <- getCurrentDirectory
input <- if clientStdin copts
then liftM Just L.getContents
else return $ fmap toUtf8 $ Nothing
let
parseData :: L.ByteString -> IO Value
parseData cts = case eitherDecode cts of
Left err -> putStrLn ("Invalid data: " ++ err) >> exitFailure
Right v -> return v
dat <- traverse parseData input
s <- socket AF_INET Stream defaultProtocol
addr' <- inet_addr "127.0.0.1"
Net.connect s (SockAddrInet (fromIntegral $ clientPort copts) addr')
bracket (socketToHandle s ReadWriteMode) hClose $ \h -> do
L.hPutStrLn h $ encode $ Message Nothing $ Request c curDir noFile (clientTimeout copts) (clientSilent copts)
hFlush h
peekResponse h
peekResponse h = do
resp <- hGetLineBS h
parseResponse h resp
parseResponse h str = case eitherDecode str of
Left e -> return $ Error e $ M.fromList [("response", toJSON $ fromUtf8 str)]
Right (Message _ r) -> do
r' <- unMmap r
case r' of
Left n -> onNotification n >> peekResponse h
Right r -> return r
runServerCommand :: ServerCommand -> IO ()
runServerCommand Version = putStrLn $cabalVersion
runServerCommand (Start sopts) = do
#if mingw32_HOST_OS
let
args = "run" : serverOptsArgs sopts
myExe <- getExecutablePath
curDir <- getCurrentDirectory
let
biescape = escape quote . escape quoteDouble
script = "try {{ start-process {} {} -WindowStyle Hidden -WorkingDirectory {} }} catch {{ $_.Exception, $_.InvocationInfo.Line }}"
~~ escape quote myExe
~~ intercalate ", " (map biescape args)
~~ escape quote curDir
r <- readProcess "powershell" [
"-Command",
script] ""
if all isSpace r
then putStrLn $ "Server started at port " ++ show (serverPort sopts)
else mapM_ putStrLn [
"Failed to start server",
"\tCommand: " ++ script,
"\tResult: " ++ r]
#else
let
forkError :: SomeException -> IO ()
forkError e = putStrLn $ "Failed to start server: " ++ show e
proxy :: IO ()
proxy = do
_ <- createSession
_ <- forkProcess serverAction
exitImmediately ExitSuccess
serverAction :: IO ()
serverAction = do
mapM_ closeFd [stdInput, stdOutput, stdError]
nullFd <- openFd "/dev/null" ReadWrite Nothing defaultFileFlags
mapM_ (dupTo nullFd) [stdInput, stdOutput, stdError]
closeFd nullFd
runServerCommand (Run sopts)
handle forkError $ do
_ <- forkProcess proxy
putStrLn $ "Server started at port " ++ show (serverPort sopts)
#endif
runServerCommand (Run sopts) = runServer sopts $ \copts -> do
commandLog copts Log.Info $ "Server started at port " ++ show (serverPort sopts)
waitListen <- newEmptyMVar
clientChan <- F.newChan
void $ forkIO $ do
accepter <- myThreadId
let
serverStop :: IO ()
serverStop = void $ forkIO $ do
void $ tryPutMVar waitListen ()
killThread accepter
s <- socket AF_INET Stream defaultProtocol
bind s $ SockAddrInet (fromIntegral $ serverPort sopts) iNADDR_ANY
listen s maxListenQueue
forever $ logAsync (commandLog copts Log.Fatal) $ logIO "accept client exception: " (commandLog copts Log.Error) $ do
s' <- fst <$> accept s
void $ forkIO $ logAsync (commandLog copts Log.Fatal) $ logIO (show s' ++ " exception: ") (commandLog copts Log.Error) $
flip finally (close s') $
bracket newEmptyMVar (`putMVar` ()) $ \done -> do
me <- myThreadId
let
timeoutWait = do
notDone <- isEmptyMVar done
when notDone $ do
void $ forkIO $ do
threadDelay 1000000
void $ tryPutMVar done ()
killThread me
takeMVar done
F.putChan clientChan timeoutWait
processClientSocket s' (copts {
commandExit = serverStop })
takeMVar waitListen
DB.readAsync (commandDatabase copts) >>= writeCache sopts (commandLog copts)
F.stopChan clientChan >>= sequence_
commandLog copts Log.Info "server stopped"
runServerCommand (Stop copts) = runServerCommand (Remote copts False Exit)
runServerCommand (Connect copts) = do
curDir <- getCurrentDirectory
s <- socket AF_INET Stream defaultProtocol
addr' <- inet_addr "127.0.0.1"
Net.connect s (SockAddrInet (fromIntegral $ clientPort copts) addr')
bracket (socketToHandle s ReadWriteMode) hClose $ \h -> forM_ [(1 :: Integer)..] $ \i -> ignoreIO $ do
input' <- hGetLineBS stdin
case eitherDecode input' of
Left _ -> L.putStrLn $ encodeValue $ object ["error" .= ("invalid command" :: String)]
Right req' -> do
L.hPutStrLn h $ encode $ Message (Just $ show i) $ Request req' curDir True (clientTimeout copts) False
waitResp h
where
pretty = clientPretty copts
encodeValue :: ToJSON a => a -> L.ByteString
encodeValue
| pretty = encodePretty
| otherwise = encode
waitResp h = do
resp <- hGetLineBS h
parseResp h resp
parseResp h str = case eitherDecode str of
Left e -> putStrLn $ "Can't decode response: " ++ e
Right (Message i r) -> do
r' <- unMmap r
putStrLn $ fromMaybe "_" i ++ ":" ++ fromUtf8 (encodeValue r')
case r of
Left _ -> waitResp h
_ -> return ()
runServerCommand (Remote copts noFile c) = sendCommand copts noFile c printValue >>= printResult where
printValue :: ToJSON a => a -> IO ()
printValue = L.putStrLn . encodeValue
printResult :: Result -> IO ()
printResult (Result r) = printValue r
printResult e = printValue e
encodeValue :: ToJSON a => a -> L.ByteString
encodeValue = if clientPretty copts then encodePretty else encode
findPath :: MonadIO m => CommandOptions -> FilePath -> m FilePath
findPath copts f = liftIO $ canonicalizePath (normalise f') where
f'
| isRelative f = commandRoot copts </> f
| otherwise = f
processRequest :: CommandOptions -> (Notification -> IO ()) -> Command -> IO Result
processRequest copts onNotify c = paths (findPath copts) c >>= Client.runCommand (copts { commandNotify = onNotify })
processClient :: String -> IO ByteString -> (ByteString -> IO ()) -> CommandOptions -> IO ()
processClient name receive send' copts = do
commandLog copts Log.Info $ name ++ " connected"
respChan <- newChan
void $ forkIO $ getChanContents respChan >>= mapM_ (send' . encode)
linkVar <- newMVar $ return ()
let
answer :: Message Response -> IO ()
answer m@(Message _ r) = do
unless (isNotification r) $
commandLog copts Log.Trace $ " << " ++ ellipsis (fromUtf8 (encode r))
writeChan respChan m
where
ellipsis :: String -> String
ellipsis s
| length s < 100 = s
| otherwise = take 100 s ++ "..."
flip finally (disconnected linkVar) $ forever $ Log.scopeLog (commandLogger copts) (T.pack name) $ do
req' <- receive
commandLog copts Log.Trace $ " => " ++ fromUtf8 req'
case eitherDecode req' of
Left _ -> do
commandLog copts Log.Warning $ "Invalid request: " ++ fromUtf8 req'
answer $ Message Nothing $ responseError "Invalid request" [
"request" .= fromUtf8 req']
Right m -> Log.scopeLog (commandLogger copts) (T.pack $ fromMaybe "_" (messageId m)) $ do
resp' <- for m $ \(Request c cdir noFile tm silent) -> do
let
onNotify n
| silent = return ()
| otherwise = traverse (const $ mmap' noFile (Left n)) m >>= answer
commandLog copts Log.Trace $ name ++ " >> " ++ fromUtf8 (encode c)
resp <- fmap Right $ handleTimeout tm $ handleError $
processRequest
(copts {
commandRoot = cdir,
commandLink = void (swapMVar linkVar $ commandExit copts) })
onNotify
c
mmap' noFile resp
answer resp'
where
handleTimeout :: Int -> IO Result -> IO Result
handleTimeout 0 = id
handleTimeout tm = fmap (fromMaybe $ Error "Timeout" M.empty) . timeout tm
handleError :: IO Result -> IO Result
handleError = handle onErr where
onErr :: SomeException -> IO Result
onErr e = return $ Error "Exception" $ M.fromList [("what", toJSON $ show e)]
mmap' :: Bool -> Response -> IO Response
#if mingw32_HOST_OS
mmap' False
| Just pool <- commandMmapPool copts = mmap pool
#endif
mmap' _ = return
disconnected :: MVar (IO ()) -> IO ()
disconnected var = do
commandLog copts Log.Info $ name ++ " disconnected"
join $ takeMVar var
processClientSocket :: Socket -> CommandOptions -> IO ()
processClientSocket s copts = do
recvChan <- F.newChan
void $ forkIO $ finally
(Net.getContents s >>= mapM_ (F.putChan recvChan) . L.lines)
(F.closeChan recvChan)
processClient (show s) (getChan_ recvChan) (sendLine s) (copts {
commandHold = forever (getChan_ recvChan) })
where
getChan_ :: F.Chan a -> IO a
getChan_ = F.getChan >=> maybe noData return
noData :: IO a
noData = throwIO $ userError "Receive chan closed"
sendLine :: Socket -> ByteString -> IO ()
sendLine sock bs = sendAll sock $ L.toStrict $ L.snoc bs '\n'
sendAll :: Socket -> BS.ByteString -> IO ()
sendAll sock bs
| BS.null bs = return ()
| otherwise = do
sent <- Net.send sock bs
when (sent > 0) $ sendAll sock (BS.drop sent bs)
#if mingw32_HOST_OS
data MmapFile = MmapFile String
instance ToJSON MmapFile where
toJSON (MmapFile f) = object ["file" .= f]
instance FromJSON MmapFile where
parseJSON = withObject "file" $ \v -> MmapFile <$> v .:: "file"
mmap :: Pool -> Response -> IO Response
mmap mmapPool r
| L.length msg <= 1024 = return r
| otherwise = withSync (responseError "timeout" []) $ \sync -> timeout 10000000 $
withName mmapPool $ \mmapName -> do
runExceptT $ flip catchError
(\e -> liftIO $ sync $ responseError e [])
(withMapFile mmapName (L.toStrict msg) $ liftIO $ do
sync $ result $ MmapFile mmapName
threadDelay 10000000)
where
msg = encode r
#endif
unMmap :: Response -> IO Response
#if mingw32_HOST_OS
unMmap (Right (Result v))
| Just (MmapFile f) <- parseMaybe parseJSON v = do
cts <- runExceptT (fmap L.fromStrict (readMapFile f))
case cts of
Left _ -> return $ responseError "Unable to read map view of file" ["file" .= f]
Right r' -> case eitherDecode r' of
Left e' -> return $ responseError "Invalid response" ["response" .= fromUtf8 r', "parser error" .= e']
Right r'' -> return r''
#endif
unMmap r = return r