forked from Codas/fast-yesod-devel
-
Notifications
You must be signed in to change notification settings - Fork 3
/
Main.hs
220 lines (208 loc) · 7.62 KB
/
Main.hs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
module Main where
import Control.Concurrent (forkIO, threadDelay)
import Control.Concurrent.STM.TChan (TChan, dupTChan, newTChan,
readTChan, writeTChan)
import Control.Exception (bracket)
import Control.Monad (forever, unless, when)
import Control.Monad.STM (atomically)
import qualified Data.ByteString.Lazy as ByteString
import Data.Digest.Pure.MD5 (md5)
import Data.Maybe
import Data.Monoid
import Data.Text (isInfixOf, pack)
import Options.Applicative
import Paths_yesod_fast_devel
import System.Console.ANSI
import System.Directory (copyFile, doesDirectoryExist,
findExecutable)
import System.Exit
import System.FilePath (takeDirectory)
import System.FilePath.Glob
import System.FilePath.Posix (takeBaseName)
import System.FSNotify (Event (..), watchTree,
withManager)
import System.IO (BufferMode (..), Handle,
hPutStrLn, hSetBuffering, stderr,
stdout)
import System.Process
data Options
= PatchDevelMain { pdmFilePath :: FilePath}
| PrintPatchedMain
| StartServer { ssFilePath :: FilePath}
options :: ParserInfo Options
options =
info
(allCommands <**> helper)
(header "Faster yesod-devel with GHCi and Browser Sync")
where
allCommands =
subparser
(patchDevelMain <> printPatchedMain <> startServer <>
metavar "patch | server| print-patched-main")
printPatchedMain =
command
"print-patched-main"
(info (pure PrintPatchedMain) (progDesc "Print the patched DevelMain"))
startServer =
command
"server"
(info
(StartServer . fromMaybe "app/DevelMain.hs" <$>
optional (argument str (metavar "devel-main-path")) <**>
helper)
(progDesc "Start the development servers"))
patchDevelMain =
command
"patch"
(info
(PatchDevelMain . fromMaybe "app/DevelMain.hs" <$>
optional (argument str (metavar "devel-main-path")) <**>
helper)
(progDesc "Patch your devel main with browser-sync"))
main :: IO ()
main = do
cmd <- execParser options
case cmd of
PatchDevelMain fp -> initYesodFastDevel fp
StartServer fp -> go fp
PrintPatchedMain ->
putStrLn =<< readFile =<< getDataFileName "PatchedDevelMain.hs"
where
go develMainPth = do
hSetBuffering stdout LineBuffering
hSetBuffering stderr LineBuffering
chan <- atomically newTChan
_ <- forkIO $ do
hPutStrLn stderr "Watching files for changes..."
watchThread chan
_ <- forkIO $ do
hPutStrLn stderr "Spawning browser-sync..."
browserSyncThread
hPutStrLn stderr "Spawning GHCi..."
_ <- replThread develMainPth chan
return ()
initYesodFastDevel :: FilePath -> IO ()
initYesodFastDevel develMainPth = do
verifyDirectory
verifyDevelMain
patchedDevelMain <- getDataFileName "PatchedDevelMain.hs"
copyFile patchedDevelMain develMainPth
putStrLn "Patched `DevelMain.hs`"
browserSyncPth <- findExecutable "browser-sync"
putStrLn "Make sure you have `foreign-store` on your cabal file"
when (isNothing browserSyncPth) $
putStrLn "Install `browser-sync` to have livereload at port 4000"
exitSuccess
where
verifyDirectory = do
let dir = takeDirectory develMainPth
putStrLn ("Verifying `" ++ dir ++ "` exists")
dexists <- doesDirectoryExist dir
unless dexists $ do
hPutStrLn stderr ("Directory `" ++ dir ++ "` not found")
exitFailure
verifyDevelMain = do
putStrLn "Verifying `DevelMain.hs` isn't modified"
userDevelMd5 <- md5 <$> ByteString.readFile develMainPth
originalDevelMd5 <-
md5 <$> (ByteString.readFile =<< getDataFileName "OriginalDevelMain.hs")
patchedDevelMd5 <-
md5 <$> (ByteString.readFile =<< getDataFileName "PatchedDevelMain.hs")
when (userDevelMd5 == patchedDevelMd5) $ do
putStrLn "DevelMain.hs is already patched"
exitSuccess
when (userDevelMd5 /= originalDevelMd5) $ do
hPutStrLn stderr "Found a weird DevelMain.hs on your project"
hPutStrLn stderr "Use `yesod-fast-devel print-patched-main`"
exitFailure
browserSyncThread :: IO ()
browserSyncThread = do
browserSyncPth <- findExecutable "browser-sync"
when (isJust browserSyncPth) $ callCommand cmd
where
cmd =
"browser-sync start --no-open --files=\"devel-main-since\" --proxy \"localhost:3000\" --port 4000"
watchThread :: TChan Event -> IO ()
watchThread writeChan =
withManager $ \mgr
-- start a watching job (in the background)
-> do
_ <- watchTree mgr "." shouldReload (reloadApplication writeChan)
-- sleep forever (until interrupted)
forever $ threadDelay 1000000000
replThread :: FilePath -> TChan Event -> IO ()
replThread develMainPth chan = do
readChan <- atomically (dupTChan chan)
bracket newRepl onError (onSuccess readChan)
where
onError (_, _, _, process) = do
interruptProcessGroupOf process
threadDelay 100000
terminateProcess process
threadDelay 100000
waitForProcess process
onSuccess readChan (Just replIn, _, _, _) = do
hSetBuffering replIn LineBuffering
threadDelay 1000000
hPutStrLn replIn loadString
hPutStrLn replIn startString
forever $ do
event <- atomically (readTChan readChan)
putStrLn "-----------------------------"
setSGR [SetColor Foreground Vivid Yellow]
print event
setSGR [Reset]
putStrLn "-----------------------------"
hPutStrLn replIn loadString
hPutStrLn replIn startString
onSuccess _ (_, _, _, _) = do
hPutStrLn stderr "Can't open GHCi's stdin"
exitFailure
startString = "update"
loadString = ":load " ++ develMainPth
shouldReload :: Event -> Bool
shouldReload event = not (or conditions)
where
fp =
case event of
Added filePath _ -> filePath
Modified filePath _ -> filePath
Removed filePath _ -> filePath
conditions =
[ notInPath ".git"
, notInPath "yesod-devel"
, notInPath "dist"
, notInFile "#"
, notInPath ".cabal-sandbox"
, notInFile "flycheck_"
, notInPath ".stack-work"
, notInGlob (compile "**/*.sqlite3-*")
, notInGlob (compile "*.sqlite3-*")
, notInFile "stack.yaml"
, notInGlob (compile "*.hi")
, notInGlob (compile "**/*.hi")
, notInGlob (compile "*.o")
, notInGlob (compile "**/*.o")
, notInFile "devel-main-since"
]
notInPath t = t `isInfixOf` pack fp
notInFile t = t `isInfixOf` pack (takeBaseName fp)
notInGlob pt = match pt fp
reloadApplication :: TChan Event -> Event -> IO ()
reloadApplication chan event = atomically (writeTChan chan event)
newRepl :: IO (Maybe Handle, Maybe Handle, Maybe Handle, ProcessHandle)
newRepl =
createProcess $ newProc "stack" ["ghci", "--ghc-options", "-O0 -fobject-code"]
newProc :: FilePath -> [String] -> CreateProcess
newProc cmd args =
CreateProcess
{ cmdspec = RawCommand cmd args
, cwd = Nothing
, env = Nothing
, std_in = CreatePipe
, std_out = Inherit
, std_err = Inherit
, close_fds = False
, create_group = True
, delegate_ctlc = False
}