{-# LANGUAGE FlexibleContexts, OverloadedStrings, MultiParamTypeClasses, RankNTypes, TypeOperators, TypeApplications #-} {-# OPTIONS_GHC -fno-warn-orphans #-} module HsDev.Database.Update ( Status(..), Progress(..), Task(..), UpdateOptions(..), UpdateM(..), runUpdate, postStatus, updater, runTask, runTasks, runTasks_, scanModules, scanFile, scanFiles, scanFileContents, scanCabal, prepareSandbox, scanSandbox, scanPackageDb, scanProjectFile, scanProjectStack, scanProject, scanDirectory, scanContents, scanPackageDbStackDocs, scanDocs, setModTypes, inferModTypes, scan, processEvents, updateEvents, applyUpdates, cacheGhcWarnings, cachedWarnings, module HsDev.Database.Update.Types, module HsDev.Watcher, module Control.Monad.Except ) where import qualified Control.Concurrent.Async as A import Control.Concurrent.MVar import Control.DeepSeq import Control.Exception (ErrorCall, evaluate, displayException) import Control.Lens import Control.Monad.Catch (catch, handle, MonadThrow, bracket_) import Control.Monad.Except import Control.Monad.Reader import Control.Monad.Writer import Control.Monad.State (get, modify, runStateT) import Data.Aeson import Data.List (intercalate) import Data.String (fromString) import qualified Data.Map.Strict as M import qualified Data.Set as S import Data.Maybe import Data.Text (Text) import Data.Time.Clock.POSIX import qualified Data.Text as T import System.FilePath import qualified System.Log.Simple as Log import HsDev.Error import qualified HsDev.Database.SQLite as SQLite import HsDev.Display import HsDev.Inspect import HsDev.Inspect.Order import HsDev.PackageDb import HsDev.Project import HsDev.Sandbox import qualified HsDev.Stack as S import HsDev.Symbols import HsDev.Tools.Ghc.Session hiding (Session, evaluate) import HsDev.Tools.Ghc.Types (fileTypes, TypedExpr) import HsDev.Tools.Types import HsDev.Tools.HDocs import qualified HsDev.Scan as S import HsDev.Scan.Browse import HsDev.Util (ordNub, fromJSON', uniqueBy, timer) import qualified HsDev.Util as Util (withCurrentDirectory) import HsDev.Server.Types (commandNotify, inSessionGhc, FileSource(..)) import HsDev.Server.Message import HsDev.Database.Update.Types import HsDev.Watcher import Text.Format import System.Directory.Paths onStatus :: UpdateMonad m => m () onStatus = asks (view (updateOptions . updateTasks)) >>= commandNotify . Notification . toJSON . reverse childTask :: UpdateMonad m => Task -> m a -> m a childTask t = local (over (updateOptions . updateTasks) (t:)) transact :: SessionMonad m => m a -> m a transact = SQLite.transaction_ SQLite.Immediate runUpdate :: ServerMonadBase m => UpdateOptions -> UpdateM m a -> ClientM m a runUpdate uopts act = Log.scope "update" $ do (r, updatedMods) <- withUpdateState uopts $ \ust -> runWriterT (runUpdateM act' `runReaderT` ust) Log.sendLog Log.Debug $ "updated {} modules" ~~ length updatedMods return r where act' = do (r, _) <- listen act -- (r, mlocs') <- listen act -- dbs <- liftM S.unions $ forM mlocs' $ \mloc' -> do -- mid <- SQLite.lookupModuleLocation mloc' -- case mid of -- Nothing -> return (S.empty :: S.Set PackageDb) -- Just mid' -> liftM (S.fromList . map SQLite.fromOnly) $ SQLite.query (SQLite.toQuery $ SQLite.select_ -- ["ps.package_db"] -- ["package_dbs as ps", "modules as m"] -- ["m.package_name == ps.package_name", "m.package_version == ps.package_version", "m.id == ?"]) (SQLite.Only mid') -- If some sourced files depends on currently scanned package-dbs -- We must resolve them and even rescan if there was errors scanning without -- dependencies provided (lack of fixities can cause errors inspecting files) -- sboxes = databaseSandboxes dbval -- sboxOf :: Path -> Maybe Sandbox -- sboxOf fpath = find (pathInSandbox fpath) sboxes -- projsRows <- SQLite.query_ "select name, cabal, version, ifnull(package_db_stack, json('[]')) from projects;" -- let -- projs = [proj' | (proj' SQLite.:. (SQLite.Only (SQLite.JSON projPdbs))) <- projsRows, -- not (S.null (S.fromList projPdbs `S.intersection` dbs))] -- stands = [] -- -- HOWTO? -- -- stands = do -- -- sloc <- dbval ^.. standaloneSlice . modules . moduleId . moduleLocation -- -- guard $ sboxUpdated $ sboxOf (sloc ^?! moduleFile) -- -- guard (notElem sloc mlocs') -- -- return (sloc, dbval ^.. databaseModules . ix sloc . inspection . inspectionOpts . each . unpacked, Nothing) -- Log.sendLog Log.Trace $ "updated package-dbs: {}, have to rescan {} projects and {} files" -- ~~ intercalate ", " (map display $ S.toList dbs) -- ~~ length projs ~~ length stands -- (_, rlocs') <- listen $ runTasks_ (scanModules [] stands : [scanProject [] (proj ^. projectCabal) | proj <- projs]) -- let -- ulocs' = filter (isJust . preview moduleFile) (ordNub $ mlocs' ++ rlocs') -- getMods :: (MonadIO m) => m [InspectedModule] -- getMods = do -- db' <- liftIO $ readAsync db -- return $ filter ((`elem` ulocs') . view inspectedKey) $ toList $ view databaseModules db' -- FIXME: Now it's broken since `Database` is not used anymore when (view updateDocs uopts) $ do Log.sendLog Log.Trace "forking inspecting source docs" Log.sendLog Log.Warning "not implemented" -- void $ fork (getMods >>= waiter . mapM_ scanDocs_) when (view updateInfer uopts) $ do Log.sendLog Log.Trace "forking inferring types" Log.sendLog Log.Warning "not implemented" -- void $ fork (getMods >>= waiter . mapM_ inferModTypes_) return r -- scanDocs_ :: UpdateMonad m => InspectedModule -> m () -- scanDocs_ im = do -- im' <- (S.scanModify (\opts -> inSessionGhc . liftGhc . inspectDocsGhc opts) im) <|> return im -- sendUpdateAction $ Log.scope "scan-docs" $ SQLite.updateModule im' -- inferModTypes_ :: UpdateMonad m => InspectedModule -> m () -- inferModTypes_ im = do -- -- TODO: locate sandbox -- im' <- (S.scanModify infer' im) <|> return im -- sendUpdateAction $ Log.scope "infer-types" $ SQLite.updateModule im' -- infer' :: UpdateMonad m => [String] -> Module -> m Module -- infer' opts m = case preview (moduleId . moduleLocation . moduleFile) m of -- Nothing -> return m -- Just _ -> inSessionGhc $ do -- targetSession opts m -- inferTypes opts m Nothing -- | Post status postStatus :: UpdateMonad m => Task -> m () postStatus t = childTask t onStatus -- | Mark module as updated updater :: UpdateMonad m => [ModuleLocation] -> m () updater mlocs = tell $!! mlocs -- | Run one task runTask :: (Display t, UpdateMonad m, NFData a) => String -> t -> m a -> m a runTask action subj act = Log.scope "task" $ do postStatus $ set taskStatus StatusWorking task x <- childTask task act x `deepseq` postStatus (set taskStatus StatusOk task) return x `catch` (\e -> postStatus (set taskStatus (StatusError e) task) >> hsdevError e) where task = Task { _taskName = action, _taskStatus = StatusWorking, _taskSubjectType = displayType subj, _taskSubjectName = display subj, _taskProgress = Nothing } -- | Run many tasks with numeration runTasks :: UpdateMonad m => [m a] -> m [a] runTasks ts = liftM catMaybes $ zipWithM taskNum [1..] (map noErr ts) where total = length ts taskNum n = local setProgress where setProgress = set (updateOptions . updateTasks . _head . taskProgress) (Just (Progress n total)) noErr v = hsdevIgnore Nothing (Just <$> v) -- | Run many tasks with numeration runTasks_ :: UpdateMonad m => [m ()] -> m () runTasks_ = void . runTasks data PreloadFailure = PreloadFailure ModuleLocation Inspection HsDevError instance NFData PreloadFailure where rnf (PreloadFailure mloc insp err) = rnf mloc `seq` rnf insp `seq` rnf err -- | Scan modules scanModules :: UpdateMonad m => [String] -> [S.ModuleToScan] -> m () scanModules opts ms = Log.scope "scan-modules" $ mapM_ (uncurry scanModules') grouped where scanModules' mproj ms' = do maybe (return ()) (sendUpdateAction . SQLite.updateProject) mproj updater $ ms' ^.. each . _1 defines <- askSession sessionDefines let pload (mloc, mopts, mcts) = runTask "preloading" mloc $ do mfcts <- maybe (S.getFileContents (mloc ^?! moduleFile)) (const $ return Nothing) mcts insp <- liftIO $ fileInspection (mloc ^?! moduleFile) (opts ++ mopts) case (mfcts ^? _Just . _1) of Just tm -> Log.sendLog Log.Trace $ "using edited file contents, mtime = {}" ~~ show tm Nothing -> return () let inspection' = maybe insp (fileContentsInspection_ (opts ++ mopts)) $ mfcts ^? _Just . _1 dirtyTag' = maybe id (const $ inspectTag DirtyTag) $ mfcts ^? _Just . _1 mcts' = mplus mcts (mfcts ^? _Just . _2) runInspect mloc $ withInspection (return inspection') $ dirtyTag' $ preload (mloc ^?! moduleFile) defines (opts ++ mopts) mcts' ploaded <- runTasks (map pload ms') mapM_ (SQLite.upsertModule . fmap (view asModule)) ploaded let mlocs' = ploaded ^.. each . inspected . preloadedId . moduleLocation updater mlocs' let mcabal = mproj ^? _Just . projectCabal (env, fixities) <- loadEnv mcabal Log.sendLog Log.Trace $ "resolved environment: {} modules" ~~ M.size env case orderBy (preview inspected) ploaded of Left err -> Log.sendLog Log.Error ("failed order dependencies for files: {}" ~~ show err) Right ordered -> do (ms'', (updEnv, updFixities)) <- flip runStateT (env, fixities) $ runTasks (map inspect' ordered) saveEnv mcabal updEnv updFixities mlocs'' <- timer "updated scanned modules" $ do Log.sendLog Log.Trace $ case mproj of Just proj -> "inserting data for resolved modules of project: {}" ~~ proj Nothing -> "inserting data for resolved standalone modules" sendUpdateAction $ Log.scope "resolved" $ updateResolveds mcabal ms'' return (ms'' ^.. each . inspectedKey) updater mlocs'' where inspect' pmod = runTask "scanning" ploc $ Log.scope "module" $ do (env', fixities') <- get r <- continueInspect pmod $ \p -> do resolved' <- msum [ resolveModule env' fixities' p, do lift (Log.sendLog Log.Trace ("error resolving module {}, falling to resolving just imports/scope" ~~ (p ^. preloadedId . moduleLocation))) resolvePreloaded env' p] eval resolved' modify $ mappend ( maybe mempty resolvedEnv (r ^? inspected), maybe mempty resolvedFixitiesTable (r ^? inspected)) return r where ploc = pmod ^?! inspected . preloadedId . moduleLocation grouped = M.toList $ M.unionsWith (++) [M.singleton (m ^? _1 . moduleProject . _Just) [m] | m <- ms] eval v = handle onError (v `deepseq` liftIO (evaluate v)) where onError :: MonadThrow m => ErrorCall -> m a onError = hsdevError . OtherError . displayException -- | Scan source file, resolve dependent modules scanFile :: UpdateMonad m => [String] -> Path -> m () scanFile opts fpath = scanFiles [(FileSource fpath Nothing, opts)] -- | Scan source files, resolving dependent modules scanFiles :: UpdateMonad m => [(FileSource, [String])] -> m () scanFiles fsrcs = runTask "scanning" ("files" :: String) $ Log.scope "files" $ hsdevLiftIO $ do Log.sendLog Log.Trace $ "scanning {} files" ~~ length fsrcs fpaths' <- traverse (liftIO . canonicalize) $ map (fileSource . fst) fsrcs forM_ fpaths' $ \fpath' -> do ex <- liftIO $ fileExists fpath' unless ex $ hsdevError $ FileNotFound fpath' mlocs <- forM fpaths' $ \fpath' -> do mids <- SQLite.query (SQLite.toQuery $ SQLite.qModuleId `mappend` SQLite.where_ ["mu.file == ?"]) (SQLite.Only fpath') if length mids > 1 then return (head mids ^. moduleLocation) else do mproj <- locateProjectInfo fpath' return $ FileModule fpath' mproj let filesMods = liftM concat $ forM fpaths' $ \fpath' -> SQLite.query "select m.id, m.file, m.cabal, m.install_dirs, m.package_name, m.package_version, m.installed_name, m.exposed, m.other_location, m.inspection_time, m.inspection_opts from modules as m where m.file == ?;" (SQLite.Only fpath') scan filesMods [(mloc, opts, mcts) | (mloc, (FileSource _ mcts, opts)) <- zip mlocs fsrcs] [] $ \mlocs' -> do mapM_ ((watch . flip watchModule) . view _1) mlocs' S.ScanContents dmods _ _ <- fmap mconcat $ mapM (S.enumDependent . view (_1 . moduleFile . path)) mlocs' Log.sendLog Log.Trace $ "dependent modules: {}" ~~ length dmods scanModules [] (mlocs' ++ dmods) -- | Scan source file with contents and resolve dependent modules scanFileContents :: UpdateMonad m => [String] -> Path -> Maybe Text -> m () scanFileContents opts fpath mcts = scanFiles [(FileSource fpath mcts, opts)] -- | Scan cabal modules, doesn't rescan if already scanned scanCabal :: UpdateMonad m => [String] -> m () scanCabal opts = Log.scope "cabal" $ scanPackageDbStack opts userDb -- | Prepare sandbox for scanning. This is used for stack project to build & configure. prepareSandbox :: UpdateMonad m => Sandbox -> m () prepareSandbox sbox@(Sandbox StackWork fpath) = Log.scope "prepare" $ runTasks_ [ runTask "building dependencies" sbox $ void $ Util.withCurrentDirectory dir $ inSessionGhc $ S.buildDeps Nothing] where dir = takeDirectory $ view path fpath prepareSandbox _ = return () -- | Scan sandbox modules, doesn't rescan if already scanned scanSandbox :: UpdateMonad m => [String] -> Sandbox -> m () scanSandbox opts sbox = Log.scope "sandbox" $ do -- prepareSandbox sbox pdbs <- inSessionGhc $ sandboxPackageDbStack sbox scanPackageDbStack opts pdbs -- | Scan top of package-db stack, usable for rescan scanPackageDb :: UpdateMonad m => [String] -> PackageDbStack -> m () scanPackageDb opts pdbs = runTask "scanning" (topPackageDb pdbs) $ Log.scope "package-db" $ do pdbState <- liftIO $ readPackageDb (topPackageDb pdbs) let packageDbMods = S.fromList $ concat $ M.elems pdbState packages' = M.keys pdbState Log.sendLog Log.Trace $ "package-db state: {} modules" ~~ length packageDbMods watch (\w -> watchPackageDb w pdbs opts) pkgs <- SQLite.query "select package_name, package_version from package_dbs where package_db == ?;" (SQLite.Only $ topPackageDb pdbs) if S.fromList packages' == S.fromList pkgs then Log.sendLog Log.Trace "nothing changes, all packages the same" else do mlocs <- liftM (filter (`S.member` packageDbMods)) $ (inSessionGhc $ listModules opts pdbs packages') Log.sendLog Log.Trace $ "{} modules found" ~~ length mlocs let packageDbMods' = SQLite.query "select m.id, m.file, m.cabal, m.install_dirs, m.package_name, m.package_version, m.installed_name, m.exposed, m.other_location, m.inspection_time, m.inspection_opts from modules as m, package_dbs as ps where m.package_name == ps.package_name and m.package_version == ps.package_version and ps.package_db == ?;" (SQLite.Only (topPackageDb pdbs)) scan packageDbMods' ((,,) <$> mlocs <*> pure [] <*> pure Nothing) opts $ \mlocs' -> do ms <- inSessionGhc $ browseModules opts pdbs (mlocs' ^.. each . _1) Log.sendLog Log.Trace $ "scanned {} modules" ~~ length ms sendUpdateAction $ timer "updated package-db modules" $ do SQLite.updateModules ms SQLite.updatePackageDb (topPackageDb pdbs) (M.keys pdbState) when hdocsSupported $ scanPackageDbStackDocs opts pdbs updater $ ms ^.. each . inspectedKey -- | Scan top of package-db stack, usable for rescan scanPackageDbStack :: UpdateMonad m => [String] -> PackageDbStack -> m () scanPackageDbStack opts pdbs = runTask "scanning" pdbs $ Log.scope "package-db-stack" $ do pdbStates <- liftIO $ mapM readPackageDb (packageDbs pdbs) let packageDbMods = S.fromList $ concat $ concatMap M.elems pdbStates packages' = ordNub $ concatMap M.keys pdbStates Log.sendLog Log.Trace $ "package-db-stack state: {} modules" ~~ length packageDbMods watch (\w -> watchPackageDbStack w pdbs opts) pkgs <- liftM concat $ forM (packageDbs pdbs) $ \pdb -> SQLite.query "select package_name, package_version from package_dbs where package_db == ?;" (SQLite.Only pdb) if S.fromList packages' == S.fromList pkgs then Log.sendLog Log.Trace "nothing changes, all packages the same" else do mlocs <- liftM (filter (`S.member` packageDbMods)) $ (inSessionGhc $ listModules opts pdbs packages') Log.sendLog Log.Trace $ "{} modules found" ~~ length mlocs let packageDbStackMods = liftM concat $ forM (packageDbs pdbs) $ \pdb -> SQLite.query "select m.id, m.file, m.cabal, m.install_dirs, m.package_name, m.package_version, m.installed_name, m.exposed, m.other_location, m.inspection_time, m.inspection_opts from modules as m, package_dbs as ps where m.package_name == ps.package_name and m.package_version == ps.package_version and ps.package_db == ?;" (SQLite.Only pdb) scan packageDbStackMods ((,,) <$> mlocs <*> pure [] <*> pure Nothing) opts $ \mlocs' -> do ms <- inSessionGhc $ browseModules opts pdbs (mlocs' ^.. each . _1) Log.sendLog Log.Trace $ "scanned {} modules" ~~ length ms sendUpdateAction $ timer "updated package-db-stack modules" $ do SQLite.updateModules ms sequence_ [SQLite.updatePackageDb pdb (M.keys pdbState) | (pdb, pdbState) <- zip (packageDbs pdbs) pdbStates] -- BUG: I don't know why, but these steps leads to segfault on my PC: -- > hsdev scan --cabal --project . -- > hsdev check -f .\src\HsDev\Client\Commands.hs -- But it works if docs are scanned, it also works from ghci -- needDocs <- asks (view updateDocs) -- ms' <- if needDocs -- then do -- docs <- inSessionGhc $ hdocsCabal pdbs opts -- return $ map (fmap $ setDocs' docs) ms -- else return ms when hdocsSupported $ scanPackageDbStackDocs opts pdbs updater $ ms ^.. each . inspectedKey -- | Scan project file scanProjectFile :: UpdateMonad m => [String] -> Path -> m Project scanProjectFile opts cabal = runTask "scanning" cabal $ do proj <- S.scanProjectFile opts cabal pdbs <- inSessionGhc $ getProjectPackageDbStack proj let proj' = set projectPackageDbStack (Just pdbs) proj sendUpdateAction $ Log.scope "scan-project-file" $ SQLite.updateProject proj' return proj' -- | Refine project info and update if necessary refineProjectInfo :: UpdateMonad m => Project -> m Project refineProjectInfo proj = do [SQLite.Only exist] <- SQLite.query "select count(*) > 0 from projects where cabal == ?;" (SQLite.Only (proj ^. projectCabal)) if exist then SQLite.loadProject (proj ^. projectCabal) else runTask "scanning" (proj ^. projectCabal) $ do proj' <- liftIO $ loadProject proj pdbs <- inSessionGhc $ getProjectPackageDbStack proj' let proj'' = set projectPackageDbStack (Just pdbs) proj' sendUpdateAction $ Log.scope "refine-project-info" $ SQLite.updateProject proj'' return proj'' -- | Get project info for module locateProjectInfo :: UpdateMonad m => Path -> m (Maybe Project) locateProjectInfo cabal = liftIO (locateProject (view path cabal)) >>= traverse refineProjectInfo -- | Scan project and related package-db stack scanProjectStack :: UpdateMonad m => [String] -> Path -> m () scanProjectStack opts cabal = do proj <- scanProjectFile opts cabal scanProject opts cabal sbox <- liftIO $ projectSandbox (view projectPath proj) maybe (scanCabal opts) (scanSandbox opts) sbox -- | Scan project scanProject :: UpdateMonad m => [String] -> Path -> m () scanProject opts cabal = runTask "scanning" (project $ view path cabal) $ Log.scope "project" $ do proj <- scanProjectFile opts cabal watch (\w -> watchProject w proj opts) S.ScanContents _ [(_, sources)] _ <- S.enumProject proj let projMods = SQLite.query "select m.id, m.file, m.cabal, m.install_dirs, m.package_name, m.package_version, m.installed_name, m.exposed, m.other_location, m.inspection_time, m.inspection_opts from modules as m where m.file is not null and m.cabal == ?;" (SQLite.Only $ proj ^. projectCabal) scan projMods sources opts $ scanModules opts -- Scan docs -- inSessionGhc $ do -- currentSession >>= maybe (return ()) (const clearTargets) -- forM_ (maybe [] targetInfos (proj ^. projectDescription)) $ \tinfo' -> do -- opts' <- getProjectTargetOpts [] proj (tinfo' ^. targetBuildInfo) -- files' <- projectTargetFiles proj tinfo' -- haddockSession opts' -- docsMap <- liftGhc $ readProjectTargetDocs opts' proj files' -- Log.sendLog Log.Debug $ "scanned logs for modules: {}, summary docs: {}" ~~ (intercalate "," (M.keys docsMap)) ~~ (sum $ map M.size $ M.elems docsMap) -- | Scan directory for source files and projects scanDirectory :: UpdateMonad m => [String] -> Path -> m () scanDirectory opts dir = runTask "scanning" dir $ Log.scope "directory" $ do S.ScanContents standSrcs projSrcs pdbss <- S.enumDirectory (view path dir) runTasks_ [scanProject opts (view projectCabal p) | (p, _) <- projSrcs] runTasks_ $ map (scanPackageDb opts) pdbss -- TODO: Don't rescan mapMOf_ (each . _1) (watch . flip watchModule) standSrcs let standaloneMods = SQLite.query "select m.id, m.file, m.cabal, m.install_dirs, m.package_name, m.package_version, m.installed_name, m.exposed, m.other_location, m.inspection_time, m.inspection_opts from modules as m where m.cabal is null and m.file is not null and m.file like ? escape '\\';" (SQLite.Only $ SQLite.escapeLike dir `T.append` "%") scan standaloneMods standSrcs opts $ scanModules opts scanContents :: UpdateMonad m => [String] -> S.ScanContents -> m () scanContents opts (S.ScanContents standSrcs projSrcs pdbss) = do projs <- liftM (map SQLite.fromOnly) $ SQLite.query_ "select cabal from projects;" pdbs <- liftM (map SQLite.fromOnly) $ SQLite.query_ "select package_db from package_dbs;" let filesMods = SQLite.query_ "select m.id, m.file, m.cabal, m.install_dirs, m.package_name, m.package_version, m.installed_name, m.exposed, m.other_location, m.inspection_time, m.inspection_opts from modules as m where m.file is not null and m.cabal is null;" runTasks_ [scanPackageDb opts pdbs' | pdbs' <- pdbss, topPackageDb pdbs' `notElem` pdbs] runTasks_ [scanProject opts (view projectCabal p) | (p, _) <- projSrcs, view projectCabal p `notElem` projs] mapMOf_ (each . _1) (watch . flip watchModule) standSrcs scan filesMods standSrcs opts $ scanModules opts -- | Scan installed docs scanPackageDbStackDocs :: UpdateMonad m => [String] -> PackageDbStack -> m () scanPackageDbStackDocs opts pdbs | hdocsSupported = Log.scope "docs" $ do docs <- inSessionGhc $ hdocsCabal pdbs opts Log.sendLog Log.Trace $ "docs scanned: {} packages, {} modules total" ~~ length docs ~~ sum (map (M.size . snd) docs) sendUpdateAction $ transact $ SQLite.executeMany "update symbols set docs = ? where name == ? and module_id in (select id from modules where name == ? and package_name == ? and package_version == ?);" $ do (ModulePackage pname pver, pdocs) <- docs (mname, mdocs) <- M.toList pdocs (nm, doc) <- M.toList mdocs return (doc, nm, mname, pname, pver) Log.sendLog Log.Trace "docs set" | otherwise = Log.sendLog Log.Warning "hdocs not supported" -- | Scan docs for inspected modules scanDocs :: UpdateMonad m => [Module] -> m () scanDocs | hdocsSupported = runTasks_ . map scanDocs' | otherwise = const $ Log.sendLog Log.Warning "hdocs not supported" where scanDocs' m = runTask "scanning docs" (view (moduleId . moduleLocation) m) $ Log.scope "docs" $ do mid <- SQLite.lookupModule (m ^. moduleId) mid' <- maybe (hsdevError $ SQLiteError "module id not found") return mid m' <- mapMOf (moduleId . moduleLocation . moduleProject . _Just) refineProjectInfo m Log.sendLog Log.Trace $ "Scanning docs for {}" ~~ view (moduleId . moduleLocation) m' docsMap <- inSessionGhc $ do (pdbs, opts') <- getModuleOpts [] m' currentSession >>= maybe (return ()) (const clearTargets) -- Calling haddock with targets set sometimes cause errors haddockSession pdbs opts' readModuleDocs opts' m' sendUpdateAction $ transact $ do SQLite.executeMany "update symbols set docs = ? where name == ? and module_id == ?;" [(doc, nm, mid') | (nm, doc) <- maybe [] M.toList docsMap] SQLite.execute "update modules set tags = json_set(tags, '$.docs', 1) where id == ?;" (SQLite.Only mid') -- | Set inferred types for module setModTypes :: UpdateMonad m => ModuleId -> [Note TypedExpr] -> m () setModTypes m ts = Log.scope "set-types" $ do mid <- SQLite.lookupModule m mid' <- maybe (hsdevError $ SQLiteError "module id not found") return mid sendUpdateAction $ transact $ do SQLite.execute "delete from types where module_id = ?;" (SQLite.Only mid') SQLite.executeMany "insert into types (module_id, line, column, line_to, column_to, expr, type) values (?, ?, ?, ?, ?, ?, ?);" [ (SQLite.Only mid' SQLite.:. view noteRegion n' SQLite.:. view note n') | n' <- uniqueBy (view noteRegion) ts] SQLite.execute "update names set inferred_type = (select type from types as t where t.module_id = ? and names.line = t.line and names.column = t.column and names.line_to = t.line_to and names.column_to = t.column_to) where module_id == ?;" (mid', mid') SQLite.execute "update symbols set type = (select type from types as t where t.module_id = ? and symbols.line = t.line and symbols.column = t.column order by t.line_to, t.column_to) where module_id == ? and type is null;" (mid', mid') SQLite.execute "update modules set tags = json_set(tags, '$.types', 1) where id == ?;" (SQLite.Only mid') -- | Infer types for modules inferModTypes :: UpdateMonad m => [Module] -> m () inferModTypes = runTasks_ . map inferModTypes' where inferModTypes' m = runTask "inferring types" (view (moduleId . moduleLocation) m) $ Log.scope "types" $ do mid <- SQLite.lookupModule (m ^. moduleId) _ <- maybe (hsdevError $ SQLiteError "module id not found") return mid m' <- mapMOf (moduleId . moduleLocation . moduleProject . _Just) refineProjectInfo m Log.sendLog Log.Trace $ "Inferring types for {}" ~~ view (moduleId . moduleLocation) m' sess <- getSession mcts <- fmap (fmap snd) $ S.getFileContents (m' ^?! moduleId . moduleLocation . moduleFile) types' <- inSessionGhc $ do targetSession [] m' cacheGhcWarnings sess (m' ^.. moduleId . moduleLocation) $ fileTypes m' mcts setModTypes (m' ^. moduleId) types' -- | Generic scan function. Removed obsolete modules and calls callback on changed modules. scan :: UpdateMonad m => m [SQLite.Only Int SQLite.:. ModuleLocation SQLite.:. Inspection] -- ^ Get affected modules, obsolete will be removed, changed will be updated -> [S.ModuleToScan] -- ^ Actual modules, other will be removed -> [String] -- ^ Extra scan options -> ([S.ModuleToScan] -> m ()) -- ^ Update function -> m () scan part' mlocs opts act = Log.scope "scan" $ do mlocs' <- liftM (M.fromList . map (\(SQLite.Only mid SQLite.:. (m SQLite.:. i)) -> (m, (mid, i)))) part' let obsolete = M.filterWithKey (\k _ -> k `S.notMember` S.fromList (map (^. _1) mlocs)) mlocs' changed <- S.changedModules (M.map snd mlocs') opts mlocs sendUpdateAction $ Log.scope "remove-obsolete" $ transact $ forM_ (M.elems obsolete) $ SQLite.removeModule . fst act changed processEvents :: ([(Watched, Event)] -> IO ()) -> MVar (A.Async ()) -> MVar [(Watched, Event)] -> [(Watched, Event)] -> ClientM IO () processEvents handleEvents updaterTask eventsVar evs = Log.scope "event" $ do Log.sendLog Log.Trace $ "events received: {}" ~~ intercalate ", " (evs ^.. each . _2 . eventPath) l <- Log.askLog liftIO $ do modifyMVar_ eventsVar (return . (++evs)) modifyMVar_ updaterTask $ \task -> do done <- fmap isJust $ poll task if done then do Log.withLog l $ Log.sendLog Log.Trace "starting update thread" A.async $ fix $ \loop -> do updates <- modifyMVar eventsVar (\es -> return ([], es)) unless (null updates) $ handleEvents updates >> loop else return task updateEvents :: ServerMonadBase m => [(Watched, Event)] -> UpdateM m () updateEvents updates = Log.scope "updater" $ do Log.sendLog Log.Trace $ "prepared to process {} events" ~~ length updates files <- fmap concat $ forM updates $ \(w, e) -> case w of WatchedProject proj projOpts | isSource e -> do Log.sendLog Log.Info $ "File '{file}' in project {proj} changed" ~~ ("file" ~% view eventPath e) ~~ ("proj" ~% view projectName proj) [SQLite.Only mopts] <- SQLite.query "select inspection_opts from modules where file == ?;" (SQLite.Only $ view eventPath e) opts <- maybe (return []) (maybe (parseErr' mopts) return . fromJSON') mopts return [(FileSource (fromFilePath $ view eventPath e) Nothing, opts)] | isCabal e -> do Log.sendLog Log.Info $ "Project {proj} changed" ~~ ("proj" ~% view projectName proj) scanProject projOpts $ view projectCabal proj return [] | otherwise -> return [] WatchedPackageDb pdbs opts | isConf e -> do Log.sendLog Log.Info $ "Package db {package} changed" ~~ ("package" ~% topPackageDb pdbs) scanPackageDb opts pdbs return [] | otherwise -> return [] WatchedModule | isSource e -> do Log.sendLog Log.Info $ "Module {file} changed" ~~ ("file" ~% view eventPath e) [SQLite.Only mopts] <- SQLite.query "select inspection_opts from modules where file == ?;" (SQLite.Only $ view eventPath e) opts <- maybe (return []) (maybe (parseErr' mopts) return . fromJSON') mopts return [(FileSource (fromFilePath $ view eventPath e) Nothing, opts)] | otherwise -> return [] scanFiles files where parseErr' mopts' = do Log.sendLog Log.Error $ "Error parsing inspection_opts: {}" ~~ show mopts' hsdevError $ SQLiteError $ "Error parsing inspection_opts: {}" ~~ show mopts' applyUpdates :: UpdateOptions -> [(Watched, Event)] -> ClientM IO () applyUpdates uopts = runUpdate uopts . updateEvents -- Save ghc warnings on loading target, because second loading won't produce any cacheGhcWarnings :: Session -> [ModuleLocation] -> GhcM a -> GhcM a cacheGhcWarnings sess mlocs act = Log.scope "cache-warnings" $ do tm <- liftIO getPOSIXTime (r, msgs) <- collectMessages act Log.sendLog Log.Trace $ "collected {} warnings" ~~ length msgs _ <- liftIO $ withSession sess $ postSessionUpdater $ refreshCache mlocs tm msgs return r where refreshCache :: [ModuleLocation] -> POSIXTime -> [Note OutputMessage] -> ServerM IO () refreshCache mlocs' tm' msgs' = Log.scope "refresh" $ bracket_ initTemp dropTemp $ do fillTemp removeOutdated insertMessages where initTemp :: SessionMonad m => m () initTemp = do SQLite.execute_ "create temporary table updating_ids (id integer not null unique);" SQLite.execute_ "create temporary table updating_messages as select * from messages where 0;" SQLite.execute_ "create index update_messages_module_id_index on updating_messages (module_id);" dropTemp :: SessionMonad m => m () dropTemp = do SQLite.execute_ "drop table if exists updating_ids;" SQLite.execute_ "drop table if exists updating_messages;" fillTemp :: SessionMonad m => m () fillTemp = do SQLite.executeMany "insert into updating_ids select distinct m.id from modules as m where (m.file = ?);" $ (map SQLite.Only $ mlocs' ^.. each . moduleFile) SQLite.executeMany "insert into updating_messages select (select m.id from modules as m where (m.file = ?)), ?, ?, ?, ?, ?, ?, ?;" msgs' SQLite.execute_ "insert into updating_ids select distinct umsgs.module_id from updating_messages as umsgs where umsgs.module_id not in (select id from updating_ids);" removeOutdated :: SessionMonad m => m () removeOutdated = SQLite.execute_ $ fromString $ unlines [ "delete from messages", "where", " module_id in (", " select um.id", " from", " updating_ids as um, modules as m", " left outer join", " load_times as lt", " on", " lt.module_id = um.id", " where", " um.id = m.id and (", " lt.load_time is null or", " lt.load_time <= m.inspection_time or", " um.id in (select distinct umsgs.module_id from updating_messages as umsgs)", " )", " );"] insertMessages :: SessionMonad m => m () insertMessages = SQLite.transaction_ SQLite.Deferred $ do SQLite.execute "insert or replace into load_times (module_id, load_time) select um.id, ? from updating_ids as um;" (SQLite.Only tm') SQLite.execute_ "insert into messages select distinct * from updating_messages;" -- | Get cached warnings cachedWarnings :: SessionMonad m => [ModuleLocation] -> m [Note OutputMessage] cachedWarnings mlocs = liftM concat $ forM (mlocs ^.. each . moduleFile) $ \f -> SQLite.query @_ @(Note OutputMessage) (SQLite.toQuery $ mconcat [ SQLite.qNote "m" "n", SQLite.from_ ["load_times as lt"], SQLite.where_ [ "lt.module_id = m.id", "m.file = ?", "lt.load_time >= m.inspection_time"]]) (SQLite.Only f) watch :: SessionMonad m => (Watcher -> IO ()) -> m () watch f = do w <- askSession sessionWatcher liftIO $ f w