{-# 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)