hsdev-0.1.0.0: tools/Commands.hs
{-# LANGUAGE OverloadedStrings, CPP #-}
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.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 withOptsArgs
srvCmds = [
cmd_ ["run"] [] "run interactive" runi',
cmd ["server", "start"] [] "start remote server" serverOpts start',
cmd ["server", "run"] [] "start server" serverOpts run',
cmd ["server", "stop"] [] "stop remote server" clientOpts stop']
runi' _ = do
dir <- getCurrentDirectory
db <- DB.newAsync
forever $ do
s <- getLine
processCmd (CommandOptions db (const $ return ()) (const $ return Nothing) dir putStrLn getLine (error "Not supported") exitSuccess) 1000 s (L.putStrLn . encode)
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 _ = do
msgs <- F.newChan
outputDone <- newEmptyMVar
forkIO $ finally
(F.readChan msgs >>= mapM_ (logMsg sopts))
(putMVar outputDone ())
let
outputStr = F.putChan msgs
waitOutput = F.closeChan msgs >> takeMVar outputDone
withCache :: a -> (FilePath -> IO a) -> IO a
withCache v onCache = case getFirst (serverCache sopts) of
Nothing -> return v
Just cdir -> onCache cdir
writeCache :: Database -> IO ()
writeCache d = withCache () $ \cdir -> do
outputStr "writing cache"
SC.dump cdir (structurize d)
readCache :: (FilePath -> ErrorT String IO Structured) -> IO (Maybe Database)
readCache act = withCache Nothing $ join . liftM (either cacheErr cacheOk) . runErrorT . act where
cacheErr e = outputStr ("Unable read cache: " ++ e) >> return Nothing
cacheOk s = do
forM_ (M.keys (structuredCabals s)) $ \c -> outputStr ("cache read: cabal " ++ show c)
forM_ (M.keys (structuredProjects s)) $ \p -> outputStr ("cache read: project " ++ p)
case allModules (structuredFiles s) of
[] -> return ()
ms -> outputStr $ "cache read: " ++ show (length ms) ++ " files"
return $ Just $ merge s
outputStr $ "Server started at port " ++ show (fromJust $ getFirst $ serverPort sopts)
logIO "server exception: " outputStr $ flip finally waitOutput $ do
db <- DB.newAsync
when (getAny $ serverLoadCache sopts) $ withCache () $ \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')
waitListen <- newEmptyMVar
clientChan <- F.newChan
linkChan <- F.newChan
let
linkToSrv :: IO ()
linkToSrv = do
v <- newEmptyMVar
F.putChan linkChan (putMVar v ())
takeMVar v
#if mingw32_HOST_OS
mmapPool <- createPool "hsdev"
let
-- | Send response as is or via memory mapped file
sendResponse :: Handle -> Response -> IO ()
sendResponse h r@(ResponseMapFile _) = L.hPutStrLn h $ encode r
sendResponse h r
| L.length msg <= 1024 = L.hPutStrLn h msg
| otherwise = do
sync <- newEmptyMVar
forkIO $ void $ withName mmapPool $ \mmapName -> do
runErrorT $ flip catchError
(\e -> liftIO $ do
sendResponse h $ Response $ object ["error" .= e]
putMVar sync ())
(withMapFile mmapName (L.toStrict msg) $ liftIO $ do
sendResponse h $ ResponseMapFile mmapName
putMVar sync ()
-- Dirty: give 10 seconds for client to read it
threadDelay 10000000)
takeMVar sync
where
msg = encode r
#else
let
sendResponse h = L.hPutStrLn h . encode
#endif
forkIO $ do
accepter <- myThreadId
let
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: " outputStr $ do
s' <- fmap fst $ accept s
outputStr $ show s' ++ " connected"
void $ forkIO $ logIO (show s' ++ " exception: ") outputStr $
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 10000000
tryPutMVar done ()
killThread me
void $ takeMVar done
F.putChan clientChan timeoutWait
req <- hGetLine' h
outputStr $ show s' ++ ": " ++ fromUtf8 req
case fmap extractCurrentDir $ eitherDecode req of
Left reqErr -> sendResponse h $ Response $ object [
"error" .= ("Invalid request" :: String),
"request" .= fromUtf8 req,
"what" .= reqErr]
Right (clientDir, reqArgs) -> processCmdArgs
(CommandOptions
db
writeCache
readCache
clientDir
outputStr
(fromUtf8 <$> hGetLine' h)
linkToSrv
serverStop)
(fromJust $ getFirst $ serverTimeout sopts) reqArgs (sendResponse h)
-- Send 'end' message and wait client
L.hPutStrLn h L.empty
outputStr $ "waiting " ++ show s'
ignoreIO $ void $ timeout 10000000 $ hGetLine' h
outputStr $ show s' ++ " disconnected"
takeMVar waitListen
withCache () $ \cdir -> do
outputStr $ "saving cache to " ++ cdir
logIO "cache saving exception: " outputStr $ do
dbval <- DB.readAsync db
SC.dump cdir $ structurize dbval
outputStr "cache saved"
outputStr "closing links"
F.stopChan linkChan >>= sequence_
outputStr "waiting for clients"
F.stopChan clientChan >>= sequence_
outputStr "server shutdown"
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 . fmap ((,) copts) . withOptsArgs)
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)
-- Send command to server
sendCmd :: IO (ClientOpts, [String]) -> IO ()
sendCmd get' = do
svar <- newEmptyMVar
race [takeMVar svar, waitResponse >> putMVar svar ()]
where
waitResponse = do
curDir <- getCurrentDirectory
(p, as) <- get'
stdinData <- if getAny (clientData p)
then do
cdata <- liftM eitherDecode 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 $ ["--current-directory=" ++ curDir] ++ setData stdinData as
responses <- liftM (takeWhile (not . L.null) . L.lines) $ L.hGetContents h
forM_ responses $
parseResponse >=>
(L.putStrLn . encodeValue (getAny $ clientPretty p))
setData :: Maybe ResultValue -> [String] -> [String]
setData Nothing = id
setData (Just d) = (++ ["--data=" ++ (fromUtf8 $ encode d)])
parseResponse r = fmap (either err' id) $ runErrorT $ do
v <- errT (eitherDecode r) `orFail` (\e -> "Can't decode response")
case v of
Response rv -> return rv
ResponseStatus sv -> return sv
#if mingw32_HOST_OS
ResponseMapFile viewFile -> do
str <- fmap L.fromStrict (readMapFile viewFile) `orFail`
(\e -> "Can't read map view of file")
lift $ parseResponse str
#else
ResponseMapFile viewFile -> return $ err' ("Not supported" :: String)
#endif
where
errT act = ErrorT $ return act
orFail act msg = act `catchError` (throwError . msg)
err' msg = object ["error" .= msg]
encodeValue True = encodePretty
encodeValue False = encode
-- Add parsing 'ClieptOpts'
addClientOpts :: Command (IO [String]) -> Command (IO (ClientOpts, [String]))
addClientOpts c = c { commandRun = run' } where
run' args = fmap (fmap (fmap $ (,) p)) $ commandRun c args' where
(ps, args', _) = getOpt RequireOrder clientOpts args
p = mconcat ps `mappend` defaultConfig
extractCurrentDir :: [String] -> (FilePath, [String])
extractCurrentDir as = (head $ cur ++ ["."], as') where
(cur, as', _) = getOpt RequireOrder curDirOpts as
curDirOpts = [Option [] ["current-directory"] (ReqArg id "path") "current directory"]
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" [
sandbox, ghcOpts, wait, status] scanCabal',
cmd' ["scan", "module"] ["module name"] "scan module in cabal" [sandbox, 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" [
sandbox,
projectArg "module project",
fileArg "module source file",
moduleArg,
packageArg, noLastArg, packageVersionArg,
allFlag] remove',
-- | Context free commands
cmd' ["list", "modules"] [] "list modules" [
projectArg "project to list modules from",
noLastArg,
packageArg,
sandbox, sourced, standaloned] listModules',
cmd_' ["list", "packages"] [] "list packages" listPackages',
cmd_' ["list", "projects"] [] "list projects" listProjects',
cmd' ["symbol"] ["name"] "get symbol info" (matches ++ [
projectArg "related project",
fileArg "source file",
moduleArg, localsArg,
packageArg, noLastArg, packageVersionArg,
sandbox, sourced, standaloned]) symbol',
cmd' ["module"] [] "get module info" [
moduleArg, localsArg,
packageArg, noLastArg, packageVersionArg,
projectArg "module project",
fileArg "module source file",
sandbox, 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" [sandbox, 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" [] link']
-- Command arguments and flags
allFlag = option_ ['a'] "all" flag "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" flag "scope of project"
localsArg = option_ ['l'] "locals" flag "look in local declarations"
noLastArg = option_ [] "no-last" flag "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" (noreq "path") "path to cabal sandbox"
sourced = option_ [] "src" flag "source files"
standaloned = option_ [] "stand" flag "standalone files"
status = option_ ['s'] "status" flag "show status of operation, works only with --wait"
wait = option_ ['w'] "wait" flag "wait for operation to complete"
-- ping server
ping' _ copts = return $ ResultOk $ ResultString "pong"
-- add data
add' as _ copts = do
dbval <- getDb copts
res <- runErrorT $ do
jsonData <- maybe (throwError $ err "Specify --data") return $ askOpt "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_ (askOpts n as) (canonicalizePath' copts >=> f (getGhcOpts as))) [
("project", Update.scanProject),
("file", Update.scanFile),
("path", Update.scanDirectory)]
-- scan cabal
scanCabal' as _ copts = error_ $ do
cabal <- getCabal copts as
lift $ updateProcess copts as $ Update.scanCabal (getGhcOpts as) cabal
-- 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 (getGhcOpts 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 <- askOpts "project" as
return $ do
p' <- getProject copts p
return $ M.fromList $ mapMaybe toPair $
selectModules (inProject p' . moduleId) dbval,
do
f <- askOpts "file" as
return $ maybe
(throwError $ "Unknown file: " ++ f)
(return . M.singleton f)
(lookupFile f dbval),
do
d <- askOpts "path" as
return $ return $ M.filterWithKey (\f _ -> isParent d f) fileMap]
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 (getGhcOpts as) . inspectedId) rescanMods
Update.scanModules (getGhcOpts as) (map (inspectedId &&& inspectionOpts . inspection) needRescan)
-- remove
remove' as _ copts = errorT $ do
dbval <- liftIO $ getDb copts
cabal <- askCabal copts as
proj <- askProject copts as
file <- traverse (canonicalizePath' copts) $ askOpt "file" as
let
cleanAll = hasOpt "all" as
filters = catMaybes [
fmap inProject proj,
fmap inFile file,
fmap inModule (askOpt "module" as),
fmap inPackage (askOpt "package" as),
fmap inVersion (askOpt "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 <- liftIO $ getDb copts
proj <- askProject copts as
cabal <- askCabal copts as
let
filters = allOf $ catMaybes [
fmap inProject proj,
fmap inPackage (askOpt "package" as),
fmap inCabal cabal,
if hasOpt "src" as then Just byFile else Nothing,
if hasOpt "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) $ liftIO $ getDb copts
proj <- askProject copts as
file <- traverse (canonicalizePath' copts) $ askOpt "file" as
cabal <- askCabal copts as
let
filters = checkModule $ allOf $ catMaybes [
fmap inProject proj,
fmap inFile file,
fmap inModule (askOpt "module" as),
fmap inPackage (askOpt "package" as),
fmap inVersion (askOpt "version" as),
fmap inCabal cabal,
if hasOpt "src" as then Just byFile else Nothing,
if hasOpt "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) $ liftIO $ getDb copts
proj <- mapErrorT (fmap $ strMsg +++ id) $ askProject copts as
cabal <- mapErrorT (fmap $ strMsg +++ id) $ askCabal copts as
file' <- traverse (canonicalizePath' copts) $ askOpt "file" as
let
filters = allOf $ catMaybes [
fmap inProject proj,
fmap inCabal cabal,
fmap inFile file',
fmap inModule (askOpt "module" as),
fmap inPackage (askOpt "package" as),
fmap inVersion (askOpt "version" as),
if hasOpt "src" as then Just byFile else Nothing]
rs <- mapErrorT (fmap $ strMsg +++ id) $
(newest as . filter (filters . moduleId)) <$> maybe
(return $ allModules dbval)
(findModule dbval)
(askOpt "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 <- askProject copts as
proj' <- maybe (throwError "Specify project name of .cabal file") return proj
return $ ResultProject proj'
-- lookup info about symbol
lookup' as [nm] copts = errorT $ do
dbval <- liftIO $ getDb copts
(srcFile, cabal) <- askCtx 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 <- liftIO $ getDb copts
(srcFile, cabal) <- askCtx 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 <- liftIO $ getDb copts
(srcFile, cabal) <- askCtx 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 <- liftIO $ getDb copts
(srcFile, cabal) <- askCtx copts as
liftM (ResultList . map ResultModuleDeclaration . filterMatch as) $ scope dbval cabal srcFile (hasOpt "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) <- askCtx 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 <- liftIO $ getDb copts
(srcFile, cabal) <- askCtx copts as
(srcFile', m, mproj) <- fileCtx dbval srcFile
tr <- GhcMod.typeOf (getGhcOpts 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 <- liftIO $ getDb copts
cabal <- getCabal copts as
let
dat = cabalDB cabal dbval
liftM (fromMaybe (ResultDatabase dat)) $ runMaybeT $ msum [
maybeOpt "path" as $ canonicalizePath' copts >=> \p ->
fork (dump (p </> cabalCache cabal) dat),
maybeOpt "file" as $ canonicalizePath' copts >=> \f ->
fork (dump f dat)]
-- dump projects
dumpProjects' as [] copts = errorT $ do
dbval <- liftIO $ getDb copts
ps' <- traverse (getProject copts) $ askOpts "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 $ canonicalizePath' copts >=> \p ->
fork (forM_ dats $ \(proj, dat) -> (dump (p </> projectCache proj) dat)),
maybeOpt "file" as $ canonicalizePath' copts >=> \f ->
fork (dump f (mconcat $ map snd dats))]
dumpProjects' as _ copts = return $ err "Invalid arguments"
-- dump files
dumpFiles' as [] copts = do
dbval <- getDb copts
let
dat = standaloneDB dbval
liftM (ResultOk . fromMaybe (ResultDatabase dat)) $ runMaybeT $ msum [
maybeOpt "path" as $ canonicalizePath' copts >=> \p ->
fork (dump (p </> standaloneCache) dat),
maybeOpt "file" as $ canonicalizePath' copts >=> \f ->
fork (dump f dat)]
dumpFiles' as _ copts = return $ err "Invalid arguments"
-- dump database
dump' as _ copts = do
dbval <- getDb copts
liftM (fromMaybe (ResultOk $ ResultDatabase dbval)) $ runMaybeT $ msum [
do
p <- MaybeT $ traverse (canonicalizePath' copts) $ askOpt "path" as
fork $ SC.dump p $ structurize dbval
return ok,
do
f <- MaybeT $ traverse (canonicalizePath' copts) $ askOpt "file" as
fork $ dump f dbval
return ok]
-- load database
load' as _ copts = do
res <- liftM (fromMaybe (err "Specify one of: --path, --file or --data")) $ runMaybeT $ msum [
do
p <- MaybeT $ return $ askOpt "path" as
forkOrWait as $ cacheLoad copts (liftA merge <$> SC.load p)
return ok,
do
f <- MaybeT $ return $ askOpt "file" as
e <- liftIO $ doesFileExist f
forkOrWait as $ when e $ cacheLoad copts (load f)
return ok,
do
dat <- MaybeT $ return $ askOpt "data" as
forkOrWait as $ cacheLoad copts (return $ eitherDecode (toUtf8 dat))
return ok]
waitDb copts as
return res
-- link to server
link' as _ copts = do
race [void (commandWaitInput copts) `finally` commandExit copts, commandLink copts]
return ok
-- exit
exit' _ copts = do
commandExit copts
return ok
-- Helper functions
cmd' :: [String] -> [String] -> String -> [OptDescr Opts] -> (Opts -> [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) $
return $ name ++ args ++ optsToArgs 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) $
return $ name ++ args ++ optsToArgs defaultConfig
getGhcOpts = askOpts "ghc"
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 (hasOpt "wait" as) $ do
commandLog copts "wait for db"
DB.wait (dbVar copts)
commandLog copts "db done"
forkOrWait as act
| hasOpt "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)
asCabal :: CommandOptions -> Maybe FilePath -> ErrorT String IO Cabal
asCabal copts = maybe
(return Cabal)
(canonicalizePath' copts >=> locateSandbox)
askCabal :: CommandOptions -> Opts -> ErrorT String IO (Maybe Cabal)
askCabal copts as = traverse (asCabal copts) $ askOptDef "sandbox" as
getCabal :: CommandOptions -> Opts -> ErrorT String IO Cabal
getCabal copts as = asCabal copts $ askOpt "sandbox" as
getProject :: CommandOptions -> String -> ErrorT String IO Project
getProject copts proj = do
db' <- getDb copts
proj' <- liftM addCabal $ canonicalizePath' copts proj
let
result =
M.lookup proj' (databaseProjects db') <|>
find ((== proj) . projectName) (M.elems $ databaseProjects db')
maybe (throwError $ "Project " ++ proj ++ " not found") return result
where
addCabal p
| takeExtension p == ".cabal" = p
| otherwise = p </> (takeBaseName p <.> "cabal")
localsDatabase :: Opts -> Database -> Database
localsDatabase as
| hasOpt "locals" as = databaseLocals
| otherwise = id
newest :: Symbol a => Opts -> [a] -> [a]
newest as
| hasOpt "no-last" as = id
| otherwise = newestPackage
askProject :: CommandOptions -> Opts -> ErrorT String IO (Maybe Project)
askProject copts = traverse (getProject copts) . askOpt "project"
askFile :: CommandOptions -> Opts -> ErrorT String IO (Maybe FilePath)
askFile copts = traverse (canonicalizePath' copts) . askOpt "file"
forceJust :: String -> ErrorT String IO (Maybe a) -> ErrorT String IO a
forceJust msg act = act >>= maybe (throwError msg) return
askCtx :: CommandOptions -> Opts -> ErrorT String IO (FilePath, Cabal)
askCtx copts as = liftM2 (,)
(forceJust "No file specified" $ askFile copts as)
(getCabal copts as)
getDb :: (MonadIO m) => CommandOptions -> m Database
getDb = liftIO . DB.readAsync . commandDatabase
dbVar :: CommandOptions -> DB.Async Database
dbVar = commandDatabase
canonicalizePath' :: MonadIO m => CommandOptions -> FilePath -> m FilePath
canonicalizePath' copts f = liftIO $ canonicalizePath (normalise f') where
f'
| isRelative f = commandRoot copts </> f
| otherwise = f
startProcess :: Opts -> ((Value -> IO ()) -> IO ()) -> IO CommandResult
startProcess as f
| hasOpt "wait" as = return $ ResultProcess (f . onMsg)
| otherwise = forkIO (f $ const $ return ()) >> return ok
where
onMsg showMsg
| hasOpt "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 -> ErrorT String (Update.UpdateDB IO) () -> IO CommandResult
updateProcess opts as act = startProcess as $ \onStatus -> Update.updateDB (Update.Settings (commandDatabase opts) (commandReadCache opts) onStatus (getGhcOpts 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 -> MaybeT m a) -> MaybeT m ResultValue
maybeOpt n as act = do
p <- MaybeT $ return $ askOpt n as
act p
return ResultNone
filterMatch :: Opts -> [ModuleDeclaration] -> [ModuleDeclaration]
filterMatch as = findMatch as . prefMatch as
findMatch :: Opts -> [ModuleDeclaration] -> [ModuleDeclaration]
findMatch as = case askOpt "find" as of
Nothing -> id
Just str -> filter (match' str)
where
match' str m = str `isInfixOf` declarationName (moduleDeclaration m)
prefMatch :: Opts -> [ModuleDeclaration] -> [ModuleDeclaration]
prefMatch as = case fmap splitIdentifier (askOpt "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 ()