diff --git a/lib/autoload/globals/net.ngs b/lib/autoload/globals/net.ngs index 2fb1be77..a2677e1c 100644 --- a/lib/autoload/globals/net.ngs +++ b/lib/autoload/globals/net.ngs @@ -173,7 +173,7 @@ ns { } } catch(e) { debug('server', "ThreadedServerDelegate before logging") - log("ThreadedServerDelegate - exception in thread: ${e}") + log("ThreadedServerDelegate - exception in thread: ${Str(e) tor ''}") debug('server', "ThreadedServerDelegate after logging") guard false } diff --git a/lib/shell.ngs b/lib/shell.ngs index 3a516f5d..5a56b96b 100644 --- a/lib/shell.ngs +++ b/lib/shell.ngs @@ -24,6 +24,52 @@ ns { }) } + section "Shell Client" { + type Client() + + + type ShellClientDelegate(net::ClientDelegate) + + global on_connect + F on_connect(scd:ShellClientDelegate, c:net::Connection) { + debug('client', 'ShellClientDelegate#on_connect()') + scd.jrc = IO::handlers::JsonRpcClient() + c.pipeline.push(IO::handlers::Splitter("\n")) + c.pipeline.push(scd.jrc) + c.pipeline.fire(IO::events::Active()) + } + + global init + F init(c:Client) { + c.last_seen_version = null + c.last_eval_result = null + c.scd = ShellClientDelegate() + c.client = net::UnixStreamClient() + c.client.connect(SOCK_FILE, c.scd) + } + + global eval + F eval(c:Client, cmd:Str) { + c.last_eval_result = c.scd.jrc.call('eval', [cmd])::{ + debug('client', {"Eval result ${A.SafeStr()}"}) + } + } + + global poll + F poll(c:Client, cb:Fun) { + while true { + debug('client', "Polling last_seen_version=${c.last_seen_version}") + p = c.scd.jrc.call('poll', { + 'last_seen_version': c.last_seen_version + }) + debug('client', "Poll result - timeline (ver. ${p.version})") + # echo(p.timeline.repr().Lines()) + c.last_seen_version = p.version + not(cb(p)) returns p + } + } + } + doc Rudimentary client as phase 1. doc Send a command and poll for results. F eval(cmd:Str, exit:Bool=true) { @@ -56,7 +102,7 @@ ns { log("Poll result - timeline (ver. ${p.version})") echo(p.timeline.repr().Lines()) last_seen_version = p.version - exit breaks + exit returns p } } @@ -164,7 +210,7 @@ ns { # TODO: handle broken pipe F poll(last_seen_version) { - log("poll(last_seen_version=${last_seen_version}) - work in progress - returning the whole Timeline") + debug('client', "poll(last_seen_version=${last_seen_version}) - work in progress - returning the whole Timeline") section "Wait for later version before retuning the result" block b { not(last_seen_version) returns while true { diff --git a/ngsfile b/ngsfile index cc2edb35..3b7a460d 100755 --- a/ngsfile +++ b/ngsfile @@ -1,14 +1,93 @@ #!/usr/bin/env ngs -ns { - F server() { - require("./lib/shell.ngs")::server(false) - } +ns(t=test) { - F ui() { - require("./lib/shell.ngs")::server(true) - } + sh = require("./lib/shell.ngs") + + F server() sh::server(false) + F ui() sh::server(true) + + # "eval" would conflict with the global eval(c:Client, ...) + # which is called from tests below. + F _eval(cmd:Str, exit:Bool=true) sh::eval(cmd, exit) + _exports.eval = _eval + + F test() { + # Tests where the server and the client run in the same process + # fail sporadically. Therefore, running the server in external process. + # srv = Thread("test-server", { + # log("Starting server") + # server() + # }) + srv = $(ngs . server &) + $(sleep 1) + c = sh::Client() + + F make_textual_command_timeline_result_pattern(ti:Str, cmd:Str, pat) { + { + 'timeline': { + 'children': Present({ + '$type': 'GroupTimelineItem' + 'children': [{ + '$type': 'TextualCommandTimelineItem' + 'id': ti + 'command': cmd + }, { + '$type': 'ResultTimelineItem' + 'children': [ + pat + ] + }] + }) + } + } + } + + test_ti = null + t("basic eval", { + test_ti = c.eval("{1+1}").assert({ + 'ti': Pfx('ti-') + }).ti + }) + + t("poll after basic eval", { + c.poll(F(response) { + # The response was observed to be already there. I don't think it's guaranteed. May need refactoring. + assert(response, make_textual_command_timeline_result_pattern(test_ti, '{1+1}', { '$type': 'Scalar', 'value': 2 })) + false + }) + }) + + t("eval with sleep", { + # Valid JSON for decode_json() + test_ti = c.eval("sleep 2 | echo 3").assert({ + 'ti': Pfx('ti-') + }).ti + }) + + t("poll immediately after eval with sleep", { + c.poll(F(response) { + assert(response, { + 'timeline': { + 'children': Present({ + '$type': 'GroupTimelineItem' + 'children': [{ + '$type': 'TextualCommandTimelineItem' + 'id': test_ti + 'command': 'sleep 2 | echo 3' + }] + }) + } + }) + false + }) + }) - F eval(cmd:Str, exit:Bool=true) { - require("./lib/shell.ngs")::eval(cmd, exit) + t("poll later after eval with sleep", { + $(sleep 2) + c.poll(F(response) { + assert(response, make_textual_command_timeline_result_pattern(test_ti, 'sleep 2 | echo 3', { '$type': 'Scalar', 'value': 3 })) + false + }) + }) } } \ No newline at end of file