Merge pull request #2875 from urbit/m/debug-dashboard

Initial debug dashboard
This commit is contained in:
Fang 2020-05-26 22:01:48 +02:00 committed by GitHub
commit 016fd9101c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
40 changed files with 10316 additions and 428 deletions

View File

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:de3b622f4ce90a9c2ce73fa233ba356e239a64d8aa4820ea7f48663110aa3fdd
size 18821765
oid sha256:6504214aef3dff2d5e9a477239e9f9cafa0e11855be6b6a2b704e7164bb8cb37
size 12901460

898
pkg/arvo/app/dbug.hoon Normal file
View File

@ -0,0 +1,898 @@
:: dbug: debug dashboard server
::
/- spider
/+ server, default-agent, verb, dbug
::
|%
+$ state-0 [%0 passcode=(unit @t)]
+$ card card:agent:gall
--
::
=| state-0
=* state -
::
%+ verb |
%- agent:dbug
^- agent:gall
=<
|_ =bowl:gall
+* this .
do ~(. +> bowl)
def ~(. (default-agent this %|) bowl)
::
++ on-init
^- (quip card _this)
:_ this
[%pass /connect %arvo %e %connect [~ /'~debug'] dap.bowl]~
::
++ on-save !>(state)
::
++ on-load
|= old=vase
^- (quip card _this)
[~ this(state !<(state-0 old))]
::
++ on-watch
|= =path
^- (quip card _this)
?. ?=([%http-response *] path)
(on-watch:def path)
[~ this]
::
++ on-poke
|= [=mark =vase]
^- (quip card _this)
?: ?=(%noun mark)
?> (team:title [our src]:bowl)
=/ code !<((unit @t) vase)
=/ msg=tape
?~ code
"Removing passcode access for debug interface."
"""
Enabling passcode access for debug interface. Anyone with this code can
view your applications' state, the people you've talked to, etc. Only
share with people you trust. To disable, run :dbug ~
"""
%- (slog leaf+msg ~)
[~ this(passcode code)]
?. ?=(%handle-http-request mark)
(on-poke:def mark vase)
=+ !<([eyre-id=@ta =inbound-request:eyre] vase)
:_ this
%+ give-simple-payload:app:server eyre-id
%+ authorize-http-request:do inbound-request
handle-http-request:do
::
++ on-arvo
|= [=wire =sign-arvo]
^- (quip card _this)
?. ?=([%e %bound *] sign-arvo)
(on-arvo:def wire sign-arvo)
~? !accepted.sign-arvo
[dap.bowl "bind rejected!" binding.sign-arvo]
[~ this]
::
++ on-peek on-peek:def
++ on-leave on-leave:def
++ on-agent on-agent:def
++ on-fail on-fail:def
--
::
|_ =bowl:gall
::
:: serving
::
++ authorize-http-request
=, server
:: if no passcode configured, only allow host ship to view
::
?~ passcode require-authorization:app
|= $: =inbound-request:eyre
handler=$-(inbound-request:eyre simple-payload:http)
==
?: authenticated.inbound-request
(handler inbound-request)
:: else, allow randos access,
:: on the condition they provide a correct ?passcode= url parameter
::
=; pass=(unit @t)
?: =(passcode pass)
(handler inbound-request)
(require-authorization:app inbound-request handler)
=/ from-url=(unit @t)
=- (~(get by -) 'passcode')
%- ~(gas by *(map @t @t))
args:(parse-request-line url.request.inbound-request)
?^ from-url from-url
:: try the referer field instead
::
=/ ref-url=(unit @t)
(get-header:http 'referer' header-list.request.inbound-request)
?~ ref-url ~
?~ (find "passcode={(trip u.passcode)}" (trip u.ref-url)) ~
passcode
::
++ handle-http-request
=, server
|= =inbound-request:eyre
^- simple-payload:http
=/ =request-line
%- parse-request-line
url.request.inbound-request
=* req-head header-list.request.inbound-request
::TODO handle POST
?. ?=(%'GET' method.request.inbound-request)
not-found:gen
(handle-get-request req-head request-line)
::
++ handle-get-request
=, server
|= [headers=header-list:http request-line]
^- simple-payload:http
=? site ?=([%'~debug' *] site) t.site
?~ ext
$(ext `%html, site [%index ~]) ::NOTE hack
:: if not json, serve static file
::
?. ?=([~ %json] ext)
=/ file=(unit octs)
(get-file-at /app/debug site u.ext)
?~ file not-found:gen
?+ u.ext not-found:gen
%html (html-response:gen u.file)
%js (js-response:gen u.file)
%css (css-response:gen u.file)
%png (png-response:gen u.file)
==
:: get data matching the json and convert it
::
=; json=(unit json)
?~ json not-found:gen
%- json-response:gen
=, html
(as-octt:mimes (en-json u.json))
=, enjs:format
?+ site ~
:: /apps.json: {appname: running?}
::
[%apps ~]
%- some
%- pairs
%+ turn all:apps
|= app=term
[app b+(running:apps app)]
::
:: /app/[appname]...
::
[%app @ *]
=* app i.t.site
::TODO ?. (dbugable:apps app) ~
=/ rest=^path t.t.site
?+ rest ~
:: /app/[appname].json: {state: }
::
~
%- some
%- pairs
:~ :- 'simpleState'
%- tank
=; head=(unit ^tank)
(fall head leaf+"unversioned")
:: try to print the state version
::
=/ version=(unit vase)
(slew 2 (state:apps app))
?~ version ~
?. ?=(%atom -.p.u.version) ~
`(sell u.version)
::
:- 'subscriptions'
%- pairs
=+ (subscriptions:apps app)
|^ ~['in'^(incoming in) 'out'^(outgoing out)]
::
++ incoming
|= =bitt:gall
^- json
:- %a
%+ turn ~(tap by bitt)
|= [d=duct [s=^ship p=^path]]
%- pairs
:~ 'duct'^a+(turn d path)
'ship'^(ship s)
'path'^(path p)
==
::
++ outgoing
|= =boat:gall
^- json
:- %a
%+ turn ~(tap by boat)
|= [[w=wire s=^ship t=term] [a=? p=^path]]
%- pairs
:~ 'wire'^(path w)
'ship'^(ship s)
'app'^s+t
'acked'^b+a
'path'^(path p)
==
--
==
::
:: /app/[appname]/state.json
:: /app/[appname]/state/[query].json
::
[%state ?(~ [@ ~])]
%- some
=- (pairs 'state'^(tank -) ~)
%+ state-at:apps app
?~ t.rest ~
(slaw %t i.t.rest)
==
::
:: /spider.json
::
[%spider %threads ~]
%- some
:: turn flat stack descriptors into object (tree) representing stacks
::
|^ (tree-to-json build-thread-tree)
::
+$ tree
$~ ~
(map tid:spider tree)
::
++ build-thread-tree
%+ roll tree:threads
|= [stack=(list tid:spider) =tree]
?~ stack tree
%+ ~(put by tree) i.stack
%_ $
stack t.stack
tree (~(gut by tree) i.stack ~)
==
::
++ tree-to-json
|= =tree
o+(~(run by tree) tree-to-json)
--
::
:: /azimuth/status
::
:: /ames/peer.json
::
[%ames %peer ~]
=/ [known=(list [^ship *]) alien=(list [^ship *])]
%+ skid ~(tap by peers:v-ames)
|= [^ship kind=?(%alien %known)]
?=(%known kind)
%- some
%- pairs
::NOTE would do (cork head ship) but can't get that to compile...
:~ 'known'^a+(turn (turn known head) ship)
'alien'^a+(turn (turn alien head) ship)
==
::
:: /ames/peer/[shipname].json
::
[%ames %peer @ ~]
=/ who=^ship
(rash i.t.t.site fed:ag)
%- some
=, v-ames
(peer-to-json (peer who))
::
:: /behn/timers.json
::
[%behn %timers ~]
%- some
:- %a
%+ turn timers:v-behn
|= [date=@da =duct]
%- pairs
:~ 'date'^(time date)
'duct'^a+(turn duct path)
==
::
:: /clay/commits.json
::
[%clay %commits ~]
(some commits-json:v-clay)
::
:: /eyre/bindings.json
::
[%eyre %bindings ~]
%- some
:- %a
%+ turn bindings:v-eyre
=, eyre
|= [binding =duct =action]
%- pairs
:~ 'location'^s+(cat 3 (fall site '*') (spat path))
'action'^(render-action:v-eyre action)
==
::
:: /eyre/connections.json
::
[%eyre %connections ~]
%- some
:- %a
%+ turn ~(tap by connections:v-eyre)
|= [=duct outstanding-connection:eyre]
%- pairs
:~ 'duct'^a+(turn duct path)
'action'^(render-action:v-eyre action)
::
:- 'request'
%- pairs
=, inbound-request
:~ 'authenticated'^b+authenticated
'secure'^b+secure
'source'^s+(scot %if +.address)
:: ?- -.address
:: %ipv4 %if
:: %ipv6 %is
:: ==
==
::
:- 'response'
%- pairs
:~ 'sent'^(numb bytes-sent)
::
:- 'header'
?~ response-header ~
=, u.response-header
%- pairs
:~ 'status-code'^(numb status-code)
::
:- 'headers'
:- %a
%+ turn headers
|=([k=@t v=@t] s+:((cury cat 3) k ': ' v))
==
==
==
::
:: /eyre/authentication.json
::
[%eyre %authentication ~]
%- some
:- %a
%+ turn
%+ sort ~(tap by sessions:auth-state:v-eyre)
|= [[@uv a=@da] [@uv b=@da]]
(gth a b)
|= [cookie=@uv session:eyre]
%- pairs
:~ 'cookie'^s+(end 3 4 (rsh 3 2 (scot %x (shax cookie))))
'expiry'^(time expiry-time)
==
::
:: /eyre/channels.json
::
[%eyre %channels ~]
%- some
:- %a
=+ channel-state:v-eyre
%+ turn ~(tap by session)
|= [key=@t channel:eyre]
%- pairs
:~ 'session'^s+key
'connected'^b+!-.state
'expiry'^?-(-.state %& (time date.p.state), %| ~)
'next-id'^(numb next-id)
'unacked'^a+(turn (sort (turn ~(tap in events) head) dor) numb)
::
:- 'subscriptions'
:- %a
%+ turn ~(tap by subscriptions)
|= [=wire [=^ship app=term =^path *]]
%- pairs
:~ 'wire'^(^path wire)
'ship'^(^ship ship)
'app'^s+app
'path'^(^path path)
==
==
==
::
++ get-file-at
|= [base=path file=path ext=@ta]
^- (unit octs)
?. ?=(?(%html %css %js %png) ext)
~
=/ =path
:* (scot %p our.bowl)
q.byk.bowl
(scot %da now.bowl)
(snoc (weld base file) ext)
==
?. .^(? %cu path) ~
%- some
%- as-octs:mimes:html
.^(@ %cx path)
::
:: applications
::
++ apps
|%
++ all
^- (list term)
%+ murn
(scry (list path) %ct %home /app)
|= =path
^- (unit term)
?. ?=([%app @ %hoon ~] path) ~
`i.t.path
::
++ running
|= app=term
(scry ? %gu app ~)
::
++ dbugable
|= app=term
^- ?
!! ::TODO how to check if it supports the /dbug scries?
::
++ state
|= app=term
^- vase
(scry-dbug vase app /state)
::
++ state-at
|= [app=term what=(unit @t)]
^- tank
=/ state=vase (state app)
?~ what (sell state)
=/ result=(each vase tang)
%- mule |.
%+ slap
(slop state !>([bowl=bowl ..zuse]))
(ream u.what)
?- -.result
%& (sell p.result)
%| (head p.result)
==
::
++ subscriptions
=, gall
|= app=term
^- [out=boat in=bitt]
(scry-dbug ,[boat bitt] app /subscriptions)
::
++ scry-dbug
|* [=mold app=term =path]
(scry mold %gx app (snoc `^path`[%dbug path] %noun))
::
::TODO but why? we can't tell if it's on or not
++ poke-verb-toggle
|= app=term
^- card
(poke /verb/[app] app %verb !>(%loud))
--
::
:: threads
::
++ threads
|%
::NOTE every (list tid:spider) represents a stack,
:: with a unique tid at the end
++ tree
(scry (list (list tid:spider)) %gx %spider /tree/noun)
::
++ poke-kill
|= =tid:spider
^- card
(poke /spider/kill/[tid] %spider %spider-stop !>([tid |]))
--
::
:: ames
::
++ v-ames
|%
++ peers
(scry (map ship ?(%alien %known)) %a %peers ~)
::
++ peer
|= who=ship
(scry ship-state:ames %a %peer /(scot %p who))
::
++ peer-to-json
=, ames
=, enjs:format
|= =ship-state
|^ ^- json
%+ frond -.ship-state
?- -.ship-state
%alien (alien +.ship-state)
%known (known +.ship-state)
==
::
++ alien
|= alien-agenda
%- pairs
:~ 'messages'^(numb (lent messages))
'packets'^(numb ~(wyt in packets))
'heeds'^(set-array heeds from-duct)
==
::
:: json for known peer is structured to closely match the peer-state type.
:: where an index is specified, the array is generally sorted by those.
::
:: { life: 123,
:: route: { direct: true, lane: 'something' },
:: qos: { kind: 'status', last-contact: 123456 }, // ms timestamp
:: flows: { forward: [snd, rcv, ...], backward: [snd, rcv, ...] }
:: -> snd:
:: { bone: 123, // index
:: duct: ['/paths', ...]
:: current: 123,
:: next: 123,
:: unsent-messages: [123, ...], // size in bytes
:: queued-message-acks: [{
:: message-num: 123, // index
:: ack: 'ok'
:: }, ...],
:: packet-pump-state: {
:: next-wake: 123456, // ms timestamp
:: live: [{
:: message-num: 123, // index
:: fragment-num: 123, // index
:: num-fragments: 123,
:: last-sent: 123456, // ms timestamp
:: retries: 123,
:: skips: 123
:: }, ...],
:: metrics: {
:: rto: 123, // seconds
:: rtt: 123, // seconds
:: rttvar: 123,
:: ssthresh: 123,
:: num-live: 123,
:: cwnd: 123,
:: counter: 123
:: }
:: }
:: }
:: -> rcv:
:: { bone: 123, // index
:: duct: ['/paths', ...] // index
:: last-acked: 123,
:: last-heard: 123,
:: pending-vane-ack: [123, ...],
:: live-messages: [{
:: message-num: 123, // index
:: num-received: 122,
:: num-fragments: 123,
:: fragments: [123, ...]
:: }, ...],
:: nax: [123, ...]
:: }
:: nax: [{
:: bone: 123, // index
:: duct: ['/paths', ...],
:: message-num: 123
:: }, ...],
:: heeds: [['/paths', ...] ...]
:: }
::
++ known
|= peer-state
%- pairs
:~ 'life'^(numb life)
::
:- 'route'
%+ maybe route
|= [direct=? =lane]
%- pairs
:~ 'direct'^b+direct
::
:- 'lane'
?- -.lane
%& (ship p.lane)
::
%|
?~ l=((soft ,[=@tas =@if =@ud]) (cue p.lane))
s+(scot %x p.lane)
=, u.l
(tape "%{(trip tas)}, {(scow %if if)}, {(scow %ud ud)}")
==
==
::
:- 'qos'
%- pairs
:~ 'kind'^s+-.qos
'last-contact'^(time last-contact.qos)
==
::
:- 'flows'
|^ =/ mix=(list flow)
=- (sort - dor)
%+ welp
(turn ~(tap by snd) (tack %snd))
(turn ~(tap by rcv) (tack %rcv))
=/ [forward=(list flow) backward=(list flow)]
%+ skid mix
|= [=bone *]
=(0 (mod bone 2))
%- pairs
:~ ['forward' a+(turn forward build)]
['backward' a+(turn backward build)]
==
::
+$ flow
$: =bone
::
$= state
$% [%snd message-pump-state]
[%rcv message-sink-state]
==
==
::
++ tack
|* =term
|* [=bone =noun]
[bone [term noun]]
::
++ build
|= flow
^- json
%+ frond -.state
?- -.state
%snd (snd-with-bone ossuary bone +.state)
%rcv (rcv-with-bone ossuary bone +.state)
==
--
::
:- 'nax'
:- %a
%+ turn (sort ~(tap in nax) dor) :: sort by bone
|= [=bone =message-num]
%- pairs
:* 'message-num'^(numb message-num)
(bone-to-pairs bone ossuary)
==
::
'heeds'^(set-array heeds from-duct)
==
::
++ snd-with-bone
|= [=ossuary =bone message-pump-state]
^- json
%- pairs
:* 'current'^(numb current)
'next'^(numb next)
::
:- 'unsent-messages' :: as byte sizes
(set-array unsent-messages (cork (cury met 3) numb))
::
'unsent-fragments'^(numb (lent unsent-fragments)) :: as lent
::
:- 'queued-message-acks'
:- %a
%+ turn (sort ~(tap by queued-message-acks) dor) :: sort by msg nr
|= [=message-num =ack]
%- pairs
:~ 'message-num'^(numb message-num)
'ack'^s+-.ack
==
::
:- 'packet-pump-state'
%- pairs
=, packet-pump-state
:~ 'next-wake'^(maybe next-wake time)
::
:- 'live'
:- %a
%+ turn (sort ~(tap in live) dor) :: sort by msg nr & frg nr
|= [live-packet-key live-packet-val]
%- pairs
:~ 'message-num'^(numb message-num)
'fragment-num'^(numb fragment-num)
'num-fragments'^(numb num-fragments)
'last-sent'^(time last-sent)
'retries'^(numb retries)
'skips'^(numb skips)
==
::
:- 'metrics'
%- pairs
=, metrics
:~ 'rto'^(numb (div rto ~s1)) ::TODO milliseconds?
'rtt'^(numb (div rtt ~s1))
'rttvar'^(numb (div rttvar ~s1))
'ssthresh'^(numb ssthresh)
'num-live'^(numb num-live)
'cwnd'^(numb cwnd)
'counter'^(numb counter)
==
==
::
(bone-to-pairs bone ossuary)
==
::
++ rcv-with-bone
|= [=ossuary =bone message-sink-state]
^- json
%- pairs
:* 'last-acked'^(numb last-acked)
'last-heard'^(numb last-heard)
::
:- 'pending-vane-ack'
=- a+(turn - numb)
(sort (turn ~(tap in pending-vane-ack) head) dor) :: sort by msg #
::
:- 'live-messages'
:- %a
%+ turn (sort ~(tap by live-messages) dor) :: sort by msg #
|= [=message-num partial-rcv-message]
%- pairs
:~ 'message-num'^(numb message-num)
'num-received'^(numb num-received)
'num-fragments'^(numb num-fragments)
'fragments'^(set-array ~(key by fragments) numb)
==
::
'nax'^a+(turn (sort ~(tap in nax) dor) numb)
::
(bone-to-pairs bone ossuary)
==
::
++ bone-to-pairs
|= [=bone ossuary]
^- (list [@t json])
:~ 'bone'^(numb bone)
'duct'^(from-duct (~(gut by by-bone) bone ~))
==
::
++ maybe
|* [unit=(unit) enjs=$-(* json)]
^- json
?~ unit ~
(enjs u.unit)
::
++ set-array
|* [set=(set) enjs=$-(* json)]
^- json
a+(turn ~(tap in set) enjs)
::
++ from-duct
|= =duct
a+(turn duct path)
--
--
::
:: behn
::
++ v-behn
|%
++ timers
(scry ,(list [date=@da =duct]) %b %timers ~)
--
::
:: clay
::
::TODO depends on new clay changes (%s care)
++ v-clay
=, clay
|%
++ start-path /(scot %p our.bowl)/home/(scot %da now.bowl)
::
+$ commit
[=tako parents=(list tako) children=(list tako) wen=@da content-hash=@uvI]
::
++ commits-json
^- json
=+ .^(desks=(set desk) %cd start-path)
=/ heads=(list [tako desk])
%+ turn ~(tap in desks)
|= =desk
=+ .^(=dome %cv /(scot %p our.bowl)/[desk]/(scot %da now.bowl))
=/ =tako (~(got by hit.dome) let.dome)
[tako desk]
=/ yakis=(set yaki)
%- silt
^- (list yaki)
%- zing
%+ turn heads
|= [=tako =desk]
(trace-tako tako)
=/ commits=(list commit) (yakis-to-commits ~(tap in yakis))
=, enjs:format
%: pairs
head+(pairs (turn heads |=([=tako =desk] (scot %uv tako)^s+desk)))
commits+(commits-to-json commits)
~
==
::
++ yakis-to-commits
|= yakis=(list yaki)
^- (list commit)
%+ turn yakis
|= =yaki
:* r.yaki p.yaki
=/ candidates
%+ turn
(skim yakis |=(can=^yaki (lien p.can |=(=tako =(r.yaki tako)))))
|= can=^yaki
r.can
~(tap in (silt candidates))
t.yaki
.^(@uvI %cs (weld start-path /hash/(scot %uv r.yaki)))
==
::
++ trace-tako
|= =tako
~+
^- (list yaki)
=+ .^(=yaki %cs (weld start-path /yaki/(scot %uv tako)))
:- yaki
(zing (turn p.yaki trace-tako))
::
++ commits-to-json
|= commits=(list commit)
^- json
:- %a
%+ turn
%+ sort commits
|= [a=commit b=commit]
(gte wen.a wen.b)
|= =commit
(commit-to-json commit)
::
++ commit-to-json
|= =commit
^- json
=, enjs:format
%: pairs
'commitHash'^(tako-to-json tako.commit)
parents+a+(turn parents.commit tako-to-json)
children+a+(turn children.commit tako-to-json)
'contentHash'^(tako-to-json content-hash.commit)
~
==
::
++ tako-to-json
|= =tako
^- json
s+(scot %uv tako)
--
::
:: eyre
::
++ v-eyre
=, eyre
|%
++ bindings
(scry ,(list [=binding =duct =action]) %e %bindings ~)
::
++ connections
(scry ,(map duct outstanding-connection) %e %connections ~)
::
++ auth-state
(scry authentication-state %e %authentication-state ~)
::
++ channel-state
(scry ^channel-state %e %channel-state ~)
::
++ render-action
|= =action
^- json
:- %s
?+ -.action -.action
%gen :((cury cat 3) '+' (spat [desk path]:generator.action))
%app (cat 3 ':' app.action)
==
--
::
:: helpers
::
++ poke
|= [=wire app=term =mark =vase]
^- card
[%pass wire %agent [our.bowl app] %poke mark vase]
::
++ scry
|* [=mold care=term =desk =path]
.^(mold care (scot %p our.bowl) desk (scot %da now.bowl) path)
--

View File

@ -0,0 +1,20 @@
<!doctype html>
<html>
<head>
<title>Debug Dashboard</title>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
<link rel="stylesheet" href="/~debug/css/index.css" />
<link rel="icon" type="image/png" href="/~launch/img/Favicon.png">
</head>
<body class="w-100 h-100">
<div id="root" class="w-100 h-100">
</div>
<script src="/~channel/channel.js"></script>
<script src="/~modulo/session.js"></script>
<script src="/~debug/js/index.js"></script>
</body>
</html>

View File

@ -2,4 +2,4 @@
:- %say
|= *
:- %tang
[.^(tank %b /=timers=) ~]
[>.^((list [date=@da =duct]) %b /=timers=)< ~]

View File

@ -90,6 +90,17 @@
==
==
::
++ on-peek
|= =path
^- (unit (unit cage))
?. ?=([@ %dbug *] path)
(on-peek:ag path)
?+ path [~ ~]
[%u %dbug ~] ``noun+!>(&)
[%x %dbug %state ~] ``noun+!>(on-save:ag)
[%x %dbug %subscriptions ~] ``noun+!>([wex sup]:bowl)
==
::
++ on-init
^- (quip card:agent:gall agent:gall)
=^ cards agent on-init:ag
@ -115,8 +126,6 @@
=^ cards agent (on-leave:ag path)
[cards this]
::
++ on-peek on-peek:ag
::
++ on-agent
|= [=wire =sign:agent:gall]
^- (quip card:agent:gall agent:gall)

View File

@ -1,5 +1,7 @@
:: Print what your agent is doing.
::
/- verb
::
|= [loud=? =agent:gall]
=| bowl-print=_|
^- agent:gall
@ -12,7 +14,7 @@
^- (quip card:agent:gall agent:gall)
%- (print bowl "{<dap.bowl>}: on-init")
=^ cards agent on-init:ag
[cards this]
[[(emit-event %on-init ~) cards] this]
::
++ on-save
^- vase
@ -24,7 +26,7 @@
^- (quip card:agent:gall agent:gall)
%- (print bowl "{<dap.bowl>}: on-load")
=^ cards agent (on-load:ag old-state)
[cards this]
[[(emit-event %on-load ~) cards] this]
::
++ on-poke
|= [=mark =vase]
@ -36,21 +38,26 @@
%bowl `this(bowl-print !bowl-print)
==
=^ cards agent (on-poke:ag mark vase)
[cards this]
[[(emit-event %on-poke mark) cards] this]
::
++ on-watch
|= =path
^- (quip card:agent:gall agent:gall)
%- (print bowl "{<dap.bowl>}: on-watch on path {<path>}")
=^ cards agent (on-watch:ag path)
[cards this]
=^ cards agent
?: ?=([%verb %events ~] path)
[~ agent]
(on-watch:ag path)
[[(emit-event %on-watch path) cards] this]
::
++ on-leave
|= =path
^- (quip card:agent:gall agent:gall)
%- (print bowl "{<dap.bowl>}: on-leave on path {<path>}")
?: ?=([%verb %event ~] path)
[~ this]
=^ cards agent (on-leave:ag path)
[cards this]
[[(emit-event %on-leave path) cards] this]
::
++ on-peek
|= =path
@ -63,21 +70,21 @@
^- (quip card:agent:gall agent:gall)
%- (print bowl "{<dap.bowl>}: on-agent on wire {<wire>}, {<-.sign>}")
=^ cards agent (on-agent:ag wire sign)
[cards this]
[[(emit-event %on-agent wire -.sign) cards] this]
::
++ on-arvo
|= [=wire =sign-arvo]
^- (quip card:agent:gall agent:gall)
%- (print bowl "{<dap.bowl>}: on-arvo on wire {<wire>}, {<[- +<]:sign-arvo>}")
=^ cards agent (on-arvo:ag wire sign-arvo)
[cards this]
[[(emit-event %on-arvo wire [- +<]:sign-arvo) cards] this]
::
++ on-fail
|= [=term =tang]
^- (quip card:agent:gall agent:gall)
%- (print bowl "{<dap.bowl>}: on-fail with term {<term>}")
=^ cards agent (on-fail:ag term tang)
[cards this]
[[(emit-event %on-fail term) cards] this]
--
::
++ print
@ -89,4 +96,9 @@
?. loud same
%- (slog leaf+tape ~)
same
::
++ emit-event
|= =event:verb
^- card:agent:gall
[%give %fact ~[/verb/events] %verb-event !>(event)]
--

View File

@ -0,0 +1,14 @@
/- *spider
|_ stop=[=tid nice=?]
++ grab
|%
++ noun ,[=tid nice=?]
++ json
(ot 'tid'^so 'nice'^bo ~):dejs:format
--
::
++ grow
|%
++ noun stop
--
--

View File

@ -0,0 +1,26 @@
/- verb
=, dejs:format
|_ =event:verb
++ grab
|%
++ noun event:verb
--
::
++ grow
|%
++ noun event
++ json
=, enjs:format
%+ frond -.event
?- -.event
%on-init ~
%on-load ~
%on-poke s+mark.event
%on-watch (path path.event)
%on-leave (path path.event)
%on-agent (pairs 'wire'^(path wire.event) 'sign'^s+sign.event ~)
%on-arvo (pairs 'wire'^(path wire.event) 'vane'^s+vane.event 'sign'^s+sign.event ~)
%on-fail s+term.event
==
--
--

12
pkg/arvo/sur/verb.hoon Normal file
View File

@ -0,0 +1,12 @@
|%
+$ event
$% [%on-init ~]
[%on-load ~]
[%on-poke =mark]
[%on-watch =path]
[%on-leave =path]
[%on-agent =wire sign=term]
[%on-arvo =wire vane=term sign=term]
[%on-fail =term]
==
--

View File

@ -123,15 +123,8 @@
|%
+| %atomics
::
+$ bone @udbone
+$ fragment @uwfragment
+$ fragment-num @udfragmentnum
+$ message-blob @udmessageblob
+$ message-num @udmessagenum
+$ private-key @uwprivatekey
+$ public-key @uwpublickey
+$ signature @uwsignature
+$ symmetric-key @uwsymmetrickey
:: $rank: which kind of ship address, by length
::
:: 0: galaxy or star -- 2 bytes
@ -215,13 +208,6 @@
:: $naxplanation: nack trace; explains which message failed and why
::
+$ naxplanation [=message-num =error]
:: $ack: positive ack, nack packet, or nack trace
::
+$ ack
$% [%ok ~]
[%nack ~]
[%naxplanation =error]
==
::
+| %statics
::
@ -249,237 +235,6 @@
$: veb=_veb-all-off
ships=(set ship)
==
:: $ship-state: all we know about a peer
::
:: %alien: no PKI data, so enqueue actions to perform once we learn it
:: %known: we know their life and public keys, so we have a channel
::
+$ ship-state
$% [%alien alien-agenda]
[%known peer-state]
==
:: $alien-agenda: what to do when we learn a peer's life and keys
::
:: messages: pleas local vanes have asked us to send
:: packets: packets we've tried to send
:: heeds: local tracking requests; passed through into $peer-state
::
+$ alien-agenda
$: messages=(list [=duct =plea])
packets=(set =blob)
heeds=(set duct)
==
:: $peer-state: state for a peer with known life and keys
::
:: route: transport-layer destination for packets to peer
:: qos: quality of service; connection status to peer
:: ossuary: bone<->duct mapper
:: snd: per-bone message pumps to send messages as fragments
:: rcv: per-bone message sinks to assemble messages from fragments
:: nax: unprocessed nacks (negative acknowledgments)
:: Each value is ~ when we've received the ack packet but not a
:: nack-trace, or an error when we've received a nack-trace but
:: not the ack packet.
::
:: When we hear a nack packet or an explanation, if there's no
:: entry in .nax, we make a new entry. Otherwise, if this new
:: information completes the packet+nack-trace, we remove the
:: entry and emit a nack to the local vane that asked us to send
:: the message.
:: heeds: listeners for %clog notifications
::
+$ peer-state
$: $: =symmetric-key
=life
=public-key
sponsor=ship
==
route=(unit [direct=? =lane])
=qos
=ossuary
snd=(map bone message-pump-state)
rcv=(map bone message-sink-state)
nax=(set [=bone =message-num])
heeds=(set duct)
==
:: $qos: quality of service; how is our connection to a peer doing?
::
:: .last-contact: last time we heard from peer, or if %unborn, when
:: we first started tracking time
::
+$ qos
$~ [%unborn *@da]
[?(%live %dead %unborn) last-contact=@da]
:: $ossuary: bone<->duct bijection and .next-bone to map to a duct
::
:: The first bone is 0. They increment by 4, since each flow includes
:: a bit for each message determining forward vs. backward and a
:: second bit for whether the message is on the normal flow or the
:: associated diagnostic flow (for naxplanations).
::
:: The least significant bit of a $bone is:
:: 1 if "forward", i.e. we send %plea's on this flow, or
:: 0 if "backward", i.e. we receive %plea's on this flow.
::
:: The second-least significant bit is 1 if the bone is a
:: naxplanation bone, and 0 otherwise. Only naxplanation
:: messages can be sent on a naxplanation bone, as %boon's.
::
+$ ossuary
$: =next=bone
by-duct=(map duct bone)
by-bone=(map bone duct)
==
:: $message-pump-state: persistent state for |message-pump
::
:: Messages queue up in |message-pump's .unsent-messages until they
:: can be packetized and fed into |packet-pump for sending. When we
:: pop a message off .unsent-messages, we push as many fragments as
:: we can into |packet-pump, which sends every packet it eats.
:: Packets rejected by |packet-pump are placed in .unsent-fragments.
::
:: When we hear a packet ack, we send it to |packet-pump to be
:: removed from its queue of unacked packets.
::
:: When we hear a message ack (positive or negative), we treat that
:: as though all fragments have been acked. If this message is not
:: .current, then this ack is for a future message and .current has
:: not yet been acked, so we place the ack in .queued-message-acks.
::
:: If we hear a message ack before we've sent all the fragments for
:: that message, clear .unsent-fragments and have |packet-pump delete
:: all sent fragments from the message. If this early message ack was
:: positive, print it out because it indicates the peer is not
:: behaving properly.
::
:: If the ack is for the current message, have |packet-pump delete
:: all packets from the message, give the message ack back
:: to the client vane, increment .current, and check if this next
:: message is in .queued-message-acks. If it is, emit the message
:: (n)ack, increment .current, and check the next message. Repeat
:: until .current is not fully acked.
::
:: The following equation is always true:
:: .next - .current == number of messages in flight
::
:: At the end of a task, |message-pump sends a %halt task to
:: |packet-pump, which can trigger a timer to be set or cleared based
:: on congestion control calculations. When the timer fires, it will
:: generally cause a packet to be re-sent.
::
:: Message sequence numbers start at 1 so that the first message will
:: be greater than .last-acked.message-sink-state on the receiver.
::
:: current: sequence number of earliest message sent or being sent
:: next: sequence number of next message to send
:: unsent-messages: messages to be sent after current message
:: unsent-fragments: fragments of current message waiting for sending
:: queued-message-acks: future message acks to be applied after current
:: packet-pump-state: state of corresponding |packet-pump
::
+$ message-pump-state
$: current=_`message-num`1
next=_`message-num`1
unsent-messages=(qeu message-blob)
unsent-fragments=(list static-fragment)
queued-message-acks=(map message-num ack)
=packet-pump-state
==
+$ static-fragment
$: =message-num
num-fragments=fragment-num
=fragment-num
=fragment
==
:: $packet-pump-state: persistent state for |packet-pump
::
:: next-wake: last timer we've set, or null
:: live: packets in flight; sent but not yet acked
:: metrics: congestion control information
::
+$ packet-pump-state
$: next-wake=(unit @da)
live=(tree [live-packet-key live-packet-val])
metrics=pump-metrics
==
:: $pump-metrics: congestion control state for a |packet-pump
::
:: This is an Ames adaptation of TCP's Reno congestion control
:: algorithm. The information signals and their responses are
:: identical to those of the "NewReno" variant of Reno; the
:: implementation differs because Ames acknowledgments differ from
:: TCP's, because this code uses functional data structures, and
:: because TCP's sequence numbers reset when a peer becomes
:: unresponsive, whereas Ames sequence numbers only change when a
:: ship breaches.
::
:: A deviation from Reno is +fast-resend-after-ack, which re-sends
:: timed-out packets when a peer starts responding again after a
:: period of unresponsiveness.
::
:: If .skips reaches 3, we perform a fast retransmit and fast
:: recovery. This corresponds to Reno's handling of "three duplicate
:: acks".
::
:: rto: retransmission timeout
:: rtt: roundtrip time estimate, low-passed using EWMA
:: rttvar: mean deviation of .rtt, also low-passed with EWMA
:: num-live: how many packets sent, awaiting ack
:: ssthresh: slow-start threshold
:: cwnd: congestion window; max unacked packets
::
+$ pump-metrics
$: rto=_~s1
rtt=_~s1
rttvar=_~s1
ssthresh=_10.000
cwnd=_1
num-live=@ud
counter=@ud
==
+$ live-packet
$: key=live-packet-key
val=live-packet-val
==
+$ live-packet-key
$: =message-num
=fragment-num
==
+$ live-packet-val
$: packet-state
num-fragments=fragment-num
=fragment
==
+$ packet-state
$: last-sent=@da
retries=@ud
skips=@ud
==
:: $message-sink-state: state of |message-sink to assemble messages
::
:: last-acked: highest $message-num we've fully acknowledged
:: last-heard: highest $message-num we've heard all fragments on
:: pending-vane-ack: heard but not processed by local vane
:: live-messages: partially received messages
::
+$ message-sink-state
$: last-acked=message-num
last-heard=message-num
pending-vane-ack=(qeu [=message-num message=*])
live-messages=(map message-num partial-rcv-message)
nax=(set message-num)
==
:: $partial-rcv-message: message for which we've received some fragments
::
:: num-fragments: total number of fragments in this message
:: num-received: how many fragments we've received so far
:: fragments: fragments we've received, eventually producing a $message
::
+$ partial-rcv-message
$: num-fragments=fragment-num
num-received=fragment-num
fragments=(map fragment-num fragment)
==
::
+| %dialectics
::
@ -947,32 +702,19 @@
?. =([%& our] why)
[~ ~]
?+ syd ~
%peers
?^ tyl [~ ~]
:^ ~ ~ %noun
!> ^- (map ship ?(%alien %known))
(~(run by peers.ames-state) head)
::
%peer
?. ?=([@ ~] tyl) [~ ~]
=/ who (slaw %p i.tyl)
?~ who [~ ~]
=/ per (~(get by peers.ames-state) u.who)
=/ res
?- per
~ %unknown
[~ %alien *] %alien
[~ %known *]
=, u.per
:* %known
symkeymug=(mug symmetric-key)
life=life
pubkey=public-key
sponsor=sponsor
route=route
qos=qos
ossuary=ossuary
snd=~(key by snd)
rcv=~(key by rcv)
nax=nax
heeds=heeds
==
==
``noun+!>(!>(res))
?~ peer=(~(get by peers.ames-state) u.who)
[~ ~]
``noun+!>(u.peer)
::
%bones
?. ?=([@ ~] tyl) [~ ~]

View File

@ -335,7 +335,7 @@
~
?. ?=(%timers syd)
[~ ~]
[~ ~ %noun !>(>(turn (tap:timer-map timers) head)<)]
[~ ~ %noun !>((turn (tap:timer-map timers) head))]
::
++ stay state
++ take

View File

@ -124,131 +124,6 @@
::
outgoing-duct=duct
==
:: +outstanding-connection: open http connections not fully complete:
::
:: This refers to outstanding connections where the connection to
:: outside is opened and we are currently waiting on ford or an app to
:: produce the results.
::
+$ outstanding-connection
$: :: action: the action that had matched
::
=action
:: inbound-request: the original request which caused this connection
::
=inbound-request
:: response-header: set when we get our first %start
::
response-header=(unit response-header:http)
:: bytes-sent: the total bytes sent in response
::
bytes-sent=@ud
==
:: +action: the action to take when a binding matches an incoming request
::
+$ action
$% :: dispatch to a generator
::
[%gen =generator]
:: dispatch to an application
::
[%app app=term]
:: internal authentication page
::
[%authentication ~]
:: gall channel system
::
[%channel ~]
:: respond with the default file not found page
::
[%four-oh-four ~]
==
:: +authentication-state: state used in the login system
::
+$ authentication-state
$: :: sessions: a mapping of session cookies to session information
::
sessions=(map @uv session)
==
:: +session: server side data about a session
::
+$ session
$: :: expiry-time: when this session expires
::
:: We check this server side, too, so we aren't relying on the browser
:: to properly handle cookie expiration as a security mechanism.
::
expiry-time=@da
::
:: TODO: We should add a system for individual capabilities; we should
:: mint some sort of long lived cookie for mobile apps which only has
:: access to a single application path.
==
:: channel-state: state used in the channel system
::
+$ channel-state
$: :: session: mapping between an arbitrary key to a channel
::
session=(map @t channel)
:: by-duct: mapping from ducts to session key
::
duct-to-key=(map duct @t)
==
:: +timer: a reference to a timer so we can cancel or update it.
::
+$ timer
$: :: date: time when the timer will fire
::
date=@da
:: duct: duct that set the timer so we can cancel
::
=duct
==
:: channel: connection to the browser
::
:: Channels are the main method where a webpage communicates with Gall
:: apps. Subscriptions and pokes are issues with PUT requests on a path,
:: while GET requests on that same path open a persistent EventSource
:: channel.
::
:: The EventSource API is a sequence number based API that browser provide
:: which allow the server to push individual events to the browser over a
:: connection held open. In case of reconnection, the browser will send a
:: 'Last-Event-Id: ' header to the server; the server then resends all
:: events since then.
::
+$ channel
$: :: channel-state: expiration time or the duct currently listening
::
:: For each channel, there is at most one open EventSource
:: connection. A 400 is issues on duplicate attempts to connect to the
:: same channel. When an EventSource isn't connected, we set a timer
:: to reap the subscriptions. This timer shouldn't be too short
:: because the
::
state=(each timer duct)
:: next-id: next sequence number to use
::
next-id=@ud
:: events: unacknowledged events
::
:: We keep track of all events where we haven't received a
:: 'Last-Event-Id: ' response from the client or a per-poke {'ack':
:: ...} call. When there's an active EventSource connection on this
:: channel, we send the event but we still add it to events because we
:: can't assume it got received until we get an acknowledgment.
::
events=(qeu [id=@ud lines=wall])
:: subscriptions: gall subscriptions
::
:: We maintain a list of subscriptions so if a channel times out, we
:: can cancel all the subscriptions we've made.
::
subscriptions=(map wire [ship=@p app=term =path duc=duct])
:: heartbeat: sse heartbeat timer
::
heartbeat=(unit timer)
==
:: channel-request: an action requested on a channel
::
+$ channel-request
@ -2462,32 +2337,38 @@
[~ ~]
?. ?=(%$ -.lot)
[~ ~]
?. ?=(%host syd)
[~ ~]
%- (lift (lift |=(a=hart:eyre [%hart !>(a)])))
^- (unit (unit hart:eyre))
?. =(our who)
?. =([%da now] p.lot)
[~ ~]
~& [%r %scry-foreign-host who]
~
=. p.lot ?.(=([%da now] p.lot) p.lot [%tas %real])
?+ p.lot
[~ ~]
?+ syd [~ ~]
%bindings ``noun+!>(bindings.server-state.ax)
%connections ``noun+!>(connections.server-state.ax)
%authentication-state ``noun+!>(authentication-state.server-state.ax)
%channel-state ``noun+!>(channel-state.server-state.ax)
::
[%tas %fake]
``[& [~ 8.443] %& /localhost]
::
[%tas %real]
=* domains domains.server-state.ax
=* ports ports.server-state.ax
=/ =host:eyre [%& ?^(domains n.domains /localhost)]
=/ secure=? &(?=(^ secure.ports) !?=(hoke:eyre host))
=/ port=(unit @ud)
?. secure
?:(=(80 insecure.ports) ~ `insecure.ports)
?> ?=(^ secure.ports)
?:(=(443 u.secure.ports) ~ secure.ports)
``[secure port host]
%host
%- (lift (lift |=(a=hart:eyre [%hart !>(a)])))
^- (unit (unit hart:eyre))
=. p.lot ?.(=([%da now] p.lot) p.lot [%tas %real])
?+ p.lot
[~ ~]
::
[%tas %fake]
``[& [~ 8.443] %& /localhost]
::
[%tas %real]
=* domains domains.server-state.ax
=* ports ports.server-state.ax
=/ =host:eyre [%& ?^(domains n.domains /localhost)]
=/ secure=? &(?=(^ secure.ports) !?=(hoke:eyre host))
=/ port=(unit @ud)
?. secure
?:(=(80 insecure.ports) ~ `insecure.ports)
?> ?=(^ secure.ports)
?:(=(443 u.secure.ports) ~ secure.ports)
``[secure port host]
==
==
--

View File

@ -491,6 +491,259 @@
:: payload: semantic message contents
::
+$ plea [vane=@tas =path payload=*]
::
:: +| %atomics
::
+$ bone @udbone
+$ fragment @uwfragment
+$ fragment-num @udfragmentnum
+$ message-blob @udmessageblob
+$ message-num @udmessagenum
+$ public-key @uwpublickey
+$ symmetric-key @uwsymmetrickey
::
:: +| %kinetics
:: $ack: positive ack, nack packet, or nack trace
::
+$ ack
$% [%ok ~]
[%nack ~]
[%naxplanation =error]
==
::
:: +| %statics
:: $ship-state: all we know about a peer
::
:: %alien: no PKI data, so enqueue actions to perform once we learn it
:: %known: we know their life and public keys, so we have a channel
::
+$ ship-state
$% [%alien alien-agenda]
[%known peer-state]
==
:: $alien-agenda: what to do when we learn a peer's life and keys
::
:: messages: pleas local vanes have asked us to send
:: packets: packets we've tried to send
:: heeds: local tracking requests; passed through into $peer-state
::
+$ alien-agenda
$: messages=(list [=duct =plea])
packets=(set =blob)
heeds=(set duct)
==
:: $peer-state: state for a peer with known life and keys
::
:: route: transport-layer destination for packets to peer
:: qos: quality of service; connection status to peer
:: ossuary: bone<->duct mapper
:: snd: per-bone message pumps to send messages as fragments
:: rcv: per-bone message sinks to assemble messages from fragments
:: nax: unprocessed nacks (negative acknowledgments)
:: Each value is ~ when we've received the ack packet but not a
:: nack-trace, or an error when we've received a nack-trace but
:: not the ack packet.
::
:: When we hear a nack packet or an explanation, if there's no
:: entry in .nax, we make a new entry. Otherwise, if this new
:: information completes the packet+nack-trace, we remove the
:: entry and emit a nack to the local vane that asked us to send
:: the message.
:: heeds: listeners for %clog notifications
::
+$ peer-state
$: $: =symmetric-key
=life
=public-key
sponsor=ship
==
route=(unit [direct=? =lane])
=qos
=ossuary
snd=(map bone message-pump-state)
rcv=(map bone message-sink-state)
nax=(set [=bone =message-num])
heeds=(set duct)
==
:: $qos: quality of service; how is our connection to a peer doing?
::
:: .last-contact: last time we heard from peer, or if %unborn, when
:: we first started tracking time
::
+$ qos
$~ [%unborn *@da]
[?(%live %dead %unborn) last-contact=@da]
:: $ossuary: bone<->duct bijection and .next-bone to map to a duct
::
:: The first bone is 0. They increment by 4, since each flow includes
:: a bit for each message determining forward vs. backward and a
:: second bit for whether the message is on the normal flow or the
:: associated diagnostic flow (for naxplanations).
::
:: The least significant bit of a $bone is:
:: 1 if "forward", i.e. we send %plea's on this flow, or
:: 0 if "backward", i.e. we receive %plea's on this flow.
::
:: The second-least significant bit is 1 if the bone is a
:: naxplanation bone, and 0 otherwise. Only naxplanation
:: messages can be sent on a naxplanation bone, as %boon's.
::
+$ ossuary
$: =next=bone
by-duct=(map duct bone)
by-bone=(map bone duct)
==
:: $message-pump-state: persistent state for |message-pump
::
:: Messages queue up in |message-pump's .unsent-messages until they
:: can be packetized and fed into |packet-pump for sending. When we
:: pop a message off .unsent-messages, we push as many fragments as
:: we can into |packet-pump, which sends every packet it eats.
:: Packets rejected by |packet-pump are placed in .unsent-fragments.
::
:: When we hear a packet ack, we send it to |packet-pump to be
:: removed from its queue of unacked packets.
::
:: When we hear a message ack (positive or negative), we treat that
:: as though all fragments have been acked. If this message is not
:: .current, then this ack is for a future message and .current has
:: not yet been acked, so we place the ack in .queued-message-acks.
::
:: If we hear a message ack before we've sent all the fragments for
:: that message, clear .unsent-fragments and have |packet-pump delete
:: all sent fragments from the message. If this early message ack was
:: positive, print it out because it indicates the peer is not
:: behaving properly.
::
:: If the ack is for the current message, have |packet-pump delete
:: all packets from the message, give the message ack back
:: to the client vane, increment .current, and check if this next
:: message is in .queued-message-acks. If it is, emit the message
:: (n)ack, increment .current, and check the next message. Repeat
:: until .current is not fully acked.
::
:: The following equation is always true:
:: .next - .current == number of messages in flight
::
:: At the end of a task, |message-pump sends a %halt task to
:: |packet-pump, which can trigger a timer to be set or cleared based
:: on congestion control calculations. When the timer fires, it will
:: generally cause a packet to be re-sent.
::
:: Message sequence numbers start at 1 so that the first message will
:: be greater than .last-acked.message-sink-state on the receiver.
::
:: current: sequence number of earliest message sent or being sent
:: next: sequence number of next message to send
:: unsent-messages: messages to be sent after current message
:: unsent-fragments: fragments of current message waiting for sending
:: queued-message-acks: future message acks to be applied after current
:: packet-pump-state: state of corresponding |packet-pump
::
+$ message-pump-state
$: current=_`message-num`1
next=_`message-num`1
unsent-messages=(qeu message-blob)
unsent-fragments=(list static-fragment)
queued-message-acks=(map message-num ack)
=packet-pump-state
==
+$ static-fragment
$: =message-num
num-fragments=fragment-num
=fragment-num
=fragment
==
:: $packet-pump-state: persistent state for |packet-pump
::
:: next-wake: last timer we've set, or null
:: live: packets in flight; sent but not yet acked
:: metrics: congestion control information
::
+$ packet-pump-state
$: next-wake=(unit @da)
live=(tree [live-packet-key live-packet-val])
metrics=pump-metrics
==
:: $pump-metrics: congestion control state for a |packet-pump
::
:: This is an Ames adaptation of TCP's Reno congestion control
:: algorithm. The information signals and their responses are
:: identical to those of the "NewReno" variant of Reno; the
:: implementation differs because Ames acknowledgments differ from
:: TCP's, because this code uses functional data structures, and
:: because TCP's sequence numbers reset when a peer becomes
:: unresponsive, whereas Ames sequence numbers only change when a
:: ship breaches.
::
:: A deviation from Reno is +fast-resend-after-ack, which re-sends
:: timed-out packets when a peer starts responding again after a
:: period of unresponsiveness.
::
:: If .skips reaches 3, we perform a fast retransmit and fast
:: recovery. This corresponds to Reno's handling of "three duplicate
:: acks".
::
:: rto: retransmission timeout
:: rtt: roundtrip time estimate, low-passed using EWMA
:: rttvar: mean deviation of .rtt, also low-passed with EWMA
:: num-live: how many packets sent, awaiting ack
:: ssthresh: slow-start threshold
:: cwnd: congestion window; max unacked packets
::
+$ pump-metrics
$: rto=_~s1
rtt=_~s1
rttvar=_~s1
ssthresh=_10.000
cwnd=_1
num-live=@ud
counter=@ud
==
+$ live-packet
$: key=live-packet-key
val=live-packet-val
==
+$ live-packet-key
$: =message-num
=fragment-num
==
+$ live-packet-val
$: packet-state
num-fragments=fragment-num
=fragment
==
+$ packet-state
$: last-sent=@da
retries=@ud
skips=@ud
==
:: $message-sink-state: state of |message-sink to assemble messages
::
:: last-acked: highest $message-num we've fully acknowledged
:: last-heard: highest $message-num we've heard all fragments on
:: pending-vane-ack: heard but not processed by local vane
:: live-messages: partially received messages
::
+$ message-sink-state
$: last-acked=message-num
last-heard=message-num
pending-vane-ack=(qeu [=message-num message=*])
live-messages=(map message-num partial-rcv-message)
nax=(set message-num)
==
:: $partial-rcv-message: message for which we've received some fragments
::
:: num-fragments: total number of fragments in this message
:: num-received: how many fragments we've received so far
:: fragments: fragments we've received, eventually producing a $message
::
+$ partial-rcv-message
$: num-fragments=fragment-num
num-received=fragment-num
fragments=(map fragment-num fragment)
==
::
-- ::ames
:: ::::
:::: ++behn :: (1b) timekeeping
@ -885,6 +1138,112 @@
==
::
--
:: +outstanding-connection: open http connections not fully complete:
::
:: This refers to outstanding connections where the connection to
:: outside is opened and we are currently waiting on ford or an app to
:: produce the results.
::
+$ outstanding-connection
$: :: action: the action that had matched
::
=action
:: inbound-request: the original request which caused this connection
::
=inbound-request
:: response-header: set when we get our first %start
::
response-header=(unit response-header:http)
:: bytes-sent: the total bytes sent in response
::
bytes-sent=@ud
==
:: +authentication-state: state used in the login system
::
+$ authentication-state
$: :: sessions: a mapping of session cookies to session information
::
sessions=(map @uv session)
==
:: +session: server side data about a session
::
+$ session
$: :: expiry-time: when this session expires
::
:: We check this server side, too, so we aren't relying on the browser
:: to properly handle cookie expiration as a security mechanism.
::
expiry-time=@da
::
:: TODO: We should add a system for individual capabilities; we should
:: mint some sort of long lived cookie for mobile apps which only has
:: access to a single application path.
==
:: channel-state: state used in the channel system
::
+$ channel-state
$: :: session: mapping between an arbitrary key to a channel
::
session=(map @t channel)
:: by-duct: mapping from ducts to session key
::
duct-to-key=(map duct @t)
==
:: +timer: a reference to a timer so we can cancel or update it.
::
+$ timer
$: :: date: time when the timer will fire
::
date=@da
:: duct: duct that set the timer so we can cancel
::
=duct
==
:: channel: connection to the browser
::
:: Channels are the main method where a webpage communicates with Gall
:: apps. Subscriptions and pokes are issues with PUT requests on a path,
:: while GET requests on that same path open a persistent EventSource
:: channel.
::
:: The EventSource API is a sequence number based API that browser provide
:: which allow the server to push individual events to the browser over a
:: connection held open. In case of reconnection, the browser will send a
:: 'Last-Event-Id: ' header to the server; the server then resends all
:: events since then.
::
+$ channel
$: :: channel-state: expiration time or the duct currently listening
::
:: For each channel, there is at most one open EventSource
:: connection. A 400 is issues on duplicate attempts to connect to the
:: same channel. When an EventSource isn't connected, we set a timer
:: to reap the subscriptions. This timer shouldn't be too short
:: because the
::
state=(each timer duct)
:: next-id: next sequence number to use
::
next-id=@ud
:: events: unacknowledged events
::
:: We keep track of all events where we haven't received a
:: 'Last-Event-Id: ' response from the client or a per-poke {'ack':
:: ...} call. When there's an active EventSource connection on this
:: channel, we send the event but we still add it to events because we
:: can't assume it got received until we get an acknowledgment.
::
events=(qeu [id=@ud lines=wall])
:: subscriptions: gall subscriptions
::
:: We maintain a list of subscriptions so if a channel times out, we
:: can cancel all the subscriptions we've made.
::
subscriptions=(map wire [ship=@p app=term =path duc=duct])
:: heartbeat: sse heartbeat timer
::
heartbeat=(unit timer)
==
:: +binding: A rule to match a path.
::
:: A +binding is a system unique mapping for a path to match. A +binding
@ -904,6 +1263,25 @@
::
path=(list @t)
==
:: +action: the action to take when a binding matches an incoming request
::
+$ action
$% :: dispatch to a generator
::
[%gen =generator]
:: dispatch to an application
::
[%app app=term]
:: internal authentication page
::
[%authentication ~]
:: gall channel system
::
[%channel ~]
:: respond with the default file not found page
::
[%four-oh-four ~]
==
:: +generator: a generator on the local ship that handles requests
::
:: This refers to a generator on the local ship, run with a set of

View File

@ -0,0 +1,183 @@
var gulp = require('gulp');
var cssimport = require('gulp-cssimport');
var rollup = require('gulp-better-rollup');
var cssnano = require('cssnano');
var postcss = require('gulp-postcss');
var sucrase = require('@sucrase/gulp-plugin');
var minify = require('gulp-minify');
var rename = require('gulp-rename');
var del = require('del');
var resolve = require('rollup-plugin-node-resolve');
var commonjs = require('rollup-plugin-commonjs');
var rootImport = require('rollup-plugin-root-import');
var globals = require('rollup-plugin-node-globals');
/***
Main config options
***/
var urbitrc = require('../urbitrc');
/***
End main config options
***/
gulp.task('css-bundle', function() {
let plugins = [
cssnano()
];
return gulp
.src('src/index.css')
.pipe(cssimport())
.pipe(postcss(plugins))
.pipe(gulp.dest('../../arvo/app/debug/css'));
});
gulp.task('jsx-transform', function(cb) {
return gulp.src('src/**/*.js')
.pipe(sucrase({
transforms: ['jsx']
}))
.pipe(gulp.dest('dist'));
});
gulp.task('tile-jsx-transform', function(cb) {
return gulp.src('tile/**/*.js')
.pipe(sucrase({
transforms: ['jsx']
}))
.pipe(gulp.dest('dist'));
});
gulp.task('js-imports', function(cb) {
return gulp.src('dist/index.js')
.pipe(rollup({
plugins: [
commonjs({
namedExports: {
'node_modules/react/index.js': [ 'Component', 'createRef', 'createElement', 'useState', 'useRef', 'useEffect', 'Fragment' ],
'node_modules/react-is/index.js': [ 'isValidElementType' ],
}
}),
rootImport({
root: `${__dirname}/dist/js`,
useEntry: 'prepend',
extensions: '.js'
}),
globals(),
resolve()
]
}, 'umd'))
.on('error', function(e){
console.log(e);
cb();
})
.pipe(gulp.dest('../../arvo/app/debug/js/'))
.on('end', cb);
});
gulp.task('tile-js-imports', function(cb) {
return gulp.src('dist/tile.js')
.pipe(rollup({
plugins: [
commonjs({
namedExports: {
'node_modules/react/index.js': [ 'Component' ],
}
}),
rootImport({
root: `${__dirname}/dist/js`,
useEntry: 'prepend',
extensions: '.js'
}),
globals(),
resolve()
]
}, 'umd'))
.on('error', function(e){
console.log(e);
cb();
})
.pipe(gulp.dest('../../arvo/app/debug/js/'))
.on('end', cb);
});
gulp.task('js-minify', function () {
return gulp.src('../../arvo/app/debug/js/index.js')
.pipe(minify())
.pipe(gulp.dest('../../arvo/app/debug/js/'));
});
gulp.task('tile-js-minify', function () {
return gulp.src('../../arvo/app/debug/js/tile.js')
.pipe(minify())
.pipe(gulp.dest('../../arvo/app/debug/js/'));
});
gulp.task('rename-index-min', function() {
return gulp.src('../../arvo/app/debug/js/index-min.js')
.pipe(rename('index.js'))
.pipe(gulp.dest('../../arvo/app/debug/js/'))
});
gulp.task('rename-tile-min', function() {
return gulp.src('../../arvo/app/debug/js/tile-min.js')
.pipe(rename('tile.js'))
.pipe(gulp.dest('../../arvo/app/debug/js/'))});
gulp.task('clean-min', function() {
return del(['../../arvo/app/debug/js/index-min.js', '../../arvo/app/debug/js/tile-min.js'], {force: true})
});
gulp.task('urbit-copy', function () {
let ret = gulp.src('../../arvo/**/*');
urbitrc.URBIT_PIERS.forEach(function(pier) {
ret = ret.pipe(gulp.dest(pier));
});
return ret;
});
gulp.task('js-bundle-dev', gulp.series('jsx-transform', 'js-imports'));
gulp.task('tile-js-bundle-dev', gulp.series('tile-jsx-transform', 'tile-js-imports'));
gulp.task('js-bundle-prod', gulp.series('jsx-transform', 'js-imports', 'js-minify'))
gulp.task('tile-js-bundle-prod',
gulp.series('tile-jsx-transform', 'tile-js-imports', 'tile-js-minify'));
gulp.task('bundle-dev',
gulp.series(
gulp.parallel(
'css-bundle',
'js-bundle-dev',
'tile-js-bundle-dev'
),
'urbit-copy'
)
);
gulp.task('bundle-prod',
gulp.series(
gulp.parallel(
'css-bundle',
'js-bundle-prod',
'tile-js-bundle-prod',
),
'rename-index-min',
'rename-tile-min',
'clean-min',
'urbit-copy'
)
);
gulp.task('default', gulp.series('bundle-dev'));
gulp.task('watch', gulp.series('default', function() {
gulp.watch('tile/**/*.js', gulp.parallel('tile-js-bundle-dev'));
gulp.watch('src/**/*.js', gulp.parallel('js-bundle-dev'));
gulp.watch('src/**/*.css', gulp.parallel('css-bundle'));
gulp.watch('../../arvo/**/*', gulp.parallel('urbit-copy'));
}));

6543
pkg/interface/dbug/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,43 @@
{
"name": "urbit-apps",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"devDependencies": {
"@sucrase/gulp-plugin": "^2.0.0",
"cssnano": "^4.1.10",
"gulp": "^4.0.0",
"gulp-better-rollup": "^4.0.1",
"gulp-cssimport": "^7.0.0",
"gulp-minify": "^3.1.0",
"gulp-postcss": "^8.0.0",
"gulp-rename": "^1.4.0",
"moment": "^2.24.0",
"rollup": "^1.6.0",
"rollup-plugin-commonjs": "^9.2.0",
"rollup-plugin-node-globals": "^1.4.0",
"rollup-plugin-node-resolve": "^4.0.0",
"rollup-plugin-root-import": "^0.2.3",
"sucrase": "^3.8.0"
},
"dependencies": {
"@gitgraph/react": "^1.5.4",
"classnames": "^2.2.6",
"del": "^5.1.0",
"lodash": "^4.17.11",
"mousetrap": "^1.6.3",
"react": "^16.5.2",
"react-dom": "^16.8.6",
"react-router-dom": "^5.0.0",
"urbit-ob": "^5.0.0",
"urbit-sigil-js": "^1.3.2"
},
"resolutions": {
"natives": "1.1.3"
}
}

View File

@ -0,0 +1,217 @@
html, body {
height: 100%;
width: 100%;
-webkit-font-smoothing: antialiased;
overflow: hidden;
}
p, h1, h2, h3, h4, h5, h6, a, input, textarea, button {
margin-block-end: unset;
margin-block-start: unset;
-webkit-margin-before: unset;
-webkit-margin-after: unset;
font-family: Inter, sans-serif;
padding: 0;
}
button, summary {
cursor: pointer;
}
h2 {
font-weight: 400;
}
a {
color: #000;
text-decoration: none;
}
.inter {
font-family: Inter, sans-serif;
}
.clamp-3 {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
.clamp-message {
max-width: calc(100% - 36px - 1.5rem);
}
.clamp-attachment {
overflow: scroll;
max-height: 10em;
max-width: 100%;
}
.lh-16 {
line-height: 16px;
}
.mono {
font-family: "Source Code Pro", monospace;
}
.list-ship {
line-height: 2.2;
}
.bg-welcome-green {
background-color: #ECF6F2;
}
.c-default {
cursor: default;
}
.m0a {
margin: 0 auto;
}
.mix-blend-diff {
mix-blend-mode: difference;
}
.focus-b--black:focus {
border-color: #000;
}
.embed-container {
position: relative;
height: 0;
overflow: hidden;
padding-bottom: 56.25%;
}
.embed-container iframe, .embed-container object, .embed-container embed {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
/* spinner */
.spin-active {
animation: spin 2s infinite;
}
@keyframes spin {
0% {transform: rotate(0deg);}
25% {transform: rotate(90deg);}
50% {transform: rotate(180deg);}
75% {transform: rotate(270deg);}
100% {transform: rotate(360deg);}
}
/* toggler checkbox */
.toggle::after {
content: "";
height: 12px;
width: 12px;
background: white;
position: absolute;
top: 2px;
left: 2px;
border-radius: 100%;
}
.toggle.checked::after {
left: 14px;
}
/* responsive */
@media all and (max-width: 34.375em) {
.dn-s {
display: none;
}
.flex-basis-100-s, .flex-basis-full-s {
flex-basis: 100%;
}
.h-100-m-40-s {
height: calc(100% - 40px);
}
.black-s {
color: #000;
}
}
@media all and (min-width: 34.375em) {
.db-ns {
display: block;
}
.flex-basis-30-ns {
flex-basis: 30vw;
}
.h-100-m-40-ns {
height: calc(100% - 40px);
}
}
@media all and (prefers-color-scheme: dark) {
body {
background-color: #333;
}
.bg-black-d {
background-color: black;
}
.white-d {
color: white;
}
.gray1-d {
color: #4d4d4d;
}
.gray2-d {
color: #7f7f7f;
}
.gray3-d {
color: #b1b2b3;
}
.gray4-d {
color: #e6e6e6;
}
.bg-gray0-d {
background-color: #333;
}
.bg-gray1-d {
background-color: #4d4d4d;
}
.b--gray0-d {
border-color: #333;
}
.b--gray1-d {
border-color: #4d4d4d;
}
.b--gray2-d {
border-color: #7f7f7f;
}
.b--white-d {
border-color: #fff;
}
.bb-d {
border-bottom-width: 1px;
border-bottom-style: solid;
}
.invert-d {
filter: invert(1);
}
.o-60-d {
opacity: .6;
}
.focus-b--white-d:focus {
border-color: #fff;
}
a {
color: #fff;
}
.hover-bg-gray1-d:hover {
background-color: #4d4d4d;
}
}

View File

@ -0,0 +1,63 @@
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 400;
src: url("https://media.urbit.org/fonts/Inter-Regular.woff2") format("woff2");
}
@font-face {
font-family: 'Inter';
font-style: italic;
font-weight: 400;
src: url("https://media.urbit.org/fonts/Inter-Italic.woff2") format("woff2");
}
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 700;
src: url("https://media.urbit.org/fonts/Inter-Bold.woff2") format("woff2");
}
@font-face {
font-family: 'Inter';
font-style: italic;
font-weight: 700;
src: url("https://media.urbit.org/fonts/Inter-BoldItalic.woff2") format("woff2");
}
@font-face {
font-family: "Source Code Pro";
src: url("https://storage.googleapis.com/media.urbit.org/fonts/scp-extralight.woff");
font-weight: 200;
}
@font-face {
font-family: "Source Code Pro";
src: url("https://storage.googleapis.com/media.urbit.org/fonts/scp-light.woff");
font-weight: 300;
}
@font-face {
font-family: "Source Code Pro";
src: url("https://storage.googleapis.com/media.urbit.org/fonts/scp-regular.woff");
font-weight: 400;
}
@font-face {
font-family: "Source Code Pro";
src: url("https://storage.googleapis.com/media.urbit.org/fonts/scp-medium.woff");
font-weight: 500;
}
@font-face {
font-family: "Source Code Pro";
src: url("https://storage.googleapis.com/media.urbit.org/fonts/scp-semibold.woff");
font-weight: 600;
}
@font-face {
font-family: "Source Code Pro";
src: url("https://storage.googleapis.com/media.urbit.org/fonts/scp-bold.woff");
font-weight: 700;
}

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,4 @@
@import "css/indigo-static.css";
@import "css/fonts.css";
@import "css/custom.css";

View File

@ -0,0 +1,16 @@
import React from 'react';
import ReactDOM from 'react-dom';
import { Root } from '/components/root';
import { api } from '/api';
import { store } from '/store';
import { subscription } from "/subscription";
api.setAuthTokens({
ship: window.ship
});
subscription.start();
ReactDOM.render((
<Root />
), document.querySelectorAll("#root")[0]);

View File

@ -0,0 +1,244 @@
import React from 'react';
import ReactDOM from 'react-dom';
import _ from 'lodash';
import { store } from '/store';
import moment from 'moment';
import { stringToTa } from './lib/util';
class UrbitApi {
setAuthTokens(authTokens) {
this.authTokens = authTokens;
this.bindPaths = [];
this.bind = this.bind.bind(this);
}
bind(path, method, ship = this.authTokens.ship, app, success, fail, quit) {
this.bindPaths = _.uniq([...this.bindPaths, path]);
window.subscriptionId = window.urb.subscribe(ship, app, path,
(err) => {
fail(err);
},
(event) => {
success({
data: event,
from: {
ship,
path
}
});
},
(qui) => {
quit(qui);
});
}
action(appl, mark, data) {
return new Promise((resolve, reject) => {
window.urb.poke(ship, appl, mark, data,
(json) => {
resolve(json);
},
(err) => {
reject(err);
});
});
}
dbugAction(data) {
return this.action("dbug", "dbug-action", data);
}
bindToVerb(app) {
return this.bind('/verb/events', 'PUT', this.authTokens.ship, app,
(result) => {
result.data.app = app;
store.handleEvent({data: { local: { verbResult: result.data }}});
},
() => {
store.handleEvent({data: { local: { verbStatus: {
app: app,
msg: 'failed to establish verb connection to ' + app
}}}});
},
() => {
store.handleEvent({data: { local: { verbStatus: {
app: app,
msg: 'verb connection to ' + app + ' was dropped'
}}}});
}
);
}
getJson(path, localTransform, onFail) {
let source = '/~debug' + path + '.json';
const query = window.location.href.split('?')[1];
if (query) source = source + '?' + query;
fetch(source)
.then((response) => {
if (!response.ok) {
console.error('Network response not ok');
onFail();
} else {
return response.json();
}
})
.then((data) => {
store.handleEvent({data: { local: localTransform(data) }});
})
.catch((error) => {
console.error(`JSON fetch on ${source} failed:`, error);
onFail();
});
}
wrapLocal(name) {
return (data) => {
let e = {};
e[name] = data;
e['status'] = null; // clear previous status
return e;
};
}
showStatus(what) {
return () => {
store.handleEvent({data: { local: { 'status': what }}});
};
}
// apps
getApps() {
this.getJson('/apps',
this.wrapLocal('apps'),
this.showStatus('error fetching apps')
);
}
getAppDetails(app) {
this.getJson('/app/'+app, (data) => {
data.app = app;
return this.wrapLocal('app')(data);
},
() => { // on fail
store.handleEvent({data: { local: { 'appFailed': app } }});
}
);
}
getAppState(app, state = '') {
if (state !== '') {
state = '/' + stringToTa(state)
}
this.getJson('/app/'+app+'/state'+state, (data) => {
data.app = app;
return this.wrapLocal('appState')(data);
},
() => { // on fail
store.handleEvent({data: { local: { 'appFailed': app } }});
});
}
// spider
getThreads() {
this.getJson('/spider/threads',
this.wrapLocal('threads'),
this.showStatus('error fetching threads')
);
}
killThread(tid) {
return this.action("spider", "spider-stop", {tid, nice: false})
.then(this.getThreads.bind(this));
}
// ames
getPeers() {
this.getJson('/ames/peer',
this.wrapLocal('amesPeers'),
this.showStatus('error fetching ames peers')
);
}
getPeer(who) {
this.getJson(`/ames/peer/${who}`, (data) => {
data.who = who;
return this.wrapLocal('amesPeer')(data);
},
this.showStatus('error fetching ames details for ' + who)
);
}
// behn
getTimers() {
this.getJson('/behn/timers',
this.wrapLocal('behnTimers'),
this.showStatus('error fetching behn timers')
);
}
// clay
getCommits() {
this.getJson('/clay/commits',
this.wrapLocal('clayCommits'),
this.showStatus('error fetching clay commits')
);
}
// eyre
getBindings() {
this.getJson('/eyre/bindings',
this.wrapLocal('eyreBindings'),
this.showStatus('error fetching eyre bindings')
);
}
getConnections() {
this.getJson('/eyre/connections',
this.wrapLocal('eyreConnections'),
this.showStatus('error fetching eyre connections')
);
}
getAuthenticationState() {
this.getJson('/eyre/authentication',
this.wrapLocal('eyreAuthentication'),
this.showStatus('error fetching eyre authentication state')
);
}
getChannels() {
this.getJson('/eyre/channels',
this.wrapLocal('eyreChannels'),
this.showStatus('error fetching eyre channels')
);
}
// local
sidebarToggle() {
let sidebarBoolean = true;
if (store.state.sidebarShown === true) {
sidebarBoolean = false;
}
store.handleEvent({
data: {
local: {
'sidebarToggle': sidebarBoolean
}
}
});
}
}
export let api = new UrbitApi();
window.api = api;

View File

@ -0,0 +1,8 @@
import React, { Component } from 'react';
import { MessageScreen } from '/components/lib/message-screen';
export class LoadingScreen extends Component {
render() {
return (<MessageScreen text="Loading..."/>);
}
}

View File

@ -0,0 +1,15 @@
import React, { Component } from 'react';
export class MessageScreen extends Component {
render() {
return (
<div className="h-100 w-100 overflow-x-hidden flex flex-column bg-white bg-gray0-d dn db-ns">
<div className="pl3 pr3 pt2 dt pb3 w-100 h-100">
<p className="f8 pt3 gray2 w-100 h-100 dtc v-mid tc">
{this.props.text}
</p>
</div>
</div>
);
}
}

View File

@ -0,0 +1,116 @@
import React, { Component } from 'react';
import { BrowserRouter, Switch, Route, Link } from "react-router-dom";
import classnames from 'classnames';
import _ from 'lodash';
import { api } from '/api';
import { subscription } from '/subscription';
import { store } from '/store';
import { Skeleton } from '/components/skeleton';
import { MessageScreen } from '/components/message-screen';
import { Apps } from '/views/apps';
import { Spider } from '/views/spider';
import { Ames } from '/views/ames';
import { Behn } from '/views/behn';
import { Clay } from '/views/clay';
import { Eyre } from '/views/eyre';
import { makeRoutePath } from '../lib/util';
export class Root extends Component {
constructor(props) {
super(props);
this.state = store.state;
store.setStateHandler(this.setState.bind(this));
}
componentDidMount() {
// preload spinner asset
new Image().src = "/~debug/img/Spinner.png";
}
render() {
const { state } = this;
return (
<BrowserRouter><Switch>
<Route exact path="/~debug"
render={(props) => {
return (
<Skeleton status={state.status} selected="">
<MessageScreen text="select a component on the left" />
</Skeleton>
);
}}
/>
<Route exact path={makeRoutePath('apps')}
render={(props) => {
return (
<Skeleton status={state.status} selected="apps">
<Apps apps={state.apps} {...props}/>
</Skeleton>
);
}}
/>
<Route exact path={makeRoutePath('spider')}
render={(props) => {
return (
<Skeleton status={state.status} selected="spider">
<Spider threads={state.threads} {...props}/>
</Skeleton>
);
}}
/>
<Route exact path={makeRoutePath('ames')}
render={(props) => {
return (
<Skeleton status={state.status} selected="ames">
<Ames peers={state.peers} {...props}/>
</Skeleton>
);
}}
/>
<Route exact path={makeRoutePath('behn')}
render={(props) => {
return (
<Skeleton status={state.status} selected="behn">
<Behn timers={state.timers} {...props}/>
</Skeleton>
);
}}
/>
<Route exact path={makeRoutePath('clay')}
render={(props) => {
return (
<Skeleton status={state.status} selected="clay">
<Clay commits={state.commits} {...props}/>
</Skeleton>
);
}}
/>
<Route exact path={makeRoutePath('eyre')}
render={(props) => {
return (
<Skeleton status={state.status} selected="eyre">
<Eyre
bindings={state.bindings}
connections={state.connections}
authentication={state.authentication}
channels={state.channels}
{...props}
/>
</Skeleton>
);
}}
/>
</Switch></BrowserRouter>
)
}
}

View File

@ -0,0 +1,49 @@
import React, { Component } from 'react';
export class SearchableList extends Component {
// expected props:
// items: [{key: 'some key', jsx: <w/e>}, ...]
// placeholder: ''
constructor(props) {
super(props);
this.state = {
query: ''
};
this.updateQuery = this.updateQuery.bind(this);
}
updateQuery(event) {
this.setState({ query: event.target.value });
}
render() {
const { state, props } = this;
const searchBar = (
<input
type="text"
placeholder={props.placeholder}
onChange={this.updateQuery}
value={state.query}
style={{border: '1px solid black'}}
/>
);
let items = props.items.filter(item => {
return state.query.split(' ').reduce((match, query) => {
return match && item.key.includes(query);
}, true);
})
items = items.map(item =>
(<div key={item.key} style={{marginTop: '4px'}}>{item.jsx}</div>)
);
return (<div style={{position: 'relative', border: '1px solid grey', padding: '4px'}}>
{props.children}
<div>{searchBar} ({items.length})</div>
<div>{items.length === 0 ? 'none' : items}</div>
</div>);
}
}

View File

@ -0,0 +1,79 @@
import React, { Component } from 'react';
import { Link } from "react-router-dom";
import classnames from 'classnames';
import { makeRoutePath } from '../lib/util';
class SidebarItem extends Component {
render() {
const { props } = this;
let selectedClass = (props.selected)
? "bg-gray5 bg-gray1-d"
: "pointer hover-bg-gray5 hover-bg-gray1-d";
return (
<Link to={makeRoutePath(props.what, true)} key="what">
<div className={"w-100 v-mid f9 ph4 z1 pv1 " + selectedClass}>
<p className="f9 dib">{props.what}</p>
</div>
</Link>
);
}
}
export class Skeleton extends Component {
render() {
const { props } = this;
let items = [
'apps',
'spider',
'ames',
'behn',
//TODO 'clay',
'eyre'
];
items = items.map(what => {
return (<SidebarItem what={what} selected={props.selected === what}/>);
});
let rightPanelHide = this.props.rightPanelHide
? "dn-s" : "";
const status = props.status
? (<div style={{
position: 'absolute', right: '16px', bottom: '16px',
padding: '8px', border: '1px solid #e22'
}}>
{props.status}
</div>)
: null;
return (
<div className="absolute h-100 w-100 mono">
<div className="cf w-100 h-100 flex">
<div className="bn br-m br-l br-xl b--gray4 b--gray1-d lh-copy h-100 flex-shrink-0 mw5-m mw5-l mw5-xl pt3 pt0-m pt0-l pt0-xl relative">
<a className="db dn-m dn-l dn-xl f8 pb3 pl3" href="/"> Landscape</a>
<div className="overflow-y-scroll h-100">
<div className="w-100 bg-transparent">
<Link
className="dib f9 pointer green2 gray4-d pa4"
to={"/~chat/join/~/~dopzod/urbit-help"}>
Get help
</Link>
</div>
{items}
</div>
</div>
{status}
<div className={"h-100 w-100 flex-auto overflow-scroll relative " + rightPanelHide} style={{
flexGrow: 1,
padding: '8px'
}}>
{this.props.children}
</div>
</div>
</div>
);
}
}

View File

@ -0,0 +1,68 @@
import React, { Component } from 'react';
import { SearchableList } from '../components/searchable-list';
import { renderDuct } from '../lib/util';
export class Subscriptions extends Component {
constructor(props) {
super(props);
}
componentDidMount() {
this.componentDidUpdate();
}
componentDidUpdate(prevProps) {
//
}
render() {
const props = this.props;
const incoming = props.in.map(inc => {
return {key: '~'+inc.ship + ' ' + inc.path, jsx: (
<div class="flex">
<div class="flex-auto" style={{maxWidth: '10%'}}>
~{inc.ship}
</div>
<div class="flex-auto" style={{maxWidth: '30%'}}>
{inc.path}
</div>
<div class="flex-auto" style={{maxWidth: '60%'}}>
{renderDuct(inc.duct)}
</div>
</div>
)};
});
const outgoing = props.out.map(out => {
return {key: `~${out.ship} ${out.app} ${out.wire} ${out.path}`, jsx: (
<div class="flex">
<div class="flex-auto" style={{maxWidth: '35%'}}>
{out.wire}
</div>
<div class="flex-auto" style={{maxWidth: '10%'}}>
~{out.ship}
</div>
<div class="flex-auto" style={{maxWidth: '10%'}}>
{out.app}
</div>
<div class="flex-auto" style={{maxWidth: '35%'}}>
{out.path}
</div>
<div class="flex-auto" style={{maxWidth: '10%'}}>
{out.acked ? 'acked' : 'not acked'}
</div>
</div>
)};
});
return (<div>
<h4>Incoming</h4>
<SearchableList placeholder="ship / path" items={incoming} />
<h4>Outgoing</h4>
<SearchableList placeholder="ship / app / wire / path" items={outgoing} />
</div>);
}
}
export default Links;

View File

@ -0,0 +1,36 @@
import React, { Component } from 'react';
export class Summary extends Component {
// expected props:
// id: 'id'
// summary: <jsx>
// details: <jsx>
// onOpen: function(id)
// onClose: function(id)
constructor(props) {
super(props);
this.onToggle = this.onToggle.bind(this);
}
onToggle(event) {
if (event.target.open) {
if (this.props.onOpen) this.props.onOpen(this.props.id);
} else {
if (this.props.onClose) this.props.onClose(this.props.id);
}
}
render() {
const { props } = this;
return (
<details onToggle={this.onToggle} {...props} style={{border: '1px solid black', padding: '4px', position: 'relative', ...props.style}}>
<summary>
{props.summary}
</summary>
<div style={{borderTop: '1px solid black'}}>{props.details}</div>
</details>
)
}
}

View File

@ -0,0 +1,70 @@
import _ from 'lodash';
import classnames from 'classnames';
export function makeRoutePath(resource, includeQuery = false) {
let query = window.location.href.split('?')[1];
if (includeQuery && query) {
query = '?' + query;
} else {
query = '';
}
return '/~debug/' + resource + query;
}
export function msToDa(ms, mil) {
const d = new Date(ms);
var fil = function(n) {
return n >= 10 ? n : "0" + n;
};
return (
`~${d.getUTCFullYear()}.` +
`${(d.getUTCMonth() + 1)}.` +
`${fil(d.getUTCDate())}..` +
`${fil(d.getUTCHours())}.` +
`${fil(d.getUTCMinutes())}.` +
`${fil(d.getUTCSeconds())}` +
`${mil ? "..0000" : ""}`
);
}
export function renderDuct(duct) {
return duct.reduce((a, b) => a + b + ' ', '');
}
// encode the string into @ta-safe format, using logic from +wood.
// for example, 'some Chars!' becomes '~.some.~43.hars~21.'
// this is equivalent to (scot %t string)
//
export function stringToTa(string) {
let out = '';
for (let i = 0; i < string.length; i++) {
const char = string[i];
let add = '';
switch (char) {
case ' ':
add = '.';
break;
case '.':
add = '~.';
break;
case '~':
add = '~~';
break;
default:
const charCode = string.charCodeAt(i);
if (
(charCode >= 97 && charCode <= 122) || // a-z
(charCode >= 48 && charCode <= 57) || // 0-9
char === '-'
) {
add = char;
} else {
// TODO behavior for unicode doesn't match +wood's,
// but we can probably get away with that for now.
add = '~' + charCode.toString(16) + '.';
}
}
out = out + add;
}
return '~~' + out;
}

View File

@ -0,0 +1,179 @@
import _ from 'lodash';
export class LocalReducer {
reduce(json, state) {
const data = _.get(json, 'local', false);
if (data) {
this.status(data, state);
//
this.apps(data, state);
this.app(data, state);
this.appState(data, state);
this.appFailed(data, state);
this.verbResult(data, state);
this.verbStatus(data, state);
//
this.threads(data, state);
//
this.amesPeers(data, state);
this.amesPeer(data, state);
//
this.behnTimers(data, state);
//
this.clayCommits(data, state);
//
this.eyreBindings(data, state);
this.eyreConnections(data, state);
this.eyreAuthentication(data, state);
this.eyreChannels(data, state);
}
}
status(obj, state) {
const data = _.get(obj, 'status', false);
if (data) {
state.status = data;
}
}
// apps
apps(obj, state) {
const data = _.get(obj, 'apps', false);
if (data) {
Object.keys(data).map(app => {
if (!state.apps[app]) {
state.apps[app] = data[app];
}
});
}
}
app(obj, state) {
const data = _.get(obj, 'app', false);
if (data) {
if (state.apps[data.app]) data.state = state.apps[data.app].state;
state.apps[data.app] = data;
}
}
appState(obj, state) {
const data = _.get(obj, 'appState', false);
if (data) {
state.apps[data.app].state = data.state;
}
}
appFailed(obj, state) {
const data = _.get(obj, 'appFailed', false);
if (data) {
console.log('loading app deets failed', data);
state.apps[data] = { noDebug: true };
}
}
verbResult(obj, state) {
const data = _.get(obj, 'verbResult', false);
if (data) {
if (!state.apps[data.app]) state.apps[data.app] = {};
if (!state.apps[data.app].events) state.apps[data.app].events = [];
let msg = 'some event';
if (data['on-init']) msg = '+on-init';
if (data['on-load']) msg = '+on-load';
if (data['on-poke']) msg = '+on-poke with mark ' + data['on-poke'];
if (data['on-watch']) msg = '+on-watch at path ' + data['on-watch'];
if (data['on-leave']) msg = '+on-leave on path ' + data['on-leave'];
if (data['on-agent']) msg = '+on-agent at wire ' + data['on-agent'].wire +
' with sign ' + data['on-agent'].sign;
if (data['on-arvo']) msg = '+on-arvo at wire ' + data['on-arvo'].wire +
' from vane ' + data['on-arvo'].vane +
' with sign ' + data['on-arvo'].sign;
if (data['on-fail']) msg = '+on-fail on ' + data['on-fail'];
state.apps[data.app].events.push(msg);
}
}
verbStatus(obj, state) {
const data = _.get(obj, 'verbStatus', false);
if (data) {
if (!state.apps[data.app]) state.apps[data.app] = {};
if (!state.apps[data.app].events) state.apps[data.app].events = [];
state.apps[data.app].events.push(data.msg);
}
}
// spider
threads(obj, state) {
const data = _.get(obj, 'threads', false);
if (data) {
state.threads = data;
}
}
// ames
amesPeers(obj, state) {
const data = _.get(obj, 'amesPeers', false);
if (data) {
state.peers.known = data.known;
state.peers.alien = data.alien;
}
}
amesPeer(obj, state) {
const data = _.get(obj, 'amesPeer', false);
if (data) {
state.peers.deets[data.who] = data;
}
}
// behn
behnTimers(obj, state) {
const data = _.get(obj, 'behnTimers', false);
if (data) {
state.timers = data;
}
}
// clay
clayCommits(obj, state) {
const data = _.get(obj, 'clayCommits', false);
if (data) {
console.log('clay comms', data);
state.commits = data;
}
}
// eyre
eyreBindings(obj, state) {
const data = _.get(obj, 'eyreBindings', false);
if (data) {
state.bindings = data;
}
}
eyreConnections(obj, state) {
const data = _.get(obj, 'eyreConnections', false);
if (data) {
state.connections = data;
}
}
eyreAuthentication(obj, state) {
const data = _.get(obj, 'eyreAuthentication', false);
if (data) {
state.authentication = data;
}
}
eyreChannels(obj, state) {
const data = _.get(obj, 'eyreChannels', false);
if (data) {
state.channels = data;
}
}
}

View File

@ -0,0 +1,44 @@
import { LocalReducer } from '/reducers/local.js';
import _ from 'lodash';
class Store {
constructor() {
this.state = {
status: null,
apps: {},
threads: {},
peers: { known: [], alien: [], deets: {}},
timers: [],
commits: [],
bindings: [],
connections: [],
authentication: [],
channels: [],
sidebarShown: true
};
this.localReducer = new LocalReducer();
this.setState = () => {};
}
setStateHandler(setState) {
this.setState = setState;
}
handleEvent(data) {
let json;
if (data.data) {
json = data.data;
} else {
json = data;
}
console.log('event', json);
this.localReducer.reduce(json, this.state);
this.setState(this.state);
}
}
export let store = new Store();
window.store = store;

View File

@ -0,0 +1,31 @@
import { api } from '/api';
import { store } from '/store';
export class Subscription {
start() {
if (api.authTokens) {
//
} else {
console.error("~~~ ERROR: Must set api.authTokens before operation ~~~");
}
}
handleEvent(diff) {
store.handleEvent(diff);
}
handleError(err) {
console.error(err);
}
handleQuitSilently(quit) {
// no-op
}
handleQuitAndResubscribe(quit) {
// TODO: resubscribe
}
}
export let subscription = new Subscription();

View File

@ -0,0 +1,316 @@
import React, { Component } from 'react';
import { Route, Link } from 'react-router-dom';
import { msToDa, renderDuct } from '../lib/util';
import urbitOb from 'urbit-ob';
import { Summary } from '../components/summary';
import { SearchableList } from '../components/searchable-list';
export class Ames extends Component {
constructor(props) {
super(props);
this.loadPeers = this.loadPeers.bind(this);
this.loadPeerDetails = this.loadPeerDetails.bind(this);
this.renderFlow = this.renderFlow.bind(this);
}
componentDidMount() {
const { known, alien } = this.props.peers;
if (known.length === 0 && alien.length === 0) {
this.loadPeers();
}
}
componentDidUpdate(prevProps, prevState) {
const { props, state } = this;
//
}
loadPeers() {
api.getPeers();
}
loadPeerDetails(who) {
api.getPeer(who);
}
renderDucts(ducts) {
const items = ducts.map(duct => {
return {
key: duct.join(' '),
jsx: (<div>{renderDuct(duct)}</div>)
}
});
return <SearchableList placeholder="duct" items={items}/>
}
renderSnd(snd) {
const unsent = snd['unsent-messages'].reduce((a, b) => {
return a + b + ' bytes, ';
}, 'unsent msg sizes: ');
const queuedAcks = snd['queued-message-acks'].map(qa => {
return {key: qa['message-num'], jsx: (
qa['message-num'] + ': ' + qa.ack
)};
});
const m = snd['packet-pump-state'].metrics;
const pumpMetrics = (<>
<table><tbody>
<tr class="inter">
<td>rto</td>
<td>rtt</td>
<td>rttvar</td>
<td>ssthresh</td>
<td>num-live</td>
<td>cwnd</td>
<td>counter</td>
</tr>
<tr>
<td>{m.rto}</td>
<td>{m.rtt}</td>
<td>{m.rttvar}</td>
<td>{m.ssthresh}</td>
<td>{m['num-live']}</td>
<td>{m.cwnd}</td>
<td>{m.counter}</td>
</tr>
</tbody></table>
</>);
const liveItems = snd['packet-pump-state'].live.map(live => {
return {key: live['message-num']+','+live['fragment-num'], jsx: (
<table><tbody>
<tr>
<td>message-num</td>
<td>fragment-num</td>
<td>num-fragments</td>
<td>last-sent</td>
<td>retries</td>
<td>skips</td>
</tr>
<tr>
<td>{live['message-num']}</td>
<td>{live['fragment-num']}</td>
<td>{live['num-fragments']}</td>
<td>{msToDa(live['last-sent'])}</td>
<td>{live.retries}</td>
<td>{live.skips}</td>
</tr>
</tbody></table>
)};
});
const live = (
<SearchableList placeholder="msg-num,frag-num" items={liveItems} />
);
const summary = (<>
<b>snd</b><br/>
{renderDuct(snd.duct)}
<table><tbody>
<tr class="inter">
<td>bone</td>
<td>current</td>
<td>next</td>
<td>next wake</td>
<td>total unsent</td>
</tr>
<tr>
<td>{snd.bone}</td>
<td>{snd.current}</td>
<td>{snd.next}</td>
<td>{msToDa(snd['packet-pump-state']['next-wake'])}</td>
<td>
{snd['unsent-messages'].reduce((a,b) => a+b, 0)} bytes
({snd['unsent-messages'].length} messages)
</td>
</tr>
</tbody></table>
</>);
const details = (<>
{pumpMetrics}
{unsent}
{queuedAcks}
{live}
</>);
const active = ( snd['unsent-messages'].length > 0 ||
snd['packet-pump-state'].live.length > 0 )
? 'active, '
: '';
return {key: 'snd ' + active + snd.bone + ', ' + renderDuct(snd.duct), jsx: (
<Summary summary={summary} details={details} />
)};
}
renderRcv(rcv) {
const pendingVaneAcks = rcv['pending-vane-ack'].reduce((a, b) => {
return a + b + ', ';
}, 'pending vane acks: ');
const nax = rcv.nax.reduce((a, b) => {
return a + b + ', ';
}, 'nacks: ');
const liveItems = rcv['live-messages'].map(live => {
return {key: live['message-num'], jsx: (<>
Message #{live['message-num']}<br/>
{live['num-received']} out of {live['num-fragments']} fragments received:<br/>
{live.fragments.reduce((a, b) => a + b + ', ', '')}
</>)};
});
const liveMessages = (<>
Live messages:<br/>
<SearchableList placeholder="message num" items={liveItems} />
</>);
const summary = (<>
<b>rcv</b><br/>
{renderDuct(rcv.duct)}
<table><tbody>
<tr>
<td>bone</td>
<td>last-acked</td>
<td>last-heard</td>
</tr>
<tr>
<td>{rcv.bone}</td>
<td>{rcv['last-acked']}</td>
<td>{rcv['last-heard']}</td>
</tr>
</tbody></table>
</>);
const details = (<>
{pendingVaneAcks}<br/>
{nax}<br/>
{liveMessages}
</>);
return {key: 'rcv ' + rcv.bone + ', ' + renderDuct(rcv.duct), jsx: (
<Summary summary={summary} details={details} />
)};
}
renderFlow(flow) {
if (flow.snd) return this.renderSnd(flow.snd);
if (flow.rcv) return this.renderRcv(flow.rcv);
console.log('weird flow', flow);
return 'weird flow';
}
//TODO use classes for styling?
render() {
const { props, state } = this;
const { known, alien, deets } = props.peers;
const renderDetails = (who) => {
const peer = deets[who];
if (!peer) {
return 'Loading...';
} else if (peer.alien) {
return (<>
Pending messages: {peer.alien.messages}
Pending packets: {peer.alien.packets}
Heeds: {this.renderDucts(peer.alien.heeds)}
</>);
} else if (peer.known) {
const p = peer.known;
const status = (<>
<h4 style={{marginTop: '1em'}}>status</h4>
<table><tbody>
<tr>
<td class="inter">Life</td>
<td>{p.life}</td>
</tr>
<tr>
<td class="inter">Route</td>
<td>
{ p.route
? `${p.route.direct ? '' : 'in'}direct, on lane ${p.route.lane}`
: 'none'
}
</td>
</tr>
<tr>
<td class="inter">QoS</td>
<td>
{p.qos.kind},
last contact {msToDa(p.qos['last-contact'])}
</td>
</tr>
</tbody></table>
</>);
const forwardItems = p.flows.forward.map(this.renderFlow);
const forward = (<>
<h4 style={{marginTop: '1em'}}>forward</h4>
<SearchableList placeholder="bone, duct" items={forwardItems} />
</>);
const backwardItems = p.flows.backward.map(this.renderFlow);
const backward = (<>
<h4 style={{marginTop: '1em'}}>backward</h4>
<SearchableList placeholder="bone, duct" items={backwardItems} />
</>);
const naxItems = p.nax.map(nack => {
return {key: nack.bone, jsx: (
<div>
bone {nack.bone}, message #{nack['message-num']}, duct:<br/>
{renderDuct(nack.duct)}
</div>
)};
});
const nax = (<>
<h4 style={{marginTop: '1em'}}>nax</h4>
<SearchableList placeholder="bone" items={naxItems} />
</>);
const heeds = (<>
<h4 style={{marginTop: '1em'}}>heeds</h4>
{this.renderDucts(p.heeds)}
</>);
return (<>
<button
style={{position: 'absolute', top: 0, right: 0}}
onClick={()=>{this.loadPeerDetails(who)}}
>
refresh
</button>
{status}
{forward}
{backward}
{nax}
{heeds}
</>);
} else {
console.log('weird peer', peer);
return '???';
}
}
const knownItems = known.map(who => {
return {key: '~'+who, jsx: (<Summary
id={who}
summary={'~'+who + ' (known)'}
details={renderDetails(who)}
onOpen={this.loadPeerDetails}
/>)};
});
const alienItems = alien.map(who => {
return {key: '~'+who, jsx: (<Summary
id={who}
summary={'~'+who + ' (alien)'}
details={renderDetails(who)}
onOpen={this.loadPeerDetails}
/>)};
});
const items = [...knownItems, ...alienItems];
return (
<SearchableList placeholder="ship name" items={items}>
<button onClick={this.loadPeers}>refresh</button>
</SearchableList>
);
}
}

View File

@ -0,0 +1,127 @@
import React, { Component } from 'react';
import { Route, Link } from 'react-router-dom';
import { makeRoutePath } from '../lib/util';
import urbitOb from 'urbit-ob';
import { Subscriptions } from '../components/subscriptions';
import { SearchableList } from '../components/searchable-list';
import { Summary } from '../components/summary';
export class Apps extends Component {
constructor(props) {
super(props);
this.state = {
stateQuery: {}
};
this.changeStateQuery = this.changeStateQuery.bind(this);
this.loadApps = this.loadApps.bind(this);
this.loadAppDetails = this.loadAppDetails.bind(this);
}
componentDidMount() {
if (Object.keys(this.props.apps).length === 0) {
this.loadApps();
}
}
componentDidUpdate(prevProps, prevState) {
const { props, state } = this;
//
}
changeStateQuery(app, event) {
this.state.stateQuery[app] = event.target.value;
this.setState({ stateQuery: this.state.stateQuery });
}
loadApps() {
api.getApps();
}
loadAppDetails(app) {
api.getAppDetails(app);
}
loadAppState(app) {
api.getAppState(app, this.state.stateQuery[app]);
}
//TODO use classes for styling?
render() {
const { props, state } = this;
const apps = Object.keys(props.apps).sort().map(app => {
const appData = props.apps[app];
const haveDeets = (typeof appData === 'object');
const running = haveDeets
? true
: appData;
const runStyle = running
? {borderLeft: '3px solid green'}
: {borderLeft: '3px solid grey'}
let deets = null;
if (!haveDeets) {
deets = running
? "Loading..."
: "App not running.";
} else if (appData.noDebug) {
deets = "App doesn't use /lib/dbug";
} else {
const data = appData;
const events = (data.events || []).map(e => {
return {key: e, jsx: (<>
{e}<br/>
</>)};
})
deets = (<>
<button
style={{position: 'absolute', top: 0, right: 0}}
onClick={()=>{this.loadAppDetails(app)}}
>
refresh
</button>
<button onClick={()=>{this.loadAppState(app)}}>query state</button>
<textarea
class="mono"
onChange={(e) => this.changeStateQuery(app, e)}
value={state.stateQuery[app]}
placeholder="-.-"
spellCheck="false"
/>
<div style={{maxHeight: '500px', overflow: 'scroll'}}>
<pre>{(data.state || data.simpleState).join('\n')}</pre>
</div>
<div>
<Subscriptions {...data.subscriptions} />
</div>
<div>
<button onClick={()=>{api.bindToVerb(app)}}>listen to verb</button>
<SearchableList placeholder="event description" items={events} />
</div>
</>)
}
const onOpen = running
? this.loadAppDetails
: null;
return {key: app, jsx: (
<Summary id={app} summary={'%'+app} details={deets} onOpen={onOpen} style={runStyle} />
)};
});
return (
<div
className={
"h-100 w-100 pa3 pt4 overflow-x-hidden " +
"bg-gray0-d white-d flex flex-column"
}>
<SearchableList placeholder="app name" items={apps}>
<button onClick={this.loadApps}>refresh</button>
</SearchableList>
</div>
);
}
}

View File

@ -0,0 +1,56 @@
import React, { Component } from 'react';
import { Route, Link } from 'react-router-dom';
import { msToDa, renderDuct } from '../lib/util';
import urbitOb from 'urbit-ob';
import { SearchableList } from '../components/searchable-list';
export class Behn extends Component {
constructor(props) {
super(props);
this.state = {};
this.loadTimers = this.loadTimers.bind(this);
}
componentDidMount() {
const { timers } = this.props;
if (timers.length === 0) {
this.loadTimers();
}
}
componentDidUpdate(prevProps, prevState) {
const { props, state } = this;
//
}
loadTimers() {
api.getTimers();
}
//TODO use classes for styling?
render() {
const { props, state } = this;
const items = props.timers.map(timer => {
const duct = renderDuct(timer.duct);
return {key: duct, jsx: (<div class="flex">
<div class="flex-auto" style={{maxWidth: '50%'}}>
{msToDa(timer.date)}
</div>
<div class="flex-auto" style={{maxWidth: '50%'}}>
{duct}
</div>
</div>)};
});
return (
<table><tbody>
<SearchableList placeholder="duct" items={items}>
<button onClick={this.loadTimers}>refresh</button>
</SearchableList>
</tbody></table>
);
}
}

View File

@ -0,0 +1,143 @@
import { Gitgraph, templateExtend, TemplateName } from "@gitgraph/react";
import React, { Component } from 'react';
import { BrowserRouter, Route } from "react-router-dom";
import _ from 'lodash';
export class Clay extends Component {
constructor(props) {
super(props);
this.clickety = this.clickety.bind(this);
this.clickCommit = this.clickCommit.bind(this);
this.submit = this.submit.bind(this);
this.graph = this.graph.bind(this);
this.template = templateExtend(TemplateName.Metro, {
branch: {
lineWidth: 6,
},
commit: {
spacing: 40,
dot: {
size: 10,
},
message: {
displayHash: false,
displayAuthor: false,
font: "normal 16pt monospace",
}
},
});
}
componentDidMount() {
api.getCommits();
}
clickety() {
let { commits, gitgraph } = this.props;
if ( !commits.commits ) return;
let commitMap = {};
commits.commits.forEach(commit => {
commitMap[commit.commitHash] = commit;
});
let data = commits.commits.map(com => {
console.log(com.commitHash,commits.head);
let ref = [];
if (com.commitHash in commits.head) {
ref = ["HEAD", commits.head[com.commitHash]];
}
return {
refs: ref,
hash: com.commitHash.slice(2), // lop off 0v for more unique hash
parents: com.parents.map(par => {return par.slice(2);}),
onMessageClick: this.clickCommit,
subject: "commit: " +
com.commitHash.slice(-5) +
", content: " +
com.contentHash.slice(-5) +
", parents: " +
com.parents.map(par => {return par.slice(-5);}),
author: {
name: "me",
email: "me",
timestamp: 1500000000000,
} } });
gitgraph.import(data);
}
clickCommit(commit, args) {
console.log("click", commit);
let val = commit.refs.slice(-1)[0];
if (!val) {
return
} else if (this.bobDesk.value == "") {
this.bobDesk.value = val;
} else {
this.aliDesk.value = val;
}
}
submit() {
//TODO hook up
api.pottery( {
ali: this.aliDesk.value,
bob: this.bobDesk.value,
germ: this.germ.value,
});
}
graph(gitgraph) {
this.setState({gitgraph: gitgraph});
}
render() {
let textAreaClasses =
"f7 mono ba bg-gray0-d white-d pa3 mb2 db " +
"focus-b--black focus-b--white-d b--gray3 b--gray2-d nowrap "
const inputs = (<>
<textarea
ref={ e => { this.bobDesk = e; } }
className={textAreaClasses}
placeholder="target desk"
spellCheck="false"
rows={1}
/>
<textarea
ref={ e => { this.aliDesk = e; } }
className={textAreaClasses}
placeholder="source desk"
spellCheck="false"
rows={1}
/>
<select
ref={ e => { this.germ = e; } }
className={textAreaClasses}>
<option value="mate">%mate: conflict if changed same lines</option>
<option value="meet">%meet: conflict if changed same files</option>
<option value="meld">%meld: annotate conflicts</option>
<option value="fine">%fine: fast-forward (requires ancestor)</option>
<option value="this">%this: use target desk's data</option>
<option value="that">%that: use source desk's data</option>
<option value="init">%init: start new desk (danger!)</option>
</select>
<button
className={textAreaClasses}
onClick={this.submit}>
Merge!
</button>
</>);
this.clickety();
return (
<div className="cf w-100 flex flex-column pa4 ba-m ba-l ba-xl b--gray2 br1 h-100 h-100-minus-40-m h-100-minus-40-l h-100-minus-40-xl f9 white-d">
<Gitgraph options={{template: this.template}}>{this.graph}</Gitgraph>
</div>
);
}
}

View File

@ -0,0 +1,174 @@
import React, { Component } from 'react';
import { Route, Link } from 'react-router-dom';
import { msToDa, renderDuct } from '../lib/util';
import urbitOb from 'urbit-ob';
import { SearchableList } from '../components/searchable-list';
import { Summary } from '../components/summary';
export class Eyre extends Component {
constructor(props) {
super(props);
this.state = {};
this.loadBindings = this.loadBindings.bind(this);
this.loadConnections = this.loadConnections.bind(this);
this.loadAuthenticationState = this.loadAuthenticationState.bind(this);
this.loadChannels = this.loadChannels.bind(this);
}
componentDidMount() {
const { props } = this;
if (props.bindings.length === 0) this.loadBindings();
if (props.connections.length == 0) this.loadConnections();
if (props.authentication.length == 0) this.loadAuthenticationState();
if (props.channels.length == 0) this.loadChannels();
}
componentDidUpdate(prevProps, prevState) {
const { props, state } = this;
//
}
loadBindings() {
api.getBindings();
}
loadConnections() {
api.getConnections();
}
loadAuthenticationState() {
api.getAuthenticationState();
}
loadChannels() {
api.getChannels();
}
//TODO use classes for styling?
render() {
const { props, state } = this;
const bindingItems = props.bindings.map(binding => {
return {key: binding.location + ' ' + binding.action, jsx: (<div class="flex">
<div class="flex-auto" style={{maxWidth: '50%'}}>
{binding.location}
</div>
<div class="flex-auto" style={{maxWidth: '50%'}}>
{binding.action}
</div>
</div>)};
});
const connectionItems = props.connections.map(c => {
return {key: c.duct + ' ' + c.action, jsx: (
<table style={{borderBottom: '1px solid black'}}><tbody>
<tr>
<td class="inter">duct</td>
<td>{c.duct}</td>
</tr>
<tr>
<td class="inter">binding</td>
<td>{c.action}</td>
</tr>
<tr>
<td class="inter">request</td>
<td>
from {c.request.source},
{c.request.authenticated ? ' ' : ' un'}authenticated and
{c.request.secure ? ' ' : ' in'}secure
</td>
</tr>
<tr>
<td class="inter">response</td>
<td>
sent {c.response.sent} bytes.<br/>
{!c.response.header ? null : <>
status {c.response.header['status-code']}<br/>
{c.response.header.headers.reduce((a, b) => a + b + ', ', '')}
</>}
</td>
</tr>
</tbody></table>
)};
});
const channelItems = props.channels.map(c => {
const summary = (<>
{c.session}
<table style={{borderBottom: '1px solid black'}}><tbody>
<tr>
<td class="inter">connected?</td>
<td>{c.connected
? 'connected'
: 'disconnected, expires ' + msToDa(c.expiry)
}</td>
</tr>
<tr>
<td class="inter">next-id</td>
<td>{c['next-id']}</td>
</tr>
<tr>
<td class="inter">unacked</td>
<td>{c.unacked.reduce((a, b) => a + b + ', ', '')}</td>
</tr>
</tbody></table>
</>);
const subscriptionItems = c.subscriptions.map(s => {
//NOTE jsx sorta copied from /components/subscriptions
return {key: `${s.wire} ${s.app} ${s.ship} ${s.path}`, jsx: (
<div class="flex">
<div class="flex-auto" style={{maxWidth: '35%'}}>
{s.wire}
</div>
<div class="flex-auto" style={{maxWidth: '15%'}}>
~{s.ship}
</div>
<div class="flex-auto" style={{maxWidth: '15%'}}>
{s.app}
</div>
<div class="flex-auto" style={{maxWidth: '35%'}}>
{s.path}
</div>
</div>
)};
});
return {key: c.session, jsx: (
<Summary summary={summary} details={(
<SearchableList
placeholder="wire, app, ship, path"
items={subscriptionItems}
/>
)} />
)};
});
const sessionItems = props.authentication.map(s => {
return (<div>
{`${s.cookie} expires ${msToDa(s.expiry)}`}
</div>);
});
return (<>
<h4>Bindings</h4>
<SearchableList placeholder="binding" items={bindingItems}>
<button onClick={this.loadBindings}>refresh</button>
</SearchableList>
<h4>Connections</h4>
<SearchableList placeholder="duct, binding" items={connectionItems}>
<button onClick={this.loadConnections}>refresh</button>
</SearchableList>
<h4>Channels</h4>
<SearchableList placeholder="session id" items={channelItems}>
<button onClick={this.loadChannels}>refresh</button>
</SearchableList>
<h4>Cookies</h4>
<button onClick={this.loadAuthenticationState}>refresh</button>
{sessionItems}
</>);
}
}

View File

@ -0,0 +1,60 @@
import React, { Component } from 'react';
import { Route, Link } from 'react-router-dom';
import { msToDa, renderDuct } from '../lib/util';
import urbitOb from 'urbit-ob';
import { SearchableList } from '../components/searchable-list';
export class Spider extends Component {
constructor(props) {
super(props);
this.state = {};
this.loadThreads = this.loadThreads.bind(this);
this.renderThreads = this.renderThreads.bind(this);
this.killThread = this.killThread.bind(this);
}
componentDidMount() {
const { threads } = this.props;
if (Object.keys(threads).length === 0) {
this.loadThreads();
}
}
componentDidUpdate(prevProps, prevState) {
const { props, state } = this;
//
}
loadThreads() {
api.getThreads();
}
killThread(tid) {
api.killThread(tid);
}
renderThreads(threads) {
return Object.keys(threads).map(thread => {
const kids = this.renderThreads(threads[thread]);
return (<>
<div>
<button style={{margin: '4px'}} onClick={()=>{this.killThread(thread)}}>kill</button>
{thread}
</div>
<div style={{paddingLeft: '16px'}}>{kids}</div>
</>);
});
}
render() {
return (<>
<button onClick={this.loadThreads}>refresh</button><br/>
{ Object.keys(this.props.threads).length === 0
? 'no running threads'
: this.renderThreads(this.props.threads)
}
</>);
}
}

View File

@ -0,0 +1,11 @@
import React, { Component } from 'react';
import classnames from 'classnames';
import _ from 'lodash';
export default class DebugTile extends Component {
render() {
return null;
}
}
window.debugTile = DebugTile;