add static server,websocket refresh watching mode (#149)

* add static server,websocket refresh watching mode

* watch optional

* we dont quit when building error in watch mode

* staticDir,publicUrl configure through dot env

* remove unused import

* serve optional
This commit is contained in:
Bung 2020-07-23 16:40:04 +08:00 • committed by GitHub
commit 98a2d1ce50
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 248 additions and 44 deletions

2
.gitignore vendored
View file

@ -3,3 +3,5 @@ nimcache/
karax/tools/karun karax/tools/karun
*.code-workspace *.code-workspace
*.exe *.exe
app.js
app.html

View file

@ -8,7 +8,8 @@ license = "MIT"
# Dependencies # Dependencies
requires "nim >= 0.16.1" requires "nim >= 0.16.1"
requires "ws"
requires "dotenv"
skipDirs = @["examples", "experiments", "tests"] skipDirs = @["examples", "experiments", "tests"]
bin = @["karax/tools/karun"] bin = @["karax/tools/karun"]

View file

@ -1,21 +1,16 @@
## Simple tool to quickly run Karax applications. Generates the HTML
## required to run a Karax app and opens it in a browser.
import os,
strutils,
parseopt,
browsers,
times,
tables
import os, strutils, browsers,times, tables
import parseopt
import threadpool
import static_server
const const
css = """ css = """
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bulma/0.7.4/css/bulma.min.css"> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bulma/0.7.4/css/bulma.min.css">
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css"> <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css">
""" """
html = """
const html = """
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head> <head>
@ -23,33 +18,79 @@ const
<title>$1</title> <title>$1</title>
$2 $2
</head> </head>
<body id="body"> <body id="body" class="site">
<div id="ROOT" /> <div id="ROOT"></div>
<script type="text/javascript" src="$1.js"></script> <script type="text/javascript" src="/app.js"></script>
$3
</body> </body>
</html> </html>
""" """
const websocket = """
<script type="text/javascript">
var ws = new WebSocket("ws://localhost:8080/ws");
ws.onopen = function(evt) {
console.log("Connection open ...");
ws.send("Hello WebSockets!");
};
ws.onmessage = function(evt) {
console.log( "Received Message: " + evt.data);
if(evt.data == "refresh"){
window.location.href = window.location.href
}
};
ws.onclose = function(evt) {
console.log("Connection closed.");
};
</script>
"""
proc exec(cmd: string) = proc exec(cmd: string) =
if os.execShellCmd(cmd) != 0: if os.execShellCmd(cmd) != 0:
quit "External command failed: " & cmd quit "External command failed: " & cmd
proc build(name: string, rest: string, selectedCss: string, run: bool) = proc build(rest: string, selectedCss: string, run: bool, watch: bool) =
echo("Building...") echo("Building...")
exec("nim js --out:" & name & ".js " & rest) let cmd = "nim js --out:" & "app" & ".js " & rest
let dest = name & ".html" if watch:
writeFile(dest, html % [name, selectedCss]) discard os.execShellCmd(cmd)
if run: openDefaultBrowser(dest) else:
exec cmd
let dest = "app" & ".html"
let script = if run and watch: websocket else: ""
writeFile(dest, html % ["app", selectedCss, script])
if run: openDefaultBrowser("http://localhost:8080")
proc watchBuild(filePath: string, selectedCss: string, rest: string) {.thread.} =
var files: Table[string, Time] = {"path": getLastModificationTime(".")}.toTable
while true:
sleep(300)
for path in walkDirRec("."):
if ".git" in path:
continue
if files.hasKey(path):
if files[path] != getLastModificationTime(path):
echo("File changed: " & path)
build(rest,selectedCss, false, true)
files[path] = getLastModificationTime(path)
else:
if absolutePath(path) in [absolutePath("app" & ".js"),absolutePath("app" & ".html")]:
continue
files[path] = getLastModificationTime(path)
proc serve(){.thread.} =
serveStatic()
proc main = proc main =
var op = initOptParser() var op = initOptParser()
var rest = op.cmdLineRest var rest = op.cmdLineRest
var file = "" var file = ""
var run = false var run = false
var selectedCss = ""
var watch = false var watch = false
var files: Table[string, Time] = {"path": getLastModificationTime(".")}.toTable var selectedCss = ""
while true: while true:
op.next() op.next()
case op.kind case op.kind
@ -59,8 +100,11 @@ proc main =
run = true run = true
rest = rest.replace("--run ") rest = rest.replace("--run ")
of "css": of "css":
selectedCss = css if op.val != "":
rest = rest.replace("--css ") selectedCss = readFile(op.val)
else:
selectedCss = css
rest = rest.substr(rest.find(" "))
else: discard else: discard
of cmdShortOption: of cmdShortOption:
if op.key == "r": if op.key == "r":
@ -73,25 +117,11 @@ proc main =
of cmdEnd: break of cmdEnd: break
if file.len == 0: quit "filename expected" if file.len == 0: quit "filename expected"
let name = file.splitFile.name if run:
build(name, rest, selectedCss, run) spawn serve()
if watch: if watch:
# TODO: launch http server spawn watchBuild(file, selectedCss, rest)
while true: build(rest,selectedCss, run, watch)
sleep(300) sync()
for path in walkDirRec("."):
if ".git" in path:
continue
if files.hasKey(path):
if files[path] != getLastModificationTime(path):
echo("File changed: " & path)
build(name, rest, selectedCss, run)
files[path] = getLastModificationTime(path)
else:
files[path] = getLastModificationTime(path)
main() main()

1
karax/tools/karun.nims Normal file
View file

@ -0,0 +1 @@
switch("threads", "on")

View file

@ -0,0 +1,170 @@
import net, os, strutils, uri, mimetypes, asyncnet, asyncdispatch, md5,
logging, httpcore, asyncfile, options
import asynchttpserver
from cgi import decodeUrl
import ws,tables,times
import dotenv
type
RawHeaders* = seq[tuple[key, val: string]]
proc toStr(headers: Option[RawHeaders]): string =
return $newHttpHeaders(headers.get(@({:})))
proc send(
request: Request, code: HttpCode, headers: Option[RawHeaders], body: string
): Future[void] =
return request.respond(
code, body, newHttpHeaders(headers.get(@({:})))
)
proc statusContent(request: Request, status: HttpCode, content: string,
headers: Option[RawHeaders]): Future[void] =
try:
result = send(request, status, headers, content)
when not defined(release):
logging.debug(" $1 $2" % [$status, toStr(headers)])
except:
logging.error("Could not send response: $1" % osErrorMsg(osLastError()))
proc sendStaticIfExists(
req: Request, paths: seq[string]
): Future[HttpCode] {.async.} =
result = Http200
let mimes = newMimetypes()
for p in paths:
if existsFile(p):
var fp = getFilePermissions(p)
if not fp.contains(fpOthersRead):
return Http403
let fileSize = getFileSize(p)
let ext = p.splitFile.ext
let mimetype = mimes.getMimetype(
if ext.len > 0: ext[1 .. ^1]
else: ""
)
if fileSize < 10_000_000: # 10 mb
var file = readFile(p)
var hashed = getMD5(file)
# If the user has a cached version of this file and it matches our
# version, let them use it
if req.headers.hasKey("If-None-Match") and req.headers["If-None-Match"] == hashed:
await req.statusContent(Http304, "", none[RawHeaders]())
else:
await req.statusContent(Http200, file, some(@({
"Content-Type": mimetype,
"ETag": hashed
})))
else:
let headers = @({
"Content-Type": mimetype,
"Content-Length": $fileSize
})
await req.statusContent(Http200, "", some(headers))
var fileStream = newFutureStream[string]("sendStaticIfExists")
var file = openAsync(p, fmRead)
# Let `readToStream` write file data into fileStream in the
# background.
asyncCheck file.readToStream(fileStream)
# The `writeFromStream` proc will complete once all the data in the
# `bodyStream` has been written to the file.
while true:
let (hasValue, value) = await fileStream.read()
if hasValue:
await req.client.send(value)
else:
break
file.close()
return
# If we get to here then no match could be found.
return Http404
proc handleFileRequest(
req: Request
): Future[HttpCode] {.async.} =
# Find static file.
var reqPath = cgi.decodeUrl(req.url.path)
var publicUrl = getEnv("publicUrl")
var staticDir = getEnv("staticDir")
if not publicUrl.endsWith("/"):
publicUrl = publicUrl & "/"
reqPath = reqPath.substr(publicUrl.len)
let path = normalizedPath(
staticDir / reqPath
)
# Verify that this isn't outside our static dir.
var status = Http400
let pathDir = path.splitFile.dir / ""
if pathDir.startsWith(publicUrl):
if existsDir(path):
status = await sendStaticIfExists(
req,
@[path / "index.html", path / "index.htm"]
)
else:
status = await sendStaticIfExists(req, @[path])
return status
proc handleWs(req: Request) {.async.} =
var ws = await newWebSocket(req)
await ws.send("Welcome to simple echo server")
var files: Table[string, Time] = {"path": getLastModificationTime(".")}.toTable
let watchedFiles = [absolutePath "app.js",absolutePath "app.html"]
for path in watchedFiles:
files[path] = getLastModificationTime(path)
while ws.readyState == Open:
await sleepAsync(500)
var changed = false
for path in watchedFiles:
if files[path] != getLastModificationTime(path):
changed = true
files[path] = getLastModificationTime(path)
if changed:
await ws.send("refresh")
changed = false
proc serveStatic*() =
if fileExists( "static.env" ):
var env:DotEnv
env = initDotEnv(getCurrentDir(), "static.env")
env.overload()
else:
loadEnvFromString("""
staticDir="./src/assets/"
publicUrl="public"
""")
var server = newAsyncHttpServer()
proc cb(req: Request) {.gcsafe, async.} =
if req.url.path == "/ws":
await handleWs(req)
if req.url.path == "/":
await req.respond(Http200, readFile "app.html" )
elif req.url.path == "/app.js":
let file = absolutePath("app" & ".js")
if not file.fileExists:
logging.error("$1 not exists!" % file )
if fpUserRead notin os.getFilePermissions(file):
logging.error("Could not read $1!" % file )
await req.respond(Http200, readFile( file ))
else:
let status = await handleFileRequest(req)
if status != Http200:
await req.respond(status,"")
waitFor server.serve(Port(8080), cb)
when isMainModule:
serveStatic()