packages feed

hsdev-0.1.0.1: tools/Commands.hs

{-# LANGUAGE OverloadedStrings, CPP, TupleSections #-}

module Commands (
	mainCommands, commands
	) where

import Control.Applicative
import Control.Arrow
import Control.Monad
import Control.Monad.Error
import Control.Monad.Trans.Maybe
import Control.Exception
import Control.Concurrent
import Data.Aeson
import Data.Aeson.Encode.Pretty
import Data.Aeson.Types
import Data.Char
import Data.Either
import Data.List
import Data.Maybe
import Data.Monoid
import qualified Data.ByteString.Char8 as B
import Data.ByteString.Lazy.Char8 (ByteString)
import qualified Data.ByteString.Lazy.Char8 as L
import Data.Map (Map)
import qualified Data.Map as M
import Data.Traversable (traverse)
import Network.Socket
import System.Directory
import System.Environment
import System.Exit
import System.IO
import System.Process
import System.Console.GetOpt
import System.Timeout
import System.FilePath
import Text.Read (readMaybe)

import qualified HsDev.Database.Async as DB
import HsDev.Commands
import HsDev.Database
import HsDev.Project
import HsDev.Symbols
import HsDev.Symbols.Util
import HsDev.Util
import HsDev.Scan
import qualified HsDev.Tools.Cabal as Cabal
import qualified HsDev.Tools.GhcMod as GhcMod (typeOf)
import qualified HsDev.Tools.Hayoo as Hayoo
import qualified HsDev.Cache.Structured as SC
import HsDev.Cache

import qualified Control.Concurrent.FiniteChan as F
import System.Command

#if mingw32_HOST_OS
import System.Win32.FileMapping.Memory (withMapFile, readMapFile)
import System.Win32.FileMapping.NamePool
#else
import System.Posix.Process
import System.Posix.IO
#endif

import qualified Update
import Types

#if mingw32_HOST_OS

translate :: String -> String
translate str = '"' : snd (foldr escape (True,"\"") str) where
	escape '"'  (b, str) = (True,  '\\' : '"'  : str)
	escape '\\' (True, str) = (True,  '\\' : '\\' : str)
	escape '\\' (False, str) = (False, '\\' : str)
	escape c (b, str) = (False, c : str)

powershell :: String -> String
powershell str
	| all isAlphaNum str = str
	| otherwise = "'" ++ translate str ++ "'"

#endif

-- | Main commands
mainCommands :: [Command (IO ())]
mainCommands = addHelp "hsdev" id $ srvCmds ++ map wrapCmd commands where
	wrapCmd :: Command CommandAction -> Command (IO ())
	wrapCmd = fmap sendCmd . addClientOpts . fmap withOptsCommand
	srvCmds = [
		cmd ["server", "start"] [] "start remote server" serverOpts start',
		cmd ["server", "run"] [] "start server" serverOpts run',
		cmd ["server", "stop"] [] "stop remote server" clientOpts stop',
		cmd ["connect"] [] "connect to send commands directly" clientOpts connect']

	start' sopts _ = do
#if mingw32_HOST_OS
		let
			args = ["server", "run"] ++ serverOptsToArgs sopts
		myExe <- getExecutablePath
		r <- readProcess "powershell" [
			"-Command",
			unwords [
				"&", "{", "start-process",
				powershell myExe,
				intercalate ", " (map powershell args),
				"-WindowStyle Hidden",
				"}"]] ""
		if all isSpace r
			then putStrLn $ "Server started at port " ++ show (fromJust $ getFirst $ serverPort sopts)
			else putStrLn $ "Failed to start server: " ++ 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
				run' sopts []

		handle forkError $ do
			forkProcess proxy
			putStrLn $ "Server started at port " ++ show (fromJust $ getFirst $ serverPort sopts)
#endif
	run' sopts _
		| getAny (serverAsClient sopts) = runServer sopts $ \copts -> do
			commandLog copts $ "Server started as client connecting at port " ++ show (fromJust $ getFirst $ serverPort sopts)
			me <- myThreadId
			s <- socket AF_INET Stream defaultProtocol
			addr' <- inet_addr "127.0.0.1"
			connect s $ SockAddrInet (fromIntegral $ fromJust $ getFirst $ serverPort sopts) addr'
			bracket (socketToHandle s ReadWriteMode) hClose $ \h ->
				processClient (show s) (hGetLine' h) (L.hPutStrLn h) sopts (copts {
					commandExit = killThread me })
		| otherwise = runServer sopts $ \copts -> do
			commandLog copts $ "Server started at port " ++ show (fromJust $ getFirst $ serverPort sopts)

			waitListen <- newEmptyMVar
			clientChan <- F.newChan

			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 $ fromJust $ getFirst $ serverPort sopts) iNADDR_ANY
				listen s maxListenQueue
				forever $ logIO "accept client exception: " (commandLog copts) $ do
					s' <- fst <$> accept s
					void $ forkIO $ logIO (show s' ++ " exception: ") (commandLog copts) $
						bracket (socketToHandle s' ReadWriteMode) hClose $ \h -> do
							bracket newEmptyMVar (`putMVar` ()) $ \done -> do
								me <- myThreadId
								let
									timeoutWait = do
										notDone <- isEmptyMVar done
										when notDone $ do
											void $ forkIO $ do
												threadDelay 1000000
												tryPutMVar done ()
												killThread me
											takeMVar done
									waitForever = forever $ hGetLine' h
								F.putChan clientChan timeoutWait
								processClient (show s') (hGetLine' h) (L.hPutStrLn h) sopts (copts {
									commandHold = waitForever,
									commandExit = serverStop })

			takeMVar waitListen
			DB.readAsync (commandDatabase copts) >>= writeCache sopts (commandLog copts)
			F.stopChan clientChan >>= sequence_
			commandLog copts "server stopped"

	stop' copts _ = run (map wrapCmd' commands) onDef onError ["exit"] where
		onDef = putStrLn "Command 'exit' not found"
		onError es = putStrLn $ "Failed to stop server: " ++ intercalate ", " es
		wrapCmd' = fmap (sendCmd . (copts,) . withOptsCommand)

	connect' copts _ = do
		curDir <- getCurrentDirectory
		s <- socket AF_INET Stream defaultProtocol
		addr' <- inet_addr "127.0.0.1"
		connect s (SockAddrInet (fromIntegral $ fromJust $ getFirst $ clientPort copts) addr')
		bracket (socketToHandle s ReadWriteMode) hClose $ \h -> forever $ ignoreIO $ do
			cmd <- hGetLine' stdin
			case eitherDecode cmd of
				Left e -> L.putStrLn $ encodeValue $ object ["error" .= ("invalid command" :: String)]
				Right cmd' -> do
					L.hPutStrLn h $ encode $ cmd' `addCallOpts` ["current-directory" %-- curDir]
					waitResp h
		where
			pretty = getAny $ clientPretty copts
			encodeValue :: ToJSON a => a -> L.ByteString
			encodeValue
				| pretty = encodePretty
				| otherwise = encode

			waitResp h = do
				resp <- hGetLine' h
				parseResp h resp

			parseResp h str = void $ runErrorT $ flip catchError (liftIO . putStrLn) $ do
				v <- ErrorT (return $ eitherDecode str) `orFail` ("Can't decode response", ["response" .= fromUtf8 str])
				case v of
					ResponseStatus s -> liftIO $ do
						L.putStrLn $ encodeValue s
						liftIO $ waitResp h
#if mingw32_HOST_OS
					ResponseMapFile viewFile -> do
						str <- fmap L.fromStrict (readMapFile viewFile) `orFail`
							("Can't read map view of file", ["file" .= viewFile])
						lift $ parseResp h str
#else
					ResponseMapFile viewFile -> throwError $ fromUtf8 $ encodeValue $
						object ["error" .= ("Not supported" :: String)]
#endif
					Response r -> liftIO $ L.putStrLn $ encodeValue r
				where
					orFail :: (Monad m, Functor m) => ErrorT String m a -> (String, [Pair]) -> ErrorT String m a
					orFail act (msg, fs) = act <|> (throwError $ fromUtf8 $ encodeValue $ object (
						("error" .= msg) : fs))

	-- Send command to server
	sendCmd :: (ClientOpts, CommandCall) -> IO ()
	sendCmd (p, cmdCall) = do
		svar <- newEmptyMVar
		race [takeMVar svar, waitResponse >> putMVar svar ()]
		where
			pretty = getAny $ clientPretty p
			encodeValue :: ToJSON a => a -> L.ByteString
			encodeValue
				| pretty = encodePretty
				| otherwise = encode

			waitResponse = do
				curDir <- getCurrentDirectory
				stdinData <- if getAny (clientData p)
					then do
						cdata <- liftM (eitherDecode :: L.ByteString -> Either String Value) L.getContents
						case cdata of
							Left cdataErr -> do
								putStrLn $ "Invalid data: " ++ cdataErr
								exitFailure
							Right dataValue -> return $ Just dataValue
					else return Nothing

				s <- socket AF_INET Stream defaultProtocol
				addr' <- inet_addr "127.0.0.1"
				connect s (SockAddrInet (fromIntegral $ fromJust $ getFirst $ clientPort p) addr')
				h <- socketToHandle s ReadWriteMode
				L.hPutStrLn h $ encode $ cmdCall `addCallOpts` [
					"current-directory" %-- curDir,
					case stdinData of
						Nothing -> mempty
						Just d -> "data" %-- (fromUtf8 $ encode d)]
				peekResponse h

			peekResponse h = do
				resp <- hGetLine' h
				parseResponse h resp

			parseResponse h str = void $ runErrorT $ flip catchError (liftIO . putStrLn) $ do
				v <- ErrorT (return $ eitherDecode str) `orFail` ("Can't decode response", ["response" .= fromUtf8 str])
				case v of
					ResponseStatus s -> liftIO $ do
						L.putStrLn $ encodeValue s
						peekResponse h
#if mingw32_HOST_OS
					ResponseMapFile viewFile -> do
						str <- fmap L.fromStrict (readMapFile viewFile) `orFail`
							("Can't read map view of file", ["file" .= viewFile])
						lift $ parseResponse h str
#else
					ResponseMapFile viewFile -> throwError $ fromUtf8 $ encodeValue $
						object ["error" .= ("Not supported" :: String)]
#endif
					Response r -> liftIO $ L.putStrLn $ encodeValue r
				where
					orFail :: (Monad m, Functor m) => ErrorT String m a -> (String, [Pair]) -> ErrorT String m a
					orFail act (msg, fs) = act <|> (throwError $ fromUtf8 $ encodeValue $ object (
						("error" .= msg) : fs))

	-- Add parsing 'ClieptOpts'
	addClientOpts :: Command CommandCall -> Command (ClientOpts, CommandCall)
	addClientOpts c = c { commandRun = run' } where
		run' args = fmap (fmap (p,)) $ commandRun c args' where
			(ps, args', _) = getOpt RequireOrder clientOpts args
			p = mconcat ps `mappend` defaultConfig

-- | Inits log chan and returns functions (print message, wait channel)
initLog :: ServerOpts -> IO (String -> IO (), IO ())
initLog sopts = do
	msgs <- F.newChan
	outputDone <- newEmptyMVar
	forkIO $ finally
		(F.readChan msgs >>= mapM_ (logMsg sopts))
		(putMVar outputDone ())
	return (F.putChan msgs, F.closeChan msgs >> takeMVar outputDone)

-- | Run server
runServer :: ServerOpts -> (CommandOptions -> IO ()) -> IO ()
runServer sopts act = bracket (initLog sopts) snd $ \(outputStr, waitOutput) -> do
	db <- DB.newAsync
	when (getAny $ serverLoadCache sopts) $ withCache sopts () $ \cdir -> do
		outputStr $ "Loading cache from " ++ cdir
		dbCache <- liftA merge <$> SC.load cdir
		case dbCache of
			Left err -> outputStr $ "Failed to load cache: " ++ err
			Right dbCache' -> DB.update db (return dbCache')
#if mingw32_HOST_OS
	mmapPool <- Just <$> createPool "hsdev"
#endif
	act $ CommandOptions
		db
		(writeCache sopts outputStr)
		(readCache sopts outputStr)
		"."
		outputStr
		waitOutput
#if mingw32_HOST_OS
		mmapPool
#endif
		(return ())
		(return ())
		(return ())

withCache :: ServerOpts -> a -> (FilePath -> IO a) -> IO a
withCache sopts v onCache = case getFirst (serverCache sopts) of
	Nothing -> return v
	Just cdir -> onCache cdir

writeCache :: ServerOpts -> (String -> IO ()) -> Database -> IO ()
writeCache sopts logMsg d = withCache sopts () $ \cdir -> do
	logMsg $ "writing cache to " ++ cdir
	logIO "cache writing exception: " logMsg $ do
		SC.dump cdir $ structurize d
	logMsg $ "cache saved to " ++ cdir

readCache :: ServerOpts -> (String -> IO ()) -> (FilePath -> ErrorT String IO Structured) -> IO (Maybe Database)
readCache sopts logMsg act = withCache sopts Nothing $ join . liftM (either cacheErr cacheOk) . runErrorT . act where
	cacheErr e = logMsg ("Error reading cache: " ++ e) >> return Nothing
	cacheOk s = do
		forM_ (M.keys (structuredCabals s)) $ \c -> logMsg ("cache read: cabal " ++ show c)
		forM_ (M.keys (structuredProjects s)) $ \p -> logMsg ("cache read: project " ++ p)
		case allModules (structuredFiles s) of
			[] -> return ()
			ms -> logMsg $ "cache read: " ++ show (length ms) ++ " files"
		return $ Just $ merge s

#if mingw32_HOST_OS
sendResponseMmap :: Pool -> (ByteString -> IO ()) -> Response -> IO ()
sendResponseMmap mmapPool send r@(ResponseMapFile _) = send $ encode r
sendResponseMmap mmapPool send r
	| L.length msg <= 1024 = send msg
	| otherwise = do
		sync <- newEmptyMVar
		forkIO $ void $ withName mmapPool $ \mmapName -> do
			runErrorT $ flip catchError
				(\e -> liftIO $ do
					sendResponseMmap mmapPool send $ Response $ object ["error" .= e]
					putMVar sync ())
				(withMapFile mmapName (L.toStrict msg) $ liftIO $ do
					sendResponseMmap mmapPool send $ ResponseMapFile mmapName
					putMVar sync ()
					-- give 10 seconds for client to read data
					threadDelay 10000000)
		takeMVar sync
	where
		msg = encode r
#endif

sendResponse :: (ByteString -> IO ()) -> Response -> IO ()
sendResponse = (. encode)

processClient :: String -> IO ByteString -> (ByteString -> IO ()) -> ServerOpts -> CommandOptions -> IO ()
processClient name receive send sopts copts = do
	commandLog copts $ name ++ " connected"
	linkVar <- newMVar $ return ()
	flip finally (disconnected linkVar) $ forever $ do
		req <- receive
		commandLog copts $ name ++ " >> " ++ fromUtf8 req
		case extractMeta <$> eitherDecode req of
			Left err -> answer True $ Response $ object [
				"error" .= ("Invalid request" :: String),
				"request" .= fromUtf8 req,
				"what" .= err]
			Right (cdir, noFile, reqArgs) -> processCmdArgs
				(copts { commandLink = void (swapMVar linkVar $ commandExit copts), commandRoot = cdir })
				(fromJust $ getFirst $ serverTimeout sopts)
				(callArgs reqArgs)
				(answer noFile)
	where
		answer :: Bool -> Response -> IO ()
		answer noFile' r = do
			commandLog copts $ name ++ " << " ++ fromUtf8 (encode r)
#if mingw32_HOST_OS
			case noFile' of
				True -> sendResponse send r
				False -> maybe (sendResponse send) (`sendResponseMmap` send) (commandMmapPool copts) r
#else
			sendResponse send r
#endif

		extractMeta :: CommandCall -> (FilePath, Bool, CommandCall)
		extractMeta c = (fpath, noFile, c `removeCallOpts` ["current-directory", "no-file"]) where
			fpath = fromMaybe (commandRoot copts) $ arg "current-directory" $ commandCallOpts c
			noFile = flag "no-file" $ commandCallOpts c

		disconnected :: MVar (IO ()) -> IO ()
		disconnected var = do
			commandLog copts $ name ++ " disconnected"
			join $ takeMVar var

commands :: [Command CommandAction]
commands = map wrapErrors $ map (fmap (fmap timeout')) cmds ++ map (fmap (fmap noTimeout)) linkCmd where
	timeout' :: (CommandOptions -> IO CommandResult) -> (Int -> CommandOptions -> IO CommandResult)
	timeout' f tm copts = fmap (fromMaybe $ err "timeout") $ timeout (tm * 1000) $ f copts
	noTimeout :: (CommandOptions -> IO CommandResult) -> (Int -> CommandOptions -> IO CommandResult)
	noTimeout f _ copts = f copts

	handleErrors :: (Int -> CommandOptions -> IO CommandResult) -> (Int -> CommandOptions -> IO CommandResult)
	handleErrors act tm copts = handle onCmdErr (act tm copts) where
		onCmdErr :: SomeException -> IO CommandResult
		onCmdErr = return . err . show

	wrapErrors :: Command CommandAction -> Command CommandAction
	wrapErrors = fmap (fmap handleErrors)

	cmds = [
		-- Ping command
		cmd_' ["ping"] [] "ping server" ping',
		-- Database commands
		cmd' ["add"] [] "add info to database" [dataArg] add',
		cmd' ["scan", "cabal"] [] "scan modules installed in cabal" (sandboxes ++ [
			ghcOpts, wait, status]) scanCabal',
		cmd' ["scan", "module"] ["module name"] "scan module in cabal" (sandboxes ++ [ghcOpts]) scanModule',
		cmd' ["scan"] [] "scan sources" [
			projectArg "project path or .cabal",
			fileArg "source file",
			pathArg "directory to scan for files and projects",
			ghcOpts, wait, status] scan',
		cmd' ["rescan"] [] "rescan sources" [
			projectArg "project path or .cabal",
			fileArg "source file",
			pathArg "path to rescan",
			ghcOpts, wait, status] rescan',
		cmd' ["remove"] [] "remove modules info" (sandboxes ++ [
			projectArg "module project",
			fileArg "module source file",
			moduleArg,
			packageArg, noLastArg, packageVersionArg,
			allFlag]) remove',
		-- | Context free commands
		cmd' ["list", "modules"] [] "list modules" (sandboxes ++ [
			projectArg "projects to list modules from",
			noLastArg,
			packageArg,
			sourced, standaloned]) listModules',
		cmd_' ["list", "packages"] [] "list packages" listPackages',
		cmd_' ["list", "projects"] [] "list projects" listProjects',
		cmd' ["symbol"] ["name"] "get symbol info" (matches ++ sandboxes ++ [
			projectArg "related project",
			fileArg "source file",
			moduleArg, localsArg,
			packageArg, noLastArg, packageVersionArg,
			sourced, standaloned]) symbol',
		cmd' ["module"] [] "get module info" (sandboxes ++ [
			moduleArg, localsArg,
			packageArg, noLastArg, packageVersionArg,
			projectArg "module project",
			fileArg "module source file",
			sourced]) modul',
		cmd' ["project"] [] "get project info" [
			projectArg "project path or name"] project',
		-- Context commands
		cmd' ["lookup"] ["symbol"] "lookup for symbol" ctx lookup',
		cmd' ["whois"] ["symbol"] "get info for symbol" ctx whois',
		cmd' ["scope", "modules"] [] "get modules accessible from module or within a project" ctx scopeModules',
		cmd' ["scope"] [] "get declarations accessible from module or within a project" (ctx ++ matches ++ [globalArg]) scope',
		cmd' ["complete"] ["input"] "show completions for input" ctx complete',
		-- Tool commands
		cmd' ["hayoo"] ["query"] "find declarations online via Hayoo" [] hayoo',
		cmd' ["cabal", "list"] ["packages..."] "list cabal packages" [] cabalList',
		cmd' ["ghc-mod", "type"] ["line", "column"] "infer type with 'ghc-mod type'" ctx ghcmodType',
		-- Dump/load commands
		cmd' ["dump", "cabal"] [] "dump cabal modules" (sandboxes ++ [cacheDir, cacheFile]) dumpCabal',
		cmd' ["dump", "projects"] [] "dump projects" [projectArg "project", cacheDir, cacheFile] dumpProjects',
		cmd' ["dump", "files"] [] "dump standalone files" [cacheDir, cacheFile] dumpFiles',
		cmd' ["dump"] [] "dump whole database" [cacheDir, cacheFile] dump',
		cmd' ["load"] [] "load data" [cacheDir, cacheFile, dataArg, wait] load',
		-- Exit
		cmd_' ["exit"] [] "exit" exit']
	linkCmd = [cmd' ["link"] [] "link to server" [holdArg] link']

	-- Command arguments and flags
	allFlag = option_ ['a'] "all" no "remove all"
	cacheDir = pathArg "cache path"
	cacheFile = fileArg "cache file"
	ctx = [fileArg "source file", sandbox]
	dataArg = option_ [] "data" (req "contents") "data to pass to command"
	fileArg = option_ ['f'] "file" (req "file")
	findArg = option_ [] "find" (req "find") "infix match"
	ghcOpts = option_ ['g'] "ghc" (req "ghc options") "options to pass to GHC"
	globalArg = option_ [] "global" no "scope of project"
	holdArg = option_ ['h'] "hold" no "don't return any response"
	localsArg = option_ ['l'] "locals" no "look in local declarations"
	noLastArg = option_ [] "no-last" no "don't select last package version"
	matches = [prefixArg, findArg]
	moduleArg = option_ ['m'] "module" (req "module name") "module name"
	packageArg = option_ [] "package" (req "package") "module package"
	pathArg = option_ ['p'] "path" (req "path")
	prefixArg = option_ [] "prefix" (req "prefix") "prefix match"
	projectArg = option [] "project" ["proj"] (req "project")
	packageVersionArg = option_ ['v'] "version" (req "version") "package version"
	sandbox = option_ [] "sandbox" (req "path") "path to cabal sandbox"
	sandboxes = [
		option_ [] "cabal" no "cabal",
		sandbox]
	sourced = option_ [] "src" no "source files"
	standaloned = option_ [] "stand" no "standalone files"
	status = option_ ['s'] "status" no "show status of operation, works only with --wait"
	wait = option_ ['w'] "wait" no "wait for operation to complete"

	-- ping server
	ping' _ copts = return $ ResultOk $ ResultMap $ M.singleton "message" (ResultString "pong")
	-- add data
	add' as _ copts = do
		dbval <- getDb copts
		res <- runErrorT $ do
			jsonData <- maybe (throwError $ err "Specify --data") return $ arg "data" as
			decodedData <- either
				(\err -> throwError (errArgs "Unable to decode data" [
					("why", ResultString err),
					("data", ResultString jsonData)]))
				return $
				eitherDecode $ toUtf8 jsonData
			let
				updateData (ResultDeclaration d) = throwError $ errArgs "Can't insert declaration" [("declaration", ResultDeclaration d)]
				updateData (ResultModuleDeclaration md) = do
					let
						ModuleId mname mloc = declarationModuleId md
						defMod = Module mname Nothing mloc [] mempty mempty
						defInspMod = Inspected InspectionNone mloc (Right defMod)
						dbmod = maybe
							defInspMod
							(\i -> i { inspectionResult = inspectionResult i <|> (Right defMod) }) $
							M.lookup mloc (databaseModules dbval)
						updatedMod = dbmod {
							inspectionResult = fmap (addDeclaration $ moduleDeclaration md) (inspectionResult dbmod) }
					DB.update (dbVar copts) $ return $ fromModule updatedMod
				updateData (ResultModuleId (ModuleId mname mloc)) = when (M.notMember mloc $ databaseModules dbval) $
					DB.update (dbVar copts) $ return $ fromModule $ Inspected InspectionNone mloc (Right $ Module mname Nothing mloc [] mempty mempty)
				updateData (ResultModule m) = DB.update (dbVar copts) $ return $ fromModule $ Inspected InspectionNone (moduleLocation m) (Right m)
				updateData (ResultInspectedModule m) = DB.update (dbVar copts) $ return $ fromModule m
				updateData (ResultProject p) = DB.update (dbVar copts) $ return $ fromProject p
				updateData (ResultList l) = mapM_ updateData l
				updateData (ResultMap m) = mapM_ updateData $ M.elems m
				updateData (ResultString s) = throwError $ err "Can't insert string"
				updateData ResultNone = return ()
			updateData decodedData
		return $ either id (const (ResultOk ResultNone)) res
	-- scan
	scan' as _ copts = updateProcess copts as $
		mapM_ (\(n, f) -> forM_ (list n as) (findPath copts >=> f (list "ghc" as))) [
			("project", Update.scanProject),
			("file", Update.scanFile),
			("path", Update.scanDirectory)]
	-- scan cabal
	scanCabal' as _ copts = error_ $ do
		cabals <- getSandboxes copts as
		lift $ updateProcess copts as $ mapM_ (Update.scanCabal $ list "ghc" as) cabals
	-- scan cabal module
	scanModule' as [] copts = return $ err "Module name not specified"
	scanModule' as ms copts = error_ $ do
		cabal <- getCabal copts as
		lift $ updateProcess copts as $
			forM_ ms (Update.scanModule (list "ghc" as) . CabalModule cabal Nothing)
	-- rescan
	rescan' as _ copts = do
		dbval <- getDb copts
		let
			fileMap = M.fromList $ mapMaybe toPair $
				selectModules (byFile . moduleId) dbval

		(errors, filteredMods) <- liftM partitionEithers $ mapM runErrorT $ concat [
			[do
				p' <- findProject copts p
				return $ M.fromList $ mapMaybe toPair $
					selectModules (inProject p' . moduleId) dbval |
				p <- list "project" as],
			[do
				f' <- findPath copts f
				maybe
					(throwError $ "Unknown file: " ++ f')
					(return . M.singleton f')
					(lookupFile f' dbval) |
				f <- list "file" as],
			[do
				d' <- findPath copts d
				return $ M.filterWithKey (\f _ -> isParent d' f) fileMap |
				d <- list "path" as]]
		let
			rescanMods = map (getInspected dbval) $
				M.elems $ if null filteredMods then fileMap else M.unions filteredMods

		if not (null errors)
			then return $ err $ intercalate ", " errors
			else updateProcess copts as $ Update.runTask (toJSON $ ("rescanning modules" :: String)) $ do
				needRescan <- Update.liftErrorT $ filterM (changedModule dbval (list "ghc" as) . inspectedId) rescanMods
				Update.scanModules (list "ghc" as) (map (inspectedId &&& inspectionOpts . inspection) needRescan)
	-- remove
	remove' as _ copts = errorT $ do
		dbval <- getDb copts
		cabal <- getCabal_ copts as
		proj <- traverse (findProject copts) $ arg "project" as
		file <- traverse (findPath copts) $ arg "file" as
		let
			cleanAll = flag "all" as
			filters = catMaybes [
				fmap inProject proj,
				fmap inFile file,
				fmap inModule (arg "module" as),
				fmap inPackage (arg "package" as),
				fmap inVersion (arg "version" as),
				fmap inCabal cabal]
			toClean = newest as $ filter (allOf filters . moduleId) (allModules dbval)
			action
				| null filters && cleanAll = liftIO $ do
					DB.modifyAsync (dbVar copts) DB.Clear
					return ResultNone
				| null filters && not cleanAll = throwError "Specify filter or explicitely set flag --all"
				| cleanAll = throwError "--all flag can't be set with filters"
				| otherwise = liftIO $ do
					DB.modifyAsync (dbVar copts) $ DB.Remove $ mconcat $ map (fromModule . getInspected dbval) toClean
					return $ ResultList $ map (ResultModuleId . moduleId) toClean
		action
	-- list modules
	listModules' as _ copts = errorT $ do
		dbval <- getDb copts
		projs <- traverse (findProject copts) $ list "project" as
		cabals <- getSandboxes copts as
		let
			packages = list "package" as
			hasFilters = not $ null projs && null packages && null cabals
			filters = allOf $ catMaybes [
				if hasFilters
					then Just $ anyOf [
						\m -> any (`inProject` m) projs,
						\m -> any (`inPackage` m) packages && any (`inCabal` m) cabals]
					else Nothing,
				if flag "src" as then Just byFile else Nothing,
				if flag "stand" as then Just standalone else Nothing]
		return $ ResultList $ map (ResultModuleId . moduleId) $ newest as $ selectModules (filters . moduleId) dbval
	-- list packages
	listPackages' _ copts = do
		dbval <- getDb copts
		return $ ResultOk $ ResultList $
			map ResultPackage $ nub $ sort $
			mapMaybe (moduleCabalPackage . moduleLocation) $
			allModules dbval
	-- list projects
	listProjects' _ copts = do
		dbval <- getDb copts
		return $ ResultOk $ ResultList $ map ResultProject $ M.elems $ databaseProjects dbval
	-- get symbol info
	symbol' as ns copts = errorT $ do
		dbval <- liftM (localsDatabase as) $ getDb copts
		proj <- traverse (findProject copts) $ arg "project" as
		file <- traverse (findPath copts) $ arg "file" as
		cabal <- getCabal_ copts as
		let
			filters = checkModule $ allOf $ catMaybes [
				fmap inProject proj,
				fmap inFile file,
				fmap inModule (arg "module" as),
				fmap inPackage (arg "package" as),
				fmap inVersion (arg "version" as),
				fmap inCabal cabal,
				if flag "src" as then Just byFile else Nothing,
				if flag "stand" as then Just standalone else Nothing]
			toResult = ResultList . map ResultModuleDeclaration . newest as . filterMatch as . filter filters
		case ns of
			[] -> return $ toResult $ allDeclarations dbval
			[nm] -> liftM toResult (findDeclaration dbval nm) `catchError` (\e ->
				throwError ("Can't find symbol: " ++ e))
			_ -> throwError "Too much arguments"
	-- get module info
	modul' as _ copts = errorT' $ do
		dbval <- liftM (localsDatabase as) $ getDb copts
		proj <- mapErrorT (fmap $ strMsg +++ id) $ traverse (findProject copts) $ arg "project" as
		cabal <- mapErrorT (fmap $ strMsg +++ id) $ getCabal_ copts as
		file' <- mapErrorT (fmap $ strMsg +++ id) $ traverse (findPath copts) $ arg "file" as
		let
			filters = allOf $ catMaybes [
				fmap inProject proj,
				fmap inCabal cabal,
				fmap inFile file',
				fmap inModule (arg "module" as),
				fmap inPackage (arg "package" as),
				fmap inVersion (arg "version" as),
				if flag "src" as then Just byFile else Nothing]
		rs <- mapErrorT (fmap $ strMsg +++ id) $
			(newest as . filter (filters . moduleId)) <$> maybe
				(return $ allModules dbval)
				(findModule dbval)
				(arg "module" as)
		case rs of
			[] -> throwError $ err "Module not found"
			[m] -> return $ ResultModule m
			ms' -> throwError $ errArgs "Ambiguous modules" [("modules", ResultList $ map (ResultModuleId . moduleId) ms')]
	-- get project info
	project' as _ copts = errorT $ do
		proj <- maybe (throwError "Specify project name or .cabal file") (findProject copts) $ arg "project" as
		return $ ResultProject proj
	-- lookup info about symbol
	lookup' as [nm] copts = errorT $ do
		dbval <- getDb copts
		(srcFile, cabal) <- getCtx copts as
		liftM (ResultList . map ResultModuleDeclaration) $ lookupSymbol dbval cabal srcFile nm
	lookup' as _ copts = return $ err "Invalid arguments"
	-- get detailed info about symbol in source file
	whois' as [nm] copts = errorT $ do
		dbval <- getDb copts
		(srcFile, cabal) <- getCtx copts as
		liftM (ResultList . map ResultModuleDeclaration) $ whois dbval cabal srcFile nm
	whois' as _ copts = return $ err "Invalid arguments"
	-- get modules accessible from module
	scopeModules' as [] copts = errorT $ do
		dbval <- getDb copts
		(srcFile, cabal) <- getCtx copts as
		liftM (ResultList . map (ResultModuleId . moduleId)) $ scopeModules dbval cabal srcFile
	scopeModules' as _ copts = return $ err "Invalid arguments"
	-- get declarations accessible from module
	scope' as [] copts = errorT $ do
		dbval <- getDb copts
		(srcFile, cabal) <- getCtx copts as
		liftM (ResultList . map ResultModuleDeclaration . filterMatch as) $ scope dbval cabal srcFile (flag "global" as)
	scope' as _ copts = return $ err "Invalid arguments"
	-- completion
	complete' as [] copts = complete' as [""] copts
	complete' as [input] copts = errorT $ do
		dbval <- getDb copts
		(srcFile, cabal) <- getCtx copts as
		liftM (ResultList . map ResultModuleDeclaration) $ completions dbval cabal srcFile input
	complete' as _ copts = return $ err "Invalid arguments"
	-- hayoo
	hayoo' as [] copts = return $ err "Query not specified"
	hayoo' as [query] copts = errorT $
		liftM
			(ResultList . map (ResultModuleDeclaration . Hayoo.hayooAsDeclaration) . Hayoo.hayooFunctions) $
			Hayoo.hayoo query
	hayoo' as _ copts = return $ err "Too much arguments"
	-- cabal list
	cabalList' as qs copts = errorT $ do
		ps <- Cabal.cabalList qs
		return $ ResultList $ map (ResultJSON . toJSON) ps
	-- ghc-mod type
	ghcmodType' as [line] copts = ghcmodType' as [line, "1"] copts
	ghcmodType' as [line, column] copts = errorT $ do
		line' <- maybe (throwError "line must be a number") return $ readMaybe line
		column' <- maybe (throwError "column must be a number") return $ readMaybe column
		dbval <- getDb copts
		(srcFile, cabal) <- getCtx copts as
		(srcFile', m, mproj) <- fileCtx dbval srcFile
		tr <- GhcMod.typeOf (list "ghc" as) cabal srcFile' mproj (moduleName m) line' column'
		return $ ResultList $ map ResultTyped tr
	ghcmodType' as [] copts = return $ err "Specify line"
	ghcmodType' as _ copts = return $ err "Too much arguments"
	-- dump cabal modules
	dumpCabal' as _ copts = errorT $ do
		dbval <- getDb copts
		cabals <- getSandboxes copts as
		let
			dats = map (id &&& flip cabalDB dbval) cabals
		liftM (fromMaybe (ResultList $ map (ResultDatabase . snd) dats)) $
			runMaybeT $ msum [
				maybeOpt "path" as $ (lift . findPath copts) >=> \p ->
					fork (forM_ dats $ \(cabal, dat) -> (dump (p </> cabalCache cabal) dat)),
				maybeOpt "file" as $ (lift . findPath copts) >=> \f ->
					fork (dump f $ mconcat $ map snd dats)]
	-- dump projects
	dumpProjects' as [] copts = errorT $ do
		dbval <- getDb copts
		ps' <- traverse (findProject copts) $ list "project" as
		let
			ps = if null ps' then M.elems (databaseProjects dbval) else ps'
			dats = map (id &&& flip projectDB dbval) ps
		liftM (fromMaybe (ResultList $ map (ResultDatabase . snd) dats)) $
			runMaybeT $ msum [
				maybeOpt "path" as $ (lift . findPath copts) >=> \p ->
					fork (forM_ dats $ \(proj, dat) -> (dump (p </> projectCache proj) dat)),
				maybeOpt "file" as $ (lift . findPath copts) >=> \f ->
					fork (dump f (mconcat $ map snd dats))]
	dumpProjects' as _ copts = return $ err "Invalid arguments"
	-- dump files
	dumpFiles' as [] copts = errorT $ do
		dbval <- getDb copts
		let
			dat = standaloneDB dbval
		liftM (fromMaybe $ ResultDatabase dat) $ runMaybeT $ msum [
			maybeOpt "path" as $ (lift . findPath copts) >=> \p ->
				fork (dump (p </> standaloneCache) dat),
			maybeOpt "file" as $ (lift . findPath copts) >=> \f ->
				fork (dump f dat)]
	dumpFiles' as _ copts = return $ err "Invalid arguments"
	-- dump database
	dump' as _ copts = errorT $ do
		dbval <- getDb copts
		liftM (fromMaybe $ ResultDatabase dbval) $ runMaybeT $ msum [
			do
				p <- MaybeT $ traverse (findPath copts) $ arg "path" as
				fork $ SC.dump p $ structurize dbval
				return ResultNone,
			do
				f <- MaybeT $ traverse (findPath copts) $ arg "file" as
				fork $ dump f dbval
				return ResultNone]
	-- load database
	load' as _ copts = do
		res <- liftM (fromMaybe (err "Specify one of: --path, --file or --data")) $ runMaybeT $ msum [
			do
				p <- MaybeT $ return $ arg "path" as
				forkOrWait as $ cacheLoad copts (liftA merge <$> SC.load p)
				return ok,
			do
				f <- MaybeT $ return $ arg "file" as
				e <- liftIO $ doesFileExist f
				forkOrWait as $ when e $ cacheLoad copts (load f)
				return ok,
			do
				dat <- MaybeT $ return $ arg "data" as
				forkOrWait as $ cacheLoad copts (return $ eitherDecode (toUtf8 dat))
				return ok]
		waitDb copts as
		return res
	-- link to server
	link' as _ copts = do
		commandLink copts
		when (flag "hold" as) $ commandHold copts
		return ok
	-- exit
	exit' _ copts = do
		commandExit copts
		return ok

	-- Helper functions
	cmd' :: [String] -> [String] -> String -> [OptDescr (Opts String)] -> (Opts String -> [String] -> a) -> Command (WithOpts a)
	cmd' name posArgs descr as act = cmd name posArgs descr as act' where
		act' os args = WithOpts (act os args) $ CommandCall name args os

	cmd_' :: [String] -> [String] -> String -> ([String] -> a) -> Command (WithOpts a)
	cmd_' name posArgs descr act = cmd_ name posArgs descr act' where
		act' args = WithOpts (act args) $ CommandCall name args defaultConfig

	findSandbox :: MonadIO m => CommandOptions -> Maybe FilePath -> ErrorT String m Cabal
	findSandbox copts = maybe
		(return Cabal)
		(findPath copts >=> mapErrorT liftIO . locateSandbox)

	findPath :: MonadIO m => CommandOptions -> FilePath -> ErrorT String m FilePath
	findPath copts f = liftIO $ canonicalizePath (normalise f') where
		f'
			| isRelative f = commandRoot copts </> f
			| otherwise = f

	getCtx :: (MonadIO m, Functor m) => CommandOptions -> Opts String -> ErrorT String m (FilePath, Cabal)
	getCtx copts as = liftM2 (,)
		(forceJust "No file specified" $ traverse (findPath copts) $ arg "file" as)
		(getCabal copts as)

	getCabal :: MonadIO m => CommandOptions -> Opts String -> ErrorT String m Cabal
	getCabal copts as
		| flag "cabal" as = findSandbox copts Nothing
		| otherwise  = findSandbox copts $ arg "sandbox" as

	getCabal_ :: (MonadIO m, Functor m) => CommandOptions -> Opts String -> ErrorT String m (Maybe Cabal)
	getCabal_ copts as
		| flag "cabal" as = Just <$> findSandbox copts Nothing
		| otherwise = case arg "sandbox" as of
			Just f -> Just <$> findSandbox copts (Just f)
			Nothing -> return Nothing

	getSandboxes :: (MonadIO m, Functor m) => CommandOptions -> Opts String -> ErrorT String m [Cabal]
	getSandboxes copts as = traverse (findSandbox copts) paths where
		paths
			| flag "cabal" as = Nothing : sboxes
			| otherwise = sboxes
		sboxes = map Just $ list "sandbox" as

	findProject :: MonadIO m => CommandOptions -> String -> ErrorT String m Project
	findProject copts proj = do
		db' <- getDb copts
		proj' <- liftM addCabal $ findPath copts proj
		let
			result =
				M.lookup proj' (databaseProjects db') <|>
				find ((== proj) . projectName) (M.elems $ databaseProjects db')
		maybe (throwError $ "Projects " ++ proj ++ " not found") return result
		where
			addCabal p
				| takeExtension p == ".cabal" = p
				| otherwise = p </> (takeBaseName p <.> "cabal")

	toPair :: Module -> Maybe (FilePath, Module)
	toPair m = case moduleLocation m of
		FileModule f _ -> Just (f, m)
		_ -> Nothing

	modCabal :: Module -> Maybe Cabal
	modCabal m = case moduleLocation m of
		CabalModule c _ _ -> Just c
		_ -> Nothing

	waitDb copts as = when (flag "wait" as) $ do
		commandLog copts "wait for db"
		DB.wait (dbVar copts)
		commandLog copts "db done"

	forkOrWait as act
		| flag "wait" as = liftIO act
		| otherwise = liftIO $ void $ forkIO act

	cacheLoad copts act = do
		db' <- act
		case db' of
			Left e -> commandLog copts e
			Right database -> DB.update (dbVar copts) (return database)

	localsDatabase :: Opts String -> Database -> Database
	localsDatabase as
		| flag "locals" as = databaseLocals
		| otherwise = id

	newest :: Symbol a => Opts String -> [a] -> [a]
	newest as
		| flag "no-last" as = id
		| otherwise = newestPackage

	forceJust :: MonadIO m => String -> ErrorT String m (Maybe a) -> ErrorT String m a
	forceJust msg act = act >>= maybe (throwError msg) return

	getDb :: (MonadIO m) => CommandOptions -> m Database
	getDb = liftIO . DB.readAsync . commandDatabase

	dbVar :: CommandOptions -> DB.Async Database
	dbVar = commandDatabase

	startProcess :: Opts String -> ((Update.Status -> IO ()) -> IO ()) -> IO CommandResult
	startProcess as f
		| flag "wait" as = return $ ResultProcess (f . onMsg)
		| otherwise = forkIO (f $ const $ return ()) >> return ok
		where
			onMsg showMsg
				| flag "status" as = showMsg
				| otherwise = const $ return ()
	error_ :: ErrorT String IO CommandResult -> IO CommandResult
	error_ = liftM (either err id) . runErrorT

	errorT :: ErrorT String IO ResultValue -> IO CommandResult
	errorT = liftM (either err ResultOk) . runErrorT

	errorT' :: ErrorT CommandResult IO ResultValue -> IO CommandResult
	errorT' = liftM (either id ResultOk) . runErrorT

	updateProcess :: CommandOptions -> Opts String -> ErrorT String (Update.UpdateDB IO) () -> IO CommandResult
	updateProcess opts as act = startProcess as $ \onStatus ->
		Update.updateDB
			(Update.Settings
				(commandDatabase opts)
				(commandReadCache opts)
				onStatus
				(list "ghc" as))
			act

	fork :: MonadIO m => IO () -> m ()
	fork = voidm . liftIO . forkIO

	voidm :: Monad m => m a -> m ()
	voidm act = act >> return ()

	maybeOpt :: Monad m => String -> Opts String -> (String -> MaybeT m a) -> MaybeT m ResultValue
	maybeOpt n as act = do
		p <- MaybeT $ return $ arg n as
		act p
		return ResultNone

	filterMatch :: Opts String -> [ModuleDeclaration] -> [ModuleDeclaration]
	filterMatch as = findMatch as . prefMatch as

	findMatch :: Opts String -> [ModuleDeclaration] -> [ModuleDeclaration]
	findMatch as = case arg "find" as of
		Nothing -> id
		Just str -> filter (match' str)
		where
			match' str m = str `isInfixOf` declarationName (moduleDeclaration m)

	prefMatch :: Opts String -> [ModuleDeclaration] -> [ModuleDeclaration]
	prefMatch as = case fmap splitIdentifier (arg "prefix" as) of
		Nothing -> id
		Just (qname, pref) -> filter (match' qname pref)
		where
			match' qname pref m =
				pref `isPrefixOf` declarationName (moduleDeclaration m) &&
				maybe True (== moduleIdName (declarationModuleId m)) qname

processCmd :: CommandOptions -> Int -> String -> (Response -> IO ()) -> IO ()
processCmd copts tm cmdLine sendResponse = processCmdArgs copts tm (splitArgs cmdLine) sendResponse

-- | Process command, returns 'False' if exit requested
processCmdArgs :: CommandOptions -> Int -> [String] -> (Response -> IO ()) -> IO ()
processCmdArgs copts tm cmdArgs sendResponse = run (map (fmap withOptsAct) commands) (asCmd unknownCommand) (asCmd . commandError) cmdArgs tm copts >>= sendResponses where
	asCmd :: CommandResult -> (Int -> CommandOptions -> IO CommandResult)
	asCmd r _ _ = return r

	unknownCommand :: CommandResult
	unknownCommand = err "Unknown command"
	commandError :: [String] -> CommandResult
	commandError errs = errArgs "Command syntax error" [("what", ResultList $ map ResultString errs)]

	sendResponses :: CommandResult -> IO ()
	sendResponses (ResultOk v) = sendResponse $ Response $ toJSON v
	sendResponses (ResultError e args) = sendResponse $ Response $ object [
		"error" .= e,
		"details" .= args]
	sendResponses (ResultProcess act) = do
		act (sendResponse . ResponseStatus)
		sendResponses ok
		`catch`
		processFailed
		where
			processFailed :: SomeException -> IO ()
			processFailed e = sendResponses $ errArgs "process throws exception" [
				("exception", ResultString $ show e)]

hGetLine' :: Handle -> IO ByteString
hGetLine' = fmap L.fromStrict . B.hGetLine

race :: [IO ()] -> IO ()
race acts = do
	v <- newEmptyMVar
	forM_ acts $ \a -> forkIO ((a `finally` putMVar v ()) `catch` ignoreError)
	takeMVar v
	where
		ignoreError :: SomeException -> IO ()
		ignoreError _ = return ()

logIO :: String -> (String -> IO ()) -> IO () -> IO ()
logIO pre out act = handle onIO act where
	onIO :: IOException -> IO ()
	onIO e = out $ pre ++ show e

ignoreIO :: IO () -> IO ()
ignoreIO = handle (const (return ()) :: IOException -> IO ())

logMsg :: ServerOpts -> String -> IO ()
logMsg sopts s = ignoreIO $ do
	putStrLn s
	case getFirst (serverLog sopts) of
		Nothing -> return ()
		Just f -> withFile f AppendMode (`hPutStrLn` s)