diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3f2774fbb..b3956d85c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -66,4 +66,4 @@ jobs: - name: Run Gerbil tests run: | export PATH=/opt/gerbil/bin:$PATH - gxtest src/gerbil/test/... src/std/... src/lang/... + gxtest src/gerbil/test/... src/std/... src/lang/... src/tools/... diff --git a/doc/.vuepress/config.js b/doc/.vuepress/config.js index f77ce0e96..efec75470 100644 --- a/doc/.vuepress/config.js +++ b/doc/.vuepress/config.js @@ -22,7 +22,7 @@ module.exports = { '/tutorials/': [ { title: 'Tutorials', - children: ['', 'languages', 'kvstore', 'proxy', 'httpd', 'ensemble'] + children: ['', 'languages', 'kvstore', 'proxy', 'httpd', 'ensemble', 'advanced-ensemble'] } ], '/reference/gerbil/runtime/': [ { title: "Gerbil Runtime", diff --git a/doc/tutorials/README.md b/doc/tutorials/README.md index a873ae518..4585289e8 100644 --- a/doc/tutorials/README.md +++ b/doc/tutorials/README.md @@ -5,3 +5,4 @@ - [Proxies: Network Programming in Gerbil](proxy.md) - [Web programming with the Gerbil http server](httpd.md) - [Working with Actor Ensembles](ensemble.md) +- [Advanced Actor Ensembles](advanced-ensemble.md) diff --git a/doc/tutorials/advanced-ensemble.md b/doc/tutorials/advanced-ensemble.md new file mode 100644 index 000000000..9e875c8e0 --- /dev/null +++ b/doc/tutorials/advanced-ensemble.md @@ -0,0 +1,603 @@ +# Advanced Actor Ensembles + +So far, in the [basic ensemble tutorial](ensemble.md) we have explored +the basics of actor ensembles in Gerbil. This is the foundation and basic +interaction, giving you the tools to control and debug unstructured ensembles. + +In this tutorial we introduce advanced ensemble concepts and structured ensembles, +whereby a supervisor controls the ensemble for some domain in a host. We explore +distributed ensembles by creating a network of such local ensembles, +connected through TLS. Control of such distributed ensembles can be operational +through a console server for scripting and operator access. + +::: tip Note +Future Work (soon enough! Planned for v0.19) will be to introduce +orchestrators, which can run in a host to setup, heathcheck and +maintain an ensemble through a dedicated tool. We will also introduce +additional supervisory services, besides the registry: a vault server +for sharing secrets without hitting the disk, a resolver for +distributed name resolution of servers, and broadcast facilities for +connecting nodes in an implicit ensemble wide network. +::: + +## Ensemble Domains + +As we all know, naming is one of the hardest problems in Computer +Sciences. Names need to be intentional, and convey information about +the role while on the same time avoiding clashes within the global +namespaces. + +Gerbil implements ensemble domains as a hierarchical +namespace. Servers have identifiers which are pairs of a symbol (the +_name_ or id of the server) and another symbol which is the +_domain_. Names cannot contain slashes while domains are hierarchically +partitioned with slashes. + +The hierarchical structure of the domain namespace serves two purposes: +- it designates control structure, where we can easily refer to the subensemble + that operations under a particular subdomain. +- it eliminates names clashes with easy conventions; for example a convention + for the namespace would be `/project/host-id/application/...` and so on. + +In addition, each server in a domain has a primary _role_, which +defines its purpose and its configuration. Servers can also have +secondary roles and provide a selector for refering to groups of +servers spanning multiple domains. + +## Structured Ensembles + +In a _structured ensemble_, a supervisor controls a subdomain and +provide necessary services for maintaining the ensemble. It manages +creation of servers, local server-level supervision, and automatically +spanws and supervises a registry for the entire subdomain in the +localhost. + +Supervisors can be recursive, whereby a supervisor supervises a nested +supervisor in a local ensemble or meta-supervises remotely as part of +an orchestrator. In general we recommend as best practice to use a +single supervisor for a local subdomain and configure meta-supervision +through orchestrators. + +## Case Study: The Gerbil httpd + +A common requirement is to spawn an httpd that can utilize all cores, +and preferably with process isolation so that a bug can't bring down +the whole server. + +Gerbil comes with a builtin httpd, runnable as `gerbil httpd`. The +server is quite capable, supports static file serving with a small +file cache, dynamically loadable handlers, and servlets which are interpreted +handlers in the server content. + +### Using a Standalone httpd +Here we walk through an example www project and how to serve it with `gerbil httpd server`. + +First, let's compile our site's code -- see [src/tutorial/advanced-ensemble](https://github.com/mighty-gerbils/gerbil/tree/master/src/tutorial/advanced-ensemble) for the code. +``` +1$ pushd site/project +1$ gerbil build +1$ popd +``` + +Now let's configure the httpd. Specifically: +- we set `www` as the root directory for the site +- we enable servlets +- we set a handler for the code we just build + +``` +1$ pushd site +1$ gerbil httpd -G project/.gerbil config --root www --enable-servlets --handlers '(("/handler" . :demo/handler))' + +1$ gerbil httpd -G project/.gerbil config --print +config: +httpd-v0 +root: +"www" +listen: +("0.0.0.0:8080") +handlers: +(("/handler" . :demo/handler)) +enable-servlets: +#t +``` + +And now let's start it and interact with it. +``` +1$ gerbil httpd -G project/.gerbil server +... + +2$ curl http://localhost:8080/ +2$ curl http://localhost:8080/servlets/hello.ss +2$ curl http://localhost:8080/handler +``` + +### Using an httpd ensemble + +A single server is just that: one process, with one thread of control +serving requests. If we want to take advantage of multicore machines +with process isolation, we can spawn more and multiplex on the socket +using `SO_REUSEPORT` (the server does it by default). + +We can do this very easily by constructing an ensemble, and by doing +that we take advantage of supervision and the all the available +tooling from `gxensemble`. + +Here is how we can configure an httpd ensemble: +- we configure the ensemble root directory +- we configure the ensemble domain and worker domain +- we configure the number of workers +- and just pass the through the httpd config. + +``` +1$ pushd site +1$ gerbil httpd -G project/.gerbil config --ensemble --ensemble-root root --ensemble-domain /test --worker-domain /test/www -n 2 --root www --enable-servlets --handlers '(("/handler" . :demo/handler))' + +1$ gerbil httpd -G project/.gerbil config --ensemble --print +config: +ensemble-v0 +roles: +((httpd server-config: + (config: + ensemble-server-v0 + application: + ((httpd config: + httpd-v0 + root: + "www" + listen: + ("0.0.0.0:8080") + handlers: + (("/handler" . :demo/handler)) + enable-servlets: + #t)) + env: + #f) + exe: + "gerbil" + prefix: + ("httpd" "server") + policy: + restart)) +preload: +(workers: ((/test/www prefix: httpd role: httpd servers: 2))) +domain: +/test +root: +"root" +``` + +This configuration will preload and supervise, restarting as needed, 2 workers. +The all we have to do is: +``` +1$ gerbil httpd -G project/.gerbil ensemble +... +``` + +So let's interact with our ensemble, and notice the multiplexing by +the server identifier in the response: +``` +2$ ps auxw | grep gerbil +2$ curl http://localhost:8080/ +2$ curl http://localhost:8080/servlets/hello.ss +2$ curl http://localhost:8080/handler + +``` + +## Local Ensembles + +What about services composed of things other than httpds? Can't we run +a general supervised ensemble that is open ended and can dynamically +add or remove servers? + +The answer is a resounding yes; in fact `gerbil httpd ensemble` runs a +generic supervisor and which just happens to be configured for httpd. +In the folling we see how we can run an httpd ensemble with a standalone supervisor. + +First, let's configure the ensemble: +- first we configure the httpd as usual -- notice the use of `-G env/local` so that + the httpd configuration is placed in the standard service configuration path. +``` +1$ gerbil httpd -G env/local config --root www --enable-servlets --handlers '(("/handler" . :demo/handler))' +``` + +And then we configure the ensemble to know about the `httpd` role: +``` +1$ gerbil ensemble -G env/local config ensemble -D '/test' --root root +1$ gerbil ensemble -G env/local config role --role httpd --exe gerbil --prefix '("httpd" "server")' --policy restart --env www --application httpd + +1$ gerbil ensemble -G env/local config ensemble --view --pretty +config: +ensemble-v0 +domain: +/test +root: +"root" +roles: +((httpd exe: + "gerbil" + prefix: + ("httpd" "server") + policy: + restart + server-config: + (config: + ensemble-server-v0 + env: + "www" + application: + ((httpd config: + httpd-v0 + root: + "www" + listen: + ("0.0.0.0:8080") + handlers: + (("/handler" . :demo/handler)) + enable-servlets: + #t))))) +``` + +And that's it, now we can start a supervisor and spawn httpd server process with it. +``` +1$ gerbil ensemble -G env/local supervisor +... + +``` + +In order to easily interact with the ensemble supervisor, let's also configure the +environment to know about our supervisor: +``` +2$ gerbil ensemble -G env/local env supervisor '(supervisor . /test)' +2$ gerbil ensemble -G env/local env known-servers --add '(supervisor . /test)' +``` + +And now we can interact with the supervisor directly: +``` +2$ gerbil ensemble -G env/local control list-servers --pretty +(((registry . /test) 2348146 running)) + +2$ gerbil ensemble -G env/local control get-ensemble-config --pretty +(config: + ensemble-v0 + domain: + /test + root: + "root" + services: + (supervisor: + (config: + ensemble-server-v0 + domain: + /test + identifier: + (supervisor . /test) + registry: + (registry . /test) + cookie: + "/home/vyzo/src/vyzo/advanced-ensemble-demo/env/local/ensemble/cookie" + admin: + "/home/vyzo/src/vyzo/advanced-ensemble-demo/env/local/ensemble/admin.pub" + role: + supervisor + exe: + "gerbil" + args: + ("ensemble" "supervisor") + root: + "/home/vyzo/src/vyzo/advanced-ensemble-demo/" + log-level: + INFO + log-dir: + "/home/vyzo/src/vyzo/advanced-ensemble-demo/root/log/test/supervisor" + log-file: + "/home/vyzo/src/vyzo/advanced-ensemble-demo/root/log/test/supervisor/server.log" + addresses: + ((unix: "dellicious" "/tmp/ensemble/test/supervisor.sock")) + known-servers: + (((registry . /test) (unix: "dellicious" "/tmp/ensemble/test/registry.sock")))) + registry: + (config: + ensemble-server-v0 + identifier: + (registry . /test) + role: + registry + exe: + "gerbil" + args: + ("ensemble" "registry" "(registry . /test)"))) + roles: + ((httpd exe: + "gerbil" + prefix: + ("httpd" "server") + policy: + restart + server-config: + (config: + ensemble-server-v0 + env: + "www" + application: + ((httpd config: + httpd-v0 + root: + "www" + listen: + ("0.0.0.0:8080") + handlers: + (("/handler" . :demo/handler)) + enable-servlets: + #t)))))) +``` + +As we can see, the only server within the ensemble under the +supervisor is the automatically spawned registry, which facilitates +local interactions between servers in the ensemble. + +Before we can spawn our httpds, we also need to _upload_ the necessary code +and content: +``` +2$ cd site/project/.gerbil/ +2$ tar czvf ../../env-www.tar.gz lib + +2$ cd site +2$ tar czvf fs-www.tar.gz www + +2$ gerbil ensemble -G env/local control upload --fs site/fs-www.tar.gz "" +2$ gerbil ensemble -G env/local control upload --env site/env-www.tar.gz www +``` + +And that's it, now we are ready to spawn our httpd servers as workers: +``` +2$ gerbil ensemble -G env/local control start-workers -d /test/www httpd httpd 2 +2$ gerbil ensemble -G env/local control list-servers --pretty +(((httpd-0 . /test/www) 2348366 running) + ((httpd-1 . /test/www) 2348367 running) + ((registry . /test) 2348146 running)) +``` + +And we can interact with them: +``` +2$ ps auxw | grep gerbil +2$ curl http://localhost:8080/ +2$ curl http://localhost:8080/servlets/hello.ss +2$ curl http://localhost:8080/handler +``` + +Finally, we can shutdown the entire ensemble, including the supervisor: +``` +2$ gerbil ensemble -G env/local control shutdown +``` + +## Distributed Ensembles + +Naturally, we are not limited to controlling a local ensemble behind a +supervisor. We can just as easy spawn a distributed ensemble that +runs in multiple hosts, all running their own supervisor. + +The individual supervisors themselves can be spawned and supervised as +a `systemd` service. + +In this tutorial, we build an ensemble with 3 hosts: 2 hosts serving +http requests with a (local) httpd ensemble each, and another host +acting as a load balancer using the example `rlb` program. + +See [src/tutorial/advanced-ensemble/rlb](https://github.com/mighty-gerbils/gerbil/tree/master/src/tutorial/advanced-ensemble/rlb) for the `rlb` code. + +::: tip Note +The IP addresses shown here were 3 linode servers, configured just for demonstration +purposes and no longer exist. +Please set up your own servers when following the tutorial! +::: + +First, a bit of configuration: if we are going to manage a distributed ensemble we need to use TLS. So let's generate a CA and create certificates for our supervisors: +``` +1$ gerbil ensemble -G env/private ca setup --domain demo.ensemble.internal +1$ gerbil ensemble -G env/private ca cert '(supervisor . /demo/linode1)' +1$ gerbil ensemble -G env/private ca cert '(supervisor . /demo/linode2)' +1$ gerbil ensemble -G env/private ca cert '(supervisor . /demo/linode3)' +``` + +Next, let's configure the supervisors for each host: notice that we +enable public access by passing the address where we listen with TLS: +``` +1$ gerbil ensemble -G env/linode1 config ensemble -D /demo/linode1 --root root --public 0.0.0.0:4999 +1$ gerbil ensemble -G env/linode2 config ensemble -D /demo/linode2 --root root --public 0.0.0.0:4999 +1$ gerbil ensemble -G env/linode3 config ensemble -D /demo/linode3 --root root --public 0.0.0.0:4999 +``` +Next, let's package and upload the necessary environment for our supervisors: +``` +1$ gerbil ensemble -G env/private package -o env/linode1.tar.gz -C env/linode1/ensemble/config '(supervisor . /demo/linode1)' +1$ gerbil ensemble -G env/private package -o env/linode2.tar.gz -C env/linode2/ensemble/config '(supervisor . /demo/linode2)' +1$ gerbil ensemble -G env/private package -o env/linode3.tar.gz -C env/linode3/ensemble/config '(supervisor . /demo/linode3)' + +1$ scp env/linode1.tar.gz root@172.233.56.134: +1$ scp env/linode2.tar.gz root@172.233.56.175: +1$ scp env/linode3.tar.gz root@172.233.56.211: +``` + +Next, let's unpack the environment and run our supervisors: +``` +# linode1 +2$ ssh root@172.233.56.134 +2$ mkdir env +2$ pushd env +2$ tar xzvf ../linode1.tar.gz +2$ popd +2$ export PATH=/opt/gerbil/bin:$PATH +2$ nohup gerbil ensemble -G env supervisor +... + +# linode2 +3$ ssh root@172.233.56.175 +3$ ... + +# linode3 +4$ ssh root@172.233.56.211 +4$ ... +``` + +Again, in order to facilitate the interaction, we setup our private environment: +``` +1$ gerbil ensemble -G env/private env known-servers --set '(supervisor . /demo/linode1)' '(tls: "172.233.56.134:4999")' +1$ gerbil ensemble -G env/private env known-servers --set '(supervisor . /demo/linode2)' '(tls: "172.233.56.175:4999")' +1$ gerbil ensemble -G env/private env known-servers --set '(supervisor . /demo/linode3)' '(tls: "172.233.56.211:4999")' +``` +Next, we configure and start our httpd servers: +``` +1$ gerbil httpd -G env/tmp config --root www --enable-servlets --handlers '(("/handler" . :demo/handler))' +1$ gerbil ensemble -G env/tmp/linode1 config role --role httpd --exe gerbil --prefix '("httpd" "server")' --policy restart --env www --application httpd -C env/tmp/config/httpd + +$ gerbil ensemble -G env/private control update-ensemble-config -S '(supervisor . /demo/linode1)' env/tmp/linode1/ensemble/config +$ gerbil ensemble -G env/private control get-ensemble-config -S '(supervisor . /demo/linode1)' --pretty +(config: + ensemble-v0 + domain: + /demo/linode1 + root: + "root" + public-address: + "0.0.0.0:4999" + services: + (supervisor: + (config: + ensemble-server-v0 + domain: + /demo/linode1 + identifier: + (supervisor . /demo/linode1) + registry: + (registry . /demo/linode1) + cookie: + "/root/env/ensemble/cookie" + admin: + "/root/env/ensemble/admin.pub" + role: + supervisor + exe: + "gerbil" + args: + ("ensemble" "supervisor") + root: + "/root/" + log-level: + INFO + log-dir: + "/root/root/log/demo/linode1/supervisor" + log-file: + "/root/root/log/demo/linode1/supervisor/server.log" + addresses: + ((unix: "localhost" "/tmp/ensemble/demo/linode1/supervisor.sock") + (tls: "0.0.0.0:4999")) + known-servers: + (((registry . /demo/linode1) + (unix: "localhost" "/tmp/ensemble/demo/linode1/registry.sock")))) + registry: + (config: + ensemble-server-v0 + identifier: + (registry . /demo/linode1) + role: + registry + exe: + "gerbil" + args: + ("ensemble" "registry" "(registry . /demo/linode1)"))) + roles: + ((httpd exe: + "gerbil" + prefix: + ("httpd" "server") + policy: + restart + server-config: + (config: + ensemble-server-v0 + env: + "www" + application: + ((httpd config: + httpd-v0 + root: + "www" + listen: + ("0.0.0.0:8080") + handlers: + (("/handler" . :demo/handler)) + enable-servlets: + #t)))))) + + +1$ gerbil ensemble -G env/private control upload -S '(supervisor . /demo/linode1)' --fs site/fs-www.tar.gz "" +1$ gerbil ensemble -G env/private control upload -S '(supervisor . /demo/linode1)' --env site/env-www.tar.gz www + +1$ gerbil ensemble -G env/private control start-workers -S '(supervisor . /demo/linode1)' -d www httpd httpd 2 +(((httpd-1 . /demo/linode1/www) . 10099) ((httpd-0 . /demo/linode1/www) . 10098)) + +1$ gerbil ensemble -G env/private control list-servers -S '(supervisor . /demo/linode1)' --pretty +(((httpd-0 . /demo/linode1/www) 10098 running) + ((httpd-1 . /demo/linode1/www) 10099 running) + ((registry . /demo/linode1) 9987 running)) + +1$ curl http://172.233.56.134:8080/ +1$ curl http://172.233.56.134:8080/handler +1$ curl http://172.233.56.134:8080/servlets/hello.ss + +1$ gerbil ensemble -G env/tmp/linode2 config role --role httpd --exe gerbil --prefix '("httpd" "server")' --policy restart --env www --application httpd -C env/tmp/config/httpd + +$ gerbil ensemble -G env/private control update-ensemble-config -S '(supervisor . /demo/linode2)' env/tmp/linode2/ensemble/config +$ gerbil ensemble -G env/private control get-ensemble-config -S '(supervisor . /demo/linode2)' --pretty + +1$ gerbil ensemble -G env/private control upload -S '(supervisor . /demo/linode2)' --fs site/fs-www.tar.gz "" +1$ gerbil ensemble -G env/private control upload -S '(supervisor . /demo/linode2)' --env site/env-www.tar.gz www +1$ gerbil ensemble -G env/private control start-workers -S '(supervisor . /demo/linode2)' -d www httpd httpd 2 +1$ gerbil ensemble -G env/private control list-servers -S '(supervisor . /demo/linode2)' --pretty + +1$ curl http://172.233.56.175:8080/ +1$ curl http://172.233.56.175:8080/handler +1$ curl http://172.233.56.175:8080/servlets/hello.ss +``` + +And finally, let's configure and start the load balancer: +``` +1$ cd rlb +1$ gerbil build + +1$ cat > env/tmp/config/rlb +config: rlb-v0 +listen: "0.0.0.0:8080" +proxies: ("172.233.56.134:8080" "172.233.56.175:8080") +^D + +1$ gerbil ensemble -G env/tmp/linode3 config role --role rlb --exe rlb --policy restart --env rlb --application rlb -C env/tmp/config/rlb + +1$ gerbil ensemble -G env/private control upload -S '(supervisor . /demo/linode3)' --exe rlb/.gerbil/bin/rlb rlb +1$ gerbil ensemble -G env/private control update-ensemble-config -S '(supervisor . /demo/linode3)' env/tmp/linode3/ensemble/config +1$ gerbil ensemble -G env/private control start-workers -S '(supervisor . /demo/linode3)' -d www rlb rlb 2 +``` + +Try it out: +``` +1$ curl http://172.233.56.211:8080/ +1$ curl http://172.233.56.211:8080/servlets/hello.ss +``` + +And something for fun, let's ping an rlb server behind a supervisor and then start a repl on it, even though it doesn't itself has a public TLS address: +``` +1$ gerbil ensemble -G env/private ping -s -S '(supervisor . /demo/linode3)' '(rlb-0 . /demo/linode3/www)' + +1$ gerbil ensemble -G env/private repl -s -S '(supervisor . /demo/linode3)' '(rlb-0 . /demo/linode3/www)' +``` + +## Ensemble Orchestration + +TODO orchiestrator and meta-supervision + +This functionality is planned for v0.19 + +## More Ensemble Services + +TODO vault, broadcast, resolver + +This functionality is planned for v0.19 diff --git a/src/gerbil/builtin.ssxi.ss b/src/gerbil/builtin.ssxi.ss index 013f58bca..e475d5996 100644 --- a/src/gerbil/builtin.ssxi.ss +++ b/src/gerbil/builtin.ssxi.ss @@ -794,7 +794,7 @@ package: gerbil (file-mode (string::t) fixnum::t effect: (io)) (file-number-of-links (string::t) fixnum::t effect: (io)) (file-owner (string::t) fixnum::t effect: (io)) - (file-size (string::t) fixnum::t effect: (io)) + (file-size (string::t) integer::t effect: (io)) (file-type (string::t) fixnum::t effect: (io)) (filter (procedure::t list::t) list::t effect: (alloc) unchecked:) (filter! (procedure::t list::t) list::t effect: (mut)) diff --git a/src/std/actor-v18/admin-test.ss b/src/std/actor-v18/admin-test.ss index 94e000f97..25c34bfd7 100644 --- a/src/std/actor-v18/admin-test.ss +++ b/src/std/actor-v18/admin-test.ss @@ -18,10 +18,10 @@ (def pubk (get-admin-pubkey pubk-path)) (def privk (get-admin-privkey passphrase privk-path)) - (def sig (admin-auth-challenge-sign privk 'a 'b '#u8(1 2 3 4))) + (def sig (admin-auth-challenge-sign privk '(a . /) '(b . /) '#u8(1 2 3 4))) - (check (admin-auth-challenge-verify pubk 'a 'b '#u8(1 2 3 4) sig) => #t) - (check (admin-auth-challenge-verify pubk 'a 'b '#u8(1 2 3 4) '#u8(1 2 3 4)) => #f) + (check (admin-auth-challenge-verify pubk '(a . /) '(b . /) '#u8(1 2 3 4) sig) => #t) + (check (admin-auth-challenge-verify pubk '(a . /) '(b . /) '#u8(1 2 3 4) '#u8(1 2 3 4)) => #f) (delete-file pubk-path) diff --git a/src/std/actor-v18/admin.ss b/src/std/actor-v18/admin.ss index fbe4c6358..f6fd35068 100644 --- a/src/std/actor-v18/admin.ss +++ b/src/std/actor-v18/admin.ss @@ -5,28 +5,29 @@ :std/crypto :std/text/utf8 :std/misc/ports - ./path) -(export default-admin-pubkey-path - default-admin-privkey-path + ./path + ./server-identifier) +(export ensemble-admin-pubkey-path + ensemble-admin-privkey-path get-admin-pubkey get-admin-privkey generate-admin-keypair! admin-auth-challenge-sign admin-auth-challenge-verify) -(def (default-admin-pubkey-path) +(def (ensemble-admin-pubkey-path) (path-expand "admin.pub" (ensemble-base-path))) -(def (default-admin-privkey-path) +(def (ensemble-admin-privkey-path) (path-expand "admin.priv" (ensemble-base-path))) -(def (get-admin-pubkey (path (default-admin-pubkey-path))) +(def (get-admin-pubkey (path (ensemble-admin-pubkey-path))) (let (path (path-expand path)) (if (file-exists? path) (bytes->public-key EVP_PKEY_ED25519 (read-file-u8vector path)) #f))) -(def (get-admin-privkey passphrase (path (default-admin-privkey-path))) +(def (get-admin-privkey passphrase (path (ensemble-admin-privkey-path))) (let (path (path-expand path)) (if (file-exists? path) (let* ((blob (read-file-u8vector path)) @@ -48,8 +49,8 @@ #f))) (def (generate-admin-keypair! passphrase - (pubk-path (default-admin-pubkey-path)) - (privk-path (default-admin-privkey-path)) + (pubk-path (ensemble-admin-pubkey-path)) + (privk-path (ensemble-admin-privkey-path)) force: (force? #f)) (let ((pubk-path (path-expand pubk-path)) (privk-path (path-expand privk-path))) @@ -89,8 +90,8 @@ (u8vector-append (string->utf8 (string-append "[gerbil:ensemble:auth:" - (symbol->string server-id) ":" - (symbol->string client-id) "]")) + (server-identifier->flat-string server-id) ":" + (server-identifier->flat-string client-id) "]")) challenge-bytes)) (digest-sign privk challenge))) @@ -99,8 +100,8 @@ (u8vector-append (string->utf8 (string-append "[gerbil:ensemble:auth:" - (symbol->string server-id) ":" - (symbol->string client-id) "]")) + (server-identifier->flat-string server-id) ":" + (server-identifier->flat-string client-id) "]")) challenge-bytes)) (digest-verify pubk sig challenge))) diff --git a/src/std/actor-v18/api.ss b/src/std/actor-v18/api.ss index 2439da93c..5c962d09c 100644 --- a/src/std/actor-v18/api.ss +++ b/src/std/actor-v18/api.ss @@ -1,29 +1,27 @@ ;;; -*- Gerbil -*- ;;; © vyzo ;;; actor v18 api -(import :std/error - :std/sugar - :std/iter - :std/logger - :std/os/hostname - ./message +(import ./message ./proto ./server + ./server-identifier ./ensemble + ./ensemble-config + ./ensemble-server + ./ensemble-supervisor ./cookie ./tls ./admin ./path ./loader) (export - call-with-ensemble-server ;; ./message actor-error? raise-actor-error - (struct-out envelope) + (struct-out envelope handle reference) + reference->handle defmessage message? - make-handle handle? handle-proxy handle-ref actor-authorized? send-message -> ->> --> -->? @@ -53,9 +51,6 @@ @unexpected @shutdown ;; ./server - (struct-out reference) - reference->handle - current-actor-server start-actor-server! stop-actor-server! actor-server-identifier @@ -63,86 +58,23 @@ connect-to-server! list-actors list-connections - default-known-servers - set-default-known-servers! default-registry-addresses set-default-registry-addresses! server-address-cache-ttl set-server-address-cache-ttl! + ;; ./cookie + (import: ./cookie) + ;; ./server-identifier + (import: ./server-identifier) ;; ./admin (import: ./admin) ;; ./ensemble (import: ./ensemble) + ;; ./ensemble-config + (import: ./ensemble-config) + ;; ./ensemble-server + (import: ./ensemble-server) + ;; ./ensemble-supervisor + (import: ./ensemble-supervisor) ;; ./path - ensemble-base-path - ensemble-server-path) - -;; call a thunk in the context of an ensemble server -;; this is the programmatic equivalent of gxensemble run -(def (call-with-ensemble-server server-id thunk - log-level: (log-level 'INFO) - log-file: (log-file #f) - listen: (listen-addrs []) - announce: (public-addrs #f) - registry: (registry-addrs #f) - roles: (roles []) - tls-context: (tls-context (get-actor-tls-context server-id)) - cookie: (cookie (get-actor-server-cookie)) - admin: (admin (get-admin-pubkey)) - auth: (auth #f)) - (current-logger-options log-level) - (when log-file - (let (path - (if (equal? log-file "-") - (path-expand "log" (ensemble-server-path server-id)) - (path-expand log-file))) - (create-directory* (path-strip-directory path)) - (start-logger! path))) - (let* ((known-servers - (cond - ((eq? server-id 'registry) - (hash-eq)) - (registry-addrs - (hash-eq (registry registry-addrs))) - (else - (hash-eq (registry (default-registry-addresses)))))) - (unix-addr [unix: (hostname) (string-append "/tmp/ensemble/" (symbol->string server-id))]) - (listen-addrs - (cons unix-addr listen-addrs)) - (public-addrs - (or public-addrs - listen-addrs))) - ;; start the actor server - (start-actor-server! identifier: server-id - tls-context: tls-context - cookie: cookie - admin: admin - auth: auth - addresses: listen-addrs - ensemble: known-servers) - ;; start the loader - (start-loader!) - ;; add the server to the ensemble - (unless (eq? server-id 'registry) - (ensemble-add-server! server-id public-addrs roles)) - ;; run it! - (try - (thunk) - (catch (e) - (display "*** ERROR " (current-error-port)) - (display-exception e (current-error-port)))) - (thread-join! (current-actor-server)) - ;; clean up unix sockets - (for (addr listen-addrs) - (match addr - ([unix: _ path] - (delete-file path)) - (else (void)))) - ;; remove the server from the ensemble - (unless (eq? server-id 'registry) - (remove-from-registry! cookie known-servers server-id)))) - -(def (remove-from-registry! cookie known-servers server-id) - (start-actor-server! cookie: cookie ensemble: known-servers identifier: server-id) - (with-catch void (cut ensemble-remove-server! server-id)) - (stop-actor-server!)) + (import: ./path)) diff --git a/src/std/actor-v18/connection.ss b/src/std/actor-v18/connection.ss index e64bb3d93..455c486d3 100644 --- a/src/std/actor-v18/connection.ss +++ b/src/std/actor-v18/connection.ss @@ -10,13 +10,15 @@ :std/crypto :std/os/error :std/os/hostname + :std/format (only-in :std/os/socket AF_UNIX SOL_SOCKET SO_KEEPALIVE) (only-in :std/srfi/1 partition) ./logger ./message ./proto ./io - ./tls) + ./tls + ./server-identifier) (export #t) (def version-magic 18) @@ -58,7 +60,7 @@ ;; no handshake needed; TLS authenticated (let ((reader (open-buffered-reader (sock.reader))) (writer (open-buffered-writer (sock.writer))) - (peer-id (actor-tls-certificate-id (TLS-peer-certificate sock)))) + (peer-id (actor-tls-certificate-server-id (TLS-peer-certificate sock)))) (if peer-id (spawn/name 'actor-connection actor-connection srv peer-id sock reader writer 'in) (begin @@ -77,7 +79,7 @@ (reader (open-buffered-reader reader)) (writer (sock.writer)) (writer (open-buffered-writer writer)) - (srv-id (thread-specific srv))) + (srv-id (actor-server-identifier srv))) (using ((reader :- BufferedReader) (writer :- BufferedWriter)) ;; set handshake timeouts @@ -94,7 +96,9 @@ (cond ((not peer-id) (fail! "bad hello; no server id")) - ((eq? srv-id peer-id) + ((not (pair? peer-id)) + (fail! "bad hello; peer-id is not fully qualified")) + ((equal? srv-id peer-id) (fail! "bad hello; client claims to be our server")) (else (let (salt (random-bytes (u8vector-length cookie))) @@ -146,21 +150,33 @@ (using (sock : StreamSocket) (if (is-TLS? sock) ;; no handshake needed; TLS authenticated - (let ((reader (open-buffered-reader (sock.reader))) - (writer (open-buffered-writer (sock.writer))) - (cert-peer-id (actor-tls-certificate-id (TLS-peer-certificate sock)))) - (if (eq? peer-id cert-peer-id) + (let* ((reader (open-buffered-reader (sock.reader))) + (writer (open-buffered-writer (sock.writer))) + (cert (TLS-peer-certificate sock)) + (cert-peer-id (actor-tls-certificate-server-id cert))) + (if (and (equal? peer-id cert-peer-id) + ;; Also check that it is in the same tls domain -- this is the + ;; application security barrier. + ;; Note: we want to relax this in the future, + ;; but this measure is to stop bugs from leaking across domains + ;; and we'd like to have a solid policy in place. + (equal? (actor-tls-host peer-id) + (actor-tls-certificate-host cert))) (spawn/name 'actor-connection actor-connection srv peer-id sock reader writer 'out) (begin - (warnf "peer id mismatch for TLS authenticated peer ~a" (peer-address sock)) + (warnf "peer id mismatch for TLS authenticated peer ~a [~a ~a] [~s ~s]" + (peer-address sock) + peer-id cert-peer-id + (actor-tls-host peer-id) (actor-tls-certificate-host cert)) (with-catch void (cut sock.close)) 'error))) ;; no TLS; do cookie authentication handshake (let/cc exit - (def (fail! what) + (def (fail! what (detail #f)) (warnf "handshake with ~a failed: ~a" peer-id what) (with-catch void (cut sock.close)) - (thread-send/check srv (!connection-failed peer-id what)) + (let (what (if detail (format "~a: ~a" what detail) what)) + (thread-send/check srv (!connection-failed peer-id what))) (exit 'error)) (try @@ -168,7 +184,7 @@ (reader (open-buffered-reader reader)) (writer (sock.writer)) (writer (open-buffered-writer writer)) - (srv-id (thread-specific srv))) + (srv-id (actor-server-identifier srv))) (using ((reader :- BufferedReader) (writer :- BufferedWriter)) ;; set handshake timeouts @@ -185,10 +201,12 @@ (cond ((not remote-id) (fail! "bad challenge; no server id")) - ((eq? remote-id srv-id) + ((not (pair? remote-id)) + (fail! "bad hello; remote-id is not fully qualified" remote-id)) + ((equal? remote-id srv-id) (fail! "bad challenge; server claims to be our server")) - ((not (eq? remote-id peer-id)) - (fail! "bad challenge; server id mismatch")) + ((not (equal? remote-id peer-id)) + (fail! "bad challenge; server id mismatch" [peer-id remote-id])) (else (let (cli-salt (random-bytes (u8vector-length cookie))) (write-delimited writer (!response (digest peer-id srv-id salt cookie) cli-salt)) @@ -323,9 +341,9 @@ (def (digest srv-id cli-id salt cookie) (sha256 (u8vector-append salt '#u8(#x3a) - (string->utf8 (symbol->string srv-id)) + (string->utf8 (server-identifier->flat-string srv-id)) '#u8(#x3a) - (string->utf8 (symbol->string cli-id)) + (string->utf8 (server-identifier->flat-string cli-id)) '#u8(#x3a) cookie))) diff --git a/src/std/actor-v18/cookie.ss b/src/std/actor-v18/cookie.ss index 94c714bee..5eec83802 100644 --- a/src/std/actor-v18/cookie.ss +++ b/src/std/actor-v18/cookie.ss @@ -7,17 +7,17 @@ ./path) (export #t) -(def (default-cookie-path) - (path-expand "cookie" (ensemble-base-path))) +(def (ensemble-cookie-path (base (ensemble-base-path))) + (path-expand "cookie" base)) -(def (get-actor-server-cookie (path (default-cookie-path))) +(def (get-actor-server-cookie (path (ensemble-cookie-path))) (let (path (path-expand path)) (if (file-exists? path) (read-file-u8vector path) (error "cookie file doesn't exist" path)))) -(def (generate-actor-server-cookie! (path (default-cookie-path)) - force: (force? #f)) +(def (generate-ensemble-cookie! (path (ensemble-cookie-path)) + force: (force? #f)) (let (path (path-expand path)) (if (file-exists? path) (if force? diff --git a/src/std/actor-v18/ensemble-config.ss b/src/std/actor-v18/ensemble-config.ss new file mode 100644 index 000000000..c3029059c --- /dev/null +++ b/src/std/actor-v18/ensemble-config.ss @@ -0,0 +1,268 @@ +;;; -*- Gerbil -*- +;;; © vyzo +;;; ensemble configuration +(import :std/config + ./path) +(export #t) + +;;; configures the ensemble supervisory domain +;;; ------------------------------------------------------------------------ +;;; ;;; config: config versioned type, current is ensemble-v0 +;;; config: ensemble-v0 +;;; ;;; supervisory domain for the ensemble +;;; domain: +;;; ;;; [optional] root path for ensemble executions +;;; root: +;;; ;;; [optional] supervisor public address (over TLS) +;;; public-address: +;;; +;;; ;;; supervisory services +;;; services: ( +;;; ;;; supervisor config +;;; supervisor: +;;; ;;; registry config for the local ensemble +;;; registry: +;;; ;;; [optional] resolver config for distributed ensemble name resolution +;;; resolver: +;;; ;;; [optional] broadcast config +;;; broadcast: +;;; ) +;;; +;;; ;;; roles -> execution mapping +;;; roles: +;;; (( ; symbol +;;; ;;; for each role define an execution rule: +;;; ;;; the program is started as ... +;;; ;;; the server configuration will be in the /config +;;; ;;; : the symbol 'self for single binary deployments +;;; ;;; or an executable path (string) +;;; exe: +;;; ;;; optional executable argument prefix +;;; prefix: ( ...) +;;; ;;; optional executable argument suffix +;;; suffix: ( ...) +;;; ;;; supervision policy +;;; policy: +;;; ;;; optional role server configuration template +;;; server-config: +;;; ) +;;; ...) +;;; +;;; ;;; [optional] preloaded server configuration; the supervisor on its own is capable +;;; ;;; of receiving remote updates, executables, and server execution instructions. +;;; preload: ( +;;; ;;; static preloaded server configuration +;;; servers: +;;; (( +;;; ;;; domain +;;; domain: +;;; ;;; primary role +;;; role: +;;; ;;; [optional] server configuration, the role template is overlayed +;;; server-config: +;;; ) +;;; ... ) +;;; +;;; ;;; dynamic preloaded worker server configuration +;;; workers: +;;; (( +;;; ;;; server id prefix; the actual server id will be - +;;; prefix: +;;; ;;; number of servers +;;; servers: +;;; ;;; primary role +;;; role: +;;; ;;; [optional] server configuration, the role template is overlayed +;;; server-config: +;;; ) +;;; ...) +;;; ) + +;;; configures a server within a supervisory domain +;;; ------------------------------------------------------------------------ +;;; config: ensemble-server-v0 +;;; +;;; ;;; ensemble +;;; domain: +;;; identifier: +;;; supervisor: +;;; registry: +;;; cookie: +;;; admin: +;;; +;;; ;;; execution +;;; role: +;;; secondary-roles: ( ...) +;;; exe: +;;; args: ( ...) +;;; policy: +;;; env: +;;; envvars: ( ...) +;;; +;;; ;;; logging +;;; log-level: +;;; log-file: +;;; log-dir: +;;; +;;; ;;; bindings +;;; addresses: (
...) +;;; auth: (( ) ...) +;;; known-servers: ((
...) ...) +;;; +;;; ;;; application specific configuration +;;; application: (( config ...) ...) + +(def (check-ensemble-config! cfg) + ;; TODO config validation + (config-check! cfg 'ensemble-v0)) + +(def (check-ensemble-server-config! cfg) + ;; TODO config validation + (config-check! cfg 'ensemble-server-v0)) + +(def (merge-select old new key) + (cond + ((or (config-get new key) (config-get old key)) + => (cut list key <>)) + (else []))) + +(def (merge-select* old new key) + (def (key-e tail) + [key (cadr tail)]) + + (cond + ((memq key new) => key-e) + ((memq key old) => key-e) + (else []))) + +(def (merge-list old new key (memf member)) + (let ((left (config-get old key)) + (right (config-get new key))) + (cond + ((and (not left) (not right)) + []) + ((not left) + [key right]) + ((not right) + [key left]) + (else + (let loop ((rest left) (result right)) + (match rest + ([val . rest] + (if (memf val result) + (loop rest result) + (loop rest (cons val result)))) + (else + [key result]))))))) + +(def (memvar x lst) + (let (prefix (substring x 0 (fx1+ (string-index x #\=)))) + (find (lambda (y) (string-prefix? prefix y)) lst))) + +(def (merge-alist old new key (assf assoc)) + (let ((left (config-get old key)) + (right (config-get new key))) + (cond + ((and (not left) (not right)) + []) + ((not left) + [key right]) + ((not right) + [key left]) + (else + (let loop ((rest left) (result right)) + (match rest + ([[k . val] . rest] + (if (assf k result) + (loop rest result) + (loop rest (cons (cons k val) result)))) + (else + [key result]))))))) + +(def (merge-plist old new key (pgetf pget)) + (let ((left (config-get old key)) + (right (config-get new key))) + (cond + ((and (not left) (not right)) + []) + ((not left) + [key right]) + ((not right) + [key left]) + (else + (let loop ((rest left) (result right)) + (match rest + ([k val . rest] + (if (pgetf k result) + (loop rest result) + (loop rest (cons* k val result)))) + (else + [key result]))))))) + +(def (ensemble-config-merge old new) + (cond + ((not old) new) + ((not new) old) + (else + (check-ensemble-config! old) + (check-ensemble-config! new) + [config: 'ensemble-v0 + (merge-select old new domain:) ... + (merge-select old new root:) ... + (merge-select old new public-address:) ... + (merge-plist old new services:) ... + (merge-alist old new roles:) ... + (merge-plist old new preload:) ... + ]))) + +(def (ensemble-server-config-merge old new) + (cond + ((not old) new) + ((not new) old) + (else + (check-ensemble-server-config! old) + (check-ensemble-server-config! new) + [config: 'ensemble-server-v0 + (merge-select old new domain:) ... + (merge-select old new identifier:) ... + (merge-select old new supervisor:) ... + (merge-select old new registry:) ... + (merge-select old new cookie:) ... + (merge-select old new admin:) ... + (merge-select old new role:) ... + (merge-list old new secondary-roles: memq) ... + (merge-select old new exe:) ... + (merge-select old new args:) ... + (merge-select old new policy:) ... + (merge-select* old new env:) ... + (merge-list old new envvars: memvar) ... + (merge-select old new log-level:) ... + (merge-select old new log-file:) ... + (merge-select old new log-dir:) ... + (merge-list old new addresses:) ... + (merge-alist old new auth:) ... + (merge-alist old new known-servers:) ... + (merge-alist old new application:) ... + ]))) + +(def (load-ensemble-config-file path) + (let (cfg (load-config path 'ensemble-v0)) + (check-ensemble-config! cfg) + cfg)) + +(def (load-ensemble-config (base (gerbil-path))) + (load-ensemble-config-file (ensemble-config-path base))) + +(def (load-ensemble-server-config server-id + (domain (ensemble-domain)) + (base (gerbil-path))) + (let (cfg (load-config (ensemble-server-config-path server-id domain base) + 'ensemble-server-v0)) + (check-ensemble-server-config! cfg) + cfg)) + +(def (empty-ensemble-config) + [config: 'ensemble-v0]) + +(def (empty-ensemble-server-config) + [config: 'ensemble-server-v0]) diff --git a/src/std/actor-v18/ensemble-server.ss b/src/std/actor-v18/ensemble-server.ss new file mode 100644 index 000000000..1bc128290 --- /dev/null +++ b/src/std/actor-v18/ensemble-server.ss @@ -0,0 +1,133 @@ +;;; -*- Gerbil -*- +;;; © vyzo +;;; ensemble servers +(import :std/error + :std/sugar + :std/iter + :std/logger + :std/config + (only-in :std/logger current-log-directory) + ./message + ./proto + ./server + ./server-identifier + ./ensemble + ./ensemble-config + ./cookie + ./tls + ./admin + ./path + ./loader) +(export become-ensemble-server! + call-with-ensemble-server) + +;;; cfg: +(def (become-ensemble-server! cfg thunk) + (check-ensemble-server-config! cfg) + (let (logdir (config-get! cfg log-dir:)) + (create-directory* logdir) + (parameterize ((current-log-directory logdir) + (current-ensemble-server-config cfg)) + (call-with-ensemble-server + (config-get! cfg identifier:) thunk + domain: (config-get! cfg domain:) + supervisor: (config-get cfg supervisor:) + registry: (config-get cfg registry:) + cookie: (get-actor-server-cookie + (config-get! cfg cookie:)) + admin: (alet (admin-path (config-get cfg admin:)) + (and (file-exists? admin-path) + (get-admin-pubkey admin-path))) + roles: (cons (config-get! cfg role:) (config-get cfg secondary-roles: [])) + log-level: (config-get! cfg log-level:) + log-file: (config-get! cfg log-file:) + listen: (config-get! cfg addresses:) + known-servers: (cond + ((config-get cfg known-servers:) + => (lambda (known-servers) + (list->hash-table known-servers))) + (else (ensemble-known-servers))))))) + +;; call a thunk in the context of an ensemble server +;; this is the programmatic equivalent of gxensemble run +(def (call-with-ensemble-server server-id thunk + log-level: (log-level 'INFO) + log-file: (log-file #f) + listen: (listen-addrs []) + announce: (public-addrs #f) + roles: (roles []) + domain: (domain (ensemble-domain)) + tls-context: (maybe-tls-context #f) + cookie: (cookie (get-actor-server-cookie)) + admin: (admin (get-admin-pubkey)) + auth: (auth #f) + known-servers: (known-servers (ensemble-known-servers)) + supervisor: (supervisor #f) + registry: (registry #f) + registry-addrs: (registry-addrs #f)) + (parameterize ((ensemble-domain domain) + (current-logger-options log-level)) + (when log-file + (let* ((path + (if (equal? log-file "-") + (path-expand "log" (ensemble-server-path server-id)) + (path-expand log-file))) + (dir (path-directory path))) + (unless (file-exists? dir) + (create-directory* dir)) + (start-logger! path))) + (let* ((tls-context (or maybe-tls-context (get-actor-tls-context server-id))) + (registry (or registry (cons 'registry domain))) + (known-servers + (cond + (known-servers known-servers) + ((or (memq 'registry roles) (memq 'supervisor roles)) + (hash)) + (registry-addrs + (hash (,registry registry-addrs))) + (else + (hash (,registry (default-registry-addresses)))))) + (unix-addr (ensemble-server-unix-addr server-id)) + (listen-addrs + (or listen-addrs [(ensemble-server-unix-addr server-id)])) + (public-addrs + (or public-addrs + listen-addrs))) + ;; start the actor server + (start-actor-server! identifier: server-id + roles: roles + tls-context: tls-context + cookie: cookie + admin: admin + auth: auth + addresses: listen-addrs + known-servers: known-servers + supervisor: supervisor + registry: registry) + ;; start the loader + (start-loader!) + ;; add the server to the ensemble + (unless (or supervisor (memq 'registry roles) (memq 'supervisor roles)) + (ensemble-add-server! server-id public-addrs roles)) + ;; run it! + (try + (thunk) + (catch (e) + (errorf "error executing actor server services: ~a" e) + (stop-actor-server! (current-actor-server)))) + (thread-join! (current-actor-server)) + ;; clean up unix sockets + (for (addr listen-addrs) + (match addr + ([unix: _ path] + (when (file-exists? path) + (delete-file path))) + (else (void)))) + ;; remove the server from the ensemble if we are not supervised + (unless (or supervisor (memq 'registry roles) (memq 'supervisor roles)) + (remove-from-registry! cookie known-servers server-id))))) + +(def (remove-from-registry! cookie known-servers server-id) + (start-actor-server! cookie: cookie known-servers: known-servers identifier: server-id) + (with-catch void (cut ensemble-remove-server! server-id)) + (stop-actor-server!)) diff --git a/src/std/actor-v18/ensemble-supervisor.ss b/src/std/actor-v18/ensemble-supervisor.ss new file mode 100644 index 000000000..46dc9fab2 --- /dev/null +++ b/src/std/actor-v18/ensemble-supervisor.ss @@ -0,0 +1,555 @@ +;;; -*- Gerbil -*- +;;; © vyzo +;;; ensemble supervisor api +(import :std/config + :std/sugar + :std/iter + :std/io + (only-in :std/logger current-log-directory) + ./path + ./cookie + ./admin + ./ensemble + ./ensemble-config + ./ensemble-util + ./ensemble-server + ./supervisor + ./executor + ./filesystem + ./message + ./proto + ./server-identifier + ./logger) +(export #t) + +;;; cfg: +(def (become-ensemble-supervisor! cfg (thunk void)) + (check-ensemble-config! cfg) + (unless (file-exists? (ensemble-cookie-path)) + (generate-ensemble-cookie!)) + (let* ((root (config-get cfg root:)) + (root (and root (path-normalize root))) + (root/log (and root (path-expand "log" root)))) + (when root/log + (create-directory* root/log)) + (parameterize ((current-log-directory (or root/log (ensemble-log-directory)))) + (let* ((cfg (ensemble-config-merge + (default-ensemble-config + (config-get! cfg domain:) + (config-get cfg public-address:)) + cfg)) + (root (: (config-get! cfg root:) :string)) + (domain (: (config-get! cfg domain:) :symbol)) + (services (: (config-get! cfg services:) :list))) + (create-directory* root) + (parameterize ((current-directory root) + (ensemble-domain domain)) + (let (supervisor-cfg (config-get! services supervisor:)) + (become-ensemble-server! supervisor-cfg + (lambda () + (start-ensemble-filesystem!) + (wait-for-actor! 'filesystem) + (start-ensemble-executor!) + (wait-for-actor! 'executor) + (start-ensemble-supervisor! cfg) + (wait-for-actor! 'supervisor) + (thunk))))))))) + +(def (default-ensemble-config domain (public-address #f)) + [config: 'ensemble-v0 + domain: domain + root: (path-normalize (current-directory)) + services: [supervisor: (default-ensemble-supervisor-config domain public-address) + registry: (default-ensemble-registry-config domain)]]) + +(def (default-ensemble-supervisor-config domain (public-address #f)) + (parameterize ((ensemble-domain domain)) + (let ((supervisor-id (cons 'supervisor domain)) + (registry-id (cons 'registry domain))) + [config: 'ensemble-server-v0 + domain: domain + identifier: supervisor-id + registry: registry-id + cookie: (ensemble-cookie-path) + admin: (ensemble-admin-pubkey-path) + role: 'supervisor + exe: "gerbil" + args: '("ensemble" "supervisor") + root: (path-normalize (current-directory)) + log-level: 'INFO + log-dir: (ensemble-server-log-directory supervisor-id) + log-file: (ensemble-server-log-file supervisor-id "server.log") + addresses: [(ensemble-server-unix-addr supervisor-id) + (if public-address + [[tls: public-address]] + []) ...] + known-servers: [[registry-id (ensemble-server-unix-addr registry-id)]]]))) + +(def (default-ensemble-registry-config domain) + (let (registry-id (cons 'registry domain)) + [config: 'ensemble-server-v0 + identifier: registry-id + role: 'registry + exe: "gerbil" + args: ["ensemble" "registry" (object->string registry-id)]])) + +(def (@supervisor super-id srv) + (handle srv (reference super-id 'supervisor))) +(def (@executor super-id srv) + (handle srv (reference super-id 'executor))) +(def (@filesystem super-id srv) + (handle srv (reference super-id 'filesystem))) + +;; lookup all known supervisors by role +;; returns a list of supervisor server ids known to the (current) actor server +(def (ensemble-lookup-supervisors (srv (current-actor-server))) + (cond + ((ensemble-lookup-servers/role 'supervisor srv) + => (cut map car <>)) + (else + (raise-actor-error ensmeble-lookup-supervisors "cannot find any supervisors")))) + +(defsyntax (ensemble-supervisors-collect stx) + (syntax-case stx () + ((_ srv collect1) + (with-syntax ((super (syntax-local-introduce 'super))) + #'(let ((supervisors (ensemble-lookup-supervisors srv)) + (try1 + (lambda (super) + (try + (cons super collect1) + (catch (e) + (errorf "error listing domain servers for supervisor: ~a: ~a" + super e) + (cons super (cons 'error e))))))) + (case (length supervisors) + ((0) []) + ((1) (try1 (car supervisors))) + (else + (map thread-join! + (map (lambda (super) (spawn try1 super)) + supervisors))))))))) + +;; list all supervised servers in a domain (and it's subdomains) +;; returns alist, associating a servisor with a list of servers +(def (ensemble-list-domain-servers + domain: (domain (ensemble-domain)) + role: (role #f) + actor-server: (srv (current-actor-server))) + (ensemble-supervisors-collect srv + (ensemble-supervisor-list-servers + supervisor: super + domain: domain + role: role + actor-server: srv))) + +(defcall-actor (ensemble-supervisor-list-servers + supervisor: (super (ensemble-domain-supervisor)) + domain: (domain #f) + role: (role #f) + actor-server: (srv (current-actor-server))) + (->> (@supervisor super srv) + (!supervisor-list-servers role domain)) + error: "error listing domain servers" + supervisor: super + role: role + domain: domain) + +;; start a new server for a (primary) role +;; returns the server identifier +(defcall-actor (ensemble-supervisor-start-server! + supervisor: (super (ensemble-domain-supervisor)) + role: role + server-id: server-id + domain: (domain (ensemble-domain)) + config: (config #f) + actor-server: (srv (current-actor-server))) + (->> (@supervisor super srv) + (!supervisor-start-server role domain server-id config)) + error: "error starting server" + supervisor: super + role: role + domain: domain + config: config + server-id: server-id) + +;; start a number of worker servers +;; returns a list of server identifiers +(defcall-actor (ensemble-supervisor-start-workers! + supervisor: (super (ensemble-domain-supervisor)) + role: role + server-id-prefix: prefix + workers: count + domain: (domain (ensemble-domain)) + config: (config #f) + actor-server: (srv (current-actor-server))) + (->> (@supervisor super srv) + (!supervisor-start-workers role domain prefix count config)) + error: "error starting workers" + supervisor: super + role: role + domain: domain + server-id-prefix: prefix + workers: count) + +;; stop some servers and remove them from the ensemble +;; servers: is an optional list of server ids +;; domain: is an optional domain to stop +;; roles: is a list of roles for the servers to stop +;; at least one of servers or roles should be specified to have any effect. +;; when specifying a domain, the roles select servers in the domain to stop. +;; if a domain but no roles or server-ids are specified, the entire domain +;; is shutdown +(defcall-actor (ensemble-supervisor-stop-servers! + supervisor: (super (ensemble-domain-supervisor)) + servers: (server-ids #f) + domain: (domain #f) + role: (role #f) + actor-server: (srv (current-actor-server))) + (->> (@supervisor super srv) + (!supervisor-stop-servers role domain server-ids)) + error: "error stopping servers" + supervisor: super + servers: server-ids + domain: domain + role: role) + +;; restart some servers +;; semantics as in stop-servers! above +(defcall-actor (ensemble-supervisor-restart-servers! + supervisor: (super (ensemble-domain-supervisor)) + servers: (server-ids #f) + domain: (domain #f) + role: (role #f) + actor-server: (srv (current-actor-server))) + (->> (@supervisor super srv) + (!supervisor-restart-servers role domain server-ids)) + error: "error restarting servers" + supervisor: super + servers: server-ids + domain: domain + role: role) + +;; get the log for some server +(defcall-actor (ensemble-supervisor-get-server-log + supervisor: (super (ensemble-domain-supervisor)) + server: server-id + file: (logf "server.log") + actor-server: (srv (current-actor-server))) + (->> (@supervisor super srv) + (!supervisor-get-server-log server-id logf)) + error: "error restarting servers" + supervisor: super + server: server-id) + +;; shutdown the entire ensemble, including the supervisor(s) +(def (ensemble-shutdown! actor-server: (srv (current-actor-server))) + (ensemble-supervisors-collect srv + (ensemble-supervisor-shutdown! supervisor: super actor-server: srv))) + +;; shutdown the (part of the) ensemble managed by a specific supervisor +(defcall-actor (ensemble-supervisor-shutdown! + supervisor: (super (ensemble-domain-supervisor)) + actor-server: (srv (current-actor-server))) + (let (result (->> (@supervisor super srv) (!shutdown))) + (when (!error? result) + (warnf "error shutting down supervisor ~a: ~a" super (!error-message result))) + result) + error: "error shutting down supervisor" + supervisor: super) + +;; restart the entire ensemble +(def (ensemble-restart! + restart-services: (restart-services? #f) + actor-server: (srv (current-actor-server))) + (ensemble-supervisors-collect srv + (ensemble-supervisor-restart! + supervisor: super + restart-services: restart-services? + actor-server: srv))) + +(defcall-actor (ensemble-supervisor-restart! + supervisor: (super (ensemble-domain-supervisor)) + restart-services: (restart-services? #f) + actor-server: (srv (current-actor-server))) + (->> (@supervisor super srv) + (!supervisor-restart restart-services?)) + error: "error restarting supervisor ensemble" + supervisor: super) + +;; update a server configuration for a supervisor +(defcall-actor (ensemble-supervisor-update-server-config! + supervisor: (super (ensemble-domain-supervisor)) + server: server-id + config: cfg + mode: (mode 'upsert) + restart: (restart? #t) + actor-server: (srv (current-actor-server))) + (->> (@supervisor super srv) + (!supervisor-update-server server-id cfg mode restart?)) + error: "error updating supervisor srever configuration" + supervisor: super + server: server-id + config: cfg) + +(defcall-actor (ensemble-supervisor-get-server-config + supervisor: (super (ensemble-domain-supervisor)) + server: server-id + actor-server: (srv (current-actor-server))) + (->> (@supervisor super srv) + (!supervisor-get-server-config server-id)) + error: "error retrieving server configuration" + supervisor: super + server: server-id) + +;; update the ensemble configuration +(def (ensemble-update-config! + config: config + mode: (mode 'upsert) + actor-server: (srv (current-actor-server))) + (ensemble-supervisors-collect srv + (ensemble-supervisor-update-config! + supervisor: super + config: config + mode: mode + actor-server: srv))) + +(defcall-actor (ensemble-supervisor-update-config! + supervisor: (super (ensemble-domain-supervisor)) + config: config + mode: (mode 'upsert) + actor-server: (srv (current-actor-server))) + (->> (@supervisor super srv) + (!supervisor-update-config config mode)) + error: "error updating supervisor configuration" + supervisor: super + config: config) + +(def (ensemble-get-config + actor-server: (srv (current-actor-server))) + (ensemble-supervisors-collect srv + (ensemble-supervisor-get-config + supervisor: super + actor-server: srv))) + +(defcall-actor (ensemble-supervisor-get-config + supervisor: (super (ensemble-domain-supervisor)) + actor-server: (srv (current-actor-server))) + (->> (@supervisor super srv) + (!supervisor-get-config)) + error: "error retrieving supervisor configuration" + supervisor: super) + +;; upload a new server executable to the ensemble supervisor executor +(def (ensemble-upload-executable! + path: executable-gz-path + deployment-path: deployment-path + actor-server: (srv (current-actor-server))) + (ensemble-supervisors-collect srv + (ensemble-supervisor-upload-executable! + supervisor: super + path: executable-path + deployment-path: deployment-path + actor-server: srv))) + +(def (ensemble-supervisor-upload-executable! + supervisor: (super (ensemble-domain-supervisor)) + path: executable-gz-path + deployment-path: deployment-path + actor-server: (srv (current-actor-server))) + (ensemble-supervisor-filesystem-upload! + supervisor: super + type: '(exe . gz) + path: executable-gz-path + deployment-path: deployment-path + actor-server: srv)) + +;; upload a new (GERBIL_PATH) enviornment as a tarball +(def (ensemble-upload-environment! + path: env-targz-path + deployment-path: deployment-path + actor-server: (srv (current-actor-server))) + (ensemble-supervisors-collect srv + (ensemble-supervisor-upload-environment! + supervisor: super + path: env-targz-path + deployment-path: deployment-path + actor-server: srv))) + +(def (ensemble-supervisor-upload-environment! + supervisor: (super (ensemble-domain-supervisor)) + path: env-targz-path + deployment-path: deployment-path + actor-server: (srv (current-actor-server))) + (ensemble-supervisor-filesystem-upload! + supervisor: super + type: '(env . tar.gz) + path: env-targz-path + deployment-path: deployment-path + actor-server: srv)) + + +;; upload a new (server accessible) filesystem structure +(def (ensemble-upload-filesystem-overlay! + fs: fs-targz-path + path: deployment-path + actor-server: (srv (current-actor-server))) + (ensemble-supervisors-collect srv + (ensemble-supervisor-upload-filesystem-overlay! + supervisor: super + path: fs-targz-path + deployment-path: deployment-path + actor-server: srv))) + +(def (ensemble-supervisor-upload-filesystem-overlay! + supervisor: (super (ensemble-domain-supervisor)) + path: fs-targz-path + deployment-path: deployment-path + actor-server: (srv (current-actor-server))) + (ensemble-supervisor-filesystem-upload! + supervisor: super + type: '(fs . tar.gz) + path: fs-targz-path + deployment-path: deployment-path + actor-server: srv)) + + +;; generic upload functionality +(defcall-actor (ensemble-supervisor-filesystem-upload! + supervisor: (super (ensemble-domain-supervisor)) + type: type + path: path + deployment-path: deployment-path + actor-server: (srv (current-actor-server))) + (let ((fs (@filesystem super srv)) + (cksum (file-digest path))) + (match (->> fs (!filesystem-upload-begin type deployment-path cksum)) + ((!filesystem-upload-continue token start) + (using (input (open-file-reader path) : Reader) + (when (fx> start 0) + (using (seeker input : Seeker) + (seeker.seek start))) + (let (buf (make-u8vector filesystem-upload-chunk-size)) + (let loop ((i 0)) + (let (in (input.read buf)) + (if (fx= in 0) + (begin + (input.close) + (->> fs (!filesystem-upload-end token))) + (begin + (-> fs (!filesystem-upload-chunk token i (subu8vector buf 0 in))) + (thread-yield!) + (loop (fx+ i in))))))))) + (result result))) + error: "error uploading to supervisor" + supervisor: super + type: type + path: path + deployment-path: deployment-path) + +;; command execution funcionality +(def (ensemble-shell-command + command: cmd + actor-server: (srv (current-actor-server))) + (ensemble-supervisors-collect srv + (ensemble-supervisor-shell-command + supervisor: super + command: cmd + actor-server: srv))) + +(defcall-actor (ensemble-supervisor-shell-command + supervisor: (super (ensemble-domain-supervisor)) + command: cmd + actor-server: (srv (current-actor-server))) + (->> (@executor super srv) + (!executor-shell-command cmd)) + error: "error executing shell command" + supervisor: super + command: cmd) + +(def (ensemble-list-processes actor-server: (srv (current-actor-server))) + (ensemble-supervisors-collect srv + (ensemble-supervisor-list-processes + supervisor: super + actor-server: srv))) + +(defcall-actor (ensemble-supervisor-list-processes + supervisor: (super (ensemble-domain-supervisor)) + actor-server: (srv (current-actor-server))) + (->> (@executor super srv) + (!executor-list-processes)) + error: "error listing processes" + supervisor: super) + +(defcall-actor (ensemble-supervisor-kill-process! + supervisor: (super (ensemble-domain-supervisor)) + pid: pid + signo: signo + actor-server: (srv (current-actor-server))) + (->> (@executor super srv) + (!executor-kill pid signo)) + error: "error killing process" + supervisor: super + pid: pid) + +(defcall-actor (ensemble-exec-process! + exe: exe + args: args + env: (env "default") + envvars: (envvars #f) + actor-server: (srv (current-actor-server))) + (ensemble-supervisors-collect srv + (ensemble-supervisor-exec-process! + supervisor: super + exe: exe + env: env + envvars: envvars + actor-server: srv))) + +(defcall-actor (ensemble-supervisor-exec-process! + supervisor: (super (ensemble-domain-supervisor)) + exe: exe + args: args + env: (env "default") + envvars: (envvars #f) + actor-server: (srv (current-actor-server))) + (->> (@executor super srv) + (!executor-exec exe args env envvars)) + error: "error executing process" + supervisor: super + exe: exe + args: args) + + +(defcall-actor (ensemble-supervisor-get-process-output + supervisor: (super (ensemble-domain-supervisor)) + pid: pid + actor-server: (srv (current-actor-server))) + (->> (@executor super srv) + (!executor-get-process-output pid)) + error: "error executing process" + supervisor: super + pid: pid) + +(defcall-actor (ensemble-supervisor-restart-process! + supervisor: (super (ensemble-domain-supervisor)) + pid: pid + actor-server: (srv (current-actor-server))) + (->> (@executor super srv) + (!executor-restart pid)) + error: "error executing process" + supervisor: super + pid: pid) + +;;; priviledged message invocations +(defcall-actor (ensemble-supervisor-invoke! + supervisor: (super (ensemble-domain-supervisor)) + actor: actor + message: msg + actor-server: (srv (current-actor-server))) + (->> (@supervisor super srv) + (!supervisor-invoke actor msg)) + error: "error invoking actor" + supervisor: super + actor: actor + message: msg) diff --git a/src/std/actor-v18/ensemble-test.ss b/src/std/actor-v18/ensemble-test.ss index 9078d70b0..aa51ff627 100644 --- a/src/std/actor-v18/ensemble-test.ss +++ b/src/std/actor-v18/ensemble-test.ss @@ -12,6 +12,7 @@ ./message ./proto ./server + ./server-identifier ./ensemble ./registry ./cookie @@ -35,7 +36,7 @@ (let* ((registry-file-path (make-temporary-file-name "registry-file")) (registry-sock (make-temporary-file-name "registry-sock")) (registry-addr [unix: (hostname) registry-sock]) - (known-servers (hash-eq (registry [registry-addr]))) + (known-servers (hash ((registry . /) [registry-addr]))) (addr1-sock (make-temporary-file-name "echo1")) (addr1 [unix: (hostname) addr1-sock]) (addr2-sock (make-temporary-file-name "echo2")) @@ -45,6 +46,7 @@ (def registry-srv (start-actor-server! cookie: cookie identifier: 'registry + roles: '(registry) addresses: [registry-addr])) (def registry-actor (start-ensemble-registry! registry-file-path registry-srv)) @@ -57,11 +59,11 @@ (def srv1 (start-actor-server! cookie: cookie addresses: [addr1] - ensemble: known-servers)) + known-servers: known-servers)) (def srv2 (start-actor-server! cookie: cookie addresses: [addr2] - ensemble: known-servers)) + known-servers: known-servers)) (def srv1-id (actor-server-identifier srv1)) @@ -110,7 +112,7 @@ ;; and do some echoing; the servers should connect through a registry lookup (check (->> actor1-proxy-srv2 'world) => '(hello . world)) (check (list-connections srv2) - => [[srv1-id addr1] ['registry registry-addr]]) + => [[srv1-id addr1] ['(registry . /) registry-addr]]) (check (->> actor2-proxy-srv1 'world) => '(hello . world)) (check (list-connections srv1) => [[srv2-id [unix: (hostname) "(local)"]]]) @@ -139,7 +141,7 @@ (let* ((registry-file-path (make-temporary-file-name "registry-file")) (registry-sock (make-temporary-file-name "registry-sock")) (registry-addr [unix: (hostname) registry-sock]) - (known-servers (hash-eq (registry [registry-addr]))) + (known-servers (hash ((registry . /) [registry-addr]))) (addr1-sock (make-temporary-file-name "echo1")) (addr1 [unix: (hostname) addr1-sock]) (addr2-sock (make-temporary-file-name "echo2")) @@ -149,6 +151,7 @@ (def registry-srv (start-actor-server! cookie: cookie identifier: 'registry + roles: '(registry) addresses: [registry-addr])) (def registry-actor (start-ensemble-registry! registry-file-path registry-srv)) @@ -161,11 +164,11 @@ (def srv1 (start-actor-server! cookie: cookie addresses: [addr1] - ensemble: known-servers)) + known-servers: known-servers)) (def srv2 (start-actor-server! cookie: cookie addresses: [addr2] - ensemble: known-servers)) + known-servers: known-servers)) (def srv1-id (actor-server-identifier srv1)) diff --git a/src/std/actor-v18/ensemble-util.ss b/src/std/actor-v18/ensemble-util.ss new file mode 100644 index 000000000..11ed7cd8b --- /dev/null +++ b/src/std/actor-v18/ensemble-util.ss @@ -0,0 +1,45 @@ +;;; -*- Gerbil -*- +;;; © vyzo +;;; ensemble utilitis +(import :std/sugar + :std/error + ./logger + ./message + ./proto + ./server-identifier) +(export #t) + +(defrule (with-error-handler what expr ...) + (try expr ... + (catch (e) + (errorf "error in ~a: ~a" what e) + (!error (error-message e))))) + +(defrule (with-error-log what expr ...) + (try expr ... + (catch (e) + (errorf "error in ~a: ~a" what e)))) + +(def (wait-for-actor! actor (srv (current-actor-server)) + wait: (wait-for .1) + attempts: (attempts 5)) + (let (dest (handle srv (if (reference? actor) actor (reference #f actor)))) + (let loop ((n 0) (wait-for wait-for)) + (thread-yield!) + (if (fx< n attempts) + (match (with-catch void (cut ->> dest (!ping))) + ((!ok) (void)) + (else + (thread-sleep! wait-for) + (loop (fx1+ n) (* 2 wait-for)))) + (raise-timeout wait-for-actor! "timeout" actor: actor))))) + +(def (local-actor? source actor) + (or (thread? source) + (and (handle? source) + (using (source :- handle) + (and (reference? source.ref) + (using (ref source.ref : reference) + (and (or (not ref.server) + (equal? (actor-server-identifier) ref.server)) + (eq? ref.actor actor)))))))) diff --git a/src/std/actor-v18/ensemble.ss b/src/std/actor-v18/ensemble.ss index 2335b97a5..c1adcc490 100644 --- a/src/std/actor-v18/ensemble.ss +++ b/src/std/actor-v18/ensemble.ss @@ -1,11 +1,11 @@ ;;; -*- Gerbil -*- ;;; © vyzo -;;; actor ensemble utilities +;;; actor ensemble basic utilities (import :std/error (only-in :std/misc/ports read-file-u8vector) ./message ./proto - ./server + ./server-identifier ./admin) (export #t) @@ -21,19 +21,19 @@ ;; lists the registered actors in a remote server (defcall-actor (remote-list-actors srv-id (srv (current-actor-server))) - (->> srv (!list-actors srv-id)) + (->> srv (!list-actors (server-identifier srv-id))) error: "error listing actors" srv-id) ;; ensures there is a connection to a server in the ensemble. ;; if the addresses are not specified, it is looked up in the registry. ;; Raises an error if the connection fails. (defcall-actor (remote-connect-to-server! from-id to-id (addrs #f) (srv (current-actor-server))) - (->> srv (!connect from-id to-id addrs)) + (->> srv (!connect (server-identifier from-id) (server-identifier to-id) addrs)) error: "error remotely connecting to server" from-id to-id) ;; list the server connections for a remote server (defcall-actor (remote-list-connections srv-id (srv (current-actor-server))) - (->> srv (!list-connections srv-id)) + (->> srv (!list-connections (server-identifier srv-id))) error: "error retrieving server connections" srv-id) ;; loads a library module in a remote server @@ -87,15 +87,15 @@ ;; adds a server to the ensemble (defcall-actor (ensemble-add-server! id addrs roles (srv (current-actor-server))) - (->> srv (!ensemble-add-server id addrs roles)) + (->> srv (!ensemble-add-server (server-identifier id) addrs roles)) error: "error adding server") ;; removes a server from the ensemble (defcall-actor (ensemble-remove-server! id (srv (current-actor-server))) - (->> srv (!ensemble-remove-server id)) + (->> srv (!ensemble-remove-server (server-identifier id))) error: "error removing server") -;; lists all known servers in the ensemble. +;; lists all known servers in the ensemble registry. ;; returns a list [[id roles addr ...] ...] (defcall-actor (ensemble-list-servers (srv (current-actor-server))) (->> srv (!ensemble-lookup-server #f #f)) @@ -104,7 +104,7 @@ ;; looks up a server in the ensemble through the registry ;; returns the server addresses: [addr ...] (defcall-actor (ensemble-lookup-server id (srv (current-actor-server))) - (->> srv (!ensemble-lookup-server id #f)) + (->> srv (!ensemble-lookup-server (server-identifier id) #f)) error: "error looking up server" id) ;; looks up servers in the ensemble registry that fullfill a role. @@ -118,7 +118,9 @@ (defcall-actor (admin-authorize privk srv-id authorized-server-id (srv (current-actor-server)) capabilities: (cap '(admin))) - (let (remote-root (handle srv (reference srv-id 0))) + (let* ((srv-id (server-identifier srv-id)) + (authorized-server-id (server-identifier authorized-server-id)) + (remote-root (handle srv (reference srv-id 0)))) (unless (and (list? cap) (andmap symbol? cap)) (raise-bad-argument authorize "capabilities: list of symbols" cap)) (match (->> remote-root (!admin-auth authorized-server-id cap)) diff --git a/src/std/actor-v18/executor.ss b/src/std/actor-v18/executor.ss new file mode 100644 index 000000000..8f72328b1 --- /dev/null +++ b/src/std/actor-v18/executor.ss @@ -0,0 +1,321 @@ +;;; -*- Gerbil -*- +;;; © vyzo +;;; ensemble executor actor +(import :std/iter + :std/sugar + :std/sort + :std/io + :std/os/signal + ./message + ./proto + ./server + ./server-identifier + ./ensemble-util + ./logger) +(export #t) + +(def executor-terminate-grace-period 5) + +(defmessage !executor-exec (exe args env envvars)) +(defmessage !executor-kill (pid signo)) +(defmessage !executor-stop (pid)) +(defmessage !executor-restart (pid)) +(defmessage !executor-monitor (pid)) +(defmessage !executor-get-process-output (pid)) +(defmessage !executor-list-processes ()) +(defmessage !executor-shell-command (cmd)) + +(defmessage !executor-notify (pid exit-code)) + +(defclass Process (process exe args env envvars start-time monitors continuations notifier kill?) + final: #t) + +(def (start-ensemble-executor! (srv (current-actor-server))) + (spawn/name 'executor ensemble-executor srv)) + +(def (ensemble-executor srv) + (register-actor! 'executor srv) + (infof "starting executor...") + + ;; add {root}/bin to $PATH + (let* ((root (path-normalize (current-directory))) + (root/bin (path-expand "bin" root)) + (root/env (path-expand "env" root)) + (root/fs (path-expand "fs" root)) + (root/proc (path-expand "proc" root))) + (setenv "PATH" (string-append root/bin ":" (getenv "PATH"))) + (when (file-exists? root/proc) + (delete-file-or-directory root/proc #t)) + (create-directory* root/proc) + (create-directory* root/fs) + + ;; and run the actor loop + (let/cc exit + (def procs (make-hash-table-eqv)) + + (def (notify! source pid exit-code) + (cond + ((hash-get procs pid) + => (lambda ((proc :- Process)) + (if (eq? proc.notifier source) + (begin + (infof "process exit notification: ~a ~a" pid exit-code) + (hash-remove! procs pid) + (close-port proc.process) + (for (monitor proc.monitors) + (-> monitor (!executor-notify pid exit-code))) + (for (cont proc.continuations) + (cont))) + (debugf "unexpected notification for ~a from ~a" pid source)))) + (else + (debugf "notification for unknown process ~a" pid)))) + + (def (kill-process! pid signo) + (infof "killing process ~a ~a" pid signo) + (with-error-handler "kill-process" + (cond + ((hash-get procs pid) + (kill pid signo) + (!ok (void))) + (else + (!error "unknown process"))))) + + (def (monitor-process source pid) + (infof "monitor process ~a: ~a" pid source) + (cond + ((hash-get procs pid) + => (lambda ((proc :- Process)) + (set! proc.monitors (cons source proc.monitors)) + (!ok pid))) + (else + (!error "unknown process")))) + + (def (get-process-output pid) + (with-error-handler "get-process-output" + (let (output + (path-expand "output" + (path-expand (object->string pid) + (path-expand root/proc)))) + + (if (file-exists? output) + (!ok (read-file-string output)) + (!error "unknown process"))))) + + (def (list-processes) + (!ok + (sort + (for/collect ([pid . proc] (hash->list procs)) + (using (proc :- Process) + [pid proc.exe proc.args proc.env proc.start-time])) + (lambda (a b) + (< (car a) (car b)))))) + + (def (execute-process! exe args env envvars) + (infof "executing process ~a" [exe args env envvars]) + (with-error-handler "execute!" + (let* ((process (open-process + [path: exe + arguments: args + directory: root/fs + environment: (process-environment env envvars) + stdin-redirection: #t + stdout-redirection: #t + stderr-redirection: #t])) + (notifier (process-monitor process)) + (pid (process-pid process)) + (proc (Process process: process + exe: exe + args: args + env: env + envvars: envvars + start-time: (##current-time-point) + monitors: [] + continuations: [] + notifier: notifier))) + (hash-put! procs pid proc) + (infof "executed process ~a: ~a" exe pid) + (!ok pid)))) + + (def (process-environment env envvars) + (def (find-envvar-prefix pre lst) + (find (cut string-prefix? pre <>) lst)) + + (def (filter-out-envvar-prefix pre lst) + (filter (lambda (str) (not (string-prefix? pre str))) lst)) + + (let* ((env-path (if env (path-expand env root/env) (gerbil-path))) + (_ (unless (file-exists? env-path) + (create-directory* env-path))) + ($env-path (string-append "GERBIL_PATH=" env-path)) + (path (or (find-envvar-prefix "PATH=" envvars) + (getenv "PATH"))) + ($path (string-append "PATH=" root/bin ":" path)) + (user (or (getenv "USER" #f) + (find-envvar-prefix "USER=" envvars))) + ($user (and user (string-append "USER=" user))) + (username (or (getenv "USERNAME" #f) + (find-envvar-prefix "USERNAME=" envvars))) + ($username (and username (string-append "USERNAME=" username))) + (lang (or (find-envvar-prefix "LANG=" envvars) + (getenv "LANG" #f))) + ($lang (and lang (string-append "LANG=" lang))) + (home (or (find-envvar-prefix "HOME=" envvars) + (getenv "HOME" #f))) + ($home (and home (string-append "HOME=" home))) + (shell (or (find-envvar-prefix "SHELL=" envvars) + (getenv "SHELL" #f))) + ($shell (and shell (string-append "SHELL=" shell))) + (envvars (filter-out-envvar-prefix "GERBIL_PATH=" envvars)) + (envvars (filter-out-envvar-prefix "PATH=" envvars)) + (envvars (filter-out-envvar-prefix "USER=" envvars)) + (envvars (filter-out-envvar-prefix "USERNAME=" envvars)) + (envvars (filter-out-envvar-prefix "HOME=" envvars)) + (envvars (filter-out-envvar-prefix "SHELL=" envvars))) + (append [$env-path + $path + (and $user [$user]) ... + (and $username [$username]) ... + (and $lang [$lang]) ... + (and $home [$home]) ... + (and $shell [$shell]) ...] + envvars))) + + (def (process-monitor process) + (let* ((pid (process-pid process)) + (executor (current-thread)) + (proc/pid (path-expand (number->string pid) root/proc)) + (proc/pid/output (path-expand "output" proc/pid))) + (when (file-exists? proc/pid) + (delete-file-or-directory proc/pid #t)) + (create-directory* proc/pid) + (close-output-port process) + (spawn/name 'executor-process-io + (lambda () + (call-with-output-file proc/pid/output + (lambda (output) + (with-catch + void + (cut io-copy! + (make-raw-binary-input-port process) + (make-raw-binary-output-port output))))))) + (spawn/name 'executor-notify + (lambda () + (let (exit-code (process-status process)) + (-> executor (!executor-notify pid exit-code))))))) + + (def (restart-process! source nonce expiry pid) + (cond + ((hash-get procs pid) + => (lambda ((proc :- Process)) + (infof "restarting process ~a" pid) + (terminate-process! pid proc + (lambda () + (-> source + (execute-process! proc.exe proc.args proc.env proc.envvars) + replyto: nonce + expiry: expiry))))) + (else + (-> source (!error "unknown process") + replyto: nonce + expiry: expiry)))) + + (def (stop-process! source nonce expiry pid) + (cond + ((hash-get procs pid) + => (lambda ((proc :- Process)) + (infof "stopping process ~a" pid) + (terminate-process! pid proc + (lambda () + (-> source (!ok pid) replyto: nonce expiry: expiry))))) + (else + (-> source (!error "unknown process") + replyto: nonce + expiry: expiry)))) + + (def (terminate-process! pid (proc : Process) cont) + (infof "terminating process ~a" pid) + (kill pid SIGTERM) + (unless proc.kill? + (set! proc.kill? #t) + (spawn after executor-terminate-grace-period (current-thread) pid)) + (set! proc.continuations (cons cont proc.continuations))) + + (def (force-terminate-process! pid) + (cond + ((hash-get procs pid) + => (lambda ((proc :- Process)) + (when proc.kill? + (infof "killing process ~a" pid) + (kill pid SIGKILL)))))) + + (def (shutdown!) + (for ([pid . proc] (hash->list procs)) + (kill pid SIGTERM) + (using (proc :- Process) + (unless proc.kill? + (set! proc.kill? #t) + (spawn after executor-terminate-grace-period (current-thread) pid)))) + + (while (fx> (hash-length procs) 0) + (<- + ((!executor-notify pid exit-code) + (notify! @source pid exit-code)) + ((!tick pid) + (force-terminate-process! pid))))) + + (infof "executor running...") + + (while #t + (<- + ((!executor-exec exe args env envvars) + (with-authorization 'executor + (execute-process! exe args env envvars))) + + ((!executor-notify pid exit-code) + (notify! @source pid exit-code)) + + ((!executor-kill pid signo) + (with-authorization 'executor + (kill-process! pid signo))) + + ((!executor-stop pid) + (if (actor-authorized? @source 'executor) + (stop-process! @source @nonce @expiry pid) + (--> (!error "not authorized")))) + + ((!executor-restart pid) + (if (actor-authorized? @source 'executor) + (restart-process! @source @nonce @expiry pid) + (--> (!error "not authorized")))) + + ((!executor-monitor pid) + (with-authorization 'executor + (monitor-process @source pid))) + + ((!executor-get-process-output pid) + (with-authorization 'executor + (get-process-output pid))) + + ((!executor-list-processes) + (with-authorization 'executor + (list-processes))) + + ((!executor-shell-command cmd) + (if (actor-authorized? @source 'executor) + (spawn/name 'executor-shell-command + (lambda () + (--> + (with-error-handler "shell-command" + (!ok (shell-command cmd #t)))))) + (--> (!error "not authorized")))) + + ((!tick pid) + (force-terminate-process! pid)) + + ;; management protocol + ,(@shutdown + (infof "executor shutting down ...") + (shutdown!) + (exit 'shutdown)) + ,(@ping) + ,(@unexpected warnf)))))) diff --git a/src/std/actor-v18/filesystem.ss b/src/std/actor-v18/filesystem.ss new file mode 100644 index 000000000..ae8032532 --- /dev/null +++ b/src/std/actor-v18/filesystem.ss @@ -0,0 +1,229 @@ +;;; -*- Gerbil -*- +;;; © vyzo +;;; ensemble filesystem actor +(import :std/io + :std/crypto/digest + :std/config + :std/iter + :std/sugar + :std/text/hex + :std/misc/process + (only-in :std/srfi/13 string-contain) + ./logger + ./message + ./proto + ./server + ./server-identifier + ./ensemble-util) +(export #t) + +(def filesystem-upload-chunk-size 16384) +(def filesystem-upload-ttl 120) + +(def (file-digest path) + (using (input (open-file-reader path) : Reader) + (let ((buf (make-u8vector filesystem-upload-chunk-size)) + (digest (make-sha256-digest))) + (let loop () + (let (in (input.read buf)) + (if (fx> in 0) + (begin + (digest-update! digest buf 0 in) + (loop)) + (begin + (input.close) + (digest-final! digest)))))))) + +(defmessage !filesystem-upload-begin (type path cksum)) +(defmessage !filesystem-upload-continue (token start)) +(defmessage !filesystem-upload-chunk (token offset data)) +(defmessage !filesystem-upload-end (token)) + +(defclass Upload + ((cksum : :u8vector) + (blob : :string) + (path : :string) + (type : :pair) + (writer : Writer) + (offset : :integer) + (expire : :flonum)) + final: #t) + +(def (upload-blob-key (cksum : :u8vector)) + (hex-encode cksum)) + +(def (upload-blob-path (key : :string)) + (path-expand key (path-expand "blob" (current-directory)))) + +(def (upload-expiration-time) + (+ (##current-time-point) filesystem-upload-ttl)) + +;; current-directory: root of the filesystem +(def (start-ensemble-filesystem! (srv (current-actor-server))) + (spawn/name 'filesystem ensemble-filesystem srv)) + +(def (ensemble-filesystem srv) + (register-actor! 'filesystem srv) + (infof "starting filesystem...") + + ;; prepare filesystem layout + (for (dir ["bin" "env" "fs" "blob" "log"]) + (create-directory* dir)) + + ;; and run the actor loop + (let/cc exit + ;; in progress uploads + (def uploads (make-hash-table)) + + ;; gc ticker + (def gc-ticker + (spawn/name 'ticker ticker (current-thread) 60)) + + (def (upload-check! (type : :pair) (path : :string)) + (unless (member type '((exe . gz) (env . tar.gz) (fs . tar.gz))) + (error "unknown upload type" type)) + (when (or (and (string-empty? path) (not (eq? (car type) 'fs))) + (string-prefix? "/" path) + (string-prefix? "." path) + (string-contains path "..")) + (error "bad deployment path" path))) + + (def (upload-begin type path cksum) + (with-error-handler "upload-begin" + (upload-check! type path) + (let (blob-key (upload-blob-key cksum)) + (cond + ((hash-get uploads blob-key) + => (lambda ((up :- Upload)) + (if (and (equal? type up.type) + (equal? path up.path)) + (begin + (set! up.expire (upload-expiration-time)) + (debugf "continuing upload ~a" blob-key) + (!filesystem-upload-continue blob-key up.offset)) + (!error "upload in progress")))) + (else + (let (blob-path (upload-blob-path blob-key)) + (if (file-exists? blob-path) + ;; already uploaded, but potentially incomplete because of a restart + (let (cksum2 (file-digest blob-path)) + (if (equal? cksum cksum2) + (upload-finish blob-key type blob-path path) + (upload-begin-at blob-key type path cksum blob-path (file-size blob-path)))) + (upload-begin-at blob-key type path cksum blob-path 0)))))))) + + (def (upload-begin-at key type path cksum blob-path offset) + (let* ((wr (open-file-writer blob-path mode: #o640)) + (up (Upload cksum: cksum + blob: blob-path + path: path + type: type + writer: wr + offset: offset + expire: (upload-expiration-time)))) + (when (> offset 0) + (using (seeker wr : Seeker) + (seeker.seek offset))) + (hash-put! uploads key up) + (debugf "begin upload ~a" key) + (!filesystem-upload-continue key offset))) + + (def (upload-chunk key offset data) + (with-error-log "upload-chunk" + (cond + ((hash-get uploads key) + => (lambda ((up :- Upload)) + (if (= offset up.offset) + (begin + (up.writer.write data) + (set! up.offset (+ offset (u8vector-length data))) + (set! up.expire (upload-expiration-time))) + (debugf "unexpected chunk for ~a: chunk-offset: ~a writer-offset: ~a" + key offset up.offset)))) + (else + (debugf "unexpected chunk for unknown upload: ~a" key))))) + + (def (upload-end key) + (with-error-handler "upload-end" + (cond + ((hash-get uploads key) + => (lambda ((up :- Upload)) + (hash-remove! uploads key) + (up.writer.close) + (if (equal? (file-digest up.blob) up.cksum) + (upload-finish key up.type up.blob up.path) + (begin + (delete-file up.blob) + (!error "checksum mismatch"))))) + (else + (!error "unknown upload"))))) + + (def (upload-finish key type blob-path deploy-path) + (def (tar-expand base) + (let (expand-path + (if (string-empty? deploy-path) + base + (path-expand deploy-path base))) + (create-directory* expand-path) + (invoke "tar" ["xzf" blob-path] directory: expand-path))) + (try + (cond + ((equal? type '(exe . gz)) + (let* ((executable (path-expand deploy-path "bin")) + (executable.gz (string-append executable ".gz"))) + (when (file-exists? executable) + (rename-file executable (string-append executable ".old." (number->string (##current-time-point))))) + (rename-file blob-path executable.gz) + (invoke "gunzip" [executable.gz]) + (invoke "chmod" ["+x" executable]))) + ((equal? type '(env . tar.gz)) + (tar-expand "env")) + ((equal? type '(fs . tar.gz)) + (tar-expand "fs"))) + (!ok key) + (finally + (when (file-exists? blob-path) + (delete-file blob-path))))) + + (def (upload-gc!) + (for ([key . up] (hash->list uploads)) + (using (up : Upload) + (when (> (##current-time-point) up.expire) + (debugf "garbage collecting stale upload ~a" key) + (up.writer.close) + (delete-file up.blob) + (hash-remove! uploads key))))) + + (def (shutdown!) + (for (up (hash-values uploads)) + (using (up :- Upload) + (up.writer.close) + (delete-file up.blob)))) + + (infof "filesystem running ...") + + (while #t + (<- + ;; uploads + ((!filesystem-upload-begin type path cksum) + (with-authorization 'filesystem (upload-begin type path cksum))) + + ((!filesystem-upload-chunk token offset data) + (when (actor-authorized? @source 'filesystem) + (upload-chunk token offset data))) + + ((!filesystem-upload-end token) + (with-authorization 'filesystem (upload-end token))) + + ;; gc + ((!tick) + (upload-gc!)) + + ;; management protocol + ,(@shutdown + (infof "filesystem shutting down ...") + (shutdown!) + (-> gc-ticker (!shutdown)) + (exit 'shutdown)) + ,(@ping) + ,(@unexpected warnf))))) diff --git a/src/std/actor-v18/loader-test.ss b/src/std/actor-v18/loader-test.ss index f78b48c0a..90b4795a7 100644 --- a/src/std/actor-v18/loader-test.ss +++ b/src/std/actor-v18/loader-test.ss @@ -9,6 +9,7 @@ :std/os/hostname :std/os/temporaries ./server + ./server-identifier ./ensemble ./cookie ./admin @@ -60,14 +61,14 @@ (def (start-test-server! server-id server-addr cookie) (open-process [path: (path-expand "bin/loader-test-server" gerbil-path) - arguments: [(symbol->string server-id) + arguments: [(symbol->string (car server-id)) (object->string [server-addr]) (object->string cookie)]])) (def (start-test-server/admin! server-id server-addr cookie pubk-path) (open-process [path: (path-expand "bin/loader-test-server" gerbil-path) - arguments: [(symbol->string server-id) + arguments: [(symbol->string (car server-id)) (object->string [server-addr]) (object->string cookie) pubk-path]])) diff --git a/src/std/actor-v18/loader.ss b/src/std/actor-v18/loader.ss index a0f5abb51..0017526a5 100644 --- a/src/std/actor-v18/loader.ss +++ b/src/std/actor-v18/loader.ss @@ -9,6 +9,7 @@ ./message ./proto ./server + ./server-identifier ./path) (export #t) diff --git a/src/std/actor-v18/message.ss b/src/std/actor-v18/message.ss index 242b993a0..02618a39b 100644 --- a/src/std/actor-v18/message.ss +++ b/src/std/actor-v18/message.ss @@ -5,9 +5,15 @@ :gerbil/gambit :std/error :std/sugar - :std/stxparam) + :std/stxparam + ./path + ./server-identifier) (export #t) +;; message types registry +(def +message-types+ (make-hash-table-eq)) +(def +message-types-mx+ (make-mutex 'message-types)) + ;; actor errors (deferror-class ActorError () actor-error?) (defraise/context (raise-actor-error where message irritants ...) @@ -60,17 +66,43 @@ (alet (expiry msg.expiry) (< (time->seconds expiry) (##current-time-point))))) -;; actor handle base type. + +;; actor references +;; - server is the actor-server identifier: a symbol uniquely identifying an +;; actor within the domain. +;; - id is the server-specific identifier of the actor; a symbol or a numeric id. +;; - domain is the ensemble domain; a symbol using / as the subdomain separator +;; or false for the default flat domain. +(defmessage reference (server actor) + constructor: :init!) + +(defmethod {:init! reference} + (lambda (self server actor (domain (ensemble-domain))) + (let (server + (cond + ((pair? server) server) + ((symbol? server) + (if domain + (cons server domain) + server)) + ((not #f) #f) + (else + (raise-bad-argument reference:::init! "symbol or symbol pair" server)))) + (set! self.server server) + (set! self.actor actor)))) + +;; actor handles ;; - proxy is the thread that handles messages on behalf of another actor. ;; - ref is a reference to an actor; see ./server -;; - authorized? is a boolean indicating whether the origin is authorized for -;; for administrative actions. +;; - capabilities is an (optional) list of capabilities of the actor (server) (defstruct handle (proxy ref capabilities) - final: #t transparent: #t + final: #t print: (ref) constructor: :init!) (defmethod {:init! handle} - (lambda (self proxy ref (capabilities #f)) + (lambda (self (proxy : :thread) + (ref : reference) + (capabilities #f)) (set! self.proxy proxy) (set! self.ref ref) (set! self.capabilities capabilities))) @@ -90,6 +122,10 @@ (find (lambda (c) (or (eq? c cap) (eq? c 'admin))) capabilities)))) (else #f))) +;; creates a proxy handle from a reference +(def (reference->handle ref (srv (current-actor-server))) + (make-handle srv ref)) + ;; sends a message to an actor ;; - actor must be a thread or handle ;; - msg must be a serializable object or message @@ -117,8 +153,9 @@ ;; sends a message and receives the reply with a timeout. (def (->> dest msg replyto: (replyto #f) - timeout: (timeo +default-reply-timeout+)) - (let* ((expiry (timeout->expiry timeo)) + timeout: (timeo +default-reply-timeout+) + expiry: (expiry #f)) ; supersedes timeout + (let* ((expiry (or expiry (timeout->expiry timeo))) (nonce (current-thread-nonce!))) (unless (send-message dest (envelope msg dest (current-thread) nonce replyto expiry #t)) (raise-actor-error send-message "actor is dead" dest)) @@ -284,9 +321,6 @@ (raise-bad-argument expiry "real or time" timeo)))) ;; message type registry -(def +message-types+ (make-hash-table-eq)) -(def +message-types-mx+ (make-mutex 'message-type-registry)) - (def (register-message-type! klass) (let (klass-id (##type-id klass)) (unless (interned-symbol? klass-id) diff --git a/src/std/actor-v18/path.ss b/src/std/actor-v18/path.ss index 7cf3ef55a..f19d2b7a4 100644 --- a/src/std/actor-v18/path.ss +++ b/src/std/actor-v18/path.ss @@ -1,12 +1,119 @@ ;;; -*- Gerbil -*- ;;; © vyzo ;;; ensemble path utils +(import :std/config + :std/os/hostname + (only-in :std/logger current-log-directory)) (export #t) -(def (ensemble-base-path) - (path-expand "ensemble" (gerbil-path))) +(def current-ensemble-server-config + (make-parameter #f)) -(def (ensemble-server-path server-id) - (path-expand (symbol->string server-id) - (path-expand "server" - (ensemble-base-path)))) +(def ensemble-domain + (make-parameter '/)) + +(def (ensemble-subdomain sub (domain (ensemble-domain))) + (let (sub-str (symbol->string sub)) + (if (string-prefix? "/" sub-str) + sub + (string->symbol + (path-expand sub-str (symbol->string domain)))))) + +(def (ensemble-domain->relative-path (domain (ensemble-domain))) + (string-join + (filter (? (not string-empty?)) + (string-split (symbol->string domain) #\/)) + #\/)) + +(def (ensemble-base-path (base (gerbil-path))) + (path-expand "ensemble" base)) + +(def (ensemble-domain-path (domain (ensemble-domain)) (base (gerbil-path))) + (let (base (ensemble-base-path base)) + (if domain + (let (domain-path + (filter (? (not string-empty?)) + (string-split (symbol->string domain) #\/))) + (let loop ((rest domain-path) (path base)) + (match rest + ([dom . rest] + (loop rest (path-expand dom (path-expand "+" path)))) + (else path)))) + base))) + +(def (ensemble-server-path server-id (domain (ensemble-domain)) (base (gerbil-path))) + (if (pair? server-id) + (ensemble-server-path (car server-id) (cdr server-id) base) + (path-expand (symbol->string server-id) + (path-expand "server" + (ensemble-domain-path domain base))))) + +(def (ensemble-config-path (base (gerbil-path))) + (path-expand "config" (ensemble-base-path base))) + +(def (ensemble-server-config-path server-id (domain (ensemble-domain)) (base (gerbil-path))) + (path-expand "config" (ensemble-server-path server-id domain base))) + +(def (ensemble-server-unix-path server-id (domain (ensemble-domain))) + (if (pair? server-id) + (ensemble-server-unix-path (car server-id) (cdr server-id)) + (let* ((domain-path (ensemble-domain->relative-path domain)) + (base (if (string-empty? domain-path) + "/tmp/ensemble" + (path-expand domain-path "/tmp/ensemble")))) + (path-expand (string-append (symbol->string server-id) ".sock") base)))) + +(def (ensemble-server-unix-addr server-id) + [unix: (hostname) (ensemble-server-unix-path server-id)]) + +(def (ensemble-known-servers-path (base (ensemble-base-path))) + (path-expand "known-servers" base)) + +(def (ensemble-known-servers (base (ensemble-base-path))) + (def (infer) + (let (path (ensemble-known-servers-path base)) + (and (file-exists? path) + (list->hash-table + (call-with-input-file path (cut read <>)))))) + (cond + ((current-ensemble-server-config) + => (lambda (config) + (or (config-get config known-servers:) + (infer)))) + (else (infer)))) + +(def (ensemble-domain-supervisor-path (base (ensemble-base-path))) + (path-expand "supervisor" base)) + +(def (ensemble-domain-supervisor (base (ensemble-base-path))) + (def (infer) + (let (path (ensemble-domain-supervisor-path base)) + (if (file-exists? path) + (call-with-input-file path read) + (cons 'supervisor (ensemble-domain))))) + (cond + ((current-ensemble-server-config) + => (lambda (config) + (or (config-get config supervisor:) + (infer)))) + (else (infer)))) + +(def (ensemble-domain-file-path (base (ensemble-base-path))) + (path-expand "domain" base)) + +(def (ensemble-server-log-directory server-id (root (ensemble-log-directory))) + (with ([id . domain] server-id) + (let (relpath (ensemble-domain->relative-path domain)) + (path-expand (symbol->string id) + (path-expand relpath root))))) + +(def (ensemble-server-log-file server-id + (file "server.log") + (root (ensemble-log-directory))) + (path-expand file (ensemble-server-log-directory server-id root))) + +(def (ensemble-log-directory (base (gerbil-path))) + (cond + ((current-log-directory)) + (else + (path-expand "log" (ensemble-base-path base))))) diff --git a/src/std/actor-v18/proto.ss b/src/std/actor-v18/proto.ss index dd35d3639..e95a1f672 100644 --- a/src/std/actor-v18/proto.ss +++ b/src/std/actor-v18/proto.ss @@ -3,7 +3,8 @@ ;;; actor protocol messages (import :std/error :std/sugar - ./message) + ./message + ./server-identifier) (export #t) (defmessage !ok (value)) @@ -25,6 +26,11 @@ ((_ (proc arg ...) expr) (defcall-actor (proc arg ...) expr error: "actor error"))) +(defrule (with-authorization cap expr) + (if (actor-authorized? @source cap) + (--> expr) + (--> (!error "not authorized")))) + (defmessage !shutdown ()) (defmessage !actor-dead (thread)) (defmessage !tick (id seqno)) diff --git a/src/std/actor-v18/registry.ss b/src/std/actor-v18/registry.ss index 63b75feb8..bd94e412a 100644 --- a/src/std/actor-v18/registry.ss +++ b/src/std/actor-v18/registry.ss @@ -11,6 +11,8 @@ ./message ./proto ./server + ./server-identifier + ./ensemble-util ./path) (export #t) @@ -42,49 +44,52 @@ (eq? (reference-server (handle-ref actor)) server-id)))) (def (sort-server-list lst) - (sort lst (lambda (a b) (symbol (!ok (void)))) - (--> (!error "not authorized")))) + (--> (with-error-handler "add-server" + (if (authorized-for? @source id) + (begin + (infof "adding server ~a ~a at ~a" id roles addrs) + (registry.add-server id addrs roles) + (!ok (void))) + (!error "not authorized"))))) ((!ensemble-remove-server id) - (if (authorized-for? @source id) - (begin - (infof "removing server ~a" id) - (registry.remove-server id) - (--> (!ok (void)))) - (--> (!error "not authorized")))) + (--> (with-error-handler "remove-server" + (if (authorized-for? @source id) + (begin + (infof "removing server ~a" id) + (registry.remove-server id) + (!ok (void))) + (!error "not authorized"))))) ((!ensemble-lookup-server id role) - (cond - (id - (debugf "looking up server ~a for ~a" id @source) - (cond - ((registry.lookup-server id) - => (lambda (value) (--> (!ok value)))) - (else - (--> (!error "unknown server"))))) - (role - (debugf "looking up servers by role ~a for ~a" role @source) - (let* ((result (registry.lookup-servers/role role)) - (result (sort-server-list result))) - (--> (!ok result)))) - (else - (debugf "listing servers for ~a" @source) - (let* ((result (registry.list-servers)) - (result (sort-server-list result))) - (--> (!ok result)))))) + (--> (with-error-handler "lookup-server" + (cond + (id + (debugf "looking up server ~a for ~a" id @source) + (cond + ((registry.lookup-server id) + => (lambda (value) (!ok value))) + (else + (!error "unknown server")))) + (role + (debugf "looking up servers by role ~a for ~a" role @source) + (let* ((result (registry.lookup-servers/role role)) + (result (sort-server-list result))) + (!ok result))) + (else + (debugf "listing servers for ~a" @source) + (let* ((result (registry.list-servers)) + (result (sort-server-list result))) + (!ok result))))))) ((!tick) (registry.flush)) @@ -110,8 +115,8 @@ (let (path (path-expand path)) (create-directory* (path-directory path)) (set! self.path path) - (set! self.servers (make-hash-table-eq)) - (set! self.roles (make-hash-table-eq)) + (set! self.servers (make-hash-table)) + (set! self.roles (make-hash-table)) (when (file-exists? path) (call-with-input-file path (lambda (file) @@ -128,7 +133,7 @@ (lambda (self id addrs roles) ;; is it an update? if so remove first (when (hash-key? self.servers id) - (registry::remove-server self id)) + {self.remove-server id}) ;; and now add it (hash-put! self.servers id (cons roles addrs)) (when roles diff --git a/src/std/actor-v18/server-identifier.ss b/src/std/actor-v18/server-identifier.ss new file mode 100644 index 000000000..2d2383259 --- /dev/null +++ b/src/std/actor-v18/server-identifier.ss @@ -0,0 +1,58 @@ +;;; -*- Gerbil -*- +;;; © vyzo +;;; actor server identifier +(import :std/error + ./path) +(export #t) + +(def current-actor-server + (make-parameter #f)) + +;; returns the actor server's identifier +(def (actor-server-identifier (srv (current-actor-server))) + (server-identifier (thread-specific srv))) + +(def (server-identifier->flat-string server-id) + (with ([id . domain] server-id) + (string-append + (symbol->string id) + "@" + (symbol->string domain)))) + +(def (string->server-identifier str) + (server-identifier (call-with-input-string str read))) + +(def (server-identifier id) + (cond + ((symbol? id) + (cons id (ensemble-domain))) + ((pair? id) + (if (and (symbol? (car id)) (symbol? (cdr id))) + id + (raise-bad-argument server-identifier "symbol or pair of symbols" id))) + (else + (raise-bad-argument server-identifier "symbol or pair of symbols" id)))) + +(def (server-identifier-id server-id) + (if (symbol? server-id) + server-id + (car server-id))) + +(def (server-identifier-domain server-id) + (if (pair? server-id) + (cdr server-id) + (ensemble-domain))) + +(def (server-identifier-at-domain server-id global-domain) + (cond + ((symbol? server-id) + (cons server-id global-domain)) + ((pair? server-id) + (with ([id . dom] server-id) + (let (dom-str (symbol->string dom)) + (if (string-prefix? "/" dom-str) + server-id + (let (global-domain-str (symbol->string global-domain)) + (cons id (string-append global-domain-str "/" dom-str))))))) + (else + (raise-bad-argument server-identifier "symbol or pair of symbols" server-id)))) diff --git a/src/std/actor-v18/server-test.ss b/src/std/actor-v18/server-test.ss index 8ba3fcd59..fd307597a 100644 --- a/src/std/actor-v18/server-test.ss +++ b/src/std/actor-v18/server-test.ss @@ -12,6 +12,7 @@ ./message ./proto ./server + ./server-identifier ./ensemble ./cookie ./admin @@ -97,16 +98,16 @@ (test-case "UNIX IPC" (let* ((tmp1 (make-temporary-file-name "actor-server")) (tmp2 (make-temporary-file-name "actor-server"))) - (test-ipc 'test-server1 [unix: (hostname) tmp1] - 'test-server2 [unix: (hostname) tmp2]) + (test-ipc '(test-server1 . /) [unix: (hostname) tmp1] + '(test-server2 . /) [unix: (hostname) tmp2]) (delete-file tmp1) (delete-file tmp2))) (test-case "TCP IPC" (let* ((addr1 (cons localhost4 33333)) (addr2 (cons localhost4 44444))) - (test-ipc 'test-server1 [tcp: addr1] - 'test-server2 [tcp: addr2]))) + (test-ipc '(test-server1 . /) [tcp: addr1] + '(test-server2 . /) [tcp: addr2]))) (test-case "implicit connection" (reset-thread!) @@ -125,7 +126,7 @@ (start-actor-server! cookie: cookie admin: #f addresses: [] - ensemble: (hash-eq (,srv1-id [addr1])))) + known-servers: (hash-eq (,srv1-id [addr1])))) (def srv2-id (actor-server-identifier srv2)) @@ -276,14 +277,16 @@ (def local-srv (start-actor-server! cookie: cookie admin: #f - ensemble: (hash (,remote-srv-id [remote-addr])))) + known-servers: (hash (,remote-srv-id [remote-addr])))) + (def local-srv-id + (actor-server-identifier local-srv)) ;; try to shutdown remote-srv without authorization first; this should fail (check-exception (remote-stop-server! remote-srv-id local-srv) (actor-error-with? "not authorized")) ;; now authorize administrative privileges and try again - (check (admin-authorize privk remote-srv-id (actor-server-identifier local-srv) local-srv) + (check (admin-authorize privk remote-srv-id local-srv-id local-srv) => (void)) (check (remote-stop-server! remote-srv-id local-srv) => (void)) (check (thread-join! remote-srv) => 'shutdown) diff --git a/src/std/actor-v18/server.ss b/src/std/actor-v18/server.ss index f0ee8d09d..6737714d5 100644 --- a/src/std/actor-v18/server.ss +++ b/src/std/actor-v18/server.ss @@ -1,8 +1,7 @@ ;;; -*- Gerbil -*- ;;; © vyzo ;;; actor server -(import :gerbil/gambit - :std/error +(import :std/error :std/sugar :std/iter :std/io @@ -16,27 +15,16 @@ (only-in :std/logger start-logger!) (only-in :std/srfi/1 reverse!) ./logger + ./path ./message ./proto ./cookie ./tls ./admin - ./connection) + ./connection + ./server-identifier) (export #t) -(def current-actor-server - (make-parameter #f)) - -;; actor references -;; - server is the actor-server identifier: a symbol identifying the server in your -;; ensemble -;; - id is the server-specific identifier of the actor; a symbol or a numeric id. -(defmessage reference (server id)) - -;; creates a proxy handle from a reference -(def (reference->handle ref (srv (current-actor-server))) - (make-handle srv ref)) - ;; starts the actor server ;; - identifier is the server identifier in your ensemble; a symbol. ;; defaults to a random identifier. @@ -52,21 +40,24 @@ ;; Addresses can be: ;; - [unix: hostname path]: a path for a unix domain socket in a host ;; - [tcp: addr]: an internet address; see :std/net/address -;; - TODO: TLS address +;; - [tls: addr]: an internet address secured with TLSv1.3 or later. ;; - known-servers is the set of known servers. ;; it is a hash table mapping server identifiers to list of addresses. ;; all servers in the ensemble must share the same cookie. ;; Returns the server thread. -(def (start-actor-server! identifier: (id (make-random-identifier)) - tls-context: (tls-context (get-actor-tls-context id)) - cookie: (cookie (get-actor-server-cookie)) - admin: (admin (get-admin-pubkey)) - auth: (auth #f) - addresses: (addrs []) - ensemble: (known-servers (default-known-servers))) +(def (start-actor-server! identifier: (id (make-random-identifier)) + roles: (roles []) + tls-context: (tls-context (get-actor-tls-context id)) + cookie: (cookie (get-actor-server-cookie)) + admin: (admin (get-admin-pubkey)) + auth: (auth #f) + addresses: (addrs []) + known-servers: (known-servers (default-known-servers)) + registry: (registry (default-registry-server)) + supervisor: (supervisor #f)) (start-logger!) (let* ((socks (actor-server-listen! addrs tls-context)) - (server (spawn/group 'actor-server actor-server id known-servers tls-context cookie admin auth socks))) + (server (spawn/group 'actor-server actor-server id roles supervisor registry known-servers tls-context cookie admin auth socks))) (current-actor-server server) (set! (thread-specific server) id) server)) @@ -76,10 +67,6 @@ (-> srv (!shutdown)) (thread-join! srv)) -;; returns the actor server's identifier -(def (actor-server-identifier (srv (current-actor-server))) - (thread-specific srv)) - ;; registers the current thread as an actor with the actor server ;; - name is a symbol; the actor's name in the server ;; returns a a reference to the actor in the server. @@ -98,7 +85,7 @@ ;; if the addresses are not specified, it is looked up in the registry. ;; Raises an error if the connection fails. (defcall-actor (connect-to-server! id (addrs #f) (srv (current-actor-server))) - (->> srv (!connect #f id addrs)) + (->> srv (!connect #f (server-identifier id) addrs)) error: "error connecting to server" id) ;; lists the server connections. @@ -107,25 +94,21 @@ (->> srv (!list-connections #f)) error: "error retrieving server connections") -;; Default registry addresses: unix /tmp/ensemble/registry -(def +default-registry-addresses+ - [[unix: (hostname) "/tmp/ensemble/registry"]]) +(def +default-registry-addresses+ []) -(def (default-registry-addresses) - +default-registry-addresses+) - -(def (set-default-registry-addresses! addrs) +(def (set-default-registry-addresses! (addrs : :list)) (set! +default-registry-addresses+ addrs)) -;; Default known servers: registry at default address -(def +default-known-servers+ - (hash-eq (registry (default-registry-addresses)))) +(def (default-registry-addresses) + (if (null? +default-registry-addresses+) + [(ensemble-server-unix-addr 'registry)] + +default-registry-addresses+)) -(def (default-known-servers) - +default-known-servers+) +(def (default-registry-server) + (server-identifier 'registry)) -(def (set-default-known-servers! servers) - (set! +default-known-servers+ servers)) +(def (default-known-servers) + (hash (,(default-registry-server) (default-registry-addresses)))) ;; Default server address cache ttl (def +server-address-cache-ttl+ 300) ; 5min @@ -147,8 +130,9 @@ ;;; Internals (def (make-random-identifier) - (string->symbol - (string-append "actor-server-" (hex-encode (subu8vector (sha256 (random-bytes 32)) 0 8))))) + (server-identifier + (string->symbol + (string-append "actor-server-" (hex-encode (subu8vector (sha256 (random-bytes 32)) 0 8)))))) (def (actor-server-listen! addrs tls-context) (let lp ((rest addrs) (socks [])) @@ -169,7 +153,12 @@ (if (or (not host) (equal? host (hostname))) (let* ((path (path-expand path)) (_ (create-directory* (path-directory path))) - (maybe-sock (with-catch values (cut unix-listen path)))) + (maybe-sock + (with-catch identity + (lambda () + (when (file-exists? path) + (delete-file path)) + (unix-listen path))))) (if (ServerSocket? maybe-sock) (lp rest (cons maybe-sock socks)) (fail! maybe-sock))) @@ -191,24 +180,26 @@ (else (reverse socks))))) -(def (actor-server id known-servers tls-context cookie admin auth socks) +(def (actor-server id roles supervisor registry known-servers tls-context cookie admin auth socks) + (def domain (ensemble-domain)) + (def id@domain (server-identifier id)) ;; next actor numeric id; 0 is self (def next-actor-id 1) ;; server address cache (def server-addrs - (let (server-addrs (make-hash-table-eq)) + (let (server-addrs (make-hash-table)) (for ([srv-id . addrs] (hash->list known-servers)) ;; user supplied addrs don't expire (hash-put! server-addrs srv-id (cons +inf.0 addrs))) server-addrs)) ;; server connections: server identifier -> [!connected ...] notification - (def conns (make-hash-table-eq)) + (def conns (make-hash-table)) ;; pending outbound conns; server id -> [contiuation ...] - (def pending-conns (make-hash-table-eq)) + (def pending-conns (make-hash-table)) ;; pending registry lookups; server id -> [continuation ...] - (def pending-lookups (make-hash-table-eq)) + (def pending-lookups (make-hash-table)) ;; pending lookup timeout nonces - (def pending-lookup-nonce (make-hash-table-eqv)) + (def pending-lookup-nonce (make-hash-table)) ;; actor table: actor-id [name or numeric identifier] -> actor thread (def actors (make-hash-table-eqv)) ;; reverse actor table: actor thread -> [actor-id ...] @@ -216,11 +207,11 @@ ;; server capability table: server-id -> [delegated|connected|preauth cap ...] (def capabilities (if auth - (list->hash-table-eq (hash-map (lambda (k v) (cons k (cons 'preauth v))) auth )) - (make-hash-table-eq))) + (list->hash-table (hash-map (lambda (k v) (cons k (cons 'preauth v))) auth)) + (make-hash-table))) ;; pending administrative server authorization: server-id -> [server-id cap challenge (def pending-admin-auth - (make-hash-table-eq)) + (make-hash-table)) ;; actor listener threads (def listeners (map (cut spawn/name 'actor-listener actor-listener (current-thread) <> cookie) socks)) @@ -247,7 +238,7 @@ actor-id)))) (def (update-server-addrs! srv-id addrs ttl) - (let (ttl (if (eq? srv-id 'registry) + (let (ttl (if (equal? srv-id registry) ;; don't expire the registry! +inf.0 ttl)) @@ -263,8 +254,11 @@ (hash-remove! server-addrs srv-id) (get-server-addrs-from-registry srv-id cont)) (cont (!ok (cdr entry)))))) - ((eq? srv-id 'registry) - (cont (!error "no registry server"))) + ((equal? registry srv-id) + (cont + ;;(!error "no registry server") + (!error (string-append "no registry server -- " (object->string (hash->list server-addrs)))) + )) (else (get-server-addrs-from-registry srv-id cont)))) @@ -276,7 +270,7 @@ (else (debugf "looking up server in registry: ~a" srv-id) (hash-put! pending-lookups srv-id [cont]) - (connect-to-server! 'registry + (connect-to-server! registry (lambda (result) (match result ((!ok notification) @@ -321,8 +315,6 @@ (def (connect-to-server! srv-id cont) (cond - ((not (symbol? srv-id)) - (cont (!error "bad server identifier"))) ((hash-get conns srv-id) => (lambda (notifications) (cont (!ok (car notifications))))) @@ -360,7 +352,7 @@ (for/fold (r []) ([srv-id . notifications] (hash->list conns)) (cons (cons srv-id (map !connected-addr notifications)) r)) (lambda (a b) - (symbol (lambda (state) (find (lambda (cap) (memq cap '(admin shutdown))) (cdr state)))) @@ -453,18 +446,24 @@ (if (or admin tls-context) (lambda (srv-id authorized-server-id) (cond - ((eq? srv-id authorized-server-id)) + ((equal? srv-id authorized-server-id)) + ((equal? srv-id supervisor)) ((hash-get capabilities srv-id) => (lambda (state) (memq 'admin (cdr state)))) (else #f))) (lambda (srv-id authorized-server-id) #t))) + (def (is-self? other-srv) + (or (eq? id other-srv) + (equal? id@domain other-srv))) + (def actor-capabilities (if (or admin tls-context) (lambda (srv-id) (cond ((hash-get capabilities srv-id) => cdr) + ((equal? srv-id supervisor) '(admin)) (else #f))) (lambda (srv-id) '(admin)))) @@ -536,10 +535,10 @@ (cond ((routed-message? msg) (using ((dest msg.dest :- handle) - (dest-ref dest.ref :- reference)) + (dest-ref dest.ref : reference)) (let* ((dest-srv-id dest-ref.server) - (dest-actor-id dest-ref.id)) - (if (or (not dest-srv-id) (eq? dest-srv-id id)) + (dest-actor-id dest-ref.actor)) + (if (or (not dest-srv-id) (is-self? dest-srv-id)) ;; local send (cond ((hash-get actors dest-actor-id) @@ -567,12 +566,12 @@ (begin (hash-put! actors name source) (hash-update! actor-threads source (cut cons name <>)) - (let (result (!ok (reference id name))) + (let (result (!ok (reference id@domain name))) (send-control-reply! msg result))))) ;; actor listing ((!list-actors srv) - (if (or (not srv) (eq? srv id)) + (if (or (not srv) (is-self? srv)) (let (result (!ok (get-actors))) (send-control-reply! msg result )) ;; remote list @@ -580,7 +579,7 @@ ;; connection listing ((!list-connections srv) - (if (or (not srv) (eq? srv id)) + (if (or (not srv) (is-self? srv)) (let (result (!ok (get-conns))) (send-control-reply! msg result)) ;; remote list @@ -588,9 +587,9 @@ ;; make a connection ((!connect srv other-srv addrs) - (if (or (not srv) (eq? srv id)) + (if (or (not srv) (is-self? srv)) (cond - ((eq? id other-srv) + ((is-self? other-srv) ;; don't self connect (let (result (!error "cannot connect to self")) (send-control-reply! msg result))) @@ -618,24 +617,27 @@ ;; ensemble control ((!ensemble-add-server srv-id addrs roles) - (unless (eq? srv-id id) + (unless (is-self? srv-id) ;; update our known address mapping (update-server-addrs! srv-id addrs +server-address-cache-ttl+)) ;; update the registry - (send-to-registry! actor-id msg)) + (unless (equal? srv-id registry) + (send-to-registry! actor-id msg))) ((!ensemble-remove-server srv-id) ;; update our known address mapping (hash-remove! server-addrs srv-id) ;; and our authorization table - (cond - ((hash-get capabilities srv-id) - => (lambda (state) - (when (eq? (car state) 'connected) - (hash-remove! capabilities srv-id))))) + (alet (state (hash-get capabilities srv-id)) + (when (eq? (car state) 'connected) + (hash-remove! capabilities srv-id) + ;; restore pre-authorized capabilities, if any + (alet (cap (hash-get auth srv-id)) + (hash-put! capabilities srv-id (cons 'preauth cap))))) (hash-remove! pending-admin-auth srv-id) ;; update the registry - (send-to-registry! actor-id msg)) + (unless (equal? srv-id registry) + (send-to-registry! actor-id msg))) ((!ensemble-lookup-server srv-id role) (send-to-registry! actor-id msg)) @@ -693,12 +695,12 @@ => (lambda (state) (with ([authorized-server-id cap bytes] state) (hash-remove! pending-admin-auth src-id) - (if (admin-auth-challenge-verify admin id authorized-server-id bytes sig) + (if (admin-auth-challenge-verify admin id@domain authorized-server-id bytes sig) (begin (infof "admin privileges authorized for ~a; capabilities: ~a" authorized-server-id cap) (update-capabilities! authorized-server-id cap - (if (eq? src-id authorized-server-id) + (if (equal? src-id authorized-server-id) 'connected 'delegated)) (send-remote-control-reply! src-id msg (!ok (void)))) @@ -720,20 +722,20 @@ ((!list-actors srv-id) (send-remote-control-reply! src-id msg - (if (or (not srv-id) (eq? srv-id id)) + (if (or (not srv-id) (is-self? srv-id)) (!ok (get-actors)) (!error "server id mismatch")))) ((!list-connections srv-id) (send-remote-control-reply! src-id msg - (if (or (not srv-id) (eq? srv-id id)) + (if (or (not srv-id) (is-self? srv-id)) (!ok (get-conns)) (!error "server id mismatch")))) ((!connect from-id to-id addrs) - (if (or (not from-id) (eq? from-id id)) + (if (or (not from-id) (is-self? from-id)) (cond - ((eq? id to-id) + ((is-self? to-id) ;; don't self connect (let (result (!error "cannot connect to self")) (send-remote-control-reply! src-id msg result))) @@ -788,7 +790,7 @@ (debugf "connected to server ~a at ~a [~a]" srv-id addr dir) (hash-update! conns srv-id (cut cons notification <>) []) (when cert - (let (cap (actor-tls-certificate-cap cert)) + (let (cap (actor-tls-certificate-capabilities cert)) (when cap (update-capabilities! srv-id cap 'connected)))) (dispatch-pending-conns! srv-id (!ok notification))) diff --git a/src/std/actor-v18/supervisor.ss b/src/std/actor-v18/supervisor.ss new file mode 100644 index 000000000..8ec4cf98d --- /dev/null +++ b/src/std/actor-v18/supervisor.ss @@ -0,0 +1,608 @@ +;;; -*- Gerbil -*- +;;; © vyzo +;;; ensemble supervisor actor +(import :std/sugar + :std/config + :std/iter + :std/sort + :std/misc/ports + :std/misc/symbol + ./logger + ./path + ./cookie + ./admin + ./message + ./proto + ./executor + ./server + ./server-identifier + ./ensemble + ./ensemble-config + ./ensemble-util) +(export #t) + +(defmessage !supervisor-list-servers (role domain)) +(defmessage !supervisor-start-server (role domain server-id config)) +(defmessage !supervisor-start-workers (role domain prefix count config)) +(defmessage !supervisor-stop-servers (role domain server-ids)) +(defmessage !supervisor-restart-servers (role domain server-ids)) +(defmessage !supervisor-get-server-log (server-id file)) +(defmessage !supervisor-get-server-config (server-id)) +(defmessage !supervisor-update-server (server-id config mode restart?)) +(defmessage !supervisor-get-config ()) +(defmessage !supervisor-update-config (config mode)) +(defmessage !supervisor-restart (services?)) +(defmessage !supervisor-invoke (actor message)) + +(defclass Server (pid config state service start-time) + final: #t) + +(def (server-config-roles cfg) + (cons (config-get! cfg role:) + (config-get cfg secondary-roles: []))) + +(def (server-role-includes? (server : Server) role) + (memq role (server-config-roles server.config))) + +(def supervisor-restarting-too-fast 1) + +(def (start-ensemble-supervisor! cfg (srv (current-actor-server))) + (spawn/name 'supervisor ensemble-supervisor cfg srv)) + +(def (ensemble-supervisor ensemble-cfg srv) + (register-actor! 'supervisor srv) + (infof "starting supervisor...") + + (let* ((root (path-normalize (current-directory))) + (root/env (path-expand "env" root)) + (root/log (path-expand "log" root))) + + (create-directory* root/env) + (create-directory* root/log) + + (let/cc exit + ;; server-identifier -> Server + (def servers (make-hash-table)) + (def servers-by-pid (make-hash-table-eqv)) + + (def @executor (handle srv (reference #f 'executor))) + (def @filesystem (handle srv (reference #f 'filesystem))) + + (def @registry-config #f) + + (def (registry-config) + (or @registry-config + (let* ((services-cfg (config-get! ensemble-cfg services:)) + (registry-cfg (config-get! services-cfg registry:)) + (registry-id (config-get! registry-cfg identifier:)) + (base-cfg (get-base-registry-config registry-id)) + (cfg (ensemble-server-config-merge base-cfg registry-cfg))) + (set! @registry-config cfg) + cfg))) + + (def (get-base-registry-config server-id) + [config: 'ensemble-server-v0 + ;; ensemble + domain: (cdr server-id) + identifier: server-id + supervisor: (actor-server-identifier) + cookie: (ensemble-cookie-path) + admin: (ensemble-admin-pubkey-path) + ;; execution + role: 'registry + ;; logging + log-level: 'INFO + log-dir: (get-server-log-dir server-id) + log-file: (get-server-log-file server-id "server.log") + ;; bindings + addresses: (get-server-default-addresses server-id) + auth: [[(actor-server-identifier) 'admin]] + ]) + + (def (registry-server-id) + (config-get! (registry-config) identifier:)) + + (def (registry-addrs) + (config-get! (registry-config) addresses:)) + + (def (get-base-server-config server-id) + (let* ((self (actor-server-identifier)) + (self-addrs (get-server-default-addresses self))) + [config: 'ensemble-server-v0 + ;; ensemble + domain: (cdr server-id) + identifier: server-id + supervisor: self + registry: (registry-server-id) + cookie: (ensemble-cookie-path) + admin: (ensemble-admin-pubkey-path) + policy: 'restart + ;; logging + log-level: 'INFO + log-dir: (get-server-log-dir server-id) + log-file: (get-server-log-file server-id "server.log") + ;; bindings + addresses: (get-server-default-addresses server-id) + known-servers: [[self self-addrs ...] + [(registry-server-id) (registry-addrs) ...]] + auth: [[self 'admin]] + ])) + + (def (get-role-server-config role server-id) + (alet* ((roles-alist (config-get ensemble-cfg roles:)) + (role-cfg (agetq role roles-alist))) + (let* ((exe (: (config-get! role-cfg exe:) :string)) + (prefix (:~ (config-get role-cfg prefix: []) (list-of? string?))) + (suffix (:~ (config-get role-cfg suffix: []) (list-of? string?))) + (args (append prefix [(object->string server-id) suffix ...])) + (policy (: (config-get role-cfg prolicy: 'restart) :symbol)) + (base-role-cfg + [config: 'ensemble-server-v0 + role: role + exe: exe + args: args + policy: policy + env: "default"]) + (role-server-cfg (config-get role-cfg server-config:))) + (ensemble-server-config-merge base-role-cfg role-server-cfg)))) + + (def (get-server-config role domain server-id) + (let* ((domain (ensemble-subdomain domain)) + (server-id (server-identifier-at-domain server-id domain)) + (base-cfg (get-base-server-config server-id)) + (role-cfg (get-role-server-config role server-id))) + (ensemble-server-config-merge base-cfg role-cfg))) + + (def (get-service-config role) + (alet* ((service-plist (config-get ensemble-cfg services:)) + (service-cfg (config-get service-plist role))) + (let* ((server-id (config-get! service-cfg identifier:)) + (server-id (server-identifier-at-domain server-id (ensemble-domain))) + (base-cfg (get-base-server-config server-id))) + (ensemble-server-config-merge base-cfg service-cfg)))) + + (def (get-server-log-dir server-id) + (ensemble-server-log-directory server-id root/log)) + + (def (get-server-log-file server-id file) + (path-expand file (get-server-log-dir server-id))) + + (def (get-server-default-addresses server-id) + [(ensemble-server-unix-addr server-id)]) + + (def (get-server-list role domain server-ids) + (if (null? server-ids) + (if domain + (let (server-list (get-server-list-by-domain domain)) + (if role + (filter (lambda (server-id) + (using (server (hash-ref servers server-id) :- Server) + (server-role-includes? server role))) + server-list) + server-list)) + (hash-keys servers)) + (filter (lambda (server-id) (hash-key? servers server-id)) + server-ids))) + + (def (get-server-list-by-domain domain) + (let* ((domain-str (symbol->string domain)) + (domain-prefix (string-append domain-str "/"))) + (filter (lambda (server-id) + (let (server-domain (cdr server-id)) + (or (eq? server-domain domain) + (let (server-domain-str (symbol->string server-domain)) + (string-prefix? domain-prefix server-domain-str))))) + (hash-keys servers)))) + + (def (write-ensemble-config!) + (call-with-output-file [path: (path-expand "config" (ensemble-base-path)) + create: 'maybe truncate: #t] + (cut write-config ensemble-cfg <>))) + + (def (write-server-config! server-id server-cfg) + (let* ((base-path + (cond + ((config-get server-cfg env:) + => (cut path-expand <> root/env)) + (else (gerbil-path)))) + (server-path (ensemble-server-path server-id #f base-path))) + (create-directory* server-path) + (call-with-output-file [path: (path-expand "config" server-path) + create: 'maybe truncate: #t ] + (cut write-config server-cfg <>)))) + + (def (server-id> @executor + (!executor-exec (config-get! server-cfg exe:) + (config-get! server-cfg args:) + (config-get server-cfg env:) + (config-get server-cfg envvars: []))) + ((!ok pid) + (infof "started server ~a: ~a" server-id pid) + (match (->> @executor (!executor-monitor pid)) + ((!ok) + (hash-put! servers server-id + (Server pid: pid + config: server-cfg + state: 'running + start-time: (##current-time-point))) + (hash-put! servers-by-pid pid server-id) + (unless (equal? server-id (registry-server-id)) + (ensemble-add-server! server-id + (config-get! server-cfg addresses:) + (server-config-roles server-cfg))) + (!ok (cons server-id pid))) + ((!error what) + (warnf "failed to monitor process ~a: ~a" pid what) + (!error "monitor failure")))) + (result result)))))) + + (def (start-workers! role domain prefix count config) + (infof "start-workers: ~a ~a ~a ~a" role domain prefix count) + (with-error-handler "start-workers" + (!ok + (for/fold (r []) (i (in-range count)) + (let (server-id (make-symbol prefix "-" i)) + (unless (hash-get servers server-id) + (match (start-server! role domain server-id config) + ((!ok value) + (cons value r)) + ((!error what) + (warnf "failed to start worker ~a: ~a" server-id what) + r)))))))) + + (def (stop-servers! role domain server-ids) + (with-error-handler "stop-servers" + (let (server-list (get-server-list role domain server-ids)) + (!ok + (if (null? server-list) + [] + (do-stop-servers! server-list)))))) + + (def (do-stop-servers! server-list) + (infof "stop-servers ~a" server-list) + (filter-map identity + (map thread-join! + (map (lambda (server-id) + (using (server (hash-ref servers server-id) :- Server) + (set! server.state 'stopping) + (spawn/name 'stop-server + (lambda () + (try + (match (->> @executor (!executor-stop server.pid)) + ((!ok pid) pid) + ((!error what) + (warnf "error stopping server ~a: ~a" server-id what) + #f)) + (catch (e) + (warnf "error stopping server ~a: ~a" server-id e) + #f)))))) + server-list)))) + + (def (restart-servers! role domain server-ids) + (with-error-handler "restart-servers" + (do-restart-servers! (get-server-list role domain server-ids)))) + + (def (do-restart-servers! server-list) + (infof "restart-servers ~a" server-list) + (!ok + (filter-map identity + (map (lambda (server-id) + (using (server (hash-ref servers server-id) :- Server) + (match (restart-server! server-id server) + ((!ok pid) + (cons server-id pid)) + ((!error what) + (warnf "failed to restart server ~a: ~a" server-id what) + #f)))) + server-list)))) + + (def (restart-server! server-id (server :- Server)) + (infof "restart-server ~a" server-id) + (with-error-handler "restart-server" + (set! server.state 'restart) + (write-server-config! server-id server.config) ; might have changed from update + (match (->> @executor (!executor-restart server.pid)) + ((!ok pid) + (hash-remove! servers-by-pid server.pid) + (hash-put! servers-by-pid pid server-id) + (set! server.pid pid) + (set! server.state 'running) + (!ok pid)) + (result result)))) + + (def (get-server-log server-id file) + (with-error-handler "get-server-log" + (let (log-file (get-server-log-file server-id (or file "server.log"))) + (if (file-exists? log-file) + (!ok (read-file-string log-file)) + (!error "no log"))))) + + (def (update-server! server-id config mode restart?) + (infof "update server ~a" server-id) + (with-error-handler "update-server" + (cond + ((hash-get servers server-id) + => (lambda ((server :- Server)) + (let (new-config + (if (eq? mode 'replace) + config + (ensemble-server-config-merge + (get-server-config + (config-get! server.config role:) + (config-get! server.config domain:) + server-id) + config))) + (set! server.config new-config) + (if restart? + (restart-server! server-id server) + (!ok (void)))))) + (else + (warnf "unkown server ~a" server-id) + (!error "unknown server"))))) + + (def (server-config server-id) + (with-error-handler "server-config" + (cond + ((hash-get servers server-id) + => (lambda ((server :- Server)) + (!ok server.config))) + (else + (!error "unknown server"))))) + + (def (update-config! config mode) + (let (new-config + (if (eq? mode 'replace) + config + (ensemble-config-merge ensemble-cfg config))) + (set! ensemble-cfg new-config) + (set! @registry-config #f) + (write-ensemble-config!) + (!ok (void)))) + + (def (get-config) + (!ok ensemble-cfg)) + + (def (notify! pid exit-code) + (with-error-log "notify" + (cond + ((hash-get servers-by-pid pid) + => (lambda (server-id) + (hash-remove! servers-by-pid pid) + (cond + ((hash-get servers server-id) + => (lambda ((server :- Server)) + (infof "server ~a has stopped" server-id) + (hash-remove! servers server-id) + (when (eq? server.state 'running) + (case (config-get! server.config policy:) + ((restart) + (if (> (##current-time-point) + (+ server.start-time supervisor-restarting-too-fast)) + (match (do-start-server! server-id server.config) + ((!ok p) + (infof "restarted server ~a as ~a" server-id (cdr p))) + ((!error what) + (warnf "error restarting server ~a: ~a" server-id what) + (unless (equal? server-id (registry-server-id)) + (with-catch void (cut ensemble-remove-server! server-id))))) + (begin + (warnf "server ~a is restarting too fast; stay down" server-id) + (unless (equal? server-id (registry-server-id)) + (with-catch void (cut ensemble-remove-server! server-id)))))))))) + (else + (debugf "notification for unknown server ~a" server-id))))) + (else + (debugf "notification for unknown pid ~a" pid))))) + + (def (shutdown!) + (do-shutdown!) + (-> srv (!shutdown))) + + (def (do-shutdown!) + (with-error-log "stop-servers" + (infof "stopping application servers") + (do-stop-servers! (get-application-servers)) + (wait-for-notifications (lambda () (> (length (get-application-servers)) 0)))) + (with-error-log "stop-services" + (infof "stopping services") + (do-stop-servers! (get-service-servers)) + (wait-for-notifications (lambda () (> (length (get-service-servers)) 0)))) + (with-error-log "stop-executor" + (->> @executor (!shutdown))) + (with-error-log "stop-filesystem" + (->> @filesystem (!shutdown)))) + + (def (wait-for-notifications wait?) + (while (wait?) + (let loop () + (<- ((!executor-notify pid exit-code) + (when (local-actor? @source 'executor) + (notify! pid exit-code) + (loop))) + (else (void)))))) + + (def (get-application-servers) + (for/fold (r []) (([id . server] (hash->list servers))) + (using (server :- Server) + (if server.service r (cons id r))))) + + (def (get-service-servers) + (for/fold (r []) (([id . server] (hash->list servers))) + (using (server :- Server) + (if server.service (cons id r) r)))) + + (def (restart! services?) + (with-error-handler "restart" + (do-restart-servers! + (if services? + (hash-keys servers) + (filter-map + (lambda (p) + (with ([server-id . server] p) + (using (server :- Server) + (and (not server.service) + server-id)))) + (hash->list servers)))))) + + (def (start-services!) + (for (role [registry: resolver: broadcast:]) + (alet (server-cfg (get-service-config role)) + (let (server-id (config-get! server-cfg identifier:)) + (infof "starting service ~a ~a" role server-id) + (match (do-start-server! server-id server-cfg) + ((!ok) + (using (server (hash-ref servers server-id) :- Server) + (set! server.service (keyword->symbol role)))) + ((!error what) + (errorf "error starting service ~a ~a" role what))) + (wait-for-actor! (reference server-id (keyword->symbol role))))))) + + (def (start-preloaded!) + (alet (preload-cfg (config-get ensemble-cfg preload:)) + (alet (servers-alist (config-get preload-cfg servers:)) + (for ([server-id . cfg] servers-alist) + (let* ((server-cfg + (get-server-config (config-get! cfg role:) + (config-get cfg domain: (ensemble-domain)) + server-id)) + (server-cfg + (ensemble-server-config-merge server-cfg + (config-get cfg server-config))) + (server-id + (config-get! server-cfg identifier:))) + (match (do-start-server! server-id server-cfg) + ((!ok) (void)) + ((!error what) + (errorf "error starting preloaded server ~a: ~a" server-id what)))))) + (alet (workers-alist (config-get preload-cfg workers:)) + (for ([domain . cfg] workers-alist) + (match (start-workers! (config-get! cfg role:) + domain + (config-get! cfg prefix:) + (config-get! cfg servers:) + (config-get cfg server-config:)) + ((!ok) (void)) + ((!error what) + (errorf "error starting preloaded workers for ~a: ~a" domain what))))))) + + (infof "starting supervisory services") + (try + (start-services!) + (catch (e) + (errorf "error starting supervisor services: ~a" e) + (exit 'error))) + + (infof "starting preloaded servers and workers") + (try + (start-preloaded!) + (catch (e) + (errorf "error starting preloaded servers: ~a" e) + (do-shutdown!) + (exit 'error))) + + (infof "supervisor running...") + + (while #t + (<- + ((!supervisor-list-servers role domain) + (with-authorization 'supervisor + (list-servers role domain))) + + ((!supervisor-start-server role domain server-id cfg) + (with-authorization 'supervisor + (start-server! role domain server-id cfg))) + + ((!supervisor-start-workers role domain prefix count config) + (with-authorization 'supervisor + (start-workers! role domain prefix count config))) + + ((!supervisor-stop-servers role domain server-ids) + (with-authorization 'supervisor + (stop-servers! role domain server-ids))) + + ((!supervisor-restart-servers role domain server-ids) + (with-authorization 'supervisor + (restart-servers! role domain server-ids))) + + ((!supervisor-get-server-log server-id file) + (with-authorization 'supervisor + (get-server-log server-id file))) + + ((!supervisor-update-server server-id config mode restart?) + (with-authorization 'supervisor + (update-server! server-id config mode restart?))) + + ((!supervisor-get-server-config server-id) + (with-authorization 'supervisor + (server-config server-id))) + + ((!supervisor-update-config config mode) + (with-authorization 'supervisor + (update-config! config mode))) + + ((!supervisor-get-config) + (with-authorization 'supervisor + (get-config))) + + ((!supervisor-restart services?) + (with-authorization 'supervisor + (restart! services?))) + + ((!supervisor-invoke actor msg) + (if (actor-authorized? @source 'supervisor) + (spawn/name 'invoke + (lambda () + (try + (--> (->> (reference->handle actor) msg + expiry: @expiry)) + (catch (e) + (debugf "actor invocation error: ~a: ~a" actor e) + (--> (!error (error-message e))))))) + (--> (!error "not authorized")))) + + ((!executor-notify pid exit-code) + (if (local-actor? @source 'executor) + (notify! pid exit-code) + (warnf "unexpected notification from ~a: ~a" @source pid))) + + ;; management protocol + ,(@shutdown + (infof "supervisor shutting down ...") + (shutdown!) + (exit 'shutdown)) + ,(@ping) + ,(@unexpected warnf)))))) diff --git a/src/std/actor-v18/test-util.ss b/src/std/actor-v18/test-util.ss index d60855a80..95c5fc628 100644 --- a/src/std/actor-v18/test-util.ss +++ b/src/std/actor-v18/test-util.ss @@ -11,10 +11,14 @@ ./message ./proto ./server + ./server-identifier ./ensemble ./cookie) (export #t) +(def (actor-server-id srv) + (car (actor-server-identifier srv))) + (def (reset-nonce!) (thread-local-set! 'nonce 0)) @@ -50,7 +54,7 @@ (member what (error-irritants exn))))) (def (sort-server-list lst) - (sort lst (lambda (a b) (symbol server-id) - (check (actor-tls-certificate-cap x509) => cap))) + (check (actor-tls-certificate-server-id x509) => server-id) + (check (actor-tls-certificate-capabilities x509) => cap))) (check-cert test-server1-id test-server1-cap) (check-cert test-server2-id test-server2-cap)) diff --git a/src/std/actor-v18/tls.ss b/src/std/actor-v18/tls.ss index 3be7f4100..90c2b1bb8 100644 --- a/src/std/actor-v18/tls.ss +++ b/src/std/actor-v18/tls.ss @@ -8,14 +8,18 @@ :std/misc/template :std/misc/process :std/misc/ports - ./path) + ./path + ./server-identifier) (export ensemble-tls-base-path ensemble-tls-server-path ensemble-tls-cafile get-actor-tls-context - actor-tls-certificate-id - actor-tls-certificate-cap + actor-tls-certificate-server-id + actor-tls-certificate-ensemble-domain + actor-tls-certificate-capabilities + actor-tls-certificate-host actor-tls-host + actor-tls-domain generate-actor-tls-root-ca! generate-actor-tls-sub-ca! generate-actor-tls-cafiles! @@ -49,29 +53,66 @@ (and (andmap file-exists? [cafile server-base chain.pem server.key]) (make-actor-tls-context caroot cafile capath chain.pem server.key)))) -(def (actor-tls-certificate-id x509) - (let (name (X509_get_subject_name x509)) - (and name - (string->symbol (car (string-split name #\.)))))) +(def (sans-uri-value->symbol pre value) + (and (string-prefix? pre value) + (string->symbol + (substring value (string-length pre) (string-length value))))) -(def (actor-tls-certificate-cap x509) - (let (san-uris (X509_get_san_uris x509)) - (and san-uris - (filter-map (lambda (x) - (and (string-prefix? "cap:" x) - (string->symbol - (substring x (string-length "cap:") (string-length x))))) - (string-split san-uris #\,))))) +(def (actor-tls-certificate-server-id x509) + (cons (actor-tls-certificate-ensemble-srv x509) + (actor-tls-certificate-ensemble-domain x509))) -(def (actor-tls-domain) - (read-file-string (path-expand "domain" (ensemble-tls-base-path)))) +(def (actor-tls-certificate-host x509) + (X509_get_subject_name x509)) -(def (actor-tls-host server-id) - (let (domain (actor-tls-domain)) - (string-append (symbol->string server-id) "." domain))) +(def (actor-tls-certificate-ensemble-srv x509) + (let (san-uris (X509_get_san_uris x509)) + (or (and san-uris + (ormap (cut sans-uri-value->symbol "srv:" <>) + (string-split san-uris #\,))) + (let (name (X509_get_subject_name x509)) + (and name + (string->symbol (car (string-split name #\.)))))))) + +(def (actor-tls-certificate-ensemble-domain x509) + (alet (san-uris (X509_get_san_uris x509)) + (or (ormap (cut sans-uri-value->symbol "dom:" <>) + (string-split san-uris #\,)) + '/))) + +(def (actor-tls-certificate-capabilities x509) + (alet (san-uris (X509_get_san_uris x509)) + (filter-map (cut sans-uri-value->symbol "cap:" <>) + (string-split san-uris #\,)))) + +(def +tls-domain+ #f) +(def (actor-tls-domain) + (cond + (+tls-domain+) + (else + (let (tls-domain + (call-with-input-file (path-expand "domain" (ensemble-tls-base-path)) + read-line)) + (set! +tls-domain+ tls-domain) + tls-domain)))) + +(def (actor-tls-host server-id + (~ensemble-domain (ensemble-domain)) + (tls-domain (actor-tls-domain))) + (if (pair? server-id) + (actor-tls-host (car server-id) (cdr server-id) tls-domain) + (let (server-id-str (symbol->string server-id)) + (string-join + [server-id-str + (reverse + (filter (? (not string-empty?)) + (string-split (symbol->string ~ensemble-domain) #\/))) + ... + tls-domain] + #\.)))) (def (generate-actor-tls-root-ca! root-ca-passphrase - domain: (domain "ensemble.local") + domain: (domain "ensemble.internal") country-name: (country-name "UN") organization-name: (organization-name "Mighty Gerbils") common-name: (common-name (string-append organization-name " Root CA"))) @@ -232,88 +273,96 @@ (def (generate-actor-tls-cert! sub-ca-passphrase server-id: server-id + ensemble-domain: (~ensemble-domain (ensemble-domain)) + tls-domain: (tls-domain (actor-tls-domain)) capabilities: (capabilities []) country-name: (country-name "UN") organization-name: (organization-name "Mighty Gerbils") location: (location "Internet")) - - (let* ((base-path (ensemble-tls-base-path)) - (root-ca-path (path-expand "root-ca" base-path)) - (sub-ca-path (path-expand "sub-ca" base-path)) - (sub-ca.conf (path-expand "sub-ca.conf" sub-ca-path)) - (server-path (ensemble-tls-server-path server-id)) - (server.conf (path-expand "server.conf" server-path)) - (server.key (path-expand "server.key" server-path)) - (server.csr (path-expand "server.csr" server-path)) - (server.crt (path-expand "server.crt" server-path)) - (chain.pem (path-expand "chain.pem" server-path)) - (domain (actor-tls-domain))) - - ;; sanity check: must have a sub-ca - (unless (file-exists? sub-ca-path) - (error "sub-ca does not exist" sub-ca-path)) - - (unless (file-exists? server-path) - (create-directory* server-path)) - - ;; server.conf - (displayln "... generate " server.conf) - (call-with-output-file server.conf - (cut write-template server.conf-template <> - server-id: server-id - capabilities: - (string-join - (map (lambda (i x) - (string-append "URI." (number->string i) " = cap:" x)) - (iota (length capabilities)) - (map symbol->string capabilities)) - "\n") - domain: domain - country-name: country-name - organization-name: organization-name - location: location)) - - ;; server.key - (unless (file-exists? server.key) - (displayln "... generate " server.key) + (parameterize ((ensemble-domain ~ensemble-domain)) + (let* ((base-path (ensemble-tls-base-path)) + (root-ca-path (path-expand "root-ca" base-path)) + (sub-ca-path (path-expand "sub-ca" base-path)) + (sub-ca.conf (path-expand "sub-ca.conf" sub-ca-path)) + (server-path (ensemble-tls-server-path server-id)) + (server.conf (path-expand "server.conf" server-path)) + (server.key (path-expand "server.key" server-path)) + (server.csr (path-expand "server.csr" server-path)) + (server.crt (path-expand "server.crt" server-path)) + (chain.pem (path-expand "chain.pem" server-path))) + + ;; sanity check: must have a sub-ca + (unless (file-exists? sub-ca-path) + (error "sub-ca does not exist" sub-ca-path)) + + (unless (file-exists? server-path) + (create-directory* server-path)) + + ;; server.conf + (displayln "... generate " server.conf) + (call-with-output-file server.conf + (cut write-template server.conf-template <> + server: + (string-append "URI.0 = srv:" + (symbol->string (server-identifier-id server-id)) + "\n") + ensemble-domain: + (string-append "URI.1 = dom:" + (symbol->string (server-identifier-domain server-id)) + "\n") + capabilities: + (string-join + (map (lambda (i x) + (string-append "URI." (number->string i) " = cap:" x)) + (iota (length capabilities) 2) + (map symbol->string capabilities)) + "\n") + server-host: (actor-tls-host server-id ~ensemble-domain tls-domain) + country-name: country-name + organization-name: organization-name + location: location)) + + ;; server.key + (unless (file-exists? server.key) + (displayln "... generate " server.key) + (invoke "openssl" + ["genpkey" + "-quiet" + "-algorithm" "RSA" + "-pkeyopt" "rsa_keygen_bits:4096" + "-out" server.key])) + + ;; server.csr + (when (file-exists? server.csr) + (rename-file server.csr + (string-append server.csr ".bak." (number->string (current-time-seconds))))) + (displayln "... generate " server.csr) (invoke "openssl" - ["genpkey" - "-quiet" - "-algorithm" "RSA" - "-pkeyopt" "rsa_keygen_bits:4096" - "-out" server.key])) - - ;; server.csr - (when (file-exists? server.csr) - (rename-file server.csr - (string-append server.csr ".bak." (number->string (current-time-seconds))))) - (displayln "... generate " server.csr) - (invoke "openssl" - ["req" "-new" - "-config" server.conf - "-key" server.key - "-out" server.csr]) - - ;; server.cert - (when (file-exists? server.crt) - (rename-file server.crt - (string-append server.crt ".bak." (number->string (current-time-seconds))))) - (displayln "... generate " server.crt) - (invoke "openssl" - ["ca" "-batch" "-notext" - "-config" sub-ca.conf - "-in" server.csr - "-out" server.crt - "-extensions" "actor_ext" - ;; TODO see above - "-passin" (string-append "pass:" sub-ca-passphrase)]) - - (displayln "... generate " chain.pem) - (call-with-output-file chain.pem - (lambda(output) - (for (f [server.crt (ensemble-tls-cafile)]) - (let (blob (read-file-u8vector f)) - (write-subu8vector blob 0 (u8vector-length blob) output))))))) + ["req" "-new" + "-config" server.conf + "-key" server.key + "-out" server.csr]) + + ;; server.cert + (when (file-exists? server.crt) + (rename-file server.crt + (string-append server.crt ".bak." (number->string (current-time-seconds))))) + (displayln "... generate " server.crt) + (invoke "openssl" + ["ca" "-batch" "-notext" + "-config" sub-ca.conf + "-in" server.csr + "-out" server.crt + "-extensions" "actor_ext" + ;; TODO see above + "-passin" (string-append "pass:" sub-ca-passphrase)]) + + (displayln "... generate " chain.pem) + (call-with-output-file chain.pem + (lambda(output) + (for (f [server.crt (ensemble-tls-cafile)]) + (let (blob (read-file-u8vector f)) + (write-subu8vector blob 0 (u8vector-length blob) output)))))))) (def root-ca.conf-template #< (lambda (pos) + (set-car! (cdr pos) val) + cfg)) + (else + (append cfg [key val])))) + +(defrule (config-push! cfg key val) + (set! cfg (config-set! cfg key val))) + +(def (config-check! cfg type) + (unless (eq? type (config-get! cfg config:)) + (error "configuration type mismatch" type cfg))) + +(def (write-config cfg (output (current-output-port)) pretty: (pretty? #f)) + (if pretty? + (for-each (lambda (x) (pretty-print x output)) cfg) + (for-each (lambda (x) (write x output) (newline output)) cfg))) + +(def (save-config! cfg path) + (let (base (path-directory path)) + (unless (or (string-empty? base) (file-exists? base)) + (create-directory* base))) + (call-with-output-file [path: path create: 'maybe truncate: #t] + (cut write-config cfg <>))) + +(def (read-config (input (current-input-port))) + (read-all input)) + +(def (load-config path type) + (let (cfg (call-with-input-file path read-config)) + (config-check! cfg type) + cfg)) + +(def (string->object str) + (call-with-input-string str read)) + +(def (string->integer str) + (let (input (string->number str)) + (unless (integer? input) + (error "expected integer" str)) + input)) diff --git a/src/std/io/interface.ss b/src/std/io/interface.ss index c93a24997..b84d6f7fb 100644 --- a/src/std/io/interface.ss +++ b/src/std/io/interface.ss @@ -16,7 +16,7 @@ ;; When `'start` is supplied, `position` must be positive. ;; When `'end` `'current` is supplied, `position` may be positive or negative ;; - from is one of 3 possible origins to seek about. Defaults to `'start`. - (seek (position : :fixnum) + (seek (position : :integer) (from :~ whence? := 'start)) => :void) diff --git a/src/std/io/port.ss b/src/std/io/port.ss index a6728f84c..8714f64d1 100644 --- a/src/std/io/port.ss +++ b/src/std/io/port.ss @@ -70,7 +70,7 @@ => close-input-port) (defport-method raw-binary-input-port (read port u8v start end need) - (let (rd (read-subu8vector u8v start end port need)) + (let (rd (read-subu8vector u8v start end port (if (fx> need 0) need 1))) (if (fx< rd need) (raise-premature-end-of-input raw-binary-input-port) rd))) diff --git a/src/std/logger.ss b/src/std/logger.ss index 0e32d7e43..8d7ef5ea0 100644 --- a/src/std/logger.ss +++ b/src/std/logger.ss @@ -5,11 +5,11 @@ (import :gerbil/gambit :std/error :std/sugar - :std/format - :std/srfi/19) + :std/format) (export start-logger! current-logger current-logger-options + current-log-directory make-logger-options logger-options? deflogger @@ -17,8 +17,7 @@ warnf infof debugf - verbosef - ) + verbosef) (def default-level 1) ; WARN (def verbose-level 4) @@ -48,6 +47,10 @@ (else (raise-bad-argument logger "log level: fixnum or symbol" level)))) +;; utility parameter to help systems find where to put their logs +(def current-log-directory + (make-parameter #f)) + ;; the current logger actor (def current-logger (make-parameter #f)) @@ -87,9 +90,9 @@ (let ((level (object->level level)) (threshold (object->level threshold))) (when (##fx<= level threshold) - (let ((now (current-date)) + (let ((now (##current-time-point)) (msg (if (null? args) fmt (apply format fmt (map exception->string args))))) - (thread-send logger (!log-message now level source msg)))))) + (thread-send logger (!log-message (current-thread) now level source msg)))))) (cond ((get-logger) @@ -138,7 +141,7 @@ (deflogger default) ;;; logger implementation -(defstruct !log-message (ts level source msg)) +(defstruct !log-message (thread ts level source msg)) (def (start-logger! (output (current-error-port))) (cond @@ -159,11 +162,13 @@ (def (logger-server port own-port?) (def (loop) (match (thread-receive) - ((!log-message ts level source msg) - (fprintf port "~a ~a ~a ~a~n" - (date->string ts "~4") + ((!log-message thread ts level source msg) + (fprintf port "~a ~a ~a [~a] ~a~n" + ts (level->symbolic level) - source msg) + source + (or (thread-name thread) "?") + msg) (force-output port) (loop)) ('shutdown diff --git a/src/std/misc/symbol.ss b/src/std/misc/symbol.ss index 7553b2ced..f3a5ec7d0 100644 --- a/src/std/misc/symbol.ss +++ b/src/std/misc/symbol.ss @@ -25,43 +25,17 @@ (lambda (x y) (cmp-e (cache-get x) (cache-get y)))) -;; comparison constructors -(def (compare-symbolstring stringstring string<=? mx?)) -(def (compare-symbol>=? (mx? #f)) - (compare-symbolic symbol->string string>=? mx?)) -(def (compare-symbol>? (mx? #f)) - (compare-symbolic symbol->string string>? mx?)) +(defrule (defsymbolic name T string-e string-cmp) + (def (name (x : T) (y : T)) + => :boolean + (string-cmp (string-e x) (string-e y)))) -;; globally cached implementations -(def symbol? - (compare-symbol>? #t)) -(def symbol>=? - (compare-symbol>=? #t)) +(defsymbolic symbolstring stringstring string=?) +(defsymbolic symbol>? :symbol symbol->string string>?) +(defsymbolic symbol>=? :symbol symbol->string string>=?) -;;; keywords -;; comparison constructors -(def (compare-keywordstring stringstring string<=? mx?)) -(def (compare-keyword>=? (mx? #f)) - (compare-symbolic keyword->string string>=? mx?)) -(def (compare-keyword>? (mx? #f)) - (compare-symbolic keyword->string string>? mx?)) - -;; globally cached implementations -(def keyword? - (compare-keyword>? #t)) -(def keyword>=? - (compare-keyword>=? #t)) +(defsymbolic keywordstring stringstring string=?) +(defsymbolic keyword>? :keyword keyword->string string>?) +(defsymbolic keyword>=? :keyword keyword->string string>=?) diff --git a/src/std/net/httpd/logger.ss b/src/std/net/httpd/logger.ss index c702e9bc8..60983560c 100644 --- a/src/std/net/httpd/logger.ss +++ b/src/std/net/httpd/logger.ss @@ -23,6 +23,8 @@ (if exists? (file-info-size (file-info path)) 0)) + (_ (unless exists? + (create-directory* (path-directory path)))) (output (open-file-writer path flags: (if exists? O_APPEND O_CREAT))) (writer diff --git a/src/tools/build.ss b/src/tools/build.ss index 374c731ca..ceca6514b 100755 --- a/src/tools/build.ss +++ b/src/tools/build.ss @@ -8,7 +8,22 @@ "gxtags" "gxpkg" "gxtest" + "gxensemble/opt" + "gxensemble/util" + "gxensemble/cmd" + "gxensemble/admin" + "gxensemble/env" + "gxensemble/control" + "gxensemble/config" + "gxensemble/ca" + "gxensemble/list" + "gxensemble/misc" + "gxensemble/repl" + "gxensemble/srv" "gxensemble" + "gxhttpd/opt" + "gxhttpd/config" + "gxhttpd/server" "gxhttpd") libdir: (path-expand "lib" (getenv "GERBIL_BUILD_PREFIX" (gerbil-home))) bindir: (path-expand "bin" (getenv "GERBIL_BUILD_PREFIX" (gerbil-home))) diff --git a/src/tools/env.ss b/src/tools/env.ss index 29be4e5a4..a305051df 100644 --- a/src/tools/env.ss +++ b/src/tools/env.ss @@ -1,16 +1,28 @@ ;;; -*- Gerbil -*- ;;; © vyzo ;;; common environment context for tools -(import (only-in :std/cli/getopt flag)) +(import (only-in :std/cli/getopt flag option)) (export #t) (def global-env-flag (flag 'global-env "-g" "--global-env" help: "use the user global env even in local package context")) +(def gerbil-path-option + (option 'gerbil-path "-G" "--gerbil-path" + help: "specifies the GERBIL_PATH for ensemble operations")) + (def (setup-local-env! opt) - (unless (hash-get opt 'global-env) - (setup-local-pkg-env! #f))) + (cond + ((hash-get opt 'gerbil-path) + => (lambda (path) + (let (path (path-expand path)) + (unless (file-exists? path) + (create-directory* path)) + (setenv "GERBIL_PATH" path) + (add-load-path! (path-expand "lib" path))))) + ((not (hash-get opt 'global-env)) + (setup-local-pkg-env! #f)))) (def (setup-local-pkg-env! create?) (unless (getenv "GERBIL_PATH" #f) diff --git a/src/tools/gxensemble.ss b/src/tools/gxensemble.ss index b64d081cf..af01670b4 100644 --- a/src/tools/gxensemble.ss +++ b/src/tools/gxensemble.ss @@ -1,23 +1,11 @@ ;;; -*- Gerbil -*- ;;; © vyzo ;;; actor ensemble management tool -(import :gerbil/expander - :std/actor - :std/actor-v18/cookie - :std/actor-v18/server - :std/actor-v18/loader - :std/actor-v18/registry - :std/actor-v18/path - :std/actor-v18/tls - :std/cli/getopt - :std/iter - :std/logger - :std/misc/ports - :std/misc/process - :std/os/hostname +(import :std/cli/getopt :std/sugar - :std/text/hex - ./env) + ./env + ./gxensemble/opt + ./gxensemble/cmd) (export main) (def (main . args) @@ -25,8 +13,12 @@ program: "gxensemble" help: "the Gerbil Actor Ensemble Manager" global-env-flag - run-cmd + gerbil-path-option + supervisor-cmd registry-cmd + run-cmd + env-cmd + control-cmd load-cmd eval-cmd repl-cmd @@ -36,333 +28,9 @@ admin-cmd list-cmd ca-cmd - package-cmd)) - -;;; -;;; getopt objects -;;; -(def logging-option - (option 'logging "--log" - value: string->symbol - default: 'INFO - help: "specifies the log level to run with")) - -(def logging-file-option - (option 'logging-file "--log-file" - default: #f - help: "specifies a log file instead of logging to stderr; if it is - then the log will be written into the ensemble server directory log")) - -(def listen-option - (option 'listen "-l" "--listen" - value: string->object - default: [] - help: "additional addresses to listen to; by default the server listens at unix /tmp/ensemble/")) - -(def announce-option - (option 'announce "-a" "--announce" - value: string->object - default: #f - help: "public addresses to announce to the registry; by default these are the listen addresses")) - -(def console-option - (option 'console "-c" "--console" - value: string->symbol - default: 'console - help: "console server id")) - -(def registry-option - (option 'registry "-r" "--registry" - value: string->object - default: #f - help: "additional registry addresses; by default the registry is reachable at unix /tmp/ensemble/registry")) - -(def roles-option - (option 'roles "--roles" - value: string->object - default: [] - help: "server role(s); a list of symbols")) - -(def library-prefix-option - (option 'library-prefix "--library-prefix" - value: string->object - default: '(gerbil scheme std) - help: "list of package prefixes to consider as library modules installed in the server")) - -(def server-id-argument - (argument 'server-id - help: "the server id" - value: string->symbol)) - -(def server-id-optional-argument - (optional-argument 'server-id - help: "the server id" - value: string->symbol)) - -(def actor-id-optional-argument - (optional-argument 'actor-id - help: "the actor's registered name" - value: string->symbol)) - -(def module-id-argument - (argument 'module-id - help: "the module id" - value: string->symbol)) - -(def server-id-or-role-argument - (argument 'server-or-role - help: "the server or role to lookup" - value: string->symbol)) - -(def authorized-server-id-argument - (argument 'authorized-server-id - help: "the server to authorize capabilities for" - value: string->symbol)) - -(def capabilities-optional-argument - (optional-argument 'capabilities - help: "the server capabilities to authorize" - value: string->object - default: '())) - -(def expr-argument - (argument 'expr - help: "the expression to eval" - value: string->object)) - -(def main-arguments - (rest-arguments 'main-args - help: "arguments for the module's main procedure")) - -(def library-flag - (flag 'library "--library" - help: "loads the code as library module; the library must be in the servers load path")) - -(def role-flag - (flag 'role "--role" - help: "lookup by role")) - -(def force-flag - (flag 'force "-f" "--force" - help: "force the action")) - -(def view-flag - (flag 'view "--view" - help: "inspect existing, don't generate")) - -(def (subcommand help) - (argument 'subcommand - help: help - value: string->symbol)) - -(def subcommand-list - (subcommand "what to do: servers|actors|connections")) - -(def subcommand-admin - (subcommand "what to do: cookie|creds|authorize|retract")) - -(def subcommand-ca - (subcommand "what to do: setup|cert")) + package-cmd + config-cmd)) -(def subcommand-arguments - (rest-arguments 'subcommand-args - help: "arguments for the subcommand")) - -(def run-cmd - (command 'run - logging-option - logging-file-option - listen-option - announce-option - registry-option - roles-option - server-id-argument - module-id-argument - main-arguments - help: "run a server in the ensemble")) - -(def registry-cmd - (command 'registry - logging-option - logging-file-option - listen-option - announce-option - help: "runs the ensemble registry")) - -(def load-cmd - (command 'load - console-option - force-flag - library-flag - registry-option - library-prefix-option - server-id-argument - module-id-argument - help: "loads code in a running server")) - -(def eval-cmd - (command 'eval - console-option - registry-option - server-id-argument - expr-argument - help: "evals code in a running server")) - -(def repl-cmd - (command 'repl - console-option - registry-option - library-prefix-option - server-id-argument - help: "provides a repl for a running server")) - -(def ping-cmd - (command 'ping - console-option - registry-option - server-id-argument - actor-id-optional-argument - help: "pings a server or actor in the server")) - -(def shutdown-cmd - (command 'shutdown - console-option - force-flag - registry-option - server-id-optional-argument - actor-id-optional-argument - help: "shuts down an actor, server, or the entire ensemble including the registry")) - -(def lookup-cmd - (command 'lookup - console-option - registry-option - role-flag - server-id-or-role-argument - help: "looks up a server by id or role")) - -(def list-cmd - (command 'list - subcommand-list - subcommand-arguments - help: "list server state")) - -(def admin-cmd - (command 'admin - subcommand-admin - subcommand-arguments - help: "ensemble administrative operations")) - -(def ca-cmd - (command 'ca - subcommand-ca - subcommand-arguments - help: "ensemble CA operations")) - -;; list subcommands -(def list-servers-cmd - (command 'servers - console-option - registry-option - help: "lists known servers")) - -(def list-actors-cmd - (command 'actors - console-option - registry-option - server-id-argument - help: "list actors registered in a server")) - -(def list-connections-cmd - (command 'connections - console-option - registry-option - server-id-argument - help: "list a server's connections")) - -;; admin subcommands -(def admin-authorize-cmd - (command 'authorize - console-option - registry-option - server-id-argument - authorized-server-id-argument - capabilities-optional-argument - help: "authorize capabilities for a server as an administrator")) - -(def admin-retract-cmd - (command 'retract - console-option - registry-option - server-id-argument - authorized-server-id-argument - help: "retract all capabilities granted to a server by an administrator")) - -(def admin-cookie-cmd - (command 'cookie - force-flag - view-flag - help: "generate or inspect the ensemble cookie")) - -(def admin-creds-cmd - (command 'creds - force-flag - view-flag - help: "generate or inspect ensemble administrator credentials")) - -;; ca subcommands -(def ca-domain-option - (option 'domain "--domain" - default: "ensemble.local" - help: "ensemble TLS domain")) - -(def ca-subject/C-option - (option 'subject/C "--subject/C" - default: "UN" - help: "ensemble TLS CA Country")) - -(def ca-subject/O-option - (option 'subject/O "--subject/O" - default: "Mighty Gerbils" - help: "ensemble TLS CA Organization")) - -(def ca-subject/L-option - (option 'subject/L "--subject/L" - default: "Internet" - help: "ensemble TLS certificate location")) - -(def ca-setup-cmd - (command 'setup - view-flag - ca-domain-option - ca-subject/C-option - ca-subject/O-option - ca-subject/L-option - help: "setup or inspect the ensemble CAs")) - -(def ca-cert-cmd - (command 'cert - force-flag - view-flag - ca-subject/C-option - ca-subject/O-option - ca-subject/L-option - server-id-argument - capabilities-optional-argument - help: "generate or inspect an actor server certificate")) - -(def package-output-option - (option 'output "-o" "--output" - default: "ensemble.tar.gz" - help: "output file for the server package")) - -(def package-cmd - (command 'package - package-output-option - server-id-argument - help: "package ensemble state to ship an actor server environment")) -;;; -;;; command implementation -;;; (defrule (defcommand-table name body ...) (def name (delay @@ -371,6 +39,9 @@ (defcommand-table main-commands (run do-run) (registry do-registry) + (supervisor do-supervisor) + (env do-env) + (control do-control) (load do-load) (eval do-eval) (repl do-repl) @@ -380,7 +51,34 @@ (shutdown do-shutdown) (admin do-admin) (ca do-ca) - (package do-package)) + (package do-package) + (config do-config)) + +(defcommand-table env-commands + (known-servers do-env-known-servers) + (domain do-env-domain) + (supervisor do-env-supervisor)) + +(defcommand-table control-commands + (list-servers do-control-list-servers) + (start-server do-control-start-server) + (start-workers do-control-start-workers) + (stop-server do-control-stop-server) + (restart-server do-control-restart-server) + (get-server-log do-control-get-server-log) + (update-server-config do-control-update-server-config) + (get-server-config do-control-get-server-config) + (update-ensemble-config do-control-update-config) + (get-ensemble-config do-control-get-config) + (shutdown do-control-shutdown) + (restart do-control-restart) + (upload do-control-upload) + (shell do-control-shell) + (list-processes do-control-list-processes) + (exec-process do-control-exec-process) + (kill-process do-control-kill-process) + (restart-process do-control-restart-process) + (get-process-output do-control-get-process-output)) (defcommand-table list-commands (servers do-list-servers) @@ -397,6 +95,12 @@ (setup do-ca-setup) (cert do-ca-cert)) +(defcommand-table config-commands + (ensemble do-config-ensemble) + (role do-config-role) + (server do-config-server) + (workers do-config-workers)) + (defrule (dispatch-command cmd opt commands) (let (table (force commands)) (cond @@ -415,568 +119,53 @@ program: name gopts ...)))) -(def (gxensemble-main cmd opt) - (setup-local-env! opt) - (dispatch-command cmd opt main-commands)) - (defcommand-nested do-admin admin-commands "gxensemble admin" admin-cookie-cmd admin-creds-cmd admin-authorize-cmd admin-retract-cmd) -(def (do-admin-cookie opt) - (if (hash-get opt 'view) - (let (cookie (get-actor-server-cookie)) - (displayln (hex-encode cookie))) - (generate-actor-server-cookie! force: (hash-get opt 'force)))) - -(def (do-admin-creds opt) - (if (hash-get opt 'view) - (let* ((pubk-path (default-admin-pubkey-path)) - (pubk-raw (read-file-u8vector pubk-path))) - (displayln (hex-encode pubk-raw))) - (let* ((passphrase (read-password prompt: "Enter passphrase: ")) - (again (read-password prompt: "Re-enter passphrase: "))) - (unless (equal? passphrase again) - (error "administrative passphrases don't match")) - (generate-admin-keypair! passphrase force: (hash-get opt 'force))))) - -(def (do-admin-authorize opt) - (start-actor-server-with-options! opt) - (let ((server-id (hash-ref opt 'server-id)) - (authorized-server-id (hash-ref opt 'authorized-server-id)) - (capabilities (hash-ref opt 'capabilities))) - (admin-authorize (get-privkey) server-id authorized-server-id - capabilities: capabilities))) - -(def (do-admin-retract opt) - (start-actor-server-with-options! opt) - (let ((server-id (hash-ref opt 'server-id)) - (authorized-server-id (hash-ref opt 'authorized-server-id))) - (maybe-authorize! server-id) - (admin-retract server-id authorized-server-id))) - -(def (do-lookup opt) - (start-actor-server-with-options! opt) - (let (what (hash-ref opt 'server-or-role)) - (display-result-list - (if (hash-get opt 'role) - (ensemble-lookup-servers/role what) - (ensemble-lookup-server what)))) - (stop-actor-server!)) +(defcommand-nested do-env env-commands "gxensemble env" + env-known-servers-cmd + env-domain-cmd + env-supervisor-cmd) + +(defcommand-nested do-control control-commands "gxensemble control" + control-list-servers-cmd + control-start-server-cmd + control-start-workers-cmd + control-stop-server-cmd + control-restart-server-cmd + control-get-server-log-cmd + control-get-server-config-cmd + control-update-server-config-cmd + control-get-config-cmd + control-update-config-cmd + control-list-processes-cmd + control-exec-process-cmd + control-kill-process-cmd + control-restart-process-cmd + control-get-process-output-cmd + control-upload-cmd + control-shell-cmd + control-shutdown-cmd + control-restart-cmd) + +(defcommand-nested do-config config-commands "gxensemble config" + config-ensemble-cmd + config-role-cmd + config-preload-server-cmd + config-preload-workers-cmd) (defcommand-nested do-list list-commands "gxensemble list" list-servers-cmd list-actors-cmd list-connections-cmd) -(def (do-list-connections opt) - (start-actor-server-with-options! opt) - (display-result-list - (remote-list-connections (hash-ref opt 'server-id))) - (stop-actor-server!)) - -(def (do-list-actors opt) - (start-actor-server-with-options! opt) - (display-result-list - (map reference-id (remote-list-actors (hash-ref opt 'server-id)))) - (stop-actor-server!)) - -(def (do-list-servers opt) - (start-actor-server-with-options! opt) - (display-result-list - (ensemble-list-servers)) - (stop-actor-server!)) - (defcommand-nested do-ca ca-commands "gxensemble ca" ca-setup-cmd ca-cert-cmd) -(def (do-ca-setup opt) - (cond - ((hash-get opt 'view) - (let* ((base-path (ensemble-tls-base-path)) - (ca-certificates (path-expand "ca-certificates" base-path))) - (for (subject '("root-ca" "sub-ca")) - (let (cert (path-expand (string-append subject ".crt") ca-certificates)) - (invoke "openssl" ["-text" "-in" cert]))))) - ((file-exists? (path-expand "caroot.pem" (ensemble-tls-base-path))) - (displayln "caroot.pem already exists")) - (else - (let* ((root-passphrase (read-password prompt: "Enter root CA passphrase: ")) - (again (read-password prompt: "Re-enter passphrase: "))) - (unless (equal? root-passphrase again) - (error "root CA passphrases don't match")) - (generate-actor-tls-root-ca! root-passphrase - domain: (hash-ref opt 'domain) - country-name: (hash-ref opt 'subject/C) - organization-name: (hash-ref opt 'subject/O)) - (let* ((sub-passphrase (read-password prompt: "Enter subordinate CA passphrase: ")) - (again (read-password prompt: "Re-enter passphrase: "))) - (unless (equal? sub-passphrase again) - (error "subordinate CA passphrases don't match")) - (generate-actor-tls-sub-ca! root-passphrase sub-passphrase - country-name: (hash-ref opt 'subject/C) - organization-name: (hash-ref opt 'subject/O)) - (generate-actor-tls-cafiles!) - (generate-actor-tls-cert! sub-passphrase - server-id: 'console - capabilities: '(admin) - country-name: (hash-ref opt 'subject/C) - organization-name: (hash-ref opt 'subject/O) - location: (hash-ref opt 'subject/L))))))) - -(def (do-ca-cert opt) - (let* ((server-id (hash-ref opt 'server-id)) - (base-path (ensemble-tls-server-path server-id))) - (cond - ((hash-get opt 'view) - (let (cert (path-expand "server.crt" base-path)) - (invoke "openssl" ["-text" "-in" cert]))) - ((and (not (hash-get opt 'force)) - (file-exists? (path-expand "server.crt" base-path))) - (displayln "server.crt already exists; use --force to force certificate generation")) - (else - (let (sub-passphrase (read-password prompt: "Enter subordinate CA passphprase: ")) - (generate-actor-tls-cert! sub-passphrase - server-id: server-id - capabilities: (hash-ref opt 'capabilities) - country-name: (hash-ref opt 'subject/C) - organization-name: (hash-ref opt 'subject/O) - location: (hash-ref opt 'subject/L))))))) - -(def (do-package opt) - (let* ((server-id (hash-ref opt 'server-id)) - (output (hash-ref opt 'output)) - (output (path-expand output (current-directory))) - (ensemble-base "ensemble/") - (ensemble-rebase - (lambda files - (map (cut string-append ensemble-base <>) files))) - (server-base - (string-append ensemble-base - "server/" - (symbol->string server-id) "/")) - (server-rebase - (lambda files - (map (cut string-append server-base <>) files)))) - - (current-directory (gerbil-path)) - (invoke "tar" - ["cavf" output - (ensemble-rebase - "cookie" - "admin.pub" - "tls/ca-certificates" - "tls/ca.pem" - "tls/caroot.pem" - "tls/domain") ... - (server-rebase "tls/chain.pem" "tls/server.key") ...]))) - -(def (do-shutdown opt) - (start-actor-server-with-options! opt) - (cond - ((hash-get opt 'server-id) - => (lambda (server-id) - (cond - ((hash-get opt 'actor-id) - => (lambda (actor-id) - (maybe-authorize! server-id) - (displayln "... shutting down " actor-id "@" server-id) - (stop-actor! (reference server-id actor-id)))) - (else - (maybe-authorize! server-id) - (displayln "... shutting down " server-id) - (remote-stop-server! server-id))))) - (else - (let/cc nope - (unless (hash-get opt 'force) - (displayln "This will shutdown every server in the ensemble, including the registry. Proceed? [y/n]") - (unless (memq (read) '(y yes Y YES)) - (nope (void)))) - - (let (servers (ensemble-list-servers)) - (for (server-id (map car servers)) - (maybe-authorize! server-id) - (displayln "... shutting down " server-id) - (with-catch void (cut remote-stop-server! server-id))) - ;; wait a second before shutting down the registry, so that servers can remove - ;; themselves. - (unless (null? servers) - (thread-sleep! 3))) - (displayln "... shutting down registry") - (maybe-authorize! 'registry) - (remote-stop-server! 'registry)))) - (stop-actor-server!)) - -(def (do-ping opt) - (start-actor-server-with-options! opt) - (let (server-id (hash-ref opt 'server-id)) - (cond - ((hash-get opt 'actor-id) - => (lambda (actor-id) - (displayln - (ping-actor (reference server-id actor-id))))) - (else - (displayln - (ping-server server-id)))))) - -(def (do-eval opt) - (start-actor-server-with-options! opt) - (let ((server-id (hash-ref opt 'server-id)) - (expr (hash-ref opt 'expr))) - (maybe-authorize! server-id) - (displayln - (remote-eval server-id expr))) - (stop-actor-server!)) - -(def (do-repl opt) - (start-actor-server-with-options! opt) - (let ((server-id (hash-ref opt 'server-id)) - (library-prefix (hash-ref opt 'library-prefix))) - (maybe-authorize! server-id) - (do-repl-for-server server-id library-prefix) - (stop-actor-server!))) - -(def (do-repl-for-server server-id library-prefix) - (def (display-help) - (displayln "Control commands: ") - (displayln " ,(import module-id) -- import a module locally for expansion") - (displayln " ,(load module-id) -- load the code and dependencies for a module") - (displayln " ,(load -f module-id) -- forcibly load a module ignoring dependencies") - (displayln " ,(load -l module-id) -- load a library module") - (displayln " ,(defined? id) -- checks if an identifier is defined at the server") - (displayln " ,(thread-state) -- display the thread state for the primordial thread group") - (displayln " ,(thread-state -g) -- display the thread state for all thread groups recursively") - (displayln " ,(thread-state sn) -- display the thread state for a thread or group identified by its serial number") - (displayln " ,(thread-backtrace sn) -- display a backtrace for a thread identified by its serial number") - (displayln " ,(shutdown) -- shut down the server and exit the repl") - (displayln " ,q ,quit -- quit the repl") - (displayln " ,h ,help -- display this help message")) - - (def module-registry #f) - (def loaded-object-files - (make-hash-table)) - - (def (library-module-loader module-id) - (string-append (module-id->string module-id) "__rt")) - - (def (library-module-rt module-id) - (string-append (module-id->string module-id) "__0")) - - (def (library-module-loaded? module-id) - (or (hash-get module-registry (library-module-loader module-id)) - (hash-get module-registry (library-module-rt module-id)))) - - (def (object-file-loaded? object-file) - (hash-get loaded-object-files object-file)) - - (def (bind-module-exports! ctx) - (let lp ((rest (module-context-export ctx))) - (match rest - ([hd . rest] - (cond - ((module-export? hd) - (when (= (module-export-phi hd) 0) - (core-bind-import! (core-module-export->import hd))) - (lp rest)) - ((export-set? hd) - (lp (foldl cons rest (export-set-exports hd)))) - (else (lp rest)))) - (else (void))))) - - (def (load-object-file object-file) - (unless (object-file-loaded? object-file) - (displayln "loading code object file " object-file) - (hash-put! loaded-object-files object-file #t) - (remote-load-code server-id object-file))) - - (def (load-library-module lib) - (unless (library-module-loaded? lib) - (displayln "loading library module " lib) - (remote-load-library-module server-id lib) - (set! module-registry - (remote-eval server-id '(current-module-registry))))) - - (def (eval-expr expr) - (let* ((expanded-expr (core-expand expr)) - (compiled-expr (core-compile-top-syntax expanded-expr)) - (raw-compiled-expr (__compile compiled-expr)) - (result (remote-eval server-id raw-compiled-expr))) - (unless (void? result) - (if (##values? result) - (display-result-list (values->list result)) - (display-result result))))) - - (gerbil-load-expander!) - (connect-to-server! server-id) - (remote-eval server-id '(##expand-source-set! identity)) - (set! module-registry - (remote-eval server-id '(current-module-registry))) - (let/cc exit - (parameterize ((current-expander-context (make-top-context))) - ;; prepare the context - (eval '(import :gerbil/core)) - (eval '(import :gerbil/gambit)) - ;; and go! - (while #t - (display server-id) - (display "> ") - (try - (match (read) - ((? eof-object?) - (exit (void))) - (['unquote command] - (match command - (['import module-id] - (bind-module-exports! (import-module module-id))) - (['load module-id] - (let ((values object-files library-modules) - (get-module-objects module-id library-prefix)) - (when module-registry - (for (lib library-modules) - (load-library-module lib))) - (for (object-file object-files) - (load-object-file object-file)))) - (['load '-f module-id] - (let (object-file (find-object-file (import-module module-id))) - (if object-file - (load-object-file object-file) - (displayln "cannot find object file")))) - (['load -l module-id] - (if module-registry - (load-library-module module-id) - (displayln "server does not support library loading"))) - (['defined? symbol] - (eval-expr - `(with-exception-catcher (lambda (_) #f) (lambda () ,symbol #t)))) - (['thread-state] - (eval-expr - '(call-with-output-string "" - (lambda (p) (##cmd-st (thread-thread-group ##primordial-thread) p))))) - (['thread-state '-g] - (eval-expr - `(call-with-output-string "" - (lambda (p) - (let thread-state ((tg (thread-thread-group ##primordial-thread))) - (display "thread group: " p) - (display tg p) - (newline p) - (##cmd-st tg p) - (for-each thread-state (thread-group->thread-group-list tg))))))) - (['thread-state (? integer? sn)] - (eval-expr - `(call-with-output-string "" - (lambda (p) - (let (thread-or-group (serial-number->object ,sn)) - (if (or (thread? thread-or-group) - (thread-group? thread-or-group)) - (##cmd-st thread-or-group p))))))) - (['thread-backtrace (? integer? sn)] - (eval-expr - `(call-with-output-string "" - (lambda (p) - (let (thread (serial-number->object ,sn)) - (if (thread? thread) - (display-continuation-backtrace - (##thread-continuation-capture thread) - p #t))))))) - (['shutdown] - (remote-stop-server! server-id) - (exit (void))) - ((or 'h 'help) - (display-help)) - ((or 'q 'quit) - (exit (void))) - (else - (displayln "unknown control command " command) - (display-help)))) - (expr (eval-expr expr))) - (catch (exn) - (display "*** ERROR ") - (display-exception exn))))))) - -(extern namespace: #f __compile) - -(def (do-load opt) - (gerbil-load-expander!) - (if (hash-get opt 'library) - (do-load-library opt) - (do-load-code opt))) - -(def (do-load-library opt) - (let ((module-id (hash-ref opt 'module-id)) - (server-id (hash-ref opt 'server-id))) - (start-actor-server-with-options! opt) - (maybe-authorize! server-id) - (displayln "... loading library module " module-id) - (displayln - (remote-load-library-module server-id module-id)) - (stop-actor-server!))) - -(def (do-load-code opt) - (let ((module-id (hash-ref opt 'module-id)) - (library-prefix (hash-ref opt 'library-prefix)) - (server-id (hash-ref opt 'server-id))) - (let ((values object-files library-modules) - (get-module-objects module-id library-prefix)) - (start-actor-server-with-options! opt) - (maybe-authorize! server-id) - ;; when forcing, we don't load the library modules - ;; useful for static executables - (unless (hash-get opt 'force) - (for (lib library-modules) - (displayln "... loading library module " lib) - (displayln - (remote-load-library-module server-id lib)))) - (for (object-file object-files) - (displayln "... loading code object file " object-file) - (displayln - (remote-load-code server-id object-file))) - (stop-actor-server!)))) - -(extern namespace: #f find-library-module) - -(def (find-object-file ctx-or-id) - (if (module-context? ctx-or-id) - (find-object-file (expander-context-id ctx-or-id)) - (find-library-module (string-append (module-id->string ctx-or-id) "__0")))) - -(def (module-id->string module-id) - (let (mod-str (symbol->string module-id)) - (if (string-prefix? ":" mod-str) - (substring mod-str 1 (string-length mod-str)) - mod-str))) - -(def (get-module-objects module-id library-prefix) - (def (fold-modules r q) - (for/fold (r r) (in q) - (cond - ((module-context? in) - (fold-modules (cons in r) (cons (core-context-prelude in) (module-context-import in)))) - ((prelude-context? in) - (cons in r)) - ((module-import? in) - (if (= (module-import-phi in) 0) - (fold-modules r [(module-import-source in)]) - r)) - ((import-set? in) - (if (= (import-set-phi in) 0) - (fold-modules r [(import-set-source in)]) - r)) - (else r)))) - - (let* ((library-prefix-str (map symbol->string library-prefix)) - (ctx (import-module module-id)) - (modules (fold-modules [] [ctx]))) - (let lp ((rest modules) (to-load []) (libraries [])) - (match rest - ([ctx . rest] - (let* ((ctx-id (expander-context-id ctx)) - (ctx-id-str (and ctx-id (symbol->string ctx-id)))) - (cond - ((not ctx-id) - (lp rest to-load libraries)) - ((string-prefix? "gerbil/" ctx-id-str) - (lp rest to-load libraries)) - ((find (cut string-prefix? <> ctx-id-str) library-prefix-str) - (lp rest to-load libraries)) - (else - (if (member ctx-id to-load) - (lp rest to-load libraries) - (lp rest (cons ctx-id to-load) libraries)))))) - (else - (let lp ((rest to-load) (object-files [])) - (match rest - ([ctx-id . rest] - (lp rest (cons (find-object-file ctx-id) object-files))) - (else - (values object-files (reverse libraries)))))))))) - -(def (do-registry opt) - (call-with-ensemble-server 'registry - (cut start-ensemble-registry!) - log-level: (hash-ref opt 'logging) - log-file: (hash-ref opt 'logging-file) - listen: (hash-ref opt 'listen) - announce: (hash-ref opt 'announce) - registry: #f - roles: #f - cookie: (get-actor-server-cookie))) - -(def (do-run opt) - (let ((module-main (get-module-main (hash-ref opt 'module-id))) - (main-args (hash-ref opt 'main-args))) - (call-with-ensemble-server (hash-ref opt 'server-id) - (cut apply module-main main-args) - log-level: (hash-ref opt 'logging) - log-file: (hash-ref opt 'logging-file) - listen: (hash-ref opt 'listen) - announce: (hash-ref opt 'announce) - registry: (hash-ref opt 'registry) - roles: (hash-ref opt 'roles) - cookie: (get-actor-server-cookie)))) - -(def (get-module-main module-id) - (def (runtime-export? exported) - (= (module-export-phi exported) 0)) - - (gerbil-load-expander!) - (let (ctx (import-module module-id #f #t)) - (let/cc return - (for (exported (module-context-export ctx)) - (when (and (runtime-export? exported) - (eq? (module-export-name exported) 'main)) - (return (eval (binding-id (core-resolve-module-export exported)))))) - (error "module does not export main" module-id)))) - -;;; utilities -(def (display-result-list lst) - (for (result lst) - (display-result result))) - -(def (display-result result) - (write result) - (newline)) - -(def (string->object str) - (call-with-input-string str read)) - -(def (start-actor-server-with-options! opt) - (cond - ((hash-get opt 'logging) - => current-logger-options)) - (let* ((known-servers - (cond - ((hash-get opt 'registry) - => (lambda (addrs) - (hash-eq (registry (append (default-registry-addresses) addrs))))) - (else - (hash-eq (registry (default-registry-addresses)))))) - (server-id - (hash-ref opt 'console)) - (listen-addrs - (hash-ref opt 'listen [])) - (cookie (get-actor-server-cookie))) - (start-actor-server! identifier: server-id - cookie: cookie - addresses: listen-addrs - ensemble: known-servers))) - -(def +admin-privkey+ #f) -(def (get-privkey) - (or +admin-privkey+ - (if (file-exists? (default-admin-privkey-path)) - (let* ((passphrase (read-password prompt: "Enter administrative passphrase: ")) - (privk (get-admin-privkey passphrase))) - (set! +admin-privkey+ privk) - privk) - (error "no administrative private key")))) - -(def (maybe-authorize! server-id) - (let (addr (connect-to-server! server-id)) - (unless (eq? tls: (car addr)) - (when (file-exists? (default-admin-privkey-path)) - (let (privk (get-privkey)) - (admin-authorize +admin-privkey+ server-id (actor-server-identifier))))))) +(def (gxensemble-main cmd opt) + (setup-local-env! opt) + (dispatch-command cmd opt main-commands)) diff --git a/src/tools/gxensemble/admin.ss b/src/tools/gxensemble/admin.ss new file mode 100644 index 000000000..5461b254e --- /dev/null +++ b/src/tools/gxensemble/admin.ss @@ -0,0 +1,41 @@ +;;; -*- Gerbil -*- +;;; © vyzo +;;; actor ensemble management tool +(import :std/actor + :std/text/hex + :std/misc/ports + ./util) +(export #t) + +;;; gerbil ensemble admin +(def (do-admin-cookie opt) + (if (hash-get opt 'view) + (let (cookie (get-actor-server-cookie)) + (displayln (hex-encode cookie))) + (generate-ensemble-cookie! force: (hash-get opt 'force)))) + +(def (do-admin-creds opt) + (if (hash-get opt 'view) + (let* ((pubk-path (ensemble-admin-pubkey-path)) + (pubk-raw (read-file-u8vector pubk-path))) + (displayln (hex-encode pubk-raw))) + (let* ((passphrase (read-password prompt: "Enter passphrase: ")) + (again (read-password prompt: "Re-enter passphrase: "))) + (unless (equal? passphrase again) + (error "administrative passphrases don't match")) + (generate-admin-keypair! passphrase force: (hash-get opt 'force))))) + +(def (do-admin-authorize opt) + (start-actor-server-with-options! opt) + (let ((server-id (hash-ref opt 'server-id)) + (authorized-server-id (hash-ref opt 'authorized-server-id)) + (capabilities (hash-ref opt 'capabilities))) + (admin-authorize (get-privkey) server-id authorized-server-id + capabilities: capabilities))) + +(def (do-admin-retract opt) + (start-actor-server-with-options! opt) + (let ((server-id (hash-ref opt 'server-id)) + (authorized-server-id (hash-ref opt 'authorized-server-id))) + (maybe-authorize! server-id) + (admin-retract server-id authorized-server-id))) diff --git a/src/tools/gxensemble/ca.ss b/src/tools/gxensemble/ca.ss new file mode 100644 index 000000000..9990a90e3 --- /dev/null +++ b/src/tools/gxensemble/ca.ss @@ -0,0 +1,66 @@ +;;; -*- Gerbil -*- +;;; © vyzo +;;; actor ensemble management tool +(import :std/actor + :std/actor-v18/tls + :std/misc/process + :std/misc/ports + :std/iter + ./util) +(export #t) + +;;; gerbil ensemble ca +(def (do-ca-setup opt) + (cond + ((hash-get opt 'view) + (let* ((base-path (ensemble-tls-base-path)) + (ca-certificates (path-expand "ca-certificates" base-path))) + (for (subject '("root-ca" "sub-ca")) + (let (cert (path-expand (string-append subject ".crt") ca-certificates)) + (invoke "openssl" ["-text" "-in" cert]))))) + ((file-exists? (path-expand "caroot.pem" (ensemble-tls-base-path))) + (displayln "caroot.pem already exists")) + (else + (let* ((root-passphrase (read-password prompt: "Enter root CA passphrase: ")) + (again (read-password prompt: "Re-enter passphrase: "))) + (unless (equal? root-passphrase again) + (error "root CA passphrases don't match")) + (generate-actor-tls-root-ca! root-passphrase + domain: (hash-ref opt 'domain) + country-name: (hash-ref opt 'subject/C) + organization-name: (hash-ref opt 'subject/O)) + (let* ((sub-passphrase (read-password prompt: "Enter subordinate CA passphrase: ")) + (again (read-password prompt: "Re-enter passphrase: "))) + (unless (equal? sub-passphrase again) + (error "subordinate CA passphrases don't match")) + (generate-actor-tls-sub-ca! root-passphrase sub-passphrase + country-name: (hash-ref opt 'subject/C) + organization-name: (hash-ref opt 'subject/O)) + (generate-actor-tls-cafiles!) + (generate-actor-tls-cert! sub-passphrase + server-id: '(console . /) + capabilities: '(admin) + country-name: (hash-ref opt 'subject/C) + organization-name: (hash-ref opt 'subject/O) + location: (hash-ref opt 'subject/L))))))) + +(def (do-ca-cert opt) + (parameterize ((ensemble-domain (get-ensemble-domain opt))) + (let* ((server-id (hash-ref opt 'server-id)) + (base-path (ensemble-tls-server-path server-id))) + (cond + ((hash-get opt 'view) + (let (cert (path-expand "server.crt" base-path)) + (invoke "openssl" ["-text" "-in" cert]))) + ((and (not (hash-get opt 'force)) + (file-exists? (path-expand "server.crt" base-path))) + (displayln "server.crt already exists; use --force to force certificate generation")) + (else + (let (sub-passphrase (read-password prompt: "Enter subordinate CA passphprase: ")) + (generate-actor-tls-cert! sub-passphrase + server-id: server-id + ensemble-domain: (ensemble-domain) + capabilities: (hash-ref opt 'capabilities) + country-name: (hash-ref opt 'subject/C) + organization-name: (hash-ref opt 'subject/O) + location: (hash-ref opt 'subject/L)))))))) diff --git a/src/tools/gxensemble/cmd.ss b/src/tools/gxensemble/cmd.ss new file mode 100644 index 000000000..d61a013ff --- /dev/null +++ b/src/tools/gxensemble/cmd.ss @@ -0,0 +1,23 @@ +;;; -*- Gerbil -*- +;;; © vyzo +;;; actor ensemble management tool +(import ./util + ./admin + ./env + ./control + ./config + ./ca + ./list + ./misc + ./repl + ./srv) +(export + (import: ./admin + ./env + ./control + ./config + ./ca + ./list + ./misc + ./repl + ./srv)) diff --git a/src/tools/gxensemble/config.ss b/src/tools/gxensemble/config.ss new file mode 100644 index 000000000..757ad7f26 --- /dev/null +++ b/src/tools/gxensemble/config.ss @@ -0,0 +1,73 @@ +;;; -*- Gerbil -*- +;;; © vyzo +;;; actor ensemble management tool +(import :std/config + :std/actor + :std/sugar + ./util) +(export #t) + +;;; ensemble configuration commands +(def (do-config-ensemble opt) + (let-hash opt + (let (cfg (get-ensemble-config)) + (if .?view + (write-config cfg pretty: .?pretty) + (begin + (cond (.?ensemble-domain => (cut config-push! cfg domain: <>))) + (cond (.?ensemble-root => (cut config-push! cfg root: <>))) + (cond (.?ensemble-public-address => (cut config-push! cfg public-address: <>))) + (save-config! cfg (ensemble-config-path))))))) + +(def (do-config-role opt) + (let-hash opt + (let* ((cfg (get-ensemble-config)) + (role (or .?role (error "role must be specified"))) + (role-alist (config-get cfg roles: [])) + (role-cfg (agetq role role-alist [])) + (role-server-cfg + (config-get role-cfg server-config: (empty-ensemble-server-config)))) + (cond (.?exe => (cut config-push! role-cfg exe: <>))) + (cond (.?prefix => (cut config-push! role-cfg prefix: <>))) + (cond (.?suffix => (cut config-push! role-cfg suffix: <>))) + (cond (.?policy => (cut config-push! role-cfg policy: <>))) + (cond (.?env => (cut config-push! role-server-cfg env: <>))) + (cond (.?envvars => (cut config-push! role-server-cfg envvars: <>))) + (cond (.?known-servers => (cut config-push! role-server-cfg known-server: <>))) + (when .?application + (let (default-config-path + (path-expand (symbol->string .application) + (path-expand "config" + (gerbil-path)))) + (unless (or .?config (file-exists? default-config-path)) + (error "application config must be specified")) + (let* ((config-path (or .config default-config-path)) + (app-config (call-with-input-file config-path read-config)) + (app-alist (config-get role-server-cfg application: []))) + (cond + ((assq .application app-alist) + => (lambda (p) (set-cdr! p app-config))) + (else + (set! app-alist [[.application app-config ...] app-alist ...]))) + (config-push! role-server-cfg application: app-alist)))) + (config-push! role-cfg server-config: role-server-cfg) + (cond + ((assq role role-alist) + => (lambda (p) (set-cdr! p role-cfg))) + (else + (set! role-alist [[role role-cfg ...] role-alist ...]))) + (config-push! cfg roles: role-alist) + (save-config! cfg (ensemble-config-path))))) + +(def (do-config-server opt) + (error "TODO: configure preloaded server")) + +(def (do-config-workers opt) + (error "TODO: configure preloaded workers")) + + +(def (get-ensemble-config) + (let (path (ensemble-config-path)) + (if (file-exists? path) + (load-ensemble-config-file path) + (empty-ensemble-config)))) diff --git a/src/tools/gxensemble/control.ss b/src/tools/gxensemble/control.ss new file mode 100644 index 000000000..ff2805008 --- /dev/null +++ b/src/tools/gxensemble/control.ss @@ -0,0 +1,314 @@ +;;; -*- Gerbil -*- +;;; © vyzo +;;; actor ensemble management tool +(import :std/actor + :std/config + :std/os/temporaries + :std/misc/process + ./util) +(export #t) + +;;; gerbil ensemble control +(def (do-control-list-servers opt) + (parameterize ((ensemble-domain (get-ensemble-domain opt))) + (let ((supervisor (or (hash-get opt 'supervisor) (ensemble-domain-supervisor))) + (domain (hash-get opt 'domain)) + (role (hash-get opt 'role))) + (call-with-console-server opt + (lambda (srv) + (let (result (ensemble-supervisor-list-servers + supervisor: supervisor + domain: domain + role: role + actor-server: srv)) + (write-result opt result))))))) + +(def (do-control-start-server opt) + (parameterize ((ensemble-domain (get-ensemble-domain opt))) + (let* ((supervisor (or (hash-get opt 'supervisor) (ensemble-domain-supervisor))) + (domain (or (hash-get opt 'domain) (ensemble-domain))) + (role (hash-ref opt 'role)) + (server-id (hash-ref opt 'server-id)) + (config-path (hash-get opt 'config)) + (config (and config-path (call-with-input-file config-path read-config)))) + (when config + (check-ensemble-server-config! config)) + (call-with-console-server opt + (lambda (srv) + (let (result (ensemble-supervisor-start-server! + supervisor: supervisor + role: role + server-id: server-id + domain: domain + config: config + actor-server: srv)) + (write-result opt result))))))) + +(def (do-control-start-workers opt) + (parameterize ((ensemble-domain (get-ensemble-domain opt))) + (let* ((supervisor (or (hash-get opt 'supervisor) (ensemble-domain-supervisor))) + (domain (or (hash-get opt 'domain) (ensemble-domain))) + (role (hash-ref opt 'role)) + (server-id (hash-ref opt 'server-id)) + (worker-count (hash-ref opt 'count)) + (config-path (hash-get opt 'config)) + (config (and config-path (call-with-input-file config-path read-config)))) + (when config + (check-ensemble-server-config! config)) + (call-with-console-server opt + (lambda (srv) + (let (result (ensemble-supervisor-start-workers! + supervisor: supervisor + role: role + server-id-prefix: server-id + workers: worker-count + domain: domain + config: config + actor-server: srv)) + (write-result opt result))))))) + +(def (do-control-stop-server opt) + (parameterize ((ensemble-domain (get-ensemble-domain opt))) + (let* ((supervisor (or (hash-get opt 'supervisor) (ensemble-domain-supervisor))) + (domain (hash-get opt 'domain)) + (role (hash-get opt 'role)) + (server-ids (hash-get opt 'server-ids))) + (call-with-console-server opt + (lambda (srv) + (let (result (ensemble-supervisor-stop-servers! + supervisor: supervisor + servers: server-ids + domain: domain + role: role + actor-server: srv)) + (write-result opt result))))))) + +(def (do-control-restart-server opt) + (parameterize ((ensemble-domain (get-ensemble-domain opt))) + (let* ((supervisor (or (hash-get opt 'supervisor) (ensemble-domain-supervisor))) + (domain (hash-get opt 'domain)) + (role (hash-get opt 'role)) + (server-ids (hash-get opt 'server-ids))) + (call-with-console-server opt + (lambda (srv) + (let (result (ensemble-supervisor-restart-servers! + supervisor: supervisor + servers: server-ids + domain: domain + role: role + actor-server: srv)) + (write-result opt result))))))) + +(def (do-control-get-server-log opt) + (parameterize ((ensemble-domain (get-ensemble-domain opt))) + (let* ((supervisor (or (hash-get opt 'supervisor) (ensemble-domain-supervisor))) + (server-id (hash-ref opt 'server-id)) + (file (hash-ref opt 'file "server.log"))) + (call-with-console-server opt + (lambda (srv) + (let (result (ensemble-supervisor-get-server-log + supervisor: supervisor + server: server-id + file: file + actor-server: srv)) + (displayln result))))))) + +(def (do-control-get-server-config opt) + (parameterize ((ensemble-domain (get-ensemble-domain opt))) + (let* ((supervisor (or (hash-get opt 'supervisor) (ensemble-domain-supervisor))) + (server-id (hash-ref opt 'server-id))) + (call-with-console-server opt + (lambda (srv) + (let (result (ensemble-supervisor-get-server-config + supervisor: supervisor + server: server-id + actor-server: srv)) + (write-result opt result))))))) + +(def (do-control-update-server-config opt) + (parameterize ((ensemble-domain (get-ensemble-domain opt))) + (let* ((supervisor (or (hash-get opt 'supervisor) (ensemble-domain-supervisor))) + (server-id (hash-ref opt 'server-id)) + (restart? (hash-get opt 'restart)) + (replace? (hash-get opt 'replace)) + (config-path (hash-ref opt 'config)) + (config (call-with-input-file config-path read-config))) + (check-ensemble-server-config! config) + (call-with-console-server opt + (lambda (srv) + (let (result (ensemble-supervisor-update-server-config! + supervisor: supervisor + server: server-id + config: config + mode: (if replace? 'replace 'upsert) + restart: restart? + actor-server: srv)) + (write-result opt result))))))) + +(def (do-control-get-config opt) + (parameterize ((ensemble-domain (get-ensemble-domain opt))) + (let (supervisor (or (hash-get opt 'supervisor) (ensemble-domain-supervisor))) + (call-with-console-server opt + (lambda (srv) + (let (result (ensemble-supervisor-get-config + supervisor: supervisor + actor-server: srv)) + (write-result opt result))))))) + +(def (do-control-update-config opt) + (parameterize ((ensemble-domain (get-ensemble-domain opt))) + (let* ((supervisor (or (hash-get opt 'supervisor) (ensemble-domain-supervisor))) + (replace? (hash-get opt 'replace)) + (config-path (hash-ref opt 'config)) + (config (call-with-input-file config-path read-config))) + (check-ensemble-config! config) + (call-with-console-server opt + (lambda (srv) + (let (result (ensemble-supervisor-update-config! + supervisor: supervisor + config: config + mode: (if replace? 'replace 'upsert) + actor-server: srv)) + (write-result opt result))))))) + +(def (do-control-shutdown opt) + (parameterize ((ensemble-domain (get-ensemble-domain opt))) + (let (supervisor (or (hash-get opt 'supervisor) (ensemble-domain-supervisor))) + (call-with-console-server opt + (lambda (srv) + (let (result (ensemble-supervisor-shutdown! + supervisor: supervisor + actor-server: srv)) + (write-result opt result))))))) + +(def (do-control-restart opt) + (parameterize ((ensemble-domain (get-ensemble-domain opt))) + (let ((supervisor (or (hash-get opt 'supervisor) (ensemble-domain-supervisor))) + (restart-services? (hash-get opt 'restart-services))) + (call-with-console-server opt + (lambda (srv) + (let (result (ensemble-supervisor-restart! + supervisor: supervisor + restart-services: restart-services? + actor-server: srv)) + (write-result opt result))))))) + +(def (do-control-upload opt) + (parameterize ((ensemble-domain (get-ensemble-domain opt))) + (let ((supervisor (or (hash-get opt 'supervisor) (ensemble-domain-supervisor))) + (exe? (hash-get opt 'exe)) + (env? (hash-get opt 'env)) + (fs? (hash-get opt 'fs)) + (file (hash-ref opt 'file)) + (path (hash-ref opt 'path))) + (unless (file-exists? file) + (error "upload file does not exist" file)) + (unless (= (+ (if exe? 1 0) (if env? 1 0) (if fs? 1 0)) 1) + (error "exactly one of --exe, --env, --fs must be specified")) + + (call-with-console-server opt + (lambda (srv) + (let (result + (cond + (exe? + ;; compress the executable first + (call-with-temporary-file-name "exe" + (lambda (tmp) + (copy-file file tmp) + (invoke "gzip" [tmp]) + (ensemble-supervisor-upload-executable! + supervisor: supervisor + path: (string-append tmp ".gz") + deployment-path: path + actor-server: srv)))) + (env? + (ensemble-supervisor-upload-environment! + supervisor: supervisor + path: file + deployment-path: path + actor-server: srv)) + (fs? + (ensemble-supervisor-upload-filesystem-overlay! + supervisor: supervisor + path: file + deployment-path: path + actor-server: srv)))) + (write-result opt result))))))) + +(def (do-control-shell opt) + (parameterize ((ensemble-domain (get-ensemble-domain opt))) + (let ((supervisor (or (hash-get opt 'supervisor) (ensemble-domain-supervisor))) + (command (hash-get opt 'command))) + (call-with-console-server opt + (lambda (srv) + (let (result (ensemble-supervisor-shell-command + supervisor: supervisor + command: command + actor-server: srv)) + (write-result opt result))))))) + +(def (do-control-list-processes opt) + (parameterize ((ensemble-domain (get-ensemble-domain opt))) + (let ((supervisor (or (hash-get opt 'supervisor) (ensemble-domain-supervisor)))) + (call-with-console-server opt + (lambda (srv) + (let (result (ensemble-supervisor-list-processes + supervisor: supervisor + actor-server: srv)) + (write-result opt result))))))) + +(def (do-control-exec-process opt) + (parameterize ((ensemble-domain (get-ensemble-domain opt))) + (let ((supervisor (or (hash-get opt 'supervisor) (ensemble-domain-supervisor))) + (exe (hash-ref opt 'exe-path)) + (args (hash-ref opt 'exe-args)) + (env (hash-get opt 'env)) + (envvars (hash-get opt 'envvars))) + (call-with-console-server opt + (lambda (srv) + (let (result (ensemble-supervisor-exec-process! + supervisor: supervisor + exe: exe + args: args + env: env + envvars: envvars + actor-server: srv)) + (write-result opt result))))))) + +(def (do-control-kill-process opt) + (parameterize ((ensemble-domain (get-ensemble-domain opt))) + (let ((supervisor (or (hash-get opt 'supervisor) (ensemble-domain-supervisor))) + (pid (hash-ref opt 'pid)) + (signo (hash-ref opt 'signo))) + (call-with-console-server opt + (lambda (srv) + (let (result (ensemble-supervisor-kill-process! + supervisor: supervisor + pid: pid + signo: signo + actor-server: srv)) + (write-result opt result))))))) + +(def (do-control-restart-process opt) + (parameterize ((ensemble-domain (get-ensemble-domain opt))) + (let ((supervisor (or (hash-get opt 'supervisor) (ensemble-domain-supervisor))) + (pid (hash-ref opt 'pid))) + (call-with-console-server opt + (lambda (srv) + (let (result (ensemble-supervisor-restart-process! + supervisor: supervisor + pid: pid + actor-server: srv)) + (write-result opt result))))))) + +(def (do-control-get-process-output opt) + (parameterize ((ensemble-domain (get-ensemble-domain opt))) + (let ((supervisor (or (hash-get opt 'supervisor) (ensemble-domain-supervisor))) + (pid (hash-ref opt 'pid))) + (call-with-console-server opt + (lambda (srv) + (let (result (ensemble-supervisor-get-process-output + supervisor: supervisor + pid: pid + actor-server: srv)) + (displayln result))))))) \ No newline at end of file diff --git a/src/tools/gxensemble/env.ss b/src/tools/gxensemble/env.ss new file mode 100644 index 000000000..af185474f --- /dev/null +++ b/src/tools/gxensemble/env.ss @@ -0,0 +1,91 @@ +;;; -*- Gerbil -*- +;;; © vyzo +;;; actor ensemble management tool +(import :std/actor + :std/iter + ./util) +(export #t) + +;;; gerbil ensemble env +(def (do-env-known-servers opt) + (def (write-known-servers! known-servers) + (call-with-output-file [path: (ensemble-known-servers-path) + create: 'maybe truncate: #t] + (cut write (hash->list known-servers) <>))) + (cond + ((hash-get opt 'add) + (let* ((server-id (hash-get opt 'server-id)) + (server-addrs (hash-get opt 'server-addresses)) + (server-addrs + (if (null? server-addrs) + [(ensemble-server-unix-addr server-id)] + server-addrs))) + (unless server-id + (error "missing server id")) + (let* ((known-servers + (or (ensemble-known-servers) + (make-hash-table))) + (current-addrs + (hash-ref known-servers server-id []))) + (for (addr server-addrs) + (unless (member addr current-addrs) + (set! current-addrs (append current-addrs [addr])))) + (hash-put! known-servers server-id current-addrs) + (write-known-servers! known-servers)))) + ((hash-get opt 'remove) + (let ((server-id (hash-get opt 'server-id)) + (server-addrs (hash-get opt 'server-addresses))) + (unless server-id + (error "missing server id")) + (alet (known-servers (ensemble-known-servers)) + (if server-addrs + (let* ((current-addrs + (hash-ref known-servers server-id [])) + (new-addrs + (filter (lambda (addr) (not (member addr current-addrs))) + current-addrs))) + (if (null? new-addrs) + (hash-remove! known-servers server-id) + (hash-put! known-servers server-id new-addrs))) + (hash-remove! known-servers server-id)) + (write-known-servers! known-servers)))) + ((hash-get opt 'set) + (let ((server-id (hash-get opt 'server-id)) + (server-addrs (hash-get opt 'server-addresses))) + (unless server-id + (error "missing server id")) + (let (known-servers + (or (ensemble-known-servers) + (make-hash-table))) + (hash-put! known-servers server-id server-addrs) + (write-known-servers! known-servers)))) + ((hash-get opt 'server-id) + => (lambda (server-id) + (unless (null? (hash-get opt 'server-addresses)) + (error "unexpected addresses")) + (alet* ((known-servers (ensemble-known-servers)) + (addrs (hash-get known-servers server-id))) + (write-result opt addrs)))) + (else + (alet (known-servers (ensemble-known-servers)) + (write-result opt (hash->list known-servers)))))) + +(def (do-env-domain opt) + (let (path (ensemble-domain-file-path)) + (cond + ((hash-get opt 'domain) + => (lambda (domain) + (call-with-output-file [path: path create: 'maybe truncate: #t] + (cut write domain <>)))) + (else + (displayln (get-ensemble-domain opt)))))) + +(def (do-env-supervisor opt) + (let (path (ensemble-domain-supervisor-path)) + (cond + ((hash-get opt 'server-id) + => (lambda (server-id) + (call-with-output-file [path: path create: 'maybe truncate: #t] + (cut write server-id <>)))) + (else + (displayln (ensemble-domain-supervisor)))))) diff --git a/src/tools/gxensemble/list.ss b/src/tools/gxensemble/list.ss new file mode 100644 index 000000000..c69a44614 --- /dev/null +++ b/src/tools/gxensemble/list.ss @@ -0,0 +1,25 @@ +;;; -*- Gerbil -*- +;;; © vyzo +;;; actor ensemble management tool +(import :std/actor + ./util) +(export #t) + +;;; gerbil ensemble list +(def (do-list-connections opt) + (start-actor-server-with-options! opt) + (display-result-list + (remote-list-connections (hash-ref opt 'server-id))) + (stop-actor-server!)) + +(def (do-list-actors opt) + (start-actor-server-with-options! opt) + (display-result-list + (map reference-actor (remote-list-actors (hash-ref opt 'server-id)))) + (stop-actor-server!)) + +(def (do-list-servers opt) + (start-actor-server-with-options! opt) + (display-result-list + (ensemble-list-servers)) + (stop-actor-server!)) diff --git a/src/tools/gxensemble/misc.ss b/src/tools/gxensemble/misc.ss new file mode 100644 index 000000000..2f1140147 --- /dev/null +++ b/src/tools/gxensemble/misc.ss @@ -0,0 +1,160 @@ +;;; -*- Gerbil -*- +;;; © vyzo +;;; actor ensemble management tool +(import :std/actor + :std/iter + :std/misc/process + :std/os/temporaries + ./util) +(export #t) + +;;; misc commands +(def (do-lookup opt) + (start-actor-server-with-options! opt) + (let (what (hash-ref opt 'server-or-role)) + (display-result-list + (if (hash-get opt 'role) + (ensemble-lookup-servers/role what) + (ensemble-lookup-server what)))) + (stop-actor-server!)) + +(def (do-package opt) + (parameterize ((ensemble-domain (or (hash-get opt 'ensemble-domain) (ensemble-domain)))) + (let* ((server-id (hash-ref opt 'server-id)) + (output (hash-ref opt 'output)) + (output (path-expand output (path-normalize (current-directory)))) + (top (path-normalize (current-directory))) + (base-path (ensemble-base-path)) + (server-path (ensemble-server-path server-id)) + (rebase-path + (lambda (base path) + (if (string-prefix? base path) + (path-expand (substring path (1+ (string-length base)) (string-length path)) + "ensemble") + (error "unexpected path: not a base subpath" base path)))) + (ensemble-rebase + (lambda (base . files) + (filter-map + (lambda (file) + (and (file-exists? file) + (rebase-path base file))) + files))) + (ensemble-file + (lambda (file (base #f)) + (if base + (path-expand file (ensemble-base-path base)) + (path-expand file base-path)))) + (server-file + (lambda (file (base #f)) + (if base + (path-expand file (ensemble-server-path server-id (ensemble-domain) base)) + (path-expand file server-path)))) + (copy-to + (lambda (base . files) + (for (path files) + (when (file-exists? path) + (let (target (path-expand path base)) + (if (eq? 'directory (file-type path)) + (begin + (create-directory* target) + (for (f (directory-files path)) + ;; TODO preserve links + (copy-file (path-expand f path) + (path-expand f target)))) + (begin + (create-directory* (path-directory target)) + (copy-file path target))))))))) + (call-with-temporary-file-name "ensemble" + (lambda (tmp) + (create-directory tmp) + (parameterize ((current-directory (gerbil-path))) + (apply copy-to tmp + (ensemble-rebase base-path + (ensemble-file "cookie") + (ensemble-file "admin.pub") + (ensemble-file "tls/ca-certificates") + (ensemble-file "tls/ca.pem") + (ensemble-file "tls/caroot.pem") + (ensemble-file "tls/domain") + (server-file "tls/chain.pem") + (server-file "tls/server.key"))) + (let (config-path (path-expand "ensemble/config" tmp)) + (create-directory* (path-expand "ensemble" tmp)) + (cond + ((hash-get opt 'config) + => (lambda (path) (copy-file (path-expand path top) config-path))) + ((file-exists? (ensemble-config-path)) + (copy-file (ensemble-config-path) config-path))))) + (invoke "tar" + ["cavf" output + (ensemble-rebase (path-expand "ensemble" tmp) + (ensemble-file "config" tmp) + (ensemble-file "cookie" tmp) + (ensemble-file "admin.pub" tmp) + (ensemble-file "tls/ca-certificates" tmp) + (ensemble-file "tls/ca.pem" tmp) + (ensemble-file "tls/caroot.pem" tmp) + (ensemble-file "tls/domain" tmp) + (server-file "tls/chain.pem" tmp) + (server-file "tls/server.key" tmp)) + ...] + directory: tmp) + (delete-file-or-directory tmp #t)))))) + +(def (do-shutdown opt) + (start-actor-server-with-options! opt) + (cond + ((hash-get opt 'server-id) + => (lambda (server-id) + (cond + ((hash-get opt 'actor-id) + => (lambda (actor-id) + (maybe-authorize! server-id) + (displayln "... shutting down " actor-id "@" server-id) + (stop-actor! (reference server-id actor-id)))) + (else + (maybe-authorize! server-id) + (displayln "... shutting down " server-id) + (remote-stop-server! server-id))))) + (else + (let/cc nope + (unless (hash-get opt 'force) + (displayln "This will shutdown every server in the ensemble, including the registry. Proceed? [y/n]") + (unless (memq (read) '(y yes Y YES)) + (nope (void)))) + + (let (servers (ensemble-list-servers)) + (for (server-id (map car servers)) + (maybe-authorize! server-id) + (displayln "... shutting down " server-id) + (with-catch void (cut remote-stop-server! server-id))) + ;; wait a second before shutting down the registry, so that servers can remove + ;; themselves. + (unless (null? servers) + (thread-sleep! 3))) + (displayln "... shutting down registry") + (maybe-authorize! 'registry) + (remote-stop-server! 'registry)))) + (stop-actor-server!)) + +(def (do-ping opt) + (let* ((server-id (hash-ref opt 'server-id)) + (actor-ref + (cond + ((hash-get opt 'actor-id) + => (lambda (actor-id) (reference server-id actor-id))) + (else (reference server-id 0))))) + (if (hash-get opt 'supervised) + (let (supervisor (or (hash-get opt 'supervisor) (ensemble-domain-supervisor))) + (call-with-console-server opt + (lambda (srv) + (let (result (ensemble-supervisor-invoke! + supervisor: supervisor + actor: actor-ref + message: (!ping) + actor-server: srv)) + (write-result opt result))))) + (call-with-console-server opt + (lambda (srv) + (let (result (ping-actor actor-ref srv)) + (write-result opt result))))))) diff --git a/src/tools/gxensemble/opt.ss b/src/tools/gxensemble/opt.ss new file mode 100644 index 000000000..124b5181e --- /dev/null +++ b/src/tools/gxensemble/opt.ss @@ -0,0 +1,798 @@ +;;; -*- Gerbil -*- +;;; © vyzo +;;; actor ensemble management tool +(import :std/cli/getopt + :std/config) +(export #t) +;;; +;;; getopt objects +;;; + +(def ensemble-domain-option + (option 'ensemble-domain "-D" "--ensemble-domain" + value: string->symbol + default: #f + help: "specifies the ensemble domain")) + +(def ensemble-public-address-option + (option 'ensemble-public-address "--public" + help: "specifies the ensemble supervisor public address for TLS")) + +(def ensemble-root-option + (option 'ensemble-root "--root" + help: "specifies the ensemble root directory")) + +(def control-domain-option + (option 'domain "-d" "--domain" + value: string->symbol + help: "specifies the control operation domain")) + +(def supervisor-option + (option 'supervisor "-S" "--supervisor" + value: string->object + help: "specifies the ensemble supervisor")) + +(def config-option + (option 'config "-C" "--config" + help: "configuration file")) + +(def exec-env-option + (option 'env "--env" + help: "execution GERBIL_PATH env" + default: "default")) + +(def exec-envvars-option + (option 'envvars "--env-vars" + help: "execution environment variables; as list of strings of the form ENV=value" + value: string->object)) + +(def exec-path-argument + (argument 'exe-path + help: "executable path in the supervisor contextr")) + +(def exec-rest-arguments + (rest-arguments 'exe-args + help: "executable arguments")) + +(def logging-option + (option 'logging "--log" + value: string->symbol + default: 'INFO + help: "specifies the log level to run with")) + +(def logging-file-option + (option 'logging-file "--log-file" + default: #f + help: "specifies a log file instead of logging to stderr; if it is - then the log will be written into the ensemble server directory log")) + +(def listen-option + (option 'listen "-l" "--listen" + value: string->object + default: [] + help: "additional addresses to listen to; by default the server listens at unix /tmp/ensemble/")) + +(def announce-option + (option 'announce "-a" "--announce" + value: string->object + default: #f + help: "public addresses to announce to the registry; by default these are the listen addresses")) + +(def console-option + (option 'console "-c" "--console" + value: string->object + default: 'console + help: "console server id")) + +(def registry-option + (option 'registry "-r" "--registry" + value: string->object + default: #f + help: "additional registry addresses; by default the registry is reachable at unix /tmp/ensemble/registry")) + +(def role-argument + (argument 'role + value: string->symbol + help: "server role")) + +(def role-option + (option 'role "--role" + value: string->symbol + help: "server role; a symbol")) + +(def roles-option + (option 'roles "--roles" + value: string->object + default: [] + help: "server roles; a list of symbols")) + +(def library-prefix-option + (option 'library-prefix "--library-prefix" + value: string->object + default: '(gerbil scheme std) + help: "list of package prefixes to consider as library modules installed in the server")) + +(def config-argument + (argument 'config + help: "configuration file")) + +(def server-id-argument + (argument 'server-id + help: "the server id" + value: string->object)) + +(def server-id-optional-argument + (optional-argument 'server-id + help: "the server id" + value: string->object)) + +(def server-id-rest-arguments + (rest-arguments 'server-ids + help: "server-id" + value: string->object)) + +(def domain-optional-argument + (optional-argument 'domain + help: "the domain" + value: string->symbol)) + +(def server-log-file-optional-argument + (optional-argument 'file + help: "the server log file to retrieve; default is server.log" + default: "server.log")) + +(def server-addresses-rest-arguments + (rest-arguments 'server-addresses + value: string->object + help: "server addresses")) + +(def pid-argument + (argument 'pid + value: string->integer + help: "process pid")) + +(def signo-argument + (argument 'signo + value: string->integer + help: "signal number")) + +(def actor-id-optional-argument + (optional-argument 'actor-id + help: "the actor's registered name" + value: string->symbol)) + +(def module-id-argument + (argument 'module-id + help: "the module id" + value: string->symbol)) + +(def server-id-or-role-argument + (argument 'server-or-role + help: "the server or role to lookup" + value: string->symbol)) + +(def worker-count-argument + (argument 'count + help: "number of workers" + value: string->integer)) + +(def upload-file-argument + (argument 'file + help: "file to upload")) + +(def upload-path-argument + (argument 'path + help: "upload path")) + +(def shell-command-argument + (argument 'command + help: "command to execute")) + +(def authorized-server-id-argument + (argument 'authorized-server-id + help: "the server to authorize capabilities for" + value: string->symbol)) + +(def capabilities-optional-argument + (optional-argument 'capabilities + help: "the server capabilities to authorize" + value: string->object + default: '())) + +(def expr-argument + (argument 'expr + help: "the expression to eval" + value: string->object)) + +(def main-arguments + (rest-arguments 'main-args + help: "arguments for the module's main procedure")) + +(def supervised-flag + (flag 'supervised "-s" "--supervised" + help: "the operation is supervised by the ensemble supervisor")) + +(def env-add-flag + (flag 'add "--add" + help: "add to the environment")) + +(def env-set-flag + (flag 'add "--set" + help: "set the environment")) + +(def env-remove-flag + (flag 'add "--remove" + help: "remove from the environment")) + +(def pretty-flag + (flag 'pretty "--pretty" + help: "pretty print")) + +(def restart-flag + (option 'restart "--restart" + help: "restart server(s) after update")) + +(def replace-mode-flag + (flag 'replace "--replace" + help: "replace the configuration instead of upserting")) + +(def restart-services-flag + (flag 'restart-services "--services" + help: "include supervisory services in the restart set")) + +(def upload-executable-flag + (flag 'exe "--exe" + help: "file is executable")) + +(def upload-env-flag + (flag 'env "--env" + help: "file is a (gzip) compressed tar archive for a gerbil environment")) + +(def upload-fs-flag + (flag 'fs "--fs" + help: "file is a (gzip) compressed tar archive for a filesystem overlay")) + +(def library-flag + (flag 'library "--library" + help: "loads the code as library module; the library must be in the servers load path")) + +(def role-flag + (flag 'role "--role" + help: "lookup by role")) + +(def force-flag + (flag 'force "-f" "--force" + help: "force the action")) + +(def view-flag + (flag 'view "--view" + help: "inspect existing, don't generate")) + +(def role-exe-option + (option 'exe "--exe" + help: "role executable path")) + +(def role-exe-prefix-option + (option 'prefix "--prefix" + value: string->object + help: "role executable arguments prefix; a list")) + +(def role-exe-suffix-option + (option 'suffix "--suffix" + value: string->object + help: "role executable arguments suffix; a list")) + +(def supervisor-policy-option + (option 'policy "--policy" + value: string->symbol + help: "role supervisory policy")) + +(def server-env-option + (option 'env "--env" + help: "role server environment")) + +(def server-envvars-option + (option 'envvars "--envvars" + value: string->object + help: "role server environment variables")) + +(def server-known-servers-option + (option 'known-servers "--known-servers" + value: string->object + help: "role server known servers for external communication")) + +(def server-application-option + (option 'application "--application" + value: string->symbol + help: "role server application name")) + +(def server-application-config-option + (option 'config "-C" "--config" + help: "role server application configuration")) + +(def (subcommand help) + (argument 'subcommand + help: help + value: string->symbol)) + +(def subcommand-env + (subcommand "see gerbil ensemble env help")) + +(def subcommand-control + (subcommand "see gerbil ensemble control help")) + +(def subcommand-list + (subcommand "see gerbil ensemble list help")) + +(def subcommand-admin + (subcommand "see gerbil ensemble admin help")) + +(def subcommand-ca + (subcommand "see gerbil ensemble ca help")) + +(def subcommand-config + (subcommand "see gerbil ensemble config help")) + +(def subcommand-arguments + (rest-arguments 'subcommand-args + help: "arguments for the subcommand")) + +(def supervisor-cmd + (command 'supervisor + help: "runs the ensemble supervisor")) + +(def registry-cmd + (command 'registry + ensemble-domain-option + logging-option + logging-file-option + listen-option + announce-option + server-id-optional-argument + help: "runs the ensemble registry")) + +(def run-cmd + (command 'run + supervised-flag + ensemble-domain-option + logging-option + logging-file-option + listen-option + announce-option + registry-option + roles-option + server-id-argument + module-id-argument + main-arguments + help: "run a server in the ensemble")) + +(def env-cmd + (command 'env + subcommand-env + subcommand-arguments + help: "ensemble environment operations")) + +(def control-cmd + (command 'control + subcommand-control + subcommand-arguments + help: "ensemble supervisory control operations")) + +(def config-cmd + (command 'config + subcommand-config + subcommand-arguments + help: "configure the ensemble")) + +(def load-cmd + (command 'load + console-option + force-flag + library-flag + registry-option + library-prefix-option + server-id-argument + module-id-argument + help: "loads code in a running server")) + +(def eval-cmd + (command 'eval + console-option + registry-option + supervised-flag + supervisor-option + server-id-argument + expr-argument + help: "evals code in a running server")) + +(def repl-cmd + (command 'repl + console-option + registry-option + library-prefix-option + supervised-flag + supervisor-option + server-id-argument + help: "provides a repl for a running server")) + +(def ping-cmd + (command 'ping + console-option + registry-option + supervised-flag + supervisor-option + server-id-argument + actor-id-optional-argument + help: "pings a server or actor in the server")) + +(def shutdown-cmd + (command 'shutdown + console-option + force-flag + registry-option + server-id-optional-argument + actor-id-optional-argument + help: "shuts down an actor, server, or the entire ensemble including the registry")) + +(def lookup-cmd + (command 'lookup + console-option + registry-option + role-flag + server-id-or-role-argument + help: "looks up a server by id or role")) + +(def list-cmd + (command 'list + subcommand-list + subcommand-arguments + help: "list server state")) + +(def admin-cmd + (command 'admin + subcommand-admin + subcommand-arguments + help: "ensemble administrative operations")) + +(def ca-cmd + (command 'ca + subcommand-ca + subcommand-arguments + help: "ensemble CA operations")) + +;; env subcommands +(def env-known-servers-cmd + (command 'known-servers + env-add-flag + env-set-flag + env-remove-flag + pretty-flag + server-id-optional-argument + server-addresses-rest-arguments + help: "edit the known-servers for the ensemble environment")) + +(def env-domain-cmd + (command 'domain + domain-optional-argument + help: "edit the known-servers for the ensemble environment")) + +(def env-supervisor-cmd + (command 'supervisor + server-id-optional-argument + help: "edit the known-servers for the ensemble environment")) + +;; control subcommands +(def control-list-servers-cmd + (command 'list-servers + ensemble-domain-option + console-option + supervisor-option + control-domain-option + pretty-flag + role-option + help: "list supervised servers in the ensemble")) + +(def control-start-server-cmd + (command 'start-server + ensemble-domain-option + supervisor-option + console-option + control-domain-option + config-option + role-argument + server-id-argument + help: "start a supervised ensemble server")) + +(def control-start-workers-cmd + (command 'start-workers + ensemble-domain-option + supervisor-option + console-option + config-option + control-domain-option + role-argument + server-id-argument + worker-count-argument + help: "start supervised ensemble workers")) + +(def control-stop-server-cmd + (command 'stop-server + ensemble-domain-option + supervisor-option + console-option + control-domain-option + role-option + server-id-rest-arguments + help: "stop some supervised ensemble servers")) + +(def control-restart-server-cmd + (command 'restart-server + ensemble-domain-option + supervisor-option + console-option + control-domain-option + role-option + server-id-rest-arguments + help: "restart some supervised ensemble servers")) + +(def control-get-server-log-cmd + (command 'get-server-log + ensemble-domain-option + supervisor-option + console-option + server-id-argument + server-log-file-optional-argument + help: "retrieve a supervised server log")) + +(def control-get-server-config-cmd + (command 'get-server-config + ensemble-domain-option + supervisor-option + console-option + pretty-flag + server-id-argument + help: "retrieve a server configuration")) + +(def control-update-server-config-cmd + (command 'update-server-config + ensemble-domain-option + supervisor-option + console-option + restart-flag + replace-mode-flag + server-id-argument + config-argument + help: "update a supervisor server configuration")) + +(def control-update-config-cmd + (command 'update-ensemble-config + ensemble-domain-option + supervisor-option + console-option + replace-mode-flag + config-argument + help: "update the supervised ensemble configuration")) + +(def control-get-config-cmd + (command 'get-ensemble-config + ensemble-domain-option + supervisor-option + console-option + pretty-flag + help: "retrieve the supervised ensemble configuration")) + +(def control-upload-cmd + (command 'upload + ensemble-domain-option + supervisor-option + console-option + upload-executable-flag + upload-env-flag + upload-fs-flag + upload-file-argument + upload-path-argument + help: "upload an executable, environment overlay image, or file system overlay image as a tarball")) + +(def control-shell-cmd + (command 'shell + ensemble-domain-option + supervisor-option + console-option + shell-command-argument + help: "execute a shell command in the context of an ensemble supervisor")) + +(def control-list-processes-cmd + (command 'list-processes + ensemble-domain-option + supervisor-option + console-option + pretty-flag + help: "list processes running in the context of an ensemble supervisor")) + +(def control-exec-process-cmd + (command 'exec-process + ensemble-domain-option + supervisor-option + console-option + exec-env-option + exec-envvars-option + exec-path-argument + exec-rest-arguments + help: "execute a process in the context of an ensemble supervisor")) + +(def control-kill-process-cmd + (command 'kill-process + ensemble-domain-option + supervisor-option + console-option + pid-argument + signo-argument + help: "send a signal to a process")) + +(def control-restart-process-cmd + (command 'restart-process + ensemble-domain-option + supervisor-option + console-option + pid-argument + help: "restart a process by pid")) + +(def control-get-process-output-cmd + (command 'get-process-output + ensemble-domain-option + supervisor-option + console-option + pid-argument + help: "get a process's output")) + +(def control-shutdown-cmd + (command 'shutdown + ensemble-domain-option + supervisor-option + console-option + help: "shutdown a supervised ensemble, including the supervisor")) + +(def control-restart-cmd + (command 'restart + ensemble-domain-option + supervisor-option + console-option + restart-services-flag + help: "mass restart servers in a supervised ensemble")) + +;; config +(def config-ensemble-cmd + (command 'ensemble + view-flag + pretty-flag + ensemble-domain-option + ensemble-root-option + ensemble-public-address-option + help: "configure the ensemble as a whole")) + +(def config-role-cmd + (command 'role + role-option + role-exe-option + role-exe-prefix-option + role-exe-suffix-option + supervisor-policy-option + server-env-option + server-envvars-option + server-known-servers-option + server-application-option + server-application-config-option + help: "configure an ensemble role")) + +(def config-preload-server-cmd + (command 'server + help: "TODO: configure a preloaded server")) + +(def config-preload-workers-cmd + (command 'workers + help: "TODO: configure preloaded workers")) + +;; list subcommands +(def list-servers-cmd + (command 'servers + console-option + registry-option + help: "lists known servers")) + +(def list-actors-cmd + (command 'actors + console-option + registry-option + server-id-argument + help: "list actors registered in a server")) + +(def list-connections-cmd + (command 'connections + console-option + registry-option + server-id-argument + help: "list a server's connections")) + +;; admin subcommands +(def admin-authorize-cmd + (command 'authorize + console-option + registry-option + server-id-argument + authorized-server-id-argument + capabilities-optional-argument + help: "authorize capabilities for a server as an administrator")) + +(def admin-retract-cmd + (command 'retract + console-option + registry-option + server-id-argument + authorized-server-id-argument + help: "retract all capabilities granted to a server by an administrator")) + +(def admin-cookie-cmd + (command 'cookie + force-flag + view-flag + help: "generate or inspect the ensemble cookie")) + +(def admin-creds-cmd + (command 'creds + force-flag + view-flag + help: "generate or inspect ensemble administrator credentials")) + +;; ca subcommands +(def ca-domain-option + (option 'domain "--domain" + default: "ensemble.internal" + help: "ensemble TLS domain")) + +(def ca-subject/C-option + (option 'subject/C "--subject/C" + default: "UN" + help: "ensemble TLS CA Country")) + +(def ca-subject/O-option + (option 'subject/O "--subject/O" + default: "Mighty Gerbils" + help: "ensemble TLS CA Organization")) + +(def ca-subject/L-option + (option 'subject/L "--subject/L" + default: "Internet" + help: "ensemble TLS certificate location")) + +(def ca-setup-cmd + (command 'setup + view-flag + ca-domain-option + ca-subject/C-option + ca-subject/O-option + ca-subject/L-option + help: "setup or inspect the ensemble CAs")) + +(def ca-cert-cmd + (command 'cert + force-flag + view-flag + ensemble-domain-option + ca-subject/C-option + ca-subject/O-option + ca-subject/L-option + server-id-argument + capabilities-optional-argument + help: "generate or inspect an actor server certificate")) + +(def package-output-option + (option 'output "-o" "--output" + default: "ensemble.tar.gz" + help: "output file for the server package")) + +(def package-cmd + (command 'package + ensemble-domain-option + package-output-option + config-option + server-id-argument + help: "package ensemble state to ship an actor server environment")) diff --git a/src/tools/gxensemble/repl.ss b/src/tools/gxensemble/repl.ss new file mode 100644 index 000000000..c8070179a --- /dev/null +++ b/src/tools/gxensemble/repl.ss @@ -0,0 +1,357 @@ +;;; -*- Gerbil -*- +;;; © vyzo +;;; actor ensemble management tool +(import :gerbil/expander + :gerbil/runtime/loader + :std/actor + :std/actor-v18/proto + :std/iter + :std/sugar + :std/error + ./util) +(export #t) + +(def (do-eval opt) + (let* ((server-id (hash-ref opt 'server-id)) + (expr (hash-ref opt 'expr)) + (eval-it + (if (hash-get opt 'supervised) + (let ((supervisor (or (hash-get opt 'supervisor) (ensemble-domain-supervisor))) + (actor-ref (reference server-id 'loader))) + (lambda () + (ensemble-supervisor-invoke! + supervisor: supervisor + actor: actor-ref + message: (!eval expr)))) + (lambda () (remote-eval server-id expr))))) + (call-with-console-server opt + (lambda (srv) + (write-result opt (eval-it)))))) + +(def (do-repl opt) + (let* ((server-id (hash-ref opt 'server-id)) + (library-prefix (hash-ref opt 'library-prefix)) + ((values eval-it load-code load-library-module connect! stop!) + (if (hash-get opt 'supervised) + (let ((supervisor (or (hash-get opt 'supervisor) (ensemble-domain-supervisor))) + (actor-ref (reference server-id 'loader))) + (values + (lambda (expr) + (ensemble-supervisor-invoke! + supervisor: supervisor + actor: actor-ref + message: (!eval expr))) + (lambda (object-file-path) + (let ((code + (cond + ((string? object-file-path) + (read-file-u8vector object-file-path)) + (else + (raise-bad-argument load-code "path: code object file" object-file-path)))) + (linker (path-strip-directory object-file-path))) + (ensemble-supervisor-invoke! + supervisor: supervisor + actor: actor-ref + message: (!load-code code linker)))) + (lambda (mod) + (let (mod-str + (cond + ((string? mod) mod) + ((symbol? mod) + (let* ((mod-str (symbol->string mod)) + (mod-str + (if (string-prefix? ":" mod-str) + (substring mod-str 1 (string-length mod-str)) + mod-str))) + mod-str)) + (else + (raise-bad-argument load-library-module "string or symbol" mod)))) + (ensemble-supervisor-invoke! + supervisor: supervisor + actor: actor-ref + message: (!load-library-module mod-str)))) + (lambda () + (connect-to-server! supervisor)) + (lambda () + (error "Cannot stop supervised server like that...")))) + (values + (lambda (expr) + (remote-eval server-id expr)) + (lambda (object-file-path) + (remote-load-code server-id object-file-path)) + (lambda (mod) + (remote-load-library-module server-id mod)) + (lambda () + (connect-to-server! server-id)) + (lambda () + (remote-stop-server! server-id)))))) + (call-with-console-server opt + (lambda (srv) + (do-repl-for-server server-id library-prefix + eval-it + load-code + load-library-module + connect! + stop!))))) + +(def (do-repl-for-server server-id library-prefix + eval-it + remote-load-code + remote-load-library-module + connect! + stop!) + (def (display-help) + (displayln "Control commands: ") + (displayln " ,(import module-id) -- import a module locally for expansion") + (displayln " ,(load module-id) -- load the code and dependencies for a module") + (displayln " ,(load -f module-id) -- forcibly load a module ignoring dependencies") + (displayln " ,(load -l module-id) -- load a library module") + (displayln " ,(defined? id) -- checks if an identifier is defined at the server") + (displayln " ,(thread-state) -- display the thread state for the primordial thread group") + (displayln " ,(thread-state -g) -- display the thread state for all thread groups recursively") + (displayln " ,(thread-state sn) -- display the thread state for a thread or group identified by its serial number") + (displayln " ,(thread-backtrace sn) -- display a backtrace for a thread identified by its serial number") + (displayln " ,(shutdown) -- shut down the server and exit the repl") + (displayln " ,q ,quit -- quit the repl") + (displayln " ,h ,help -- display this help message")) + + (def module-registry #f) + (def loaded-object-files + (make-hash-table)) + + (def (library-module-loader module-id) + (string-append (module-id->string module-id) "__rt")) + + (def (library-module-rt module-id) + (string-append (module-id->string module-id) "__0")) + + (def (library-module-loaded? module-id) + (or (hash-get module-registry (library-module-loader module-id)) + (hash-get module-registry (library-module-rt module-id)))) + + (def (object-file-loaded? object-file) + (hash-get loaded-object-files object-file)) + + (def (bind-module-exports! ctx) + (let lp ((rest (module-context-export ctx))) + (match rest + ([hd . rest] + (cond + ((module-export? hd) + (when (= (module-export-phi hd) 0) + (core-bind-import! (core-module-export->import hd))) + (lp rest)) + ((export-set? hd) + (lp (foldl cons rest (export-set-exports hd)))) + (else (lp rest)))) + (else (void))))) + + (def (load-object-file object-file) + (unless (object-file-loaded? object-file) + (displayln "loading code object file " object-file) + (hash-put! loaded-object-files object-file #t) + (remote-load-code object-file))) + + (def (load-library-module lib) + (unless (library-module-loaded? lib) + (displayln "loading library module " lib) + (remote-load-library-module lib) + (get-module-registry))) + + (def (get-module-registry) + (set! module-registry + (list->hash-table (eval-it '(hash->list __modules))))) + + (def (eval-expr expr) + (let* ((expanded-expr (core-expand expr)) + (compiled-expr (core-compile-top-syntax expanded-expr)) + (raw-compiled-expr (__compile compiled-expr)) + (result (eval-it raw-compiled-expr))) + (unless (void? result) + (if (##values? result) + (display-result-list (values->list result)) + (display-result result))))) + + (gerbil-load-expander!) + (connect!) + (eval-it '(##expand-source-set! identity)) + (get-module-registry) + (let/cc exit + (parameterize ((current-expander-context (make-top-context))) + ;; prepare the context + (eval '(import :gerbil/core)) + (eval '(import :gerbil/gambit)) + ;; and go! + (while #t + (display server-id) + (display "> ") + (try + (match (read) + ((? eof-object?) + (exit (void))) + (['unquote command] + (match command + (['import module-id] + (bind-module-exports! (import-module module-id))) + (['load module-id] + (let ((values object-files library-modules) + (get-module-objects module-id library-prefix)) + (when module-registry + (for (lib library-modules) + (load-library-module lib))) + (for (object-file object-files) + (load-object-file object-file)))) + (['load '-f module-id] + (let (object-file (find-object-file (import-module module-id))) + (if object-file + (load-object-file object-file) + (displayln "cannot find object file")))) + (['load -l module-id] + (if module-registry + (load-library-module module-id) + (displayln "server does not support library loading"))) + (['defined? symbol] + (eval-expr + `(with-exception-catcher (lambda (_) #f) (lambda () ,symbol #t)))) + (['thread-state] + (eval-expr + '(call-with-output-string "" + (lambda (p) (##cmd-st (thread-thread-group ##primordial-thread) p))))) + (['thread-state '-g] + (eval-expr + `(call-with-output-string "" + (lambda (p) + (let thread-state ((tg (thread-thread-group ##primordial-thread))) + (display "thread group: " p) + (display tg p) + (newline p) + (##cmd-st tg p) + (for-each thread-state (thread-group->thread-group-list tg))))))) + (['thread-state (? integer? sn)] + (eval-expr + `(call-with-output-string "" + (lambda (p) + (let (thread-or-group (serial-number->object ,sn)) + (if (or (thread? thread-or-group) + (thread-group? thread-or-group)) + (##cmd-st thread-or-group p))))))) + (['thread-backtrace (? integer? sn)] + (eval-expr + `(call-with-output-string "" + (lambda (p) + (let (thread (serial-number->object ,sn)) + (if (thread? thread) + (display-continuation-backtrace + (##thread-continuation-capture thread) + p #t))))))) + (['shutdown] + (stop!) + (exit (void))) + ((or 'h 'help) + (display-help)) + ((or 'q 'quit) + (exit (void))) + (else + (displayln "unknown control command " command) + (display-help)))) + (expr (eval-expr expr))) + (catch (exn) + (display "*** ERROR ") + (display-exception exn))))))) + +(extern namespace: #f __compile) + +(def (do-load opt) + (error "FIXME: do-load") + (gerbil-load-expander!) + (if (hash-get opt 'library) + (do-load-library opt) + (do-load-code opt))) + +(def (do-load-library opt) + (let ((module-id (hash-ref opt 'module-id)) + (server-id (hash-ref opt 'server-id))) + (start-actor-server-with-options! opt) + (maybe-authorize! server-id) + (displayln "... loading library module " module-id) + (displayln + (remote-load-library-module server-id module-id)) + (stop-actor-server!))) + +(def (do-load-code opt) + (error "FIXME: do-load-code") + (let ((module-id (hash-ref opt 'module-id)) + (library-prefix (hash-ref opt 'library-prefix)) + (server-id (hash-ref opt 'server-id))) + (let ((values object-files library-modules) + (get-module-objects module-id library-prefix)) + (start-actor-server-with-options! opt) + (maybe-authorize! server-id) + ;; when forcing, we don't load the library modules + ;; useful for static executables + (unless (hash-get opt 'force) + (for (lib library-modules) + (displayln "... loading library module " lib) + (displayln + (remote-load-library-module server-id lib)))) + (for (object-file object-files) + (displayln "... loading code object file " object-file) + (displayln + (remote-load-code server-id object-file))) + (stop-actor-server!)))) + +(def (find-object-file ctx-or-id) + (if (module-context? ctx-or-id) + (find-object-file (expander-context-id ctx-or-id)) + (__find-library-module (string-append (module-id->string ctx-or-id) "__0")))) + +(def (module-id->string module-id) + (let (mod-str (symbol->string module-id)) + (if (string-prefix? ":" mod-str) + (substring mod-str 1 (string-length mod-str)) + mod-str))) + +(def (get-module-objects module-id library-prefix) + (def (fold-modules r q) + (for/fold (r r) (in q) + (cond + ((module-context? in) + (fold-modules (cons in r) (cons (core-context-prelude in) (module-context-import in)))) + ((prelude-context? in) + (cons in r)) + ((module-import? in) + (if (= (module-import-phi in) 0) + (fold-modules r [(module-import-source in)]) + r)) + ((import-set? in) + (if (= (import-set-phi in) 0) + (fold-modules r [(import-set-source in)]) + r)) + (else r)))) + + (let* ((library-prefix-str (map symbol->string library-prefix)) + (ctx (import-module module-id)) + (modules (fold-modules [] [ctx]))) + (let lp ((rest modules) (to-load []) (libraries [])) + (match rest + ([ctx . rest] + (let* ((ctx-id (expander-context-id ctx)) + (ctx-id-str (and ctx-id (symbol->string ctx-id)))) + (cond + ((not ctx-id) + (lp rest to-load libraries)) + ((string-prefix? "gerbil/" ctx-id-str) + (lp rest to-load libraries)) + ((find (cut string-prefix? <> ctx-id-str) library-prefix-str) + (lp rest to-load libraries)) + (else + (if (member ctx-id to-load) + (lp rest to-load libraries) + (lp rest (cons ctx-id to-load) libraries)))))) + (else + (let lp ((rest to-load) (object-files [])) + (match rest + ([ctx-id . rest] + (lp rest (cons (find-object-file ctx-id) object-files))) + (else + (values object-files (reverse libraries)))))))))) \ No newline at end of file diff --git a/src/tools/gxensemble/srv.ss b/src/tools/gxensemble/srv.ss new file mode 100644 index 000000000..99742f6e7 --- /dev/null +++ b/src/tools/gxensemble/srv.ss @@ -0,0 +1,65 @@ +;;; -*- Gerbil -*- +;;; © vyzo +;;; actor ensemble management tool +(import :gerbil/expander + :std/actor + :std/actor-v18/registry + :std/iter + ./util) +(export #t) + +(def (do-registry opt) + (let ((server-id (hash-get opt 'server-id)) + (domain (get-ensemble-domain opt))) + (if server-id + ;; run as a supervisory process + (let (cfg (load-ensemble-server-config (server-identifier-at-domain server-id domain))) + (become-ensemble-server! cfg (cut start-ensemble-registry!))) + ;; standalone registry mode + (call-with-ensemble-server + (cons 'registry domain) + (cut start-ensemble-registry!) + domain: domain + log-level: (hash-ref opt 'logging) + log-file: (hash-ref opt 'logging-file) + listen: (hash-ref opt 'listen) + announce: (hash-ref opt 'announce) + registry: #f + roles: #f + cookie: (get-actor-server-cookie))))) + +(def (do-supervisor opt) + (let (config (load-ensemble-config)) + (become-ensemble-supervisor! config))) + +(def (do-run opt) + (let ((module-main (get-module-main (hash-ref opt 'module-id))) + (main-args (hash-ref opt 'main-args))) + (if (hash-get opt 'supervised) + (let* ((domain (get-ensemble-domain opt)) + (server-id (hash-ref opt 'server-id)) + (cfg (load-ensemble-server-config server-id domain))) + (become-ensemble-server! cfg (cut apply module-main main-args))) + (call-with-ensemble-server (hash-ref opt 'server-id) + (cut apply module-main main-args) + log-level: (hash-ref opt 'logging) + log-file: (hash-ref opt 'logging-file) + listen: (hash-ref opt 'listen) + announce: (hash-ref opt 'announce) + registry: (hash-ref opt 'registry) + roles: (hash-ref opt 'roles) + domain: (hash-ref opt 'ensemble-domain) + cookie: (get-actor-server-cookie))))) + +(def (get-module-main module-id) + (def (runtime-export? exported) + (= (module-export-phi exported) 0)) + + (gerbil-load-expander!) + (let (ctx (import-module module-id #f #t)) + (let/cc return + (for (exported (module-context-export ctx)) + (when (and (runtime-export? exported) + (eq? (module-export-name exported) 'main)) + (return (eval (binding-id (core-resolve-module-export exported)))))) + (error "module does not export main" module-id)))) diff --git a/src/tools/gxensemble/util.ss b/src/tools/gxensemble/util.ss new file mode 100644 index 000000000..6f3105cba --- /dev/null +++ b/src/tools/gxensemble/util.ss @@ -0,0 +1,90 @@ +;;; -*- Gerbil -*- +;;; © vyzo +;;; actor ensemble management tool +(import :std/actor + :std/actor-v18/server + :std/misc/ports + :std/sugar + :std/logger + :std/iter) +(export #t) + +;;; utilities +(def (write-result opt result) + (unless (void? result) + (cond + ((##values? result) + (for (val (##values->list result)) + (write-result opt val))) + ((hash-get opt 'pretty) + (pretty-print result)) + (else + (write result) + (newline))))) + +(def (display-result-list lst) + (for (result lst) + (display-result result))) + +(def (display-result result) + (write result) + (newline)) + +(def (get-ensemble-domain opt) + (cond + ((hash-get opt 'ensemble-domain)) + ((file-exists? (ensemble-domain-file-path)) + (call-with-input-file (ensemble-domain-file-path) read)) + (else (ensemble-domain)))) + +(def (call-with-console-server opt proc) + (parameterize ((ensemble-domain (get-ensemble-domain opt))) + (let (srv (start-actor-server-with-options! opt)) + (maybe-authorize! (or (hash-get opt 'supervisor) (ensemble-domain-supervisor))) + (with-catch display-exception (cut proc srv)) + (stop-actor-server! srv)))) + +(def (start-actor-server-with-options! opt) + (parameterize ((ensemble-domain (get-ensemble-domain opt)) + (current-logger-options (hash-ref opt 'logging 'WARN))) + (let* ((known-servers (ensemble-known-servers)) + (known-servers + (cond + ((hash-get opt 'registry) + => (lambda (addrs) + (let ((key (default-registry-server)) + (addrs (append (default-registry-addresses) addrs))) + (if known-servers + (begin + (hash-put! known-servers key addrs) + known-servers) + (hash (,key addrs)))))) + ((not known-servers) + (hash (,(default-registry-server) (default-registry-addresses)))) + (else known-servers))) + (server-id + (server-identifier (hash-ref opt 'console))) + (listen-addrs + (hash-ref opt 'listen [])) + (cookie (get-actor-server-cookie))) + (start-actor-server! identifier: server-id + cookie: cookie + addresses: listen-addrs + known-servers: known-servers)))) + +(def +admin-privkey+ #f) +(def (get-privkey) + (or +admin-privkey+ + (if (file-exists? (ensemble-admin-privkey-path)) + (let* ((passphrase (read-password prompt: "Enter administrative passphrase: ")) + (privk (get-admin-privkey passphrase))) + (set! +admin-privkey+ privk) + privk) + (error "no administrative private key")))) + +(def (maybe-authorize! server-id) + (let (addr (connect-to-server! server-id)) + (unless (eq? tls: (car addr)) + (when (file-exists? (ensemble-admin-privkey-path)) + (let (privk (get-privkey)) + (admin-authorize +admin-privkey+ server-id (actor-server-identifier))))))) diff --git a/src/tools/gxhttpd-ensemble-test.ss b/src/tools/gxhttpd-ensemble-test.ss index 926712492..220bdba88 100644 --- a/src/tools/gxhttpd-ensemble-test.ss +++ b/src/tools/gxhttpd-ensemble-test.ss @@ -8,8 +8,7 @@ :gerbil/gambit) (export gxhttpd-ensemble-test test-setup! test-cleanup!) -(def httpd-process #f) -(def registry-process #f) +(def supervisor-process #f) (def current-gerbil-path #f) (def test-directory @@ -17,34 +16,48 @@ (path-expand "gxhttpd-test" this-directory))) (def (test-setup!) - (delete-file-or-directory "/tmp/ensemble" #t) - (set! current-gerbil-path (getenv "GERBIL_PATH")) + (when (file-exists? "/tmp/ensemble") + (delete-file-or-directory "/tmp/ensemble" #t)) + (set! current-gerbil-path (getenv "GERBIL_PATH" #f)) (setenv "GERBIL_PATH") (invoke "gerbil" ["build"] directory: test-directory) + (invoke "gerbil" ["httpd" "config" + "--set" + "--ensemble" + "--ensemble-domain" "/test" + "--ensemble-root" (path-expand "root" test-directory) + "-n" "2" + "--root" "content" + "--listen" (object->string '("127.0.0.1:8080")) + "--handlers" (object->string '(("/handler" . :test/site/handler))) + "--enable-servlets"] + directory: test-directory) + (invoke "gerbil" ["ensemble" "env" "supervisor" + (object->string '(supervisor . /test))] + directory: test-directory) + (invoke "gerbil" ["ensemble" "env" "known-servers" + "--add" (object->string '(supervisor . /test))] + directory: test-directory) (ignore-errors - (invoke "gerbil" ["ensemble" "admin" "cookie"] directory: test-directory)) - (set! registry-process - (open-process [path: "gerbil" arguments: ["ensemble" "registry"] directory: test-directory])) - (thread-sleep! 1) - (set! httpd-process - (open-process [path: "gerbil" arguments: ["httpd" "-e" "-c" "ensemble.config"] directory: test-directory])) + (invoke "gerbil" ["ensemble" "admin" "cookie"] + directory: test-directory)) + (set! supervisor-process + (open-process [path: "gerbil" arguments: ["httpd" "ensemble"] + directory: test-directory])) (thread-sleep! 2)) (def (test-cleanup!) - (when httpd-process + (when supervisor-process (ignore-errors - (invoke "gerbil" ["ensemble" "shutdown" "httpd-ensemble" "ensemble-supervisor"] directory: test-directory)) - (ignore-errors - (invoke "gerbil" ["ensemble" "shutdown" "-f"] directory: test-directory)) - (thread-sleep! 1) - (ignore-errors (kill (process-pid httpd-process) SIGTERM)) - (process-status httpd-process) - (ignore-errors (kill (process-pid registry-process) SIGTERM)) - (process-status registry-process) + (invoke "gerbil" ["ensemble" "control" "shutdown"] directory: test-directory)) + (ignore-errors (kill (process-pid supervisor-process) SIGTERM)) + (process-status supervisor-process) (thread-sleep! 1)) (let (test-directory-dot-gerbil (path-expand ".gerbil" test-directory)) (delete-file-or-directory test-directory-dot-gerbil #t)) - (setenv "GERBIL_PATH" current-gerbil-path)) + (if current-gerbil-path + (setenv "GERBIL_PATH" current-gerbil-path) + (setenv "GERBIL_PATH"))) (def gxhttpd-ensemble-test (test-suite "httpd ensemble" diff --git a/src/tools/gxhttpd-test.ss b/src/tools/gxhttpd-test.ss index 011e1c18c..878f79a7d 100644 --- a/src/tools/gxhttpd-test.ss +++ b/src/tools/gxhttpd-test.ss @@ -19,8 +19,15 @@ (set! current-gerbil-path (getenv "GERBIL_PATH" #f)) (setenv "GERBIL_PATH") (invoke "gerbil" ["build"] directory: test-directory) + (invoke "gerbil" ["httpd" "config" + "--root" "content" + "--listen" (object->string '("127.0.0.1:8080")) + "--handlers" (object->string '(("/handler" . :test/site/handler))) + "--enable-servlets"] + directory: test-directory) (set! httpd-process - (open-process [path: "gerbil" arguments: ["httpd" "-c" "server.config"] directory: test-directory])) + (open-process [path: "gerbil" arguments: ["httpd" "server"] + directory: test-directory])) (thread-sleep! 1)) (def (test-cleanup!) diff --git a/src/tools/gxhttpd-test/ensemble.config b/src/tools/gxhttpd-test/ensemble.config deleted file mode 100644 index dc289a325..000000000 --- a/src/tools/gxhttpd-test/ensemble.config +++ /dev/null @@ -1,9 +0,0 @@ -config: httpd-ensemble-v0 -ensemble-servers: (httpd1 httpd2) -;; comment to disable request logging (slightly faster, but no logs) -ensemble-request-log: #t -server-configuration: -(root: "content" - handlers: (("/handler" . :test/site/handler)) - enable-servlets: #t - listen: ("127.0.0.1:8080")) diff --git a/src/tools/gxhttpd-test/server.config b/src/tools/gxhttpd-test/server.config deleted file mode 100644 index 124c24319..000000000 --- a/src/tools/gxhttpd-test/server.config +++ /dev/null @@ -1,8 +0,0 @@ -config: httpd-v0 -root: "content" -handlers: (("/handler" . :test/site/handler)) -enable-servlets: #t -server-log: "/tmp/server.log" -;; comment to disable request logging (slightly faster, but no logs) -request-log: "/tmp/request.log" -listen: ("127.0.0.1:8080") diff --git a/src/tools/gxhttpd.ss b/src/tools/gxhttpd.ss index e50e0d1c8..8407f8194 100644 --- a/src/tools/gxhttpd.ss +++ b/src/tools/gxhttpd.ss @@ -2,22 +2,14 @@ ;;; © vyzo ;;; The Gerbil HTTP Daemon ;;; -(import :std/sugar - :std/contract - :std/cli/getopt - :std/net/address - :std/net/httpd - :std/mime/types +(import :std/cli/getopt + :std/sugar + :std/config :std/actor - :std/iter - :std/hash-table - :std/misc/ports - (only-in :std/logger start-logger! deflogger current-logger-options) - (only-in :std/os/socket SO_REUSEADDR SO_REUSEPORT) - (only-in :std/srfi/13 string-contains) - :gerbil/expander - :gerbil/gambit - ./env) + ./env + ./gxhttpd/opt + ./gxhttpd/config + ./gxhttpd/server) (export main) (def (main . args) @@ -25,529 +17,56 @@ program: "gxhttpd" help: "The Gerbil HTTP Daemon" global-env-flag - config-option - ensemble-flag - )) + gerbil-path-option + server-cmd + ensemble-cmd + config-cmd)) -(def config-option - (option 'config "-c" "--config" - help: "location of configuration file; defaults to $GERBIL_PATH/httpd/config for single server configuration and $GERBIL_PATH/ensemble/httpd-ensemble/config for ensemble configuration")) - -(def ensemble-flag - (flag 'ensemble "-e" "--ensemble" - help: "run an ensemble of httpds with SO_REUSEPORT.")) - -(def (gxhttpd-main opt) +(def (gxhttpd-main cmd opt) (setup-local-env! opt) (let-hash opt - (if .?ensemble - (let (cfg - (cond - (.?config => load-ensemble-config) - (else (load-default-ensemble-config)))) - (run-ensemble! cfg)) - (let (cfg - (cond - (.?config => load-server-config) - (else (load-default-server-config)))) - (run-server! cfg))))) - -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -;;; Server Config: a flat (keyword) plist with the following keys -;;; User is free to have additional configuration, interpreted by handlers -;;;----------------------------------------------------------------- -;;; ;;; config: config versioned type, current is httpd-v0 -;;; config: httpd-v0 -;;; ;;; root: the root for serving files -;;; root: "path/to/server/root" -;;; ;;; handlers: alist mapping server paths to handler modules -;;; ;;; a handler module is a module that exports a `handle-request` procedure, -;;; ;;; with the signature of a request handler. -;;; ;;; if the module also exports a handler-init! procedure, it will be invoked -;;; ;;; with the current config after loading the module. -;;; handlers: (("path/to/handler" . module) ...) -;;; ;;; servlets: a boolean indicating whether servlets are enabled -;;; enable-servlets: #t | #f -;;; ;;; request-log: [optional] the file path for logging requests -;;; request-log: "path/to/request-log-file" | #f -;;; ;;; server-log: the file path for the server logger -;;; server-log: "path/to/server-log-file" -;;; ;;; listen: a list of addresses where the server should listen -;;; ;;; use (ssl: path-to-cert inet-address) for https -;;; listen: (server-address ...) -;;; ;;; reuse-port: whether to bind with SO_REUSEPORT; used for ensembles -;;; reuse-port: #t|#f -;;; ;;; log-level: [optional] log level -;;; log-level: symbol -;;; ;;; ensemble-server-id: [optional] run as an ensemble server with the given id -;;; ensemble-server-id: symbol|#f -;;; ;;; ensemble-supervisor-id: [optional] ensemble supervisor id to authorize -;;; ensemble-supervisor-id: symbol|#f -;;; ;;; ensemble-registry: [optional] list of registry addresses -;;; ensemble-registry: (actor-address ...) -;;; ;;; max-token-length: The request handler parser buffer size -;;; max-token-length: integer -;;;---------------------------------------------------------------- - -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -;;; Ensemble Config: a flat (keyword) plist with the following keys -;;; Additional configuration keys are _ignored_ -;;;---------------------------------------------------------------- -;;; ;;; config: config versioned type, current is httpd-ensemble-v0 -;;; config: httpd-ensemble-v0 -;;; ;;; ensemble-supervisor-id: [optional] id of the ensemble supervisor. -;;; ;;; defaults to httpd-ensemble -;;; ensemble-supervisor-id: symbol | #f -;;; ;;; servers: list of httpd server ids to start in the ensemble -;;; ensemble-servers: (symbol ...) -;;; ;;; ensemble-registry: [optional] list of registry addresses for the ensemble -;;; ensemble-registry: (actor-address ...) | #f -;;; ;;; ensemble-listen: [optional] list of supervisor listen addresses -;;; ensemble-listen: (actor-address ...) -;;; ;;; ensemble-announce: [optional] list of supervisor announced addresses -;;; ensemble-announce: (actor-address ...) | #f -;;; ;;; ensemble-request-log: boolean, whether to enable request logging -;;; ensemble-request-log: #t | #f -;;; ;;; server-configuration: the server specific configuration -;;; server-configuration: (key: value ...) -;;; ;;; log-level: [optional] log level -;;; log-level: symbol -;;;------------------------------------------------------------------- - -(def (load-ensemble-config path) - (load-config path 'httpd-ensemble-v0)) - -(def (load-default-ensemble-config) - (load-ensemble-config - (path-expand "config" (ensemble-server-path 'httpd-ensemble)))) - -(def (load-server-config path) - (load-config path 'httpd-v0)) - -(def (load-default-server-config) - (load-server-config - (path-expand "httpd/config" (gerbil-path)))) - -(def (load-config path type) - (let (cfg (call-with-input-file path read-all)) - (unless (eq? type (config-get! cfg config:)) - (error "Bad configuration file; configuration type mismatch" path type cfg)) - cfg)) - -(def (config-get cfg key (default #f)) - (pgetq key cfg default)) - -(def (config-get! cfg key) - (or (pgetq key cfg) - (error "missing configuration key" key: key config: cfg))) - -(def (config-set! cfg key val) - (cond - ((memq key cfg) - => (lambda (pos) (set-car! (cdr pos) val))) - (else - (append cfg [key val])))) - -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -;; Ensemble Supervisor Implementation -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -(def (run-ensemble! cfg) - (create-ensemble-paths! cfg) - (let ((log-level (config-get cfg log-level: 'INFO)) - (server-id (config-get cfg ensemble-supervisor-id: 'httpd-ensemble))) - (let (run-ensemble (cut start-httpd-ensemble! cfg)) - (call-with-ensemble-server - server-id run-ensemble - log-level: log-level - log-file: "-" - listen: (config-get cfg ensemble-listen: []) - announce: (config-get cfg ensemble-announce:) - registry: (config-get cfg ensemble-registry:) - roles: '(httpd-supervisor) - ;; for testing mostly - auth: (or (alet (auth (config-get cfg ensemble-auth:)) - (list->hash-table-eq auth)) - (hash-eq (console '(admin)))))))) - -(def (start-httpd-ensemble! cfg) - (let (thread (spawn/name 'ensemble-supervisor ensemble-supervisor cfg)) - (thread-join! thread))) - -(def (ensemble-supervisor cfg) - (register-actor! 'ensemble-supervisor) - (let/cc exit - (deflogger supervisor) - (def servers (make-hash-table-symbolic)) - - (def (start-server! srv-id) - (let (srv-address (path-expand (symbol->string srv-id) "/tmp/ensemble")) - (when (file-exists? srv-address) - (delete-file srv-address))) - (let* ((config-path (path-expand "config" (ensemble-server-path srv-id))) - (process (open-process [path: "gerbil" - arguments: ["httpd" "-c" config-path]]))) - (hash-put! servers srv-id process) - (spawn process-monitor (current-thread) srv-id process))) - - (def (shutdown!) - (for ((values srv-id process) (in-hash servers)) - (try - (infof "shutting down ~a" srv-id) - (remote-stop-server! srv-id) - ;; TODO maybe kill it after a second or two? - (process-status process) - (catch (e) - (warnf "error shutting down ~a: ~a" srv-id e))))) - - (def (prepare!) - ;; shutdown extant httpd workers in case of restart - (let (wait? #f) - (for (srv-id (config-get! cfg ensemble-servers:)) - (ignore-errors - (when (eq? 'OK (ping-server srv-id)) - (infof "shutting down leftover server ~a" srv-id) - (remote-stop-server! srv-id) - (set! wait? #t)))) - (when wait? - (thread-sleep! 1)))) - - (try - (prepare!) - (for (srv-id (config-get! cfg ensemble-servers:)) - (start-server! srv-id)) - (catch (e) - (errorf "error starting httpd server: ~a" e) - (shutdown!) - (exit 'error))) - - (while #t - (<- - ((process-dead srv-id status) - (warnf "http server ~a has exited with status ~a; restarting" srv-id status) - ;; TODO maybe delay restarting and have an adaptive policy for too many - ;; crashes? - (start-server! srv-id)) - ,(@shutdown - (infof "shutting down httpd ensemble...") - (shutdown!) - (exit 'shutdown)) - ,(@ping) - ,(@unexpected warnf))))) - -(defmessage process-dead (srv-id status)) - -(def (process-monitor supervisor srv-id process) - (let (status (process-status process)) - (-> supervisor (process-dead srv-id status)))) - -(def (create-ensemble-paths! cfg) - (create-directory* (ensemble-server-path (config-get cfg ensemble-supervisor-id: 'httpd-ensemble))) - (for (srv-id (config-get! cfg ensemble-servers:)) - (let* ((srv-path (ensemble-server-path srv-id)) - (srv-config-path (path-expand "config" srv-path)) - (srv-config (make-ensemble-server-config cfg srv-id))) - (create-directory* srv-path) - (call-with-output-file [path: srv-config-path truncate: #t] - (lambda (output) - (for (el srv-config) - (write el output) - (newline output))))))) - -(def (make-ensemble-server-config cfg srv-id) - (let (srv-path (ensemble-server-path srv-id)) - `(config: - httpd-v0 - request-log: ,(and (config-get cfg ensemble-request-log:) (path-expand "request.log" srv-path)) - server-log: ,(path-expand "server.log" srv-path) - reuse-port: #t - log-level: ,(config-get cfg log-level: 'INFO) - ensemble-server-id: ,srv-id - ensemble-supervisor-id: ,(config-get cfg ensemble-supervisor-id: 'httpd-ensemble) - ensemble-registry: ,(config-get cfg ensemble-registry:) - ,@(config-get cfg server-configuration: [])))) - -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -;; Server Implementation -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -(def current-http-server-config - (make-parameter #f)) - -(def (run-server! cfg) - (create-server-paths! cfg) - (current-logger-options (config-get cfg log-level: 'INFO)) - (start-logger! (config-get! cfg server-log:)) - (let* ((sockopts - (if (config-get cfg reuse-port:) - [SO_REUSEADDR SO_REUSEPORT] - [SO_REUSEADDR])) - (mux (make-mux cfg)) - (request-logger (get-request-logger cfg)) - (addresses (config-get! cfg listen:)) - (max-token-length (: (config-get cfg max-token-length: 1024) :fixnum)) - (run-httpd - (lambda () - (set-httpd-max-token-length! max-token-length) - (parameterize ((current-http-server-config cfg)) - (let (srv (apply start-http-server! - mux: mux - sockopts: sockopts - request-logger: request-logger - addresses)) - (thread-join! srv)))))) - (cond - ((config-get cfg ensemble-server-id:) - => (lambda (server-id) - (call-with-ensemble-server - server-id run-httpd - registry: (config-get cfg ensemble-registry:) - roles: '(httpd) - auth: (hash-eq (,(config-get cfg ensemble-supervisor-id: - 'httpd-ensemble) - '(admin)))))) - (else - (run-httpd))))) - -(def (create-server-paths! cfg) - (create-directory* (config-get! cfg root:)) - (alet (request-log (config-get cfg request-log:)) - (create-directory* (path-directory request-log))) - (create-directory* (path-directory (config-get! cfg server-log:)))) - -(def (get-request-logger cfg) - (alet (path (config-get cfg request-log:)) - (make-request-logger path))) - -(def (make-mux cfg) - (Mux (make-dynamic-mux cfg))) - -(defstruct dynamic-mux ((root :- :string) - (handlers :- HashTable) - (servlets :- HashTable) - (mx :- :mutex) - (cache :- HashTable) - (cache-ttl :- :real) - (cache-max-size :- :fixnum)) - constructor: :init! final: #t) - -(defstruct cache-entry ((handler :- :procedure) - (expire :- :flonum) - (preserve? :- :procedure)) - final: #t) - -(defmethod {:init! dynamic-mux} - (lambda (self cfg) - (let ((root (: (config-get! cfg root:) :string)) - (handlers (: (config-get cfg handlers: []) :list)) - (servlets? (: (config-get cfg enable-servlets:) :boolean))) - (set! self.root root) - (set! self.cache (make-hash-table-string)) - (set! self.cache-ttl (: (inexact (config-get cfg cache-ttl: 120)) :real)) - (set! self.cache-max-size (: (config-get cfg cache-max-size: 16384) :fixnum)) - (set! self.handlers (make-hash-table-string)) - (when servlets? - (set! self.servlets (make-hash-table-string)) - (set! self.mx (make-mutex 'mux-loader))) - (for ([path . handler-module] handlers) - (let* ((ctx (import-module handler-module #f #t)) - (init! (find-runtime-symbol ctx 'handler-init!)) - (handle-request (find-runtime-symbol ctx 'handle-request))) - (unless handle-request - (error "handler module does not export handle-request procedure" - module: handler-module)) - (when init! - ((: (eval init!) :procedure) cfg)) - (hash-put! self.handlers path (: (eval handle-request) :procedure))))))) - -(defmethod {get-handler dynamic-mux} - (lambda (self host (path :- :string)) - ;; flush the cache if it gets too big - (when (fx> (hash-length self.cache) self.cache-max-size) - (set! self.cache (make-hash-table-string))) - (cond - ((hash-get self.cache path) - => (lambda (cache-entry) - (let (now (##current-time-point)) - (cond - ((fl< now (&cache-entry-expire cache-entry)) - (&cache-entry-handler cache-entry)) - (((&cache-entry-preserve? cache-entry)) - (set! (&cache-entry-expire cache-entry) - (fl+ now self.cache-ttl)) - (&cache-entry-handler cache-entry)) - (else - {self.__get-handler path}))))) - (else - {self.__get-handler path})))) - -(defmethod {__get-handler dynamic-mux} - (lambda (self (path :- :string)) - (defrule (not-found-cache-entry expire) - (cache-entry not-found-handler expire (lambda () #f))) - - (defrule (file-cache-entry file-path expire created handler) - (let (preserve? - (lambda () - (and (file-exists? file-path) - (fl< (time->seconds - (file-info-last-modification-time - (file-info file-path #t))) - created)))) - (cache-entry handler expire preserve?))) - - (let* ((now (##current-time-point)) - (expire (+ now self.cache-ttl)) - (entry - (let (server-path (server-request-path path)) - (cond - ((not server-path) - (not-found-cache-entry expire)) - ((find-handler self.handlers server-path) - => (lambda (handler) - (cache-entry handler expire (lambda () #t)))) - (else - (let (file-path (string-append self.root server-path)) - (if (file-exists? file-path) - (if (and self.servlets (equal? ".ss" (path-extension file-path))) - (file-cache-entry file-path expire now - (find-servlet-handler self.servlets self.mx file-path)) - (file-cache-entry file-path expire now - (file-handler file-path))) - (not-found-cache-entry expire)))))))) - (hash-put! self.cache path entry) - (&cache-entry-handler entry)))) - -(defmethod {put-handler! dynamic-mux} - (lambda (self host (path :- :string) (handler :- :procedure)) - (hash-put! self.handlers path handler))) - -(def (not-found-handler req res) - (http-response-write-condition res Not-Found)) - -(def (forbidden-handler req res) - (http-response-write-condition res Forbidden)) - -(defstruct servlet ((handler :- :procedure) - (path :- :string) - (timestamp :- :flonum)) - final: #t) - -(def (find-servlet-handler servlet-tab mx file-path) - (def (load-servlet! file-path reload?) - (let* ((load-time (time->seconds (current-time))) - (ctx (with-lock mx (cut import-module file-path reload? #t))) - (init! (find-runtime-symbol ctx 'handler-init!)) - (handle-request (find-runtime-symbol ctx 'handle-request))) - (unless handle-request - (error "servlet does not export handle-request" file-path)) - (when init! - ((eval init!) (current-http-server-config))) - (let* ((handle-request (: (eval handle-request) :procedure)) - (srv (servlet handle-request file-path load-time))) - (hash-put! servlet-tab file-path srv) - srv))) - + (case cmd + ;; run a server + ((server) + (if .?server-id + ;; run as part of an ensemble + (let (cfg (load-ensemble-server-config .server-id)) + (become-ensemble-server! cfg (cut run-ensemble-server! cfg))) + ;; run standalone + (let (cfg (get-httpd-config opt)) + (run-server! cfg)))) + ;; run an ensemble of httpds + ((ensemble) + (let (cfg (get-ensemble-config opt)) + (prepare-ensemble-filesystem! cfg) + (become-ensemble-supervisor! cfg))) + ;; configure the server or the ensemble + ((config) + (do-config opt))))) + +(def (prepare-ensemble-filesystem! cfg) + (let* ((root (config-get cfg root: (current-directory))) + (root (path-normalize root)) + (fs (path-expand "fs" root)) + (www (config-get! + (agetq 'httpd + (config-get! + (config-get! + (agetq 'httpd (config-get! cfg roles:)) + server-config:) + application:)) + root:))) + (unless (file-exists? www) + (error "httpd content root directory doesn't exist" www)) + (unless (string-prefix? "/" www) + (let (fs/www (path-expand www fs)) + (create-directory* (path-directory fs/www)) + (unless (file-exists? fs/www) + (create-symbolic-link www fs/www)))))) + +(def (run-ensemble-server! cfg) (cond - ((hash-get servlet-tab file-path) - => (lambda (srv) - (using (srv :- servlet) - (let (modtime - (time->seconds - (file-info-last-modification-time - (file-info file-path #t)))) - (if (> modtime srv.timestamp) - (servlet-handler (load-servlet! file-path #t)) - srv.handler))))) + ((agetq 'httpd (config-get! cfg application:)) + => run-server!) (else - (servlet-handler (load-servlet! file-path #f))))) - -(def (file-handler path) - => :procedure - (let (info (file-info path #t)) - (if (eq? (file-info-type info) 'directory) - (let (index-html-path (path-expand "index.html" path)) - (if (file-exists? index-html-path) - (serve-file index-html-path (file-info index-html-path #t)) - forbidden-handler)) - (serve-file path info)))) - -(def max-file-cache-size 32768) ; size of i/o buffer for http-response-file - -(def (serve-file path info) - => :procedure - (let* ((content-type (path-extension->mime-type-name path)) - (headers - [(if content-type - ["Content-Type" :: content-type] - ["Content-Type" :: "application/octet-stream"]) - ["Last-Modified" :: (number->string (exact (floor (time->seconds (file-info-last-modification-time info)))))] - ["Content-Length" :: (number->string (file-info-size info))]])) - - (if (fx<= (file-info-size info) max-file-cache-size) - ;; cache the content - (let (buf (read-file-u8vector path)) - (lambda (req res) - (using (req :- http-request) - (case req.method - ((GET) - (http-response-write res 200 headers buf)) - ((HEAD) - (http-response-write res 200 headers #f)) - (else - (http-response-write-condition res Forbidden)))))) - ;; don't cache - (lambda (req res) - (using (req :- http-request) - (case req.method - ((GET) - (http-response-file res headers path)) - ((HEAD) - (http-response-write res 200 headers #f)) - (else - (http-response-write-condition res Forbidden)))))))) - -(def (find-handler tab server-path) - (let loop ((path server-path)) - (cond - ((string-empty? path) #f) - ((hash-get tab path)) - ((string-rindex path #\/) - => (lambda (index) (loop (substring path 0 index)))) - (else #f)))) - -(def (server-request-path path) - (let (components (string-split path #\/)) - (let loop ((rest components) (r [])) - (match rest - ([hd . rest] - (case hd - (("" ".") (loop rest r)) - (("..") - (if (null? r) - #f ; invalid, out of root bounds - (loop rest (cdr r)))) - (else - (loop rest (cons hd r))))) - (else - (if (null? r) - "/" - (string-join (cons "" (reverse r)) "/"))))))) - -(def (find-runtime-symbol ctx id) - (cond - ((find-export-binding ctx id) - => (lambda (bind) - (unless (runtime-binding? bind) - (error "export is not a runtime binding" symbol: id)) - (binding-id bind))) - (else #f))) - -(def (find-export-binding ctx id) - (cond - ((find (match <> - ((? module-export? xport) - (and (eqv? (module-export-phi xport) 0) - (eq? (module-export-name xport) id))) - (else #f)) - (module-context-export ctx)) - => core-resolve-module-export) - (else #f))) + (error "missing httpd configuration" cfg)))) diff --git a/src/tools/gxhttpd/config.ss b/src/tools/gxhttpd/config.ss new file mode 100644 index 000000000..b6532819c --- /dev/null +++ b/src/tools/gxhttpd/config.ss @@ -0,0 +1,151 @@ +;;; -*- Gerbil -*- +;;; © vyzo +;;; The Gerbil HTTP Daemon +;;; +(import :std/config + :std/actor + :std/sugar) +(export #t) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;;; Server Config: a flat (keyword) plist with the following keys +;;; User is free to have additional configuration, interpreted by handlers +;;;----------------------------------------------------------------- +;;; ;;; config: httpd-v0 +;;; config: httpd-v0 +;;; ;;; root: the root for serving files +;;; root: "path/to/server/root" +;;; ;;; handlers: alist mapping server paths to handler modules +;;; ;;; a handler module is a module that exports a `handle-request` procedure, +;;; ;;; with the signature of a request handler. +;;; ;;; if the module also exports a handler-init! procedure, it will be invoked +;;; ;;; with the current config after loading the module. +;;; handlers: (("/path/to/handler" . module) ...) +;;; ;;; servlets: a boolean indicating whether servlets are enabled +;;; enable-servlets: #t | #f +;;; ;;; request-log: [optional] the file path for logging requests +;;; request-log: "path/to/request-log-file" | #f +;;; ;;; listen: a list of addresses where the server should listen +;;; ;;; use (ssl: path-to-cert inet-address) for https +;;; listen: (server-address ...) +;;; ;;; max-token-length: The request handler parser buffer size +;;; max-token-length: integer +;;;---------------------------------------------------------------- + +(def (do-config opt) + (let-hash opt + (cond + (.?print + (if .?ensemble + (write-config (get-ensemble-config opt) pretty: #t) + (write-config (get-httpd-config opt) pretty: #t))) + (.?ensemble + (cond + (.?set + (do-ensemble-config opt (empty-ensemble-config))) + (else + (do-ensemble-config opt (get-ensemble-config opt))))) + (.?set + (do-httpd-config opt (empty-httpd-config))) + (else + (do-httpd-config opt (get-httpd-config opt)))))) + + +(def (do-ensemble-config opt cfg) + (let (cfg (set-ensemble-config! opt cfg)) + (save-config! cfg (or (hash-get opt 'config) (ensemble-config-path))))) + +(def (do-httpd-config opt cfg) + (let (cfg (set-httpd-config! opt cfg)) + (save-config! cfg (or (hash-get opt 'config) (httpd-config-path))))) + +(def (set-ensemble-config! opt cfg) + (let-hash opt + (let* ((domain .ensemble-domain) + (worker-domain (ensemble-subdomain (or .?worker-domain 'www) domain)) + (role 'httpd) + (role-alist (config-get cfg roles: [])) + (role-cfg (agetq role role-alist [])) + (httpd-server-cfg + (config-get role-cfg server-config: (empty-ensemble-server-config))) + (httpd-app-alist + (config-get httpd-server-cfg application: [])) + (httpd-cfg + (agetq role httpd-app-alist (empty-httpd-config))) + (preload-cfg (config-get cfg preload: [])) + (worker-alist (config-get preload-cfg workers: [])) + (worker-cfg (agetq worker-domain worker-alist []))) + + (set! httpd-cfg (set-httpd-config! opt httpd-cfg)) + (cond + ((assq role httpd-app-alist) + => (lambda (p) + (set-cdr! p httpd-cfg))) + (else + (set! httpd-app-alist [[role httpd-cfg ...] httpd-app-alist ...]))) + (config-push! httpd-server-cfg application: httpd-app-alist) + (config-push! httpd-server-cfg env: #f) + (config-push! role-cfg server-config: httpd-server-cfg) + + (config-push! role-cfg exe: "gerbil") + (config-push! role-cfg prefix: '("httpd" "server")) + (config-push! role-cfg policy: 'restart) + (cond + ((assq role role-alist) + => (lambda (p) + (set-cdr! p role-cfg))) + (else + (set! role-alist [[role role-cfg ...] role-alist ...]))) + (config-push! cfg roles: role-alist) + + (config-push! worker-cfg prefix: 'httpd) + (config-push! worker-cfg role: role) + (cond + (.?workers => (cut config-push! worker-cfg servers: <>)) + ((not (config-get worker-cfg servers:)) + (config-push! worker-cfg servers: 1))) + (cond + ((assq worker-domain worker-alist) + => (lambda (p) + (set-cdr! p worker-cfg))) + (else + (set! worker-alist [[worker-domain worker-cfg ...] worker-alist ...]))) + (config-push! preload-cfg workers: worker-alist) + (config-push! cfg preload: preload-cfg) + (config-push! cfg domain: domain) + (cond (.ensemble-root => (cut config-push! cfg root: <>)))) + cfg)) + +(def (set-httpd-config! opt cfg) + (let-hash opt + (cond (.?root => (cut config-push! cfg root: <>))) + (cond (.?listen => (cut config-push! cfg listen: <>))) + (cond (.?handlers => (cut config-push! cfg handlers: <>))) + (cond (.?enable-servlets => (cut config-push! cfg enable-servlets: <>))) + (cond (.?log-dir => (cut config-push! cfg log-dir: <>))) + (cond (.?max-token-length => (cut config-push! cfg max-token-length: <>)))) + cfg) + +(def (empty-httpd-config) + [config: 'httpd-v0]) + +(def (load-httpd-config path) + (load-config path 'httpd-v0)) + +(def (load-default-server-config) + (load-httpd-config (httpd-config-path))) + +(def (httpd-config-path (base (gerbil-path))) + (path-expand "config/httpd" (gerbil-path))) + +(def (get-ensemble-config opt) + (let (path (or (hash-get opt 'config) (ensemble-config-path))) + (if (file-exists? path) + (load-ensemble-config-file path) + (empty-ensemble-config)))) + +(def (get-httpd-config opt) + (let (path (or (hash-get opt 'config) (httpd-config-path))) + (if (file-exists? path) + (load-httpd-config path) + (empty-httpd-config)))) diff --git a/src/tools/gxhttpd/opt.ss b/src/tools/gxhttpd/opt.ss new file mode 100644 index 000000000..705779844 --- /dev/null +++ b/src/tools/gxhttpd/opt.ss @@ -0,0 +1,119 @@ +;;; -*- Gerbil -*- +;;; © vyzo +;;; The Gerbil HTTP Daemon +;;; +(import :std/cli/getopt + :std/config) +(export #t) + +(def server-config-option + (option 'config "-c" "--config" + help: "location of the httpd configuration; when running as a standalone server it defaults to $GERBIL_PATH/httpd/config; when running as part of an ensemble this option is ignored")) + +(def ensemble-config-option + (option 'config "-c" "--config" + help: "location of the httpd ensemble configuration; it defaults to $GERBIL_PATH/ensemble/config")) + +(def server-id-optional-argument + (optional-argument 'server-id + value: string->object + help: "when running as part of an ensemble, this is the ensemble server id")) + +(def server-cmd + (command 'server + server-config-option + server-id-optional-argument + help: "runs a single httpd server")) + +(def ensemble-cmd + (command 'ensemble + ensemble-config-option + help: "runs a supervied httpd server ensemble")) + +(def config-ensemble-flag + (flag 'ensemble "--ensemble" + help: "configure the httpd ensemble")) + +(def config-print-flag + (flag 'print "-p" "--print" + help: "print the configuration")) + +(def config-set-flag + (flag 'set "--set" + help: "override the configuration instead of merging")) + +(def config-path-option + (option 'config "-c" "--config" + help: "specify the configuration path")) + +(def config-httpd-root-option + (option 'root "--root" + default: "www" + help: "specify the httpd server's content root path")) + +(def config-httpd-listen-option + (option 'listen "--listen" + value: string->object + default: '("0.0.0.0:8080") + help: "specify the httpd server's listen addresses")) + +(def config-httpd-handlers-option + (option 'handlers "--handlers" + value: string->object + default: [] + help: "specify the httpd server's handler list")) + +(def config-httpd-servlets-flag + (flag 'enable-servlets "--enable-servlets" + help: "enable servlets")) + +(def config-httpd-log-option + (option 'log-dir "--log-dir" + help: "specify the httpd log directory")) + +(def config-httpd-max-token-length-option + (option 'max-token-length "--max-token-length" + value: string->integer + help: "specify the httpd max token length")) + +(def config-ensemble-workers-option + (option 'workers "-n" "--workers" + value: string->integer + help: "specify the preloaded number of httpd workers in the ensemble")) + +(def config-ensemble-domain-option + (option 'ensemble-domain "-D" "--ensemble-domain" + value: string->symbol + default: '/ + help: "specify the ensemble domain")) + +(def config-ensemble-worker-domain-option + (option 'worker-domain "--worker-domain" + value: string->symbol + default: 'www + help: "specify the httpd ensemble worker (sub) domain")) + +(def config-ensemble-root-option + (option 'ensemble-root "--ensemble-root" + help: "specify the ensemble root directory")) + +(def config-cmd + (command 'config + config-ensemble-flag + config-set-flag + config-print-flag + config-path-option + ;; server configuration options + config-httpd-root-option + config-httpd-listen-option + config-httpd-handlers-option + config-httpd-servlets-flag + config-httpd-log-option + config-httpd-max-token-length-option + ;; ensemble configuration options + config-ensemble-domain-option + config-ensemble-root-option + config-ensemble-workers-option + config-ensemble-worker-domain-option + ;; help! + help: "edit httpd server or ensemble configuration")) diff --git a/src/tools/gxhttpd/server.ss b/src/tools/gxhttpd/server.ss new file mode 100644 index 000000000..dba9db65b --- /dev/null +++ b/src/tools/gxhttpd/server.ss @@ -0,0 +1,281 @@ +;;; -*- Gerbil -*- +;;; © vyzo +;;; The Gerbil HTTP Daemon +(import :gerbil/expander + :std/config + :std/net/address + :std/net/httpd + :std/mime/types + :std/iter + :std/misc/ports + :std/hash-table + :std/logger + (only-in :std/os/socket SO_REUSEADDR SO_REUSEPORT) + (only-in :std/srfi/13 string-contains)) +(export #t) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; Server Implementation +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +(def current-http-server-config + (make-parameter #f)) + +(def (run-server! cfg) + (let* ((sockopts [SO_REUSEADDR SO_REUSEPORT]) + (mux (make-mux cfg)) + (request-logger (get-request-logger cfg)) + (addresses (config-get! cfg listen:)) + (max-token-length (: (config-get cfg max-token-length: 1024) :fixnum))) + (set-httpd-max-token-length! max-token-length) + (parameterize ((current-http-server-config cfg)) + (let (srv (apply start-http-server! + mux: mux + sockopts: sockopts + request-logger: request-logger + addresses)) + (thread-join! srv))))) + +(def (get-request-logger cfg) + (alet (path + (cond + ((config-get cfg request-log:)) + ((current-log-directory) + => (lambda (logdir) + (path-expand "httpd/request.log" logdir))) + (else #f))) + (make-request-logger path))) + +(def (make-mux cfg) + (Mux (make-dynamic-mux cfg))) + +(defstruct dynamic-mux ((root :- :string) + (handlers :- HashTable) + (servlets :- HashTable) + (mx :- :mutex) + (cache :- HashTable) + (cache-ttl :- :real) + (cache-max-size :- :fixnum)) + constructor: :init! final: #t) + +(defstruct cache-entry ((handler :- :procedure) + (expire :- :flonum) + (preserve? :- :procedure)) + final: #t) + +(defmethod {:init! dynamic-mux} + (lambda (self cfg) + (let ((root (: (config-get! cfg root:) :string)) + (handlers (: (config-get cfg handlers: []) :list)) + (servlets? (: (config-get cfg enable-servlets:) :boolean))) + (set! self.root root) + (set! self.cache (make-hash-table-string)) + (set! self.cache-ttl (: (inexact (config-get cfg cache-ttl: 120)) :real)) + (set! self.cache-max-size (: (config-get cfg cache-max-size: 16384) :fixnum)) + (set! self.handlers (make-hash-table-string)) + (when servlets? + (set! self.servlets (make-hash-table-string)) + (set! self.mx (make-mutex 'mux-loader))) + (for ([path . handler-module] handlers) + (let* ((ctx (import-module handler-module #f #t)) + (init! (find-runtime-symbol ctx 'handler-init!)) + (handle-request (find-runtime-symbol ctx 'handle-request))) + (unless handle-request + (error "handler module does not export handle-request procedure" + module: handler-module)) + (when init! + ((: (eval init!) :procedure) cfg)) + (hash-put! self.handlers path (: (eval handle-request) :procedure))))))) + +(defmethod {get-handler dynamic-mux} + (lambda (self host (path :- :string)) + ;; flush the cache if it gets too big + (when (fx> (hash-length self.cache) self.cache-max-size) + (set! self.cache (make-hash-table-string))) + (cond + ((hash-get self.cache path) + => (lambda (cache-entry) + (let (now (##current-time-point)) + (cond + ((fl< now (&cache-entry-expire cache-entry)) + (&cache-entry-handler cache-entry)) + (((&cache-entry-preserve? cache-entry)) + (set! (&cache-entry-expire cache-entry) + (fl+ now self.cache-ttl)) + (&cache-entry-handler cache-entry)) + (else + {self.__get-handler path}))))) + (else + {self.__get-handler path})))) + +(defmethod {__get-handler dynamic-mux} + (lambda (self (path :- :string)) + (defrule (not-found-cache-entry expire) + (cache-entry not-found-handler expire (lambda () #f))) + + (defrule (file-cache-entry file-path expire created handler) + (let (preserve? + (lambda () + (and (file-exists? file-path) + (fl< (time->seconds + (file-info-last-modification-time + (file-info file-path #t))) + created)))) + (cache-entry handler expire preserve?))) + + (let* ((now (##current-time-point)) + (expire (+ now self.cache-ttl)) + (entry + (let (server-path (server-request-path path)) + (cond + ((not server-path) + (not-found-cache-entry expire)) + ((find-handler self.handlers server-path) + => (lambda (handler) + (cache-entry handler expire (lambda () #t)))) + (else + (let (file-path (string-append self.root server-path)) + (if (file-exists? file-path) + (if (and self.servlets (equal? ".ss" (path-extension file-path))) + (file-cache-entry file-path expire now + (find-servlet-handler self.servlets self.mx file-path)) + (file-cache-entry file-path expire now + (file-handler file-path))) + (not-found-cache-entry expire)))))))) + (hash-put! self.cache path entry) + (&cache-entry-handler entry)))) + +(defmethod {put-handler! dynamic-mux} + (lambda (self host (path :- :string) (handler :- :procedure)) + (hash-put! self.handlers path handler))) + +(def (not-found-handler req res) + (http-response-write-condition res Not-Found)) + +(def (forbidden-handler req res) + (http-response-write-condition res Forbidden)) + +(defstruct servlet ((handler :- :procedure) + (path :- :string) + (timestamp :- :flonum)) + final: #t) + +(def (find-servlet-handler servlet-tab mx file-path) + (def (load-servlet! file-path reload?) + (let* ((load-time (time->seconds (current-time))) + (ctx (with-lock mx (cut import-module file-path reload? #t))) + (init! (find-runtime-symbol ctx 'handler-init!)) + (handle-request (find-runtime-symbol ctx 'handle-request))) + (unless handle-request + (error "servlet does not export handle-request" file-path)) + (when init! + ((eval init!) (current-http-server-config))) + (let* ((handle-request (: (eval handle-request) :procedure)) + (srv (servlet handle-request file-path load-time))) + (hash-put! servlet-tab file-path srv) + srv))) + + (cond + ((hash-get servlet-tab file-path) + => (lambda (srv) + (using (srv :- servlet) + (let (modtime + (time->seconds + (file-info-last-modification-time + (file-info file-path #t)))) + (if (> modtime srv.timestamp) + (servlet-handler (load-servlet! file-path #t)) + srv.handler))))) + (else + (servlet-handler (load-servlet! file-path #f))))) + +(def (file-handler path) + => :procedure + (let (info (file-info path #t)) + (if (eq? (file-info-type info) 'directory) + (let (index-html-path (path-expand "index.html" path)) + (if (file-exists? index-html-path) + (serve-file index-html-path (file-info index-html-path #t)) + forbidden-handler)) + (serve-file path info)))) + +(def max-file-cache-size 32768) ; size of i/o buffer for http-response-file + +(def (serve-file path info) + => :procedure + (let* ((content-type (path-extension->mime-type-name path)) + (headers + [(if content-type + ["Content-Type" :: content-type] + ["Content-Type" :: "application/octet-stream"]) + ["Last-Modified" :: (number->string (exact (floor (time->seconds (file-info-last-modification-time info)))))] + ["Content-Length" :: (number->string (file-info-size info))]])) + + (if (fx<= (file-info-size info) max-file-cache-size) + ;; cache the content + (let (buf (read-file-u8vector path)) + (lambda (req res) + (using (req :- http-request) + (case req.method + ((GET) + (http-response-write res 200 headers buf)) + ((HEAD) + (http-response-write res 200 headers #f)) + (else + (http-response-write-condition res Forbidden)))))) + ;; don't cache + (lambda (req res) + (using (req :- http-request) + (case req.method + ((GET) + (http-response-file res headers path)) + ((HEAD) + (http-response-write res 200 headers #f)) + (else + (http-response-write-condition res Forbidden)))))))) + +(def (find-handler tab server-path) + (let loop ((path server-path)) + (cond + ((string-empty? path) #f) + ((hash-get tab path)) + ((string-rindex path #\/) + => (lambda (index) (loop (substring path 0 index)))) + (else #f)))) + +(def (server-request-path path) + (let (components (string-split path #\/)) + (let loop ((rest components) (r [])) + (match rest + ([hd . rest] + (case hd + (("" ".") (loop rest r)) + (("..") + (if (null? r) + #f ; invalid, out of root bounds + (loop rest (cdr r)))) + (else + (loop rest (cons hd r))))) + (else + (if (null? r) + "/" + (string-join (cons "" (reverse r)) "/"))))))) + +(def (find-runtime-symbol ctx id) + (cond + ((find-export-binding ctx id) + => (lambda (bind) + (unless (runtime-binding? bind) + (error "export is not a runtime binding" symbol: id)) + (binding-id bind))) + (else #f))) + +(def (find-export-binding ctx id) + (cond + ((find (match <> + ((? module-export? xport) + (and (eqv? (module-export-phi xport) 0) + (eq? (module-export-name xport) id))) + (else #f)) + (module-context-export ctx)) + => core-resolve-module-export) + (else #f))) diff --git a/src/tutorial/advanced-ensemble/rlb/build.ss b/src/tutorial/advanced-ensemble/rlb/build.ss new file mode 100755 index 000000000..107fc090d --- /dev/null +++ b/src/tutorial/advanced-ensemble/rlb/build.ss @@ -0,0 +1,5 @@ +#!/usr/bin/env gxi +(import :std/build-script) + +(defbuild-script + '((exe: "rlb"))) diff --git a/src/tutorial/advanced-ensemble/rlb/gerbil.pkg b/src/tutorial/advanced-ensemble/rlb/gerbil.pkg new file mode 100644 index 000000000..24364cc02 --- /dev/null +++ b/src/tutorial/advanced-ensemble/rlb/gerbil.pkg @@ -0,0 +1 @@ +(package: demo) diff --git a/src/tutorial/advanced-ensemble/rlb/rlb.ss b/src/tutorial/advanced-ensemble/rlb/rlb.ss new file mode 100644 index 000000000..3f0a1f5cf --- /dev/null +++ b/src/tutorial/advanced-ensemble/rlb/rlb.ss @@ -0,0 +1,65 @@ +(import :std/cli/getopt + :std/actor + :std/config + :std/logger + :std/sugar + :std/io + :std/misc/shuffle + :std/os/socket) +(export main) + +(deflogger rlb) + +(def (main . args) + (call-with-getopt rlb-main args + program: "rlb" + help: "A simple random selection load balancer" + (argument 'server-id + help: "the server id"))) + +(def (rlb-main opt) + (let* ((server-id (hash-ref opt 'server-id)) + (cfg (load-ensemble-server-config (string->server-identifier server-id)))) + (become-ensemble-server! cfg (cut start-rlb! cfg)))) + +(def (start-rlb! cfg) + (let* ((app-config (config-get! cfg application:)) + (rlb-config (agetq 'rlb app-config)) + (listen (config-get! rlb-config listen:)) + (proxies (config-get! rlb-config proxies:)) + (server-sock (tcp-listen listen sockopts: [SO_REUSEADDR SO_REUSEPORT]))) + (when (null? proxies) + (error "No proxies specified")) + (spawn/name 'rlb-proxy rlb-proxy server-sock proxies) + (spawn/name 'rlb rlb-actor server-sock))) + +(def (rlb-actor server-sock) + (let/cc exit + (while #t + (<- + ,(@shutdown + (infof "shutting down...") + (using (closer server-sock : Closer) + (closer.close)) + (exit 'shutdown)) + ,(@ping) + ,(@unexpected warnf))))) + +(def (rlb-proxy (server-sock : ServerSocket) proxies) + (while #t + (using (client (server-sock.accept) : StreamSocket) + (let (proxy (car (shuffle proxies))) + (infof "forwarding ~a to ~a" (client.peer-address) proxy) + (spawn/name 'rlb-forward rlb-forward client proxy))))) + +(def (rlb-forward (client : StreamSocket) proxy) + (try + (using (server (tcp-connect proxy) : StreamSocket) + (let ((thr1 (spawn/name 'io-copy io-copy! (client.reader) (server.writer))) + (thr2 (spawn/name 'io-copy io-copy! (server.reader) (client.writer)))) + (with-catch void (cut thread-join! thr1)) + (with-catch void (cut thread-join! thr2))) + (with-catch void (cut client.close)) + (with-catch void (cut server.close))) + (catch (e) + (errorf "error forwarding ~a to ~a: ~a" (client.peer-address) proxy e)))) diff --git a/src/tutorial/advanced-ensemble/site/project/build.ss b/src/tutorial/advanced-ensemble/site/project/build.ss new file mode 100755 index 000000000..236e9ec8b --- /dev/null +++ b/src/tutorial/advanced-ensemble/site/project/build.ss @@ -0,0 +1,5 @@ +#!/usr/bin/env gxi +(import :std/build-script) + +(defbuild-script + '("handler")) diff --git a/src/tutorial/advanced-ensemble/site/project/gerbil.pkg b/src/tutorial/advanced-ensemble/site/project/gerbil.pkg new file mode 100644 index 000000000..24364cc02 --- /dev/null +++ b/src/tutorial/advanced-ensemble/site/project/gerbil.pkg @@ -0,0 +1 @@ +(package: demo) diff --git a/src/tutorial/advanced-ensemble/site/project/handler.ss b/src/tutorial/advanced-ensemble/site/project/handler.ss new file mode 100644 index 000000000..510a1d177 --- /dev/null +++ b/src/tutorial/advanced-ensemble/site/project/handler.ss @@ -0,0 +1,11 @@ +(import :std/net/httpd + :std/format + :std/actor) +(export handle-request) + +(def (handle-request req res) + (http-response-write res 200 '(("Content-Type" . "text/plain")) + (format "hello! I am a dynamic handler and in ~a~n" + (if (current-actor-server) + (actor-server-identifier) + '(unknown))))) diff --git a/src/tutorial/advanced-ensemble/site/www/files/hello.txt b/src/tutorial/advanced-ensemble/site/www/files/hello.txt new file mode 100644 index 000000000..270c611ee --- /dev/null +++ b/src/tutorial/advanced-ensemble/site/www/files/hello.txt @@ -0,0 +1 @@ +hello, world! diff --git a/src/tutorial/advanced-ensemble/site/www/index.html b/src/tutorial/advanced-ensemble/site/www/index.html new file mode 100644 index 000000000..1171b3051 --- /dev/null +++ b/src/tutorial/advanced-ensemble/site/www/index.html @@ -0,0 +1,7 @@ + + + hello + + + hello, world! + diff --git a/src/tutorial/advanced-ensemble/site/www/index.txt b/src/tutorial/advanced-ensemble/site/www/index.txt new file mode 100644 index 000000000..270c611ee --- /dev/null +++ b/src/tutorial/advanced-ensemble/site/www/index.txt @@ -0,0 +1 @@ +hello, world! diff --git a/src/tutorial/advanced-ensemble/site/www/servlets/hello.ss b/src/tutorial/advanced-ensemble/site/www/servlets/hello.ss new file mode 100644 index 000000000..6b847ae03 --- /dev/null +++ b/src/tutorial/advanced-ensemble/site/www/servlets/hello.ss @@ -0,0 +1,11 @@ +(import :std/net/httpd + :std/format + :std/actor) +(export handle-request) + +(def (handle-request req res) + (http-response-write res 200 '(("Content-Type" . "text/plain")) + (format "hello! I am a servlet in ~a~n" + (if (current-actor-server) + (actor-server-identifier) + '(unknown)))))