summaryrefslogtreecommitdiff
path: root/Biz/Que
diff options
context:
space:
mode:
authorBen Sima <ben@bsima.me>2020-12-04 11:16:25 -0500
committerBen Sima <ben@bsima.me>2020-12-05 07:55:13 -0500
commit330e4363d8abb509031d2c8c1a89dcc6f955e2c1 (patch)
tree915c8c50a7125bf6eb9e560f8d00a80592f41c77 /Biz/Que
parent32f53350a3a3d701e9a1474e670a8454342adc40 (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.md3
-rwxr-xr-xBiz/Que/Client.py186
-rw-r--r--Biz/Que/Host.hs253
-rw-r--r--Biz/Que/Host.nix46
-rw-r--r--Biz/Que/Index.md73
-rw-r--r--Biz/Que/Prod.nix61
-rw-r--r--Biz/Que/Quescripts.md50
-rw-r--r--Biz/Que/Site.hs137
-rw-r--r--Biz/Que/Site.nix61
-rw-r--r--Biz/Que/Style.css136
-rw-r--r--Biz/Que/Tutorial.md53
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}'"