diff options
author | Ben Sima <ben@bsima.me> | 2020-04-15 09:54:10 -0700 |
---|---|---|
committer | Ben Sima <ben@bsima.me> | 2020-04-15 10:06:56 -0700 |
commit | f4b8c0df041b063c0b47d2ec6c818a9c202fd833 (patch) | |
tree | 01ad246a83fda29c079847b3397ca6509a7f6106 /Que | |
parent | 6ed475ca94209ce92e75f48764cb9d361029ea26 (diff) |
Re-namespacing
Moving away from the DNS-driven namespacing toward more condensed names,
mostly because I don't like typing so much.
Diffstat (limited to 'Que')
-rw-r--r-- | Que/Prod.nix | 44 | ||||
-rw-r--r-- | Que/Server.hs | 240 | ||||
-rw-r--r-- | Que/Server.nix | 46 | ||||
-rw-r--r-- | Que/Website.hs | 126 | ||||
-rw-r--r-- | Que/Website.nix | 59 | ||||
-rw-r--r-- | Que/apidocs.md | 3 | ||||
-rwxr-xr-x | Que/client.py | 149 | ||||
-rw-r--r-- | Que/index.md | 73 | ||||
-rw-r--r-- | Que/quescripts.md | 50 | ||||
-rw-r--r-- | Que/style.css | 136 | ||||
-rw-r--r-- | Que/tutorial.md | 53 |
11 files changed, 979 insertions, 0 deletions
diff --git a/Que/Prod.nix b/Que/Prod.nix new file mode 100644 index 0000000..97749c8 --- /dev/null +++ b/Que/Prod.nix @@ -0,0 +1,44 @@ +{ config, pkgs, lib, ... }: +{ + imports = [ <nixpkgs/nixos/modules/profiles/qemu-guest.nix> ]; + boot.loader.grub.device = "/dev/vda"; + fileSystems."/" = { device = "/dev/vda1"; fsType = "ext4"; }; + networking.firewall.allowedTCPPorts = [ 22 80 443 ]; + services.que-server = { + enable = true; + port = 80; + package = pkgs.que-server; + }; + services.que-website = { + enable = true; + namespace = "_"; + package = pkgs.que-website; + }; + 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.udev.extraRules = '' + ATTR{address}=="7a:92:a5:c6:db:c3", NAME="eth0" + ''; +} diff --git a/Que/Server.hs b/Que/Server.hs new file mode 100644 index 0000000..841cbfa --- /dev/null +++ b/Que/Server.hs @@ -0,0 +1,240 @@ +{-# LANGUAGE NoImplicitPrelude #-} +{-# LANGUAGE GeneralizedNewtypeDeriving #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE LambdaCase #-} +{-# LANGUAGE DeriveGeneric #-} + +-- | 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/> +-- +-- : exe que-server +-- +-- : dep async +-- : dep envy +-- : dep protolude +-- : dep scotty +-- : dep stm +-- : dep unagi-chan +-- : dep unordered-containers +module Que.Server + ( main + ) +where + +import Alpha hiding ( Text + , get + , 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 Data.Text.Lazy ( Text + , fromStrict + ) +import qualified Data.Text.Lazy.IO as Text +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 System.Exit as Exit +import qualified Web.Scotty.Trans as Scotty + +main :: IO () +main = Exception.bracket startup shutdown <| uncurry Warp.run + where + startup = Envy.decodeEnv >>= \case + Left e -> Exit.die e + Right c -> do + sync <- STM.newTVarIO initialAppState + let runActionToIO m = runReaderT (runApp m) sync + waiapp <- Scotty.scottyAppT runActionToIO routes + putText <| "port:" <> (show <| quePort 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)) + +data AppState = AppState + { ques :: HashMap Namespace Quebase + } + +initialAppState :: AppState +initialAppState = AppState { ques = mempty } + +data Config = Config + { quePort :: Warp.Port -- ^ QUE_PORT + } deriving (Generic, Show) + +instance Envy.DefConfig Config where + defConfig = Config 3000 + +instance Envy.FromEnv Config + +routes :: Scotty.ScottyT Text App () +routes = 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.readFile "/run/keys/que-admin" + 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 + app . modify <| upsertNamespace ns + q <- app <| que ns qp + poll <- Scotty.param "poll" !: (pure . const False) + guardNs ns ["pub", "_"] + case poll of + True -> Scotty.stream $ streamQue q + _ -> 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.readFile "/run/keys/que-admin" + (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 -> [Text] -> Scotty.ActionT 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. +(!:) + :: Scotty.ActionT Text App a -- ^ action that might throw + -> (Text -> Scotty.ActionT Text App a) -- ^ a function providing a default response instead + -> Scotty.ActionT Text App a +(!:) = Scotty.rescue + +-- | Forever write the data from 'Que' to 'Wai.StreamingBody'. +streamQue :: Que -> Wai.StreamingBody +streamQue q write _ = Go.mult q >>= loop + where + loop c = + Go.tap c + >>= (write . Builder.byteStringInsert) + >> (write <| Builder.byteStringInsert "\n") + >> 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 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 + +type Namespace = Text -- ^ housing for a set of que paths +type Que = Go.Channel Message -- ^ a que is just a channel of bytes +type Quepath = Text -- ^ any path can serve as an identifier for a que +type Message = ByteString -- ^ any opaque data +type Quebase = HashMap Quepath Que -- ^ a collection of ques + +-- | 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/Que/Server.nix b/Que/Server.nix new file mode 100644 index 0000000..e326483 --- /dev/null +++ b/Que/Server.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/Que/Website.hs b/Que/Website.hs new file mode 100644 index 0000000..e75f2bd --- /dev/null +++ b/Que/Website.hs @@ -0,0 +1,126 @@ +{-# LANGUAGE RecordWildCards #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE NoImplicitPrelude #-} +{-# LANGUAGE LambdaCase #-} + +-- | spawns a few processes that serve the que.run website +-- +-- : exe que-website +-- +-- : dep async +-- : dep config-ini +-- : dep process +-- : dep protolude +-- : dep req +module Que.Website + ( 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 >> return () + 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/Que/Website.nix b/Que/Website.nix new file mode 100644 index 0000000..6a24d9d --- /dev/null +++ b/Que/Website.nix @@ -0,0 +1,59 @@ +{ 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/Que/apidocs.md b/Que/apidocs.md new file mode 100644 index 0000000..f400889 --- /dev/null +++ b/Que/apidocs.md @@ -0,0 +1,3 @@ +% que.run Api Docs + +coming soon diff --git a/Que/client.py b/Que/client.py new file mode 100755 index 0000000..3d9291d --- /dev/null +++ b/Que/client.py @@ -0,0 +1,149 @@ +#!/usr/bin/env python3 +""" +simple client for que.run +""" + +import argparse +import configparser +import http.client +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): + ns = args.target.split("/")[0] + if ns == "pub": + return None + else: + 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[ns]["key"] + + +def send(args): + "Send a message to the que." + 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: + while not time.sleep(1): + request.urlopen(req, data=data, timeout=MAX_TIMEOUT) + + else: + request.urlopen(req, data=data, timeout=MAX_TIMEOUT) + + +def recv(args): + "Receive a message from the que." + + def _recv(_req): + msg = autodecode(_req.read()) + print(msg) + if args.then: + subprocess.run( + args.then.replace("\msg", msg).replace("\que", args.target), shell=True + ) + + 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: + while not time.sleep(1): + _recv(_req) + else: + _recv(_req) + + +def autodecode(b): + """Attempt to decode bytes `b` 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> + + """ + codecs = ["utf-8", "ascii"] + for codec in codecs: + try: + return b.decode(codec) + except UnicodeDecodeError: + pass + return b + + +def get_args(): + cli = argparse.ArgumentParser(description=__doc__) + 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," + "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__": + args = get_args() + try: + if args.infile: + send(args) + else: + recv(args) + except KeyboardInterrupt: + sys.exit(0) + except urllib.error.HTTPError as e: + print(e) + sys.exit(1) + except http.client.RemoteDisconnected as e: + print("disconnected... retrying in 5 seconds") + time.sleep(5) + if args.infile: + send(args) + else: + recv(args) diff --git a/Que/index.md b/Que/index.md new file mode 100644 index 0000000..a9db12e --- /dev/null +++ b/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/Que/quescripts.md b/Que/quescripts.md new file mode 100644 index 0000000..9a2e6e0 --- /dev/null +++ b/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: + + 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/Que/style.css b/Que/style.css new file mode 100644 index 0000000..f8d1ca4 --- /dev/null +++ b/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/Que/tutorial.md b/Que/tutorial.md new file mode 100644 index 0000000..66ecd3c --- /dev/null +++ b/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'" |