diff options
author | Ben Sima <ben@bsima.me> | 2020-12-04 11:16:25 -0500 |
---|---|---|
committer | Ben Sima <ben@bsima.me> | 2020-12-05 07:55:13 -0500 |
commit | 330e4363d8abb509031d2c8c1a89dcc6f955e2c1 (patch) | |
tree | 915c8c50a7125bf6eb9e560f8d00a80592f41c77 /Biz/Que | |
parent | 32f53350a3a3d701e9a1474e670a8454342adc40 (diff) |
Renamespace Devalloc and Que
Move them under the Biz root so that we know they are specific to Biz stuff. Biz
is for proprietary stuff that we own.
I also had to refactor the bild namespace parsing code because it couldn't
handle a namespace with 3 parts. I really need to get that namespace library
written and tested.
Diffstat (limited to 'Biz/Que')
-rw-r--r-- | Biz/Que/Apidocs.md | 3 | ||||
-rwxr-xr-x | Biz/Que/Client.py | 186 | ||||
-rw-r--r-- | Biz/Que/Host.hs | 253 | ||||
-rw-r--r-- | Biz/Que/Host.nix | 46 | ||||
-rw-r--r-- | Biz/Que/Index.md | 73 | ||||
-rw-r--r-- | Biz/Que/Prod.nix | 61 | ||||
-rw-r--r-- | Biz/Que/Quescripts.md | 50 | ||||
-rw-r--r-- | Biz/Que/Site.hs | 137 | ||||
-rw-r--r-- | Biz/Que/Site.nix | 61 | ||||
-rw-r--r-- | Biz/Que/Style.css | 136 | ||||
-rw-r--r-- | Biz/Que/Tutorial.md | 53 |
11 files changed, 1059 insertions, 0 deletions
diff --git a/Biz/Que/Apidocs.md b/Biz/Que/Apidocs.md new file mode 100644 index 0000000..f400889 --- /dev/null +++ b/Biz/Que/Apidocs.md @@ -0,0 +1,3 @@ +% que.run Api Docs + +coming soon diff --git a/Biz/Que/Client.py b/Biz/Que/Client.py new file mode 100755 index 0000000..1063eb8 --- /dev/null +++ b/Biz/Que/Client.py @@ -0,0 +1,186 @@ +#!/usr/bin/env python3 +""" +simple client for que.run +""" + +import argparse +import configparser +import functools +import http.client +import logging +import os +import subprocess +import sys +import time +import urllib.parse +import urllib.request as request + +MAX_TIMEOUT = 99999999 # basically never timeout + + +def auth(args): + "Returns the auth key for the given ns from ~/.config/que.conf" + logging.debug("auth") + namespace = args.target.split("/")[0] + if namespace == "pub": + return None + conf_file = os.path.expanduser("~/.config/que.conf") + if not os.path.exists(conf_file): + sys.exit("you need a ~/.config/que.conf") + cfg = configparser.ConfigParser() + cfg.read(conf_file) + return cfg[namespace]["key"] + + +def autodecode(bytestring): + """Attempt to decode bytes `bs` into common codecs, preferably utf-8. If + no decoding is available, just return the raw bytes. + + For all available codecs, see: + <https://docs.python.org/3/library/codecs.html#standard-encodings> + + """ + logging.debug("autodecode") + codecs = ["utf-8", "ascii"] + for codec in codecs: + try: + return bytestring.decode(codec) + except UnicodeDecodeError: + pass + return bytestring + + +def retry(exception, tries=4, delay=3, backoff=2): + "Decorator for retrying an action." + + def decorator(func): + @functools.wraps(func) + def func_retry(*args, **kwargs): + mtries, mdelay = tries, delay + while mtries > 1: + try: + return func(*args, **kwargs) + except exception as ex: + logging.debug(ex) + logging.debug("retrying...") + time.sleep(mdelay) + mtries -= 1 + mdelay *= backoff + return func(*args, **kwargs) + + return func_retry + + return decorator + + +def send(args): + "Send a message to the que." + logging.debug("send") + key = auth(args) + data = args.infile + req = request.Request(f"{args.host}/{args.target}") + req.add_header("User-AgenT", "Que/Client") + if key: + req.add_header("Authorization", key) + if args.serve: + logging.debug("serve") + while not time.sleep(1): + request.urlopen(req, data=data, timeout=MAX_TIMEOUT) + + else: + request.urlopen(req, data=data, timeout=MAX_TIMEOUT) + + +def then(args, msg): + "Perform an action when passed `--then`." + if args.then: + logging.debug("then") + subprocess.run( + args.then.format(msg=msg, que=args.target), check=False, shell=True, + ) + + +@retry(http.client.IncompleteRead, tries=10, delay=5, backoff=1) +@retry(http.client.RemoteDisconnected, tries=10, delay=2, backoff=2) +def recv(args): + "Receive a message from the que." + logging.debug("recv on: %s", args.target) + params = urllib.parse.urlencode({"poll": args.poll}) + req = request.Request(f"{args.host}/{args.target}?{params}") + req.add_header("User-Agent", "Que/Client") + key = auth(args) + if key: + req.add_header("Authorization", key) + with request.urlopen(req) as _req: + if args.poll: + logging.debug("poll") + while not time.sleep(1): + logging.debug("reading") + msg = autodecode(_req.readline()) + logging.debug("read") + print(msg, end="") + then(args, msg) + else: + msg = autodecode(_req.read()) + print(msg) + then(args, msg) + + +def get_args(): + "Command line parser" + cli = argparse.ArgumentParser(description=__doc__) + cli.add_argument("--debug", action="store_true", help="log to stderr") + cli.add_argument( + "--host", default="http://que.run", help="where que-server is running" + ) + cli.add_argument( + "--poll", default=False, action="store_true", help="stream data from the que" + ) + cli.add_argument( + "--then", + help=" ".join( + [ + "when polling, run this shell command after each response,", + "presumably for side effects," + r"replacing '{que}' with the target and '{msg}' with the body of the response", + ] + ), + ) + cli.add_argument( + "--serve", + default=False, + action="store_true", + help=" ".join( + [ + "when posting to the que, do so continuously in a loop.", + "this can be used for serving a webpage or other file continuously", + ] + ), + ) + cli.add_argument( + "target", help="namespace and path of the que, like 'ns/path/subpath'" + ) + cli.add_argument( + "infile", + nargs="?", + type=argparse.FileType("rb"), + help="data to put on the que. Use '-' for stdin, otherwise should be a readable file", + ) + return cli.parse_args() + + +if __name__ == "__main__": + ARGV = get_args() + if ARGV.debug: + logging.basicConfig( + format="%(asctime)s %(message)s", + level=logging.DEBUG, + datefmt="%Y.%m.%d..%H.%M.%S", + ) + try: + if ARGV.infile: + send(ARGV) + else: + recv(ARGV) + except KeyboardInterrupt: + sys.exit(0) diff --git a/Biz/Que/Host.hs b/Biz/Que/Host.hs new file mode 100644 index 0000000..4817fd6 --- /dev/null +++ b/Biz/Que/Host.hs @@ -0,0 +1,253 @@ +{-# LANGUAGE DeriveGeneric #-} +{-# LANGUAGE GeneralizedNewtypeDeriving #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE NoImplicitPrelude #-} + +-- | Interprocess communication +-- +-- Prior art: +-- - <https://github.com/jb55/httpipe> +-- - <https://patchbay.pub> +-- - <https://github.com/hargettp/courier> +-- - sorta: <https://ngrok.com/> and <https://localtunnel.github.io/www/> +-- +-- : out que-server +-- +-- : dep async +-- : dep envy +-- : dep protolude +-- : dep scotty +-- : dep stm +-- : dep unagi-chan +-- : dep unordered-containers +module Biz.Que.Host + ( main, + ) +where + +import Alpha hiding (gets, modify, poll) +import qualified Control.Concurrent.Go as Go +import qualified Control.Concurrent.STM as STM +import qualified Control.Exception as Exception +import Control.Monad.Reader (MonadTrans) +import qualified Data.ByteString.Builder.Extra as Builder +import qualified Data.ByteString.Lazy as BSL +import Data.HashMap.Lazy (HashMap) +import qualified Data.HashMap.Lazy as HashMap +import qualified Data.Text.Encoding as Encoding +import qualified Data.Text.Lazy as Text.Lazy +import qualified Data.Text.Lazy.IO as Text.Lazy.IO +import qualified Network.HTTP.Types.Status as Http +import qualified Network.Wai as Wai +import qualified Network.Wai.Handler.Warp as Warp +import Network.Wai.Middleware.RequestLogger + ( logStdout, + ) +import qualified System.Envy as Envy +import qualified Web.Scotty.Trans as Scotty +import qualified Prelude + +{-# ANN module ("HLint: ignore Reduce duplication" :: Prelude.String) #-} + +main :: IO () +main = Exception.bracket startup shutdown <| uncurry Warp.run + where + startup = + Envy.decodeWithDefaults Envy.defConfig >>= \c -> do + sync <- STM.newTVarIO initialAppState + let runActionToIO m = runReaderT (runApp m) sync + waiapp <- Scotty.scottyAppT runActionToIO <| routes c + putText "*" + putText "Que.Host" + putText <| "port: " <> (show <| quePort c) + putText <| "skey: " <> (show <| queSkey c) + return (quePort c, waiapp) + shutdown :: a -> IO a + shutdown = pure . identity + +newtype App a = App + { runApp :: ReaderT (STM.TVar AppState) IO a + } + deriving + ( Applicative, + Functor, + Monad, + MonadIO, + MonadReader + (STM.TVar AppState) + ) + +newtype AppState = AppState + { ques :: HashMap Namespace Quebase + } + +initialAppState :: AppState +initialAppState = AppState {ques = mempty} + +data Config = Config + { -- | QUE_PORT + quePort :: Warp.Port, + -- | QUE_SKEY + queSkey :: FilePath + } + deriving (Generic, Show) + +instance Envy.DefConfig Config where + defConfig = Config 3000 "/run/skey/que-admin" + +instance Envy.FromEnv Config + +routes :: Config -> Scotty.ScottyT Text.Lazy.Text App () +routes cfg = do + Scotty.middleware logStdout + let quepath = "^\\/([[:alnum:]_-]+)\\/([[:alnum:]._/-]*)$" + let namespace = "^\\/([[:alnum:]_-]+)\\/?$" -- matches '/ns' and '/ns/' but not '/ns/path' + + -- GET /index.html + Scotty.get (Scotty.literal "/index.html") <| Scotty.redirect "/_/index" + Scotty.get (Scotty.literal "/") <| Scotty.redirect "/_/index" + -- GET /_/dash + Scotty.get (Scotty.literal "/_/dash") <| do + authkey <- fromMaybe "" </ Scotty.header "Authorization" + adminkey <- liftIO <| lchomp </ Text.Lazy.IO.readFile (queSkey cfg) + if authkey == adminkey + then do + d <- app <| gets ques + Scotty.json d + else do + Scotty.status Http.methodNotAllowed405 + Scotty.text "not allowed" + -- Namespace management + Scotty.matchAny (Scotty.regex namespace) <| do + Scotty.status Http.notImplemented501 + Scotty.text "namespace management coming soon" + -- GET que + -- + -- Receive a value from a que. Blocks until a value is received, + -- then returns. If 'poll=true', then stream data from the Que to the + -- client. + Scotty.get (Scotty.regex quepath) <| do + (ns, qp) <- extract + guardNs ns ["pub", "_"] + app . modify <| upsertNamespace ns + q <- app <| que ns qp + poll <- Scotty.param "poll" !: (pure . const False) + if poll + then Scotty.stream <| streamQue q + else do + r <- liftIO <| Go.read q + Scotty.html <| fromStrict <| Encoding.decodeUtf8 r + -- POST que + -- + -- Put a value on a que. Returns immediately. + Scotty.post (Scotty.regex quepath) <| do + authkey <- fromMaybe "" </ Scotty.header "Authorization" + adminkey <- liftIO <| lchomp </ Text.Lazy.IO.readFile (queSkey cfg) + (ns, qp) <- extract + -- Only allow my IP or localhost to publish to '_' namespace + when ("_" == ns && authkey /= adminkey) + <| Scotty.status Http.methodNotAllowed405 + >> Scotty.text "not allowed: _ is a reserved namespace" + >> Scotty.finish + guardNs ns ["pub", "_"] + -- passed all auth checks + app . modify <| upsertNamespace ns + q <- app <| que ns qp + qdata <- Scotty.body + _ <- liftIO <| Go.write q <| BSL.toStrict qdata + return () + +-- | Given `guardNs ns whitelist`, if `ns` is not in the `whitelist` +-- list, return a 405 error. +guardNs :: Text.Lazy.Text -> [Text.Lazy.Text] -> Scotty.ActionT Text.Lazy.Text App () +guardNs ns whitelist = + when (not <| ns `elem` whitelist) <| do + Scotty.status Http.methodNotAllowed405 + Scotty.text + <| "not allowed: use 'pub' namespace or signup to protect '" + <> ns + <> "' at https://que.run" + Scotty.finish + +-- | recover from a scotty-thrown exception. +(!:) :: + -- | action that might throw + Scotty.ActionT Text.Lazy.Text App a -> + -- | a function providing a default response instead + (Text.Lazy.Text -> Scotty.ActionT Text.Lazy.Text App a) -> + Scotty.ActionT Text.Lazy.Text App a +(!:) = Scotty.rescue + +-- | Forever write the data from 'Que' to 'Wai.StreamingBody'. +streamQue :: Que -> Wai.StreamingBody +streamQue q write _ = loop q + where + loop c = + Go.read c + >>= (write . Builder.byteStringInsert) + >> loop c + +-- | Gets the thing from the Hashmap. Call's 'error' if key doesn't exist. +grab :: (Eq k, Hashable k) => k -> HashMap k v -> v +grab = flip (HashMap.!) + +-- | Inserts the namespace in 'AppState' if it doesn't exist. +upsertNamespace :: Namespace -> AppState -> AppState +upsertNamespace ns as = + if HashMap.member ns (ques as) + then as + else as {ques = HashMap.insert ns mempty (ques as)} + +-- | Inserts the que at the proper 'Namespace' and 'Quepath'. +insertQue :: Namespace -> Quepath -> Que -> AppState -> AppState +insertQue ns qp q as = as {ques = newQues} + where + newQues = HashMap.insert ns newQbase (ques as) + newQbase = HashMap.insert qp q <| grab ns <| ques as + +extract :: Scotty.ActionT Text.Lazy.Text App (Namespace, Quepath) +extract = do + ns <- Scotty.param "1" + path <- Scotty.param "2" + return (ns, path) + +-- | A synonym for 'lift' in order to be explicit about when we are +-- operating at the 'App' layer. +app :: MonadTrans t => App a -> t App a +app = lift + +-- | Get something from the app state +gets :: (AppState -> b) -> App b +gets f = ask >>= liftIO . STM.readTVarIO >>= return </ f + +-- | Apply a function to the app state +modify :: (AppState -> AppState) -> App () +modify f = ask >>= liftIO . atomically . flip STM.modifyTVar' f + +-- | housing for a set of que paths +type Namespace = Text.Lazy.Text + +-- | a que is just a channel of bytes +type Que = Go.Channel Message + +-- | any path can serve as an identifier for a que +type Quepath = Text + +-- | any opaque data +type Message = ByteString + +-- | a collection of ques +type Quebase = HashMap Quepath Que + +-- | Lookup or create a que +que :: Namespace -> Quepath -> App Que +que ns qp = do + _ques <- gets ques + let qbase = grab ns _ques + queExists = HashMap.member qp qbase + if queExists + then return <| grab qp qbase + else do + c <- liftIO <| Go.chan 1 + modify (insertQue ns qp c) + gets ques /> grab ns /> grab qp diff --git a/Biz/Que/Host.nix b/Biz/Que/Host.nix new file mode 100644 index 0000000..e326483 --- /dev/null +++ b/Biz/Que/Host.nix @@ -0,0 +1,46 @@ +{ options +, lib +, config +, pkgs +, modulesPath +}: + +let + cfg = config.services.que-server; +in +{ + options.services.que-server = { + enable = lib.mkEnableOption "Enable the que-server service"; + port = lib.mkOption { + type = lib.types.int; + default = 3000; + description = '' + The port on which que-server will listen for + incoming HTTP traffic. + ''; + }; + package = lib.mkOption { + type = lib.types.package; + description = "que-server package to use"; + }; + }; + config = lib.mkIf cfg.enable { + systemd.services.que-server = { + path = [ cfg.package ]; + wantedBy = [ "multi-user.target" ]; + script = '' + ${cfg.package}/bin/que-server + ''; + description = '' + Que server + ''; + serviceConfig = { + Environment = ["QUE_PORT=${toString cfg.port}"]; + KillSignal = "INT"; + Type = "simple"; + Restart = "on-abort"; + RestartSec = "1"; + }; + }; + }; +} diff --git a/Biz/Que/Index.md b/Biz/Que/Index.md new file mode 100644 index 0000000..a9db12e --- /dev/null +++ b/Biz/Que/Index.md @@ -0,0 +1,73 @@ +% que.run + +que.run is the concurrent, async runtime in the cloud + + - runtime concurrency anywhere you have a network connection + - multilanguage communicating sequential processes + - add Go-like channels to any language + - connect your microservices together with the simplest possible + plumbing + - async programming as easy as running two terminal commands + +HTTP routes on `que.run` are Golang-like channels with a namespace and a +path. For example: `https://que.run/pub/path/subpath`. + +## Quickstart + +There is a simple script `que` that acts as a client you can use to +interact with the `que.run` service. + +Download it to somewhere on your `$PATH` and make it executable: + + curl https://que.run/_/client > ~/bin/que + chmod +x ~/bin/que + que --help + +The client requires a recent version of Python 3. + +## Powerup + +que.run is free for limited use, but the real power of an asynchronous, +concurrent runtime in the cloud is unlocked with some extra power-user +features. + +- Free + - security by obscurity + - all protocols and data formats supported + - bandwidth and message sizes limited + - concurrent connections limited + - request rate limited +- Power + - protect your data with private namespaces + - remove bandwidth and size limits + - private dashboard to see all of your active ques + - 99.999% uptime +- Pro + - add durability to your ques so messages are never lost + - powerful batch api + - incredible query api + - Linux FUSE filesystem integration +- Enterprise + - all of the Power & Pro features + - on-prem deployment + - advanced que performance monitoring + - SLA for support from que.run experts + +Email `ben@bsima.me` if you want to sign up for the Power, Pro, or +Enterprise packages. + +## Quescripts + +We are collecting a repository of scripts that make awesome use of que: + +- remote desktop notifications +- two-way communication with your phone +- ephemeral, serverless chat rooms +- collaborative jukebox + +<a id="quescripts-btn" href="/_/quescripts">See the scripts</a> + +## Docs + +- [tutorial](/_/tutorial) +- [api docs](/_/apidocs) diff --git a/Biz/Que/Prod.nix b/Biz/Que/Prod.nix new file mode 100644 index 0000000..12da1eb --- /dev/null +++ b/Biz/Que/Prod.nix @@ -0,0 +1,61 @@ +{ bild, lib }: + +# The production server for que.run + +bild.os { + imports = [ + ../OsBase.nix + ../Packages.nix + ../Users.nix + ./Host.nix + ./Site.nix + ]; + networking.hostName = "prod-que"; + networking.domain = "que.run"; + services.que-server = { + enable = true; + port = 80; + package = bild.ghc ./Host.hs; + }; + boot.loader.grub.device = "/dev/vda"; + fileSystems."/" = { device = "/dev/vda1"; fsType = "ext4"; }; + swapDevices = [ + { device = "/swapfile"; } # 4GB + ]; + networking.firewall.allowedTCPPorts = [ 22 80 443 ]; + networking = { + nameservers = [ + "67.207.67.2" + "67.207.67.3" + ]; + defaultGateway = "157.245.224.1"; + defaultGateway6 = "2604:a880:2:d1::1"; + dhcpcd.enable = false; + usePredictableInterfaceNames = lib.mkForce true; + interfaces = { + eth0 = { + ipv4.addresses = [ + { address="157.245.236.44"; prefixLength=20; } + { address="10.46.0.5"; prefixLength=16; } + ]; + ipv6.addresses = [ + { address="2604:a880:2:d1::a2:5001"; prefixLength=64; } + { address="fe80::7892:a5ff:fec6:dbc3"; prefixLength=64; } + ]; + ipv4.routes = [ { address = "157.245.224.1"; prefixLength = 32; } ]; + ipv6.routes = [ { address = "2604:a880:2:d1::1"; prefixLength = 32; } ]; + }; + }; + }; + services = { + que-website = { + enable = true; + namespace = "_"; + package = bild.ghc ./Site.hs; + }; + + udev.extraRules = '' + ATTR{address}=="7a:92:a5:c6:db:c3", NAME="eth0" + ''; + }; +} diff --git a/Biz/Que/Quescripts.md b/Biz/Que/Quescripts.md new file mode 100644 index 0000000..77e7004 --- /dev/null +++ b/Biz/Que/Quescripts.md @@ -0,0 +1,50 @@ +% Quescripts + +## Remote desktop notifications + +Lets say we are running a job that takes a long time, maybe we are +compiling or running a large test suite. Instead of watching the +terminal until it completes, or flipping back to check on it every so +often, we can create a listener that displays a popup notification when +the job finishes. + +In one terminal run the listener: + + que pub/notify --then "notify-send '{que}' '{msg}'" + +In some other terminal run the job that takes forever: + + runtests ; echo "tests are done" | que pub/notify - + +When terminal 2 succeeds, terminal 1 will print "tests are done", then +call the `notify-send` command, which displays a notification toast in +Linux with title "`pub/notify`" and content "`tests are done`". + +Que paths are multi-producer and multi-consumer, so you can add as many +terminals as you want. + +On macOS you could use something like this (just watch your quotes): + + osascript -e "display notification \"{msg}\" with title \"{que}\"" + +in place of notify-send. + +## Ephemeral, serverless chat rooms + +coming soon + +## Collaborative jukebox + +It's surprisingly easy to make a collaborative jukebox. + +First start up a music player: + + que --poll pub/music --then "playsong '{msg}'" + +where `playsong` is a script that plays a file from data streaming to +`stdin`. For example [vlc](https://www.videolan.org/vlc/) does this when +you run it like `vlc -`. + +Then, anyone can submit songs with: + + que pub/music song.mp3 diff --git a/Biz/Que/Site.hs b/Biz/Que/Site.hs new file mode 100644 index 0000000..99486a4 --- /dev/null +++ b/Biz/Que/Site.hs @@ -0,0 +1,137 @@ +{-# LANGUAGE LambdaCase #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE RecordWildCards #-} +{-# LANGUAGE NoImplicitPrelude #-} + +-- | spawns a few processes that serve the que.run website +-- +-- : out que-website +-- +-- : dep async +-- : dep config-ini +-- : dep process +-- : dep protolude +-- : dep req +module Biz.Que.Site + ( main, + ) +where + +import Alpha +import qualified Control.Concurrent.Async as Async +import qualified Data.ByteString.Char8 as BS +import qualified Data.Ini.Config as Config +import qualified Data.Text as Text +import Data.Text.Encoding (encodeUtf8) +import qualified Data.Text.IO as Text +import Network.HTTP.Req +import qualified System.Directory as Directory +import System.Environment as Environment +import qualified System.Exit as Exit +import System.FilePath ((</>)) +import qualified System.Process as Process + +main :: IO () +main = do + (src, ns) <- + Environment.getArgs >>= \case + [src] -> return (src, "_") -- default to _ ns which is special + [src, ns] -> return (src, Text.pack ns) + _ -> Exit.die "usage: que-website <srcdir> [namespace]" + mKey <- getKey ns + putText <| "serving " <> Text.pack src <> " at " <> ns + run mKey ns + <| Sources + { index = src </> "Index.md", + client = src </> "Client.py", + quescripts = src </> "Quescripts.md", + style = src </> "Style.css", + apidocs = src </> "Apidocs.md", + tutorial = src </> "Tutorial.md" + } + +getKey :: Namespace -> IO (Maybe Key) +getKey ns = do + home <- Directory.getHomeDirectory + let file = home </> ".config" </> "que.conf" + exists <- Directory.doesFileExist file + unless exists <| panic <| "not found: " <> Text.pack file + conf <- Text.readFile file + print (home </> ".config" </> "que.conf") + auth ns + |> Config.parseIniFile conf + |> either errorParsingConf identity + |> return + +errorParsingConf :: error +errorParsingConf = panic "could not parse ~/.config/que.conf" + +data Sources = Sources + { index :: FilePath, + quescripts :: FilePath, + client :: FilePath, + style :: FilePath, + tutorial :: FilePath, + apidocs :: FilePath + } + +type Namespace = Text + +type Key = Text + +auth :: Namespace -> Config.IniParser (Maybe Key) +auth "pub" = pure Nothing +auth ns = Config.sectionMb ns <| Config.field "key" + +run :: Maybe Key -> Text -> Sources -> IO () +run key ns Sources {..} = Async.runConcurrently actions |> void + where + actions = + traverse + Async.Concurrently + [ forever <| toHtml index >>= serve key ns "index", + forever <| toHtml quescripts >>= serve key ns "quescripts", + forever <| BS.readFile client >>= serve key ns "client", + forever <| toHtml tutorial >>= serve key ns "tutorial", + forever <| toHtml apidocs >>= serve key ns "apidocs" + ] + toHtml :: FilePath -> IO ByteString + toHtml md = + BS.pack + </ Process.readProcess + "pandoc" + [ "--include-in-header", + style, + "-i", + md, + "--from", + "markdown", + "--to", + "html" + ] + [] + +serve :: Maybe Key -> Namespace -> Text -> ByteString -> IO () +serve Nothing "pub" path content = + runReq defaultHttpConfig <| do + _ <- + req + POST + (http "que.run" /: "pub" /: path) + (ReqBodyBs content) + ignoreResponse + mempty + liftIO <| return () +serve Nothing p _ _ = panic <| "no auth key provided for ns: " <> p +serve (Just key) ns path content = + runReq defaultHttpConfig <| do + let options = + header "Authorization" (encodeUtf8 key) <> responseTimeout maxBound + _ <- + req + POST + (http "que.run" /: ns /: path) + (ReqBodyBs content) + ignoreResponse + options + liftIO <| return () diff --git a/Biz/Que/Site.nix b/Biz/Que/Site.nix new file mode 100644 index 0000000..ba2eeb2 --- /dev/null +++ b/Biz/Que/Site.nix @@ -0,0 +1,61 @@ +{ options +, lib +, config +, pkgs +, modulesPath +}: + + + +let + cfg = config.services.que-website; + static = pkgs.stdenv.mkDerivation { + src = ./.; + name = "que-website-static"; + installPhase = '' + mkdir -p $out + cp ${./Apidocs.md} $out/Apidocs.md + cp ${./Index.md} $out/Index.md + cp ${./Quescripts.md} $out/Quescripts.md + cp ${./Style.css} $out/Style.css + cp ${./Tutorial.md} $out/Tutorial.md + cp ${./Client.py} $out/Client.py + ''; + }; +in +{ + options.services.que-website = { + enable = lib.mkEnableOption "Enable the que-website service"; + namespace = lib.mkOption { + type = lib.types.str; + default = "_"; + description = '' + The que namespace on which que-website will broadcast. + ''; + }; + package = lib.mkOption { + type = lib.types.package; + description = "que-website package to use"; + }; + }; + config = lib.mkIf cfg.enable { + systemd.services.que-website = { + path = [ cfg.package pkgs.pandoc ]; + wantedBy = [ "multi-user.target" ]; + script = '' + ${cfg.package}/bin/que-website ${static} ${cfg.namespace} + ''; + description = '' + Que website server + ''; + serviceConfig = { + User = "root"; + Environment = "HOME=/root"; + KillSignal = "INT"; + Type = "simple"; + Restart = "on-abort"; + RestartSec = "1"; + }; + }; + }; +} diff --git a/Biz/Que/Style.css b/Biz/Que/Style.css new file mode 100644 index 0000000..f8d1ca4 --- /dev/null +++ b/Biz/Que/Style.css @@ -0,0 +1,136 @@ +<link href="https://fonts.googleapis.com/css2?family=Source+Code+Pro:ital,wght@0,400;0,700;1,400;1,700&family=Source+Sans+Pro:ital,wght@0,400;0,700;1,400;1,700&display=swap" rel="stylesheet"> +<style> +:root { + /* base (http://chriskempson.com/projects/base16/) */ + --base00: #181818; + --base01: #282828; + --base02: #383838; + --base03: #585858; + --base04: #b8b8b8; + --base05: #d8d8d8; + --base06: #e8e8e8; + --base07: #f8f8f8; + + /* highlights */ + --base08: #ab4642; + --base09: #dc9656; + --base0A: #f7ca88; + --base0B: #a1b56c; + --base0C: #86c1b9; + --base0D: #7cafc2; + --base0E: #ba8baf; + --base0F: #a16946; +} + +/* dark theme */ +@media ( prefers-color-scheme: dark ), + ( prefers-color-scheme: no-preference ) +{ + body + { color: var(--base05); + ; background: var(--base00) + } + + header, h1, h2, h3 + { color: var(--base0A) } + + a:link, a:visited + { color: var(--base0D) } + + a:hover + { color: var(--base0C) } + + pre + { background-color: var(--base01) } + + code + { color: var(--base0B) + } + + hr + { border: 0 + ; height: 1px + ; width: 100% + ; margin: 2rem + ; background-image: linear-gradient( + to right, + /* same as --base0A */ + rgba(186, 139, 175, 0), + rgba(186, 139, 175, 0.75), + rgba(186, 139, 175, 0)) + } +} + +/* light theme */ + +@media ( prefers-color-scheme: light) +{ + body + { background-color: var(--base07) + ; color: var(--base00) + } + + a:link, a:visited + { color: var(--base0D) } + + a:hover + { color: var(--base0C) } + + pre + { background-color: var(--base06) } + + code + { color: var(--base0B) } +} + +/* structure and layout */ + +body +{ max-width: 900px +; margin: 40px auto +; padding: 0 10px +; font: 18px/1.5 + "Source Sans Pro", + sans-serif, + "Apple Color Emoji", + "Segoe UI Emoji", + "Segoe UI Symbol", + "Noto Color Emoji" +; display: flex +; flex-direction: column +; align-items: auto +} + +header#title-block-header, +h1, +h2, +h3 +{ line-height: 1.2 +; align-self: center +; text-transform: lowercase +} + +pre +{ padding: .5rem } + +pre, code +{ overflow-x: scroll +; white-space: pre +; font-family: "Source Code Pro", monospace; +} + +#quescripts-btn +{ border-width: 2px +; border-style: solid +} + +#quescripts-btn +{ font-size: 1.2rem +; padding: 1rem +; text-decoration: none +; text-align: center +; display: block +; max-width: 400px +; margin: auto +} +</style> diff --git a/Biz/Que/Tutorial.md b/Biz/Que/Tutorial.md new file mode 100644 index 0000000..6542ad3 --- /dev/null +++ b/Biz/Que/Tutorial.md @@ -0,0 +1,53 @@ +% que.run Tutorial + +## Ques + +A que is a multi-consumer, multi-producer channel available anywhere you +have a network connection. If you are familiar with Go channels, they +are pretty much the same thing. Put some values in one end, and take +them out the other end at a different time, or in a different process. + +Ques are created dynamically for every HTTP request you make. Here we +use the `que` client to create a new que at the path `pub/new-que`: + + que pub/new-que + +The `que` client is useful, but you can use anything to make the HTTP +request, for example here's the same thing with curl: + + curl https://que.run/pub/new-que + +These requests will block until a value is placed on the other +end. Let's do that now. In a separate terminal: + + echo "hello world" | que pub/new-que - + +This tells the `que` client to read the value from `stdin` and then send +it to `example/new-que`. Or with curl: + + curl https://que.run/pub/new-que -d "hello world" + +This will succeed immediately and send the string "`hello world`" over +the channel, which will be received and printed by the listener in the +other terminal. + +You can have as many producers and consumers attached to a channel as +you want. + +## Namespaces + +Ques are organized into namespaces, identified by the first fragment of +the path. In the above commands we used `pub` as the namespace, which is +a special publically-writable namespace. The other special namespace is +`_` which is reserved for internal use only. You can't write to the `_` +namespace. + +To use other namespaces and add authentication/access controls, you can +[sign up for the Power package](/_/index). + +## Events + +Just reading and writing data isn't very exciting, so let's throw in +some events. We can very quickly put together a job processor. + + que pub/new-que --then "./worker.sh '{msg}'" |