From 5fecac210fcb3c844498421056354698d349e2d2 Mon Sep 17 00:00:00 2001 From: Sander van der Burg Date: Tue, 28 Jan 2020 00:11:29 +0100 Subject: [PATCH] Initial commit --- LICENSE | 18 + README.md | 458 ++++++++++++++++++ examples/services-agnostic/apache.nix | 34 ++ examples/services-agnostic/constructors.nix | 49 ++ examples/services-agnostic/mysql.nix | 49 ++ examples/services-agnostic/postgresql.nix | 57 +++ examples/services-agnostic/processes.nix | 48 ++ .../simple-appserving-tomcat.nix | 29 ++ .../static-webapp-apache.nix | 73 +++ examples/services-agnostic/tomcat.nix | 61 +++ examples/services-agnostic/webapp/index.html | 11 + examples/webapps-agnostic/constructors.nix | 29 ++ .../webapps-agnostic/distribution-empty.nix | 5 + examples/webapps-agnostic/distribution.nix | 6 + .../webapps-agnostic/errorpage/index.html | 10 + examples/webapps-agnostic/infra-bootstrap.nix | 4 + examples/webapps-agnostic/network-logical.nix | 41 ++ .../webapps-agnostic/network-physical.nix | 14 + .../webapps-agnostic/nginx-reverse-proxy.nix | 84 ++++ examples/webapps-agnostic/nginx.nix | 43 ++ .../webapps-agnostic/processes-advanced.nix | 96 ++++ examples/webapps-agnostic/processes.nix | 35 ++ examples/webapps-agnostic/services.nix | 45 ++ examples/webapps-agnostic/webapp-daemon.nix | 39 ++ examples/webapps-agnostic/webapp-fg.nix | 37 ++ examples/webapps-agnostic/webapp.nix | 40 ++ examples/webapps-sysvinit/constructors.nix | 34 ++ .../webapps-sysvinit/distribution-empty.nix | 5 + examples/webapps-sysvinit/distribution.nix | 6 + .../webapps-sysvinit/errorpage/index.html | 10 + examples/webapps-sysvinit/network-logical.nix | 17 + .../webapps-sysvinit/network-physical.nix | 14 + .../webapps-sysvinit/nginx-reverse-proxy.nix | 67 +++ examples/webapps-sysvinit/nginx.nix | 17 + .../webapps-sysvinit/processes-advanced.nix | 94 ++++ examples/webapps-sysvinit/processes.nix | 33 ++ examples/webapps-sysvinit/services.nix | 36 ++ examples/webapps-sysvinit/webapp.nix | 33 ++ nixproc/create-credentials/default.nix | 43 ++ .../agnostic/build-process-env-universal.nix | 40 ++ .../agnostic/create-agnostic-config.nix | 14 + .../create-managed-process-from-config.nix | 26 + .../create-managed-process-universal.nix | 98 ++++ .../agnostic/create-managed-process.nix | 59 +++ .../agnostic/generate-bsdrc-script.nix | 37 ++ .../agnostic/generate-cygrunsrv-params.nix | 57 +++ .../agnostic/generate-foreground-wrapper.nix | 83 ++++ .../agnostic/generate-launchd-daemon.nix | 73 +++ .../agnostic/generate-prestart-script.nix | 11 + .../agnostic/generate-supervisord-program.nix | 60 +++ .../agnostic/generate-systemd-service.nix | 52 ++ .../agnostic/generate-sysvinit-script.nix | 32 ++ .../bsdrc/build-bsdrc-env.nix | 30 ++ .../bsdrc/create-bsdrc-script.nix | 237 +++++++++ .../create-managed-process/bsdrc/rcsubr.nix | 12 + .../cygrunsrv/build-cygrunsrv-env.nix | 30 ++ .../cygrunsrv/create-cygrunsrv-params.nix | 112 +++++ .../launchd/build-launchd-env.nix | 30 ++ .../launchd/create-launchd-daemon.nix | 96 ++++ .../supervisord/build-supervisord-env.nix | 33 ++ .../create-supervisord-program.nix | 71 +++ .../supervisord/supervisord.conf | 10 + .../systemd/build-systemd-env.nix | 30 ++ .../systemd/create-systemd-service.nix | 117 +++++ .../create-managed-process/sysvinit/README.md | 457 +++++++++++++++++ .../sysvinit/build-sysvinit-env.nix | 30 ++ .../sysvinit/create-sysvinit-script.nix | 247 ++++++++++ .../sysvinit/init-functions.nix | 26 + tools/bsdrc/default.nix | 21 + tools/bsdrc/nixproc-bsdrc-runactivity.in | 91 ++++ tools/bsdrc/nixproc-bsdrc-switch.in | 197 ++++++++ tools/build/default.nix | 15 + tools/build/nixproc-build.in | 138 ++++++ tools/commonchecks | 96 ++++ tools/cygrunsrv/default.nix | 14 + tools/cygrunsrv/nixproc-cygrunsrv-switch.in | 167 +++++++ tools/default.nix | 37 ++ tools/generate-config/default.nix | 14 + .../nixproc-generate-config.in | 111 +++++ tools/launchd/default.nix | 15 + tools/launchd/nixproc-launchd-switch.in | 173 +++++++ tools/supervisord/default.nix | 32 ++ .../nixproc-supervisord-deploy-stateless.in | 95 ++++ .../supervisord/nixproc-supervisord-start.in | 92 ++++ .../supervisord/nixproc-supervisord-switch.in | 178 +++++++ tools/supervisord/supervisordchecks | 10 + tools/systemd/default.nix | 14 + tools/systemd/nixproc-systemd-switch.in | 198 ++++++++ tools/sysvinit/default.nix | 22 + .../sysvinit/nixproc-sysvinit-runactivity.in | 100 ++++ tools/sysvinit/nixproc-sysvinit-switch.in | 181 +++++++ tools/sysvinit/sysvinitchecks | 9 + webapp/Makefile | 14 + webapp/README.md | 18 + webapp/daemonize.c | 259 ++++++++++ webapp/daemonize.h | 29 ++ webapp/default.nix | 11 + webapp/main.c | 44 ++ webapp/service.c | 158 ++++++ webapp/service.h | 9 + 100 files changed, 6596 insertions(+) create mode 100644 LICENSE create mode 100644 README.md create mode 100644 examples/services-agnostic/apache.nix create mode 100644 examples/services-agnostic/constructors.nix create mode 100644 examples/services-agnostic/mysql.nix create mode 100644 examples/services-agnostic/postgresql.nix create mode 100644 examples/services-agnostic/processes.nix create mode 100644 examples/services-agnostic/simple-appserving-tomcat.nix create mode 100644 examples/services-agnostic/static-webapp-apache.nix create mode 100644 examples/services-agnostic/tomcat.nix create mode 100644 examples/services-agnostic/webapp/index.html create mode 100644 examples/webapps-agnostic/constructors.nix create mode 100644 examples/webapps-agnostic/distribution-empty.nix create mode 100644 examples/webapps-agnostic/distribution.nix create mode 100644 examples/webapps-agnostic/errorpage/index.html create mode 100644 examples/webapps-agnostic/infra-bootstrap.nix create mode 100644 examples/webapps-agnostic/network-logical.nix create mode 100644 examples/webapps-agnostic/network-physical.nix create mode 100644 examples/webapps-agnostic/nginx-reverse-proxy.nix create mode 100644 examples/webapps-agnostic/nginx.nix create mode 100644 examples/webapps-agnostic/processes-advanced.nix create mode 100644 examples/webapps-agnostic/processes.nix create mode 100644 examples/webapps-agnostic/services.nix create mode 100644 examples/webapps-agnostic/webapp-daemon.nix create mode 100644 examples/webapps-agnostic/webapp-fg.nix create mode 100644 examples/webapps-agnostic/webapp.nix create mode 100644 examples/webapps-sysvinit/constructors.nix create mode 100644 examples/webapps-sysvinit/distribution-empty.nix create mode 100644 examples/webapps-sysvinit/distribution.nix create mode 100644 examples/webapps-sysvinit/errorpage/index.html create mode 100644 examples/webapps-sysvinit/network-logical.nix create mode 100644 examples/webapps-sysvinit/network-physical.nix create mode 100644 examples/webapps-sysvinit/nginx-reverse-proxy.nix create mode 100644 examples/webapps-sysvinit/nginx.nix create mode 100644 examples/webapps-sysvinit/processes-advanced.nix create mode 100644 examples/webapps-sysvinit/processes.nix create mode 100644 examples/webapps-sysvinit/services.nix create mode 100644 examples/webapps-sysvinit/webapp.nix create mode 100644 nixproc/create-credentials/default.nix create mode 100644 nixproc/create-managed-process/agnostic/build-process-env-universal.nix create mode 100644 nixproc/create-managed-process/agnostic/create-agnostic-config.nix create mode 100644 nixproc/create-managed-process/agnostic/create-managed-process-from-config.nix create mode 100644 nixproc/create-managed-process/agnostic/create-managed-process-universal.nix create mode 100644 nixproc/create-managed-process/agnostic/create-managed-process.nix create mode 100644 nixproc/create-managed-process/agnostic/generate-bsdrc-script.nix create mode 100644 nixproc/create-managed-process/agnostic/generate-cygrunsrv-params.nix create mode 100644 nixproc/create-managed-process/agnostic/generate-foreground-wrapper.nix create mode 100644 nixproc/create-managed-process/agnostic/generate-launchd-daemon.nix create mode 100644 nixproc/create-managed-process/agnostic/generate-prestart-script.nix create mode 100644 nixproc/create-managed-process/agnostic/generate-supervisord-program.nix create mode 100644 nixproc/create-managed-process/agnostic/generate-systemd-service.nix create mode 100644 nixproc/create-managed-process/agnostic/generate-sysvinit-script.nix create mode 100644 nixproc/create-managed-process/bsdrc/build-bsdrc-env.nix create mode 100644 nixproc/create-managed-process/bsdrc/create-bsdrc-script.nix create mode 100644 nixproc/create-managed-process/bsdrc/rcsubr.nix create mode 100644 nixproc/create-managed-process/cygrunsrv/build-cygrunsrv-env.nix create mode 100644 nixproc/create-managed-process/cygrunsrv/create-cygrunsrv-params.nix create mode 100644 nixproc/create-managed-process/launchd/build-launchd-env.nix create mode 100644 nixproc/create-managed-process/launchd/create-launchd-daemon.nix create mode 100644 nixproc/create-managed-process/supervisord/build-supervisord-env.nix create mode 100644 nixproc/create-managed-process/supervisord/create-supervisord-program.nix create mode 100644 nixproc/create-managed-process/supervisord/supervisord.conf create mode 100644 nixproc/create-managed-process/systemd/build-systemd-env.nix create mode 100644 nixproc/create-managed-process/systemd/create-systemd-service.nix create mode 100644 nixproc/create-managed-process/sysvinit/README.md create mode 100644 nixproc/create-managed-process/sysvinit/build-sysvinit-env.nix create mode 100644 nixproc/create-managed-process/sysvinit/create-sysvinit-script.nix create mode 100644 nixproc/create-managed-process/sysvinit/init-functions.nix create mode 100644 tools/bsdrc/default.nix create mode 100644 tools/bsdrc/nixproc-bsdrc-runactivity.in create mode 100644 tools/bsdrc/nixproc-bsdrc-switch.in create mode 100644 tools/build/default.nix create mode 100644 tools/build/nixproc-build.in create mode 100644 tools/commonchecks create mode 100644 tools/cygrunsrv/default.nix create mode 100644 tools/cygrunsrv/nixproc-cygrunsrv-switch.in create mode 100644 tools/default.nix create mode 100644 tools/generate-config/default.nix create mode 100644 tools/generate-config/nixproc-generate-config.in create mode 100644 tools/launchd/default.nix create mode 100644 tools/launchd/nixproc-launchd-switch.in create mode 100644 tools/supervisord/default.nix create mode 100644 tools/supervisord/nixproc-supervisord-deploy-stateless.in create mode 100644 tools/supervisord/nixproc-supervisord-start.in create mode 100644 tools/supervisord/nixproc-supervisord-switch.in create mode 100644 tools/supervisord/supervisordchecks create mode 100644 tools/systemd/default.nix create mode 100644 tools/systemd/nixproc-systemd-switch.in create mode 100644 tools/sysvinit/default.nix create mode 100644 tools/sysvinit/nixproc-sysvinit-runactivity.in create mode 100644 tools/sysvinit/nixproc-sysvinit-switch.in create mode 100644 tools/sysvinit/sysvinitchecks create mode 100644 webapp/Makefile create mode 100644 webapp/README.md create mode 100644 webapp/daemonize.c create mode 100644 webapp/daemonize.h create mode 100644 webapp/default.nix create mode 100644 webapp/main.c create mode 100644 webapp/service.c create mode 100644 webapp/service.h diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..b85841c --- /dev/null +++ b/LICENSE @@ -0,0 +1,18 @@ +Copyright (c) 2020 Sander van der Burg + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..9aac954 --- /dev/null +++ b/README.md @@ -0,0 +1,458 @@ +Nix-based process management framework +====================================== +This repository contains a very experimental prototype implementation of a +operating system and process manager agnostic Nix-based process managed +framework that can be used to run multiple instances of services on a single +machine, using Nix to deliver and isolate all required package dependencies and +configuration files. + +Features: +* It uses *simple conventions* to specify system configurations: function + definitions (corresponding to constructors), function invocations (that + compose running process instances from constructors) and Nix profiles (that + assembles multiple process configurations into one package). +* The ability to deploy *multiple instances* of processes. +* Deploying processes/services as an *unprivileged* user. +* Operating system and process manager *agnostic* -- it can be used on any + operating system that supports the Nix package manager and works with a + variety of process managers. +* Advanced concepts and features, such as namespaces and cgroups, are not + required. + +Supported process managers +========================== +Currently, the following process managers are supported: + +* `sysvinit`: sysvinit scripts, also known as [LSB Init scripts](https://wiki.debian.org/LSBInitScripts) +* `bsdrc`: BSD [rc scripts](https://www.freebsd.org/doc/en_US.ISO8859-1/articles/rc-scripting/index.html) +* `supervisord`: [supervisord](http://supervisord.org) +* `systemd`: [systemd](https://www.freedesktop.org/wiki/Software/systemd) +* `launchd`: [launchd](https://www.launchd.info) +* `cygrunsrv`: Cygwin's [cygrunsrv](http://web.mit.edu/cygwin/cygwin_v1.3.2/usr/doc/Cygwin/cygrunsrv.README) + +Prerequisites +============= +To use this framework, you first need to install: + +* [The Nix package manager](http://nixos.org/nix) +* [The Nixpkgs collection](http://nixos.org/nixpkgs) +* [Dysnomia](http://github.com/svanderburg/dysnomia), if you want to manage users and groups + +Installation +============ +First, make a Git clone of this repository. + +The next step is installing the build tool: + +```bash +$ cd tools +$ nix-env -f default.nix -iA build +``` + +Then, at least one tool that deploys a configuration for a supported process +manager must be installed. + +For example, to work with sysvinit scripts, you must install: + +```bash +$ nix-env -f default.nix -iA sysvinit +``` + +To work with a different process manager, you should replace `sysvinit` with +any of the supported process managers listed above. + +Usage +===== +For each kind of process you need to write a Nix expression that constructs +its configuration from sources and its dependencies. This can be done in a +process manager-specific and a process manager-agnostic way. + +Then you need to create a constructors and processes expression. + +The processes expression can be used by a deploy tool that works with a +specific process manager. + +Writing a process manager-specific process management configuration +------------------------------------------------------------------- +The following expression is an example of a configuration that deploys +a sysvinit script that can be used to control a simple web application +process that just returns a static HTML page: + +```nix +{createSystemVInitScript, runtimeDir}: +{port, instanceSuffix ? ""}: + +let + webapp = import ../../webapp; + instanceName = "webapp${instanceSuffix}"; +in +createSystemVInitScript { + name = instanceName; + inherit instanceName; + process = "${webapp}/bin/webapp"; + args = [ "-D" ]; + environment = { + PORT = port; + PID_FILE = "${runtimeDir}/${instanceName}.pid"; + }; + + runlevels = [ 3 4 5 ]; + + user = instanceName; + + credentials = { + groups = { + "${instanceName}" = {}; + }; + users = { + "${instanceName}" = { + group = instanceName; + description = "Webapp"; + }; + }; + }; +} +``` + +A process expression defines a nested function: + +* The outer function (first line) allows properties to be configured that apply + to all process instances, such as the the function that constructs sysvinit + scripts (`createSystemVInitScript`) and the `runtimeDir` in which PID files + are stored (on most systems this defaults to: `/var/run`). +* The inner function (second line) refers to all instance specific parameters. + To allow multiple instances to co-exist, instance parameters must be + configured in such a way that they no longer conflict. For example, if we + assign two unique TCP `port` numbers and we append the process name with a + unique suffix, we can run two instances of the web application at the same + time. + +In the body, we invoke the `createSystemVInitScript` function to declaratively +construct a sysvinit script: + +* The `process` parameter specifies which process should be managed. From this + parameter, the generator will automatically derive `start`, `stop`, `restart` + and `reload` activities. +* The `args` parameter specifies the command line parameters propagated to the + process. The `-D` parameter specifies that the webapp process should run in + daemon mode (i.e. the process spawns another process that keeps running in + the background and then terminates immediately). +* The `environment` attribute set defines all environment variables that the + webapp process needs -- `PORT` is used to specify the TCP port it should + listen to and `PID_FILE` specifies the path to the PID file that stores + the process ID (PID) of the daemon process +* To allow multiple processes to co-exist, we must make sure that each has + a unique PID file name. We can use the `instanceName` parameter to specify + what name this PID file should have. By default, it gets the same name as + the process name. +* The `runlevels` parameter specifies in which run levels the process should + be started (in the above example: 3, 4, and 5. It will automatically + configure the init system to stop the process in the other runlevels: + 0, 1, 2, and 6. +* It is also typically not recommended to run a service as root user. The + `credentials` attribute specifies which group and user account need to be + created. The `user` parameter specifies as which user the process needs to + run at. + +The `createSystemVInitScript` function support many more parameters than +described in the example above. For example, it is also possible to directly +specify how activities should be implemented. + +It can also be used to specify dependencies on other sysvinit scripts -- the +system will automatically derive the sequence numbers so that they are activated +and deactivated in the right order. + +In addition to sysvinit, there are also functions that can be used to create +configurations for the other supported process managers, e.g. +`createSystemdUnit`, `createSupervisordProgram`, `createBSDRCScript`. Check +the implementations in `nixproc/create-managed-process` for more information. + +Writing a process manager-agnostic process management configuration +------------------------------------------------------------------- +This repository contains generator functions for a variety of process managers. +What you will notice is that they require parameters that looks quite similar. + +When it is desired to target multiple process managers, it is also possible to +write a process manager-agnostic configuration from which a variety of +configurations can be generated. + +This expression is a process manager-agnostic version of the previous example: + +```nix +{createManagedProcess, runtimeDir}: +{port, instanceSuffix ? ""}: + +let + webapp = import ../../webapp; + instanceName = "webapp${instanceSuffix}"; +in +createManagedProcess { + name = instanceName; + description = "Simple web application"; + inherit instanceName; + + # This expression can both run in foreground or daemon mode. + # The process manager can pick which mode it prefers. + process = "${webapp}/bin/webapp"; + daemonArgs = [ "-D" ]; + + environment = { + PORT = port; + PID_FILE = "${runtimeDir}/${instanceName}.pid"; + }; + user = instanceName; + credentials = { + groups = { + "${instanceName}" = {}; + }; + users = { + "${instanceName}" = { + group = instanceName; + description = "Webapp"; + }; + }; + }; + + overrides = { + sysvinit = { + runlevels = [ 3 4 5 ]; + }; + }; +} +``` + +In the above example, we invoke `createManagedProcess` to construct a +configuration for a process manager. It captures similar properties that +are described in the sysvinit-specific configuration, as shown in the previous +example. + +To allow it to target a variety of process managers, we must specify: + +* How the process can be started in foreground mode. The `process` + parameter gets translated to `foregroundProcess` and `daemon`. The former + specifies how the service should be started as a foreground process and the + latter how it should start as a daemon. +* The `daemonArgs` parameter specifies which command-line parameters the process + should take + +Under the hood, the `createManagedProcess` function invokes a generator function +which calls the corresponding process manager-specific create function. + +The `createManagedProcess` abstraction function does not support all +functionality that the process manager-specific abstraction functions provide -- +It only supports a common subset. To get non-standardized functionality working, +you can also define `overrides`, that augments the generated function parameters +with process manager-specific parameters. + +In the above example, we define an override to specify the `runlevels`. Runlevels +is a concept only supported by sysvinit scripts. + +Writing a constructors expression +--------------------------------- +As shown in the previous sections, a process configuration is a nested function. +To be able to deploy a certain process configuration, it needs to be composed +twice. + +The common parameters (the outer function) are composed in a so-called +constructors expression, that has the following structure: + +```nix +{ pkgs +, stateDir +, logDir +, runtimeDir +, tmpDir +, forceDisableUserChange +, processManager +}: + +let + createManagedProcess = import ../../nixproc/create-managed-process/agnostic/create-managed-process-universal.nix { + inherit pkgs runtimeDir tmpDir forceDisableUserChange processManager; + }; +in +{ + webapp = import ./webapp.nix { + inherit createManagedProcess runtimeDir; + }; + + nginxReverseProxy = import ./nginx-reverse-proxy.nix { + inherit createManagedProcess stateDir logDir runtimeDir forceDisableUserChange; + inherit (pkgs) stdenv writeTextFile nginx; + }; +} +``` + +The above Nix expression defines a function that takes common state +configuration parameters that applies to all services: + +* `pkgs` refers to the Nixpkgs collection +* `stateDir` refers to the base directory where all variable data needs to be + stored. The default on most systems is `/var`. +* `runtimeDir` refers to the base directory where all PID files are stored +* `tmpDir` refers to the base directory where all temp files are stored +* `forceDisableUserChange` can be used to globally switch of the creation of + users and groups and changing users. +* `processManager` specifies which process manager we want to use (when it is + desired to do process manager agnostic deployments). + +In the body, the function returns an attribute set in which every value refers +to a constructor function that can be used to construct process instances. + +The `webapp` attribute refers to a constructor function that can be used to +construct one or more running `webapp` processes. It takes the common parameters +that it requires as function arguments. + +The constructors attribute facilitates multiple constructor functions. +`nginxReverseProxy` refers to a process configuration that launches the +[Nginx](http://nginx.com) HTTP server and configures it to act as a reverse +proxy for an arbitrary number of web application processes. + +Writing a processes expression +------------------------------ +The processes Nix expression makes it possible to construct one or more +instances of processes: + +```nix +{ pkgs ? import { inherit system; } +, system ? builtins.currentSystem +, stateDir ? "/var" +, runtimeDir ? "${stateDir}/run" +, logDir ? "${stateDir}/log" +, tmpDir ? (if stateDir == "/var" then "/tmp" else "${stateDir}/tmp") +, forceDisableUserChange ? false +, processManager +}: + +let + constructors = import ./constructors.nix { + inherit pkgs stateDir runtimeDir logDir tmpDir forceDisableUserChange processManager; + }; +in +rec { + webapp1 = rec { + port = 5000; + dnsName = "webapp1.local"; + + pkg = constructors.webapp { + inherit port; + }; + }; + + webapp2 = rec { + port = 5001; + dnsName = "webapp2.local"; + + pkg = constructors.webapp { + inherit port; + }; + }; + + nginxReverseProxy = rec { + port = 8080; + + pkg = constructors.nginxReverseProxy { + webapps = [ webapp1 webapp2 ]; + inherit port; + } {}; + }; +} +``` + +The above Nix expression is a function in which the parameters (again) specify +common state configuration parameters. It makes a reference the constructors +expression shown in the previous example. + +The function body returns an attribute set that defines three process +instances: + +* `webapp1` is a web application process that listens on TCP port 5000 +* `webapp2` is a web application process that listens on TCP port 5001 +* `nginxReverseProxy` is an Nginx server that forwards requests to the + web application processes. If the virtual host is `webapp1.local` then the + first `webapp1` process responds, if the virtual host is `webapp2.local` then + the second process (`webapp2`) responds. + +Building a process configurations profile +----------------------------------------- +We can build all required packages and generate all configuration artifacts for +a specific process manager by running the following command: + +```bash +$ nixproc-build --process-manager sysvinit processes.nix +result/ +``` + +The above command generates sysvinit scripts and start and stop symlinks to +ensure that the webapp processes are started before the Nginx reverse proxy. + +The `--process-manager` parameter can be changed to generate configuration files +for different process managers. For example, if we would use +`--process-manager systemd` then the resulting Nix profile contains a collection +of systemd unit configuration files. + +Deploying a process configurations profile +------------------------------------------ +In addition to generating configuration files that can be consumed by a process +manager, we can also invoke the process manager to deploy all process defined in +our process Nix expression. + +The following command automatically starts all sysvinit scripts (and stop all +obsolete sysvinit scripts, in case of an upgrade): + +```bash +$ nixproc-sysvinit-switch processes.nix +``` + +Consult the help pages of the corresponding process manager specific tools to +get a better understanding on how they work. + +Changing the state directories +------------------------------ +By default, the all processes use the `/var` directory as a base directory to +store all state. This location can be adjusted by using the `--state-dir` +parameter. + +The following command deploys all process instances and stores their state in +`/home/sander/var`: + +```bash +$ nixproc-sysvinit-switch --state-dir /home/sander/var processes.nix +``` + +Similarly, it is also possible to adjust the base locations of the runtime +files, log files and temp files, if desired. + +Deploying process instances as an unprivileged user +--------------------------------------------------- +It is also possible to do unprivileged user deployments. Unfortunately, +unprivileged users cannot create new groups and/or users or change permissions +of running processes. + +This can be globally disabled with the `--force-disable-user-change` parameter. + +The following command makes it possible to deploy all processes as an +unprivileged user: + +```bash +$ nixproc-sysvinit-switch --state-dir /home/sander/var --force-disable-user-change processes.nix +``` + +Examples +======== +This repository contains three example systems, that can be found in the +`examples/` folder: + +* `webapps-sysvinit` is a processes configuration set using the example `webapp` + process described in this README, with `nginx` reverse proxies. The + configuration is specifically written to use sysvinit as a process manager. +* `webapps-agnostic` is the same as the previous example, but using a process + manager agnostic configuration. It can be used to target all process managers + that this toolset supports. +* `services-agnostic` is a process manager-agnostic configuration set of common + system services: Apache HTTP server, MySQL, PostgreSQL and Apache Tomcat. + +License +======= +The contents of this package is available under the same license as Nixpkgs -- +the [MIT](https://opensource.org/licenses/MIT) license. diff --git a/examples/services-agnostic/apache.nix b/examples/services-agnostic/apache.nix new file mode 100644 index 0000000..e81d2f8 --- /dev/null +++ b/examples/services-agnostic/apache.nix @@ -0,0 +1,34 @@ +{createManagedProcess, apacheHttpd}: +{instanceSuffix ? "", configFile, initialize ? ""}: + +let + instanceName = "httpd${instanceSuffix}"; + user = instanceName; + group = instanceName; +in +createManagedProcess { + name = instanceName; + inherit instanceName initialize; + + process = "${apacheHttpd}/bin/httpd"; + args = [ "-f" configFile ]; + foregroundProcessExtraArgs = [ "-DFOREGROUND" ]; + + credentials = { + groups = { + "${group}" = {}; + }; + users = { + "${user}" = { + inherit group; + description = "Apache HTTP daemon user"; + }; + }; + }; + + overrides = { + sysvinit = { + runlevels = [ 3 4 5 ]; + }; + }; +} diff --git a/examples/services-agnostic/constructors.nix b/examples/services-agnostic/constructors.nix new file mode 100644 index 0000000..17e84f0 --- /dev/null +++ b/examples/services-agnostic/constructors.nix @@ -0,0 +1,49 @@ +{ pkgs +, stateDir +, logDir +, runtimeDir +, tmpDir +, forceDisableUserChange +, processManager +}: + +let + createManagedProcess = import ../../nixproc/create-managed-process/agnostic/create-managed-process-universal.nix { + inherit pkgs runtimeDir tmpDir forceDisableUserChange processManager; + }; +in +{ + apache = import ./apache.nix { + inherit createManagedProcess; + inherit (pkgs) apacheHttpd; + }; + + static-webapp-apache = import ./static-webapp-apache.nix { + inherit createManagedProcess logDir runtimeDir forceDisableUserChange; + inherit (pkgs) stdenv apacheHttpd writeTextFile; + }; + + mysql = import ./mysql.nix { + inherit createManagedProcess stateDir runtimeDir forceDisableUserChange; + inherit (pkgs) stdenv mysql; + }; + + postgresql = import ./postgresql.nix { + inherit createManagedProcess stateDir runtimeDir forceDisableUserChange; + inherit (pkgs) stdenv postgresql; + }; + + tomcat = import ./tomcat.nix { + inherit createManagedProcess stateDir runtimeDir tmpDir forceDisableUserChange; + inherit (pkgs) stdenv; + jre = pkgs.jre8; + tomcat = pkgs.tomcat9; + }; + + simple-appserving-tomcat = import ./simple-appserving-tomcat.nix { + inherit createManagedProcess stateDir runtimeDir tmpDir forceDisableUserChange; + inherit (pkgs) stdenv; + jre = pkgs.jre8; + tomcat = pkgs.tomcat9; + }; +} diff --git a/examples/services-agnostic/mysql.nix b/examples/services-agnostic/mysql.nix new file mode 100644 index 0000000..e485d0b --- /dev/null +++ b/examples/services-agnostic/mysql.nix @@ -0,0 +1,49 @@ +{createManagedProcess, stdenv, mysql, stateDir, runtimeDir, forceDisableUserChange}: +{port ? 3306, instanceSuffix ? ""}: + +let + instanceName = "mysqld${instanceSuffix}"; + dataDir = "${stateDir}/db/${instanceName}"; + user = instanceName; + group = instanceName; +in +createManagedProcess { + name = instanceName; + inherit instanceName user; + + initialize = '' + mkdir -m0700 -p ${dataDir} + mkdir -m0700 -p ${runtimeDir} + + ${stdenv.lib.optionalString (!forceDisableUserChange) '' + chown ${user}:${group} ${dataDir} + ''} + + if [ ! -e "${dataDir}/mysql" ] + then + ${mysql}/bin/mysql_install_db --basedir=${mysql} --datadir=${dataDir} ${if forceDisableUserChange then "" else "--user=${user}"} + fi + ''; + + foregroundProcess = "${mysql}/bin/mysqld"; + foregroundProcessArgs = [ "--basedir" mysql "--datadir" dataDir "--port" port "--socket" "${runtimeDir}/${instanceName}.sock" ] + ++ stdenv.lib.optionals (!forceDisableUserChange) [ "--user" user ]; + + credentials = { + groups = { + "${group}" = {}; + }; + users = { + "${user}" = { + inherit group; + description = "MySQL user"; + }; + }; + }; + + overrides = { + sysvinit = { + runlevels = [ 3 4 5 ]; + }; + }; +} diff --git a/examples/services-agnostic/postgresql.nix b/examples/services-agnostic/postgresql.nix new file mode 100644 index 0000000..a1d3733 --- /dev/null +++ b/examples/services-agnostic/postgresql.nix @@ -0,0 +1,57 @@ +{createManagedProcess, stdenv, postgresql, stateDir, runtimeDir, forceDisableUserChange}: +{port ? 5432, instanceSuffix ? ""}: + +let + instanceName = "postgresql${instanceSuffix}"; + dataDir = "${stateDir}/db/${instanceName}/data"; + socketDir = "${runtimeDir}/${instanceName}"; + user = instanceName; + group = instanceName; +in +createManagedProcess rec { + name = instanceName; + inherit instanceName user; + initialize = '' + mkdir -m0700 -p ${socketDir} + mkdir -m0700 -p ${dataDir} + + ${stdenv.lib.optionalString (!forceDisableUserChange) '' + chown ${user}:${group} ${socketDir} + chown ${user}:${group} ${dataDir} + ''} + + if [ ! -e "${dataDir}/PG_VERSION" ] + then + ${postgresql}/bin/initdb -D ${dataDir} --no-locale + fi + ''; + + process = "${postgresql}/bin/postgres"; + args = [ "-D" dataDir "-p" port "-k" socketDir ]; + + credentials = { + groups = { + "${group}" = {}; + }; + users = { + "${user}" = { + inherit group; + description = "PostgreSQL user"; + }; + }; + }; + + overrides = { + sysvinit = { + runlevels = [ 3 4 5 ]; + + instructions.start = { + activity = "Starting"; + instruction = '' + ${initialize} + ${postgresql}/bin/pg_ctl -D ${dataDir} -o "-p ${toString port} -k ${socketDir}" start + ''; + }; + }; + }; +} diff --git a/examples/services-agnostic/processes.nix b/examples/services-agnostic/processes.nix new file mode 100644 index 0000000..2bff03d --- /dev/null +++ b/examples/services-agnostic/processes.nix @@ -0,0 +1,48 @@ +{ pkgs ? import { inherit system; } +, system ? builtins.currentSystem +, stateDir ? "/var" +, runtimeDir ? "${stateDir}/run" +, logDir ? "${stateDir}/log" +, tmpDir ? (if stateDir == "/var" then "/tmp" else "${stateDir}/tmp") +, forceDisableUserChange ? false +, processManager ? "sysvinit" +}: + +let + constructors = import ./constructors.nix { + inherit pkgs stateDir runtimeDir logDir tmpDir forceDisableUserChange processManager; + }; +in +rec { + static-webapp-apache = rec { + port = 8080; + + pkg = constructors.static-webapp-apache { + inherit port; + }; + }; + + mysql = rec { + port = 3307; + + pkg = constructors.mysql { + inherit port; + }; + }; + + postgresql = rec { + port = 6432; + + pkg = constructors.postgresql { + inherit port; + }; + }; + + simple-appserving-tomcat = rec { + httpPort = 8081; + + pkg = constructors.simple-appserving-tomcat { + inherit httpPort; + }; + }; +} diff --git a/examples/services-agnostic/simple-appserving-tomcat.nix b/examples/services-agnostic/simple-appserving-tomcat.nix new file mode 100644 index 0000000..cb241dd --- /dev/null +++ b/examples/services-agnostic/simple-appserving-tomcat.nix @@ -0,0 +1,29 @@ +{createManagedProcess, stdenv, tomcat, jre, stateDir, runtimeDir, tmpDir, forceDisableUserChange}: +{instanceSuffix ? "", serverPort ? 8005, httpPort ? 8080, httpsPort ? 8443, ajpPort ? 8009}: + +let + tomcatConfigFiles = stdenv.mkDerivation { + name = "tomcat-config-files"; + buildCommand = '' + mkdir -p $out + cd $out + + mkdir conf + cp ${tomcat}/conf/* conf + sed -i \ + -e 's|||' \ + -e 's| + + + + Hello world! + + + +

Hello world!

+ + diff --git a/examples/webapps-agnostic/constructors.nix b/examples/webapps-agnostic/constructors.nix new file mode 100644 index 0000000..82378c3 --- /dev/null +++ b/examples/webapps-agnostic/constructors.nix @@ -0,0 +1,29 @@ +{ pkgs +, stateDir +, logDir +, runtimeDir +, tmpDir +, forceDisableUserChange +, processManager +, webappMode # set to 'foreground' to make them all foreground process, 'daemon' to make them all daemons. null is to pick best option for the selected processManager +}: + +let + createManagedProcess = import ../../nixproc/create-managed-process/agnostic/create-managed-process-universal.nix { + inherit pkgs runtimeDir tmpDir forceDisableUserChange processManager; + }; + + webappExpr = if webappMode == "foreground" then ./webapp-fg.nix + else if webappMode == "daemon" then ./webapp-daemon.nix + else ./webapp.nix; +in +{ + webapp = import webappExpr { + inherit createManagedProcess runtimeDir; + }; + + nginxReverseProxy = import ./nginx-reverse-proxy.nix { + inherit createManagedProcess stateDir logDir runtimeDir forceDisableUserChange; + inherit (pkgs) stdenv writeTextFile nginx; + }; +} diff --git a/examples/webapps-agnostic/distribution-empty.nix b/examples/webapps-agnostic/distribution-empty.nix new file mode 100644 index 0000000..f303833 --- /dev/null +++ b/examples/webapps-agnostic/distribution-empty.nix @@ -0,0 +1,5 @@ +{infrastructure}: + +{ + +} diff --git a/examples/webapps-agnostic/distribution.nix b/examples/webapps-agnostic/distribution.nix new file mode 100644 index 0000000..c372a47 --- /dev/null +++ b/examples/webapps-agnostic/distribution.nix @@ -0,0 +1,6 @@ +{infrastructure}: + +{ + webapp = [ infrastructure.test1 ]; + nginxReverseProxy = [ infrastructure.test2 ]; +} diff --git a/examples/webapps-agnostic/errorpage/index.html b/examples/webapps-agnostic/errorpage/index.html new file mode 100644 index 0000000..afaaead --- /dev/null +++ b/examples/webapps-agnostic/errorpage/index.html @@ -0,0 +1,10 @@ + + + + Disnix VirtualHosts example + + + + This web application is unavailable at the moment! + + diff --git a/examples/webapps-agnostic/infra-bootstrap.nix b/examples/webapps-agnostic/infra-bootstrap.nix new file mode 100644 index 0000000..b2f66e7 --- /dev/null +++ b/examples/webapps-agnostic/infra-bootstrap.nix @@ -0,0 +1,4 @@ +{ + test1.properties.hostname = "test1"; + test2.properties.hostname = "test2"; +} diff --git a/examples/webapps-agnostic/network-logical.nix b/examples/webapps-agnostic/network-logical.nix new file mode 100644 index 0000000..416042e --- /dev/null +++ b/examples/webapps-agnostic/network-logical.nix @@ -0,0 +1,41 @@ +let + nixproc-generate-config = (import ../../tools {}).generate-config; +in +{ + test1 = {pkgs, ...}: + + { + dysnomia = { + extraContainerProperties = { + managed-process = { + processManager = "systemd"; + NIX_PATH = "/root/.nix-defexpr/channels:nixpkgs=/nix/var/nix/profiles/per-user/root/channels/nixos:nixos-config=/etc/nixos/configuration.nix:/nix/var/nix/profiles/per-user/root/channels"; + }; + }; + }; + + services.disnix.enable = true; + services.openssh.enable = true; + networking.firewall.enable = false; + environment.systemPackages = [ pkgs.pythonPackages.supervisor nixproc-generate-config ]; + }; + + test2 = {pkgs, ...}: + + { + dysnomia = { + extraContainerProperties = { + managed-process = { + processManager = "sysvinit"; + NIX_PATH = "/root/.nix-defexpr/channels:nixpkgs=/nix/var/nix/profiles/per-user/root/channels/nixos:nixos-config=/etc/nixos/configuration.nix:/nix/var/nix/profiles/per-user/root/channels"; + }; + }; + + }; + + services.disnix.enable = true; + services.openssh.enable = true; + networking.firewall.enable = false; + environment.systemPackages = [ pkgs.pythonPackages.supervisor nixproc-generate-config ]; + }; +} diff --git a/examples/webapps-agnostic/network-physical.nix b/examples/webapps-agnostic/network-physical.nix new file mode 100644 index 0000000..b4928f6 --- /dev/null +++ b/examples/webapps-agnostic/network-physical.nix @@ -0,0 +1,14 @@ +{ + test1 = {pkgs, ...}: + { + deployment.targetEnv = "virtualbox"; + deployment.virtualbox.memorySize = 4096; # megabytes + }; + + test2 = {pkgs, ...}: + { + deployment.targetEnv = "virtualbox"; + deployment.virtualbox.memorySize = 4096; # megabytes + }; + +} diff --git a/examples/webapps-agnostic/nginx-reverse-proxy.nix b/examples/webapps-agnostic/nginx-reverse-proxy.nix new file mode 100644 index 0000000..109763d --- /dev/null +++ b/examples/webapps-agnostic/nginx-reverse-proxy.nix @@ -0,0 +1,84 @@ +{createManagedProcess, stdenv, writeTextFile, nginx, runtimeDir, stateDir, logDir, forceDisableUserChange}: +{port ? 80, webapps ? [], instanceSuffix ? ""}: +interDeps: + +let + instanceName = "nginx${instanceSuffix}"; + user = instanceName; + group = instanceName; + + nginxStateDir = "${stateDir}/${instanceName}"; + dependencies = webapps ++ (builtins.attrValues interDeps); + + generateNginxConf = daemon: + writeTextFile { + name = "nginx.conf"; + text = '' + error_log ${nginxStateDir}/logs/error.log; + + ${stdenv.lib.optionalString (!forceDisableUserChange) '' + user ${user} ${group}; + ''} + + ${if daemon then '' + pid ${runtimeDir}/${instanceName}.pid; + '' else '' + daemon off; + ''} + + events { + worker_connections 190000; + } + + http { + ${stdenv.lib.concatMapStrings (dependency: '' + upstream webapp${toString dependency.port} { + server localhost:${toString dependency.port}; + } + '') webapps} + + ${stdenv.lib.concatMapStrings (dependencyName: + let + dependency = builtins.getAttr dependencyName interDeps; + in + '' + upstream webapp${toString dependency.port} { + server ${dependency.target.properties.hostname}:${toString dependency.port}; + } + '') (builtins.attrNames interDeps)} + + # Fallback virtual host displaying an error page. This is what users see + # if they connect to a non-deployed web application. + # Without it, nginx redirects to the first available virtual host, giving + # unpredictable results. This could happen while an upgrade is in progress. + + server { + listen ${toString port}; + server_name aaaa; + root ${./errorpage}; + } + + ${stdenv.lib.concatMapStrings (dependency: '' + server { + listen ${toString port}; + server_name ${dependency.dnsName}; + + location / { + proxy_pass http://webapp${toString dependency.port}; + } + } + '') dependencies} + } + ''; + }; +in +import ./nginx.nix { + inherit createManagedProcess stdenv nginx stateDir forceDisableUserChange; +} { + inherit instanceSuffix; + + dependencies = map (webapp: webapp.pkg) dependencies; + + daemonConfigFile = generateNginxConf true; + foregroundConfigFile = generateNginxConf false; +} diff --git a/examples/webapps-agnostic/nginx.nix b/examples/webapps-agnostic/nginx.nix new file mode 100644 index 0000000..4c14a95 --- /dev/null +++ b/examples/webapps-agnostic/nginx.nix @@ -0,0 +1,43 @@ +{createManagedProcess, stdenv, nginx, stateDir, forceDisableUserChange}: +{daemonConfigFile, foregroundConfigFile, dependencies ? [], instanceSuffix ? ""}: + +let + instanceName = "nginx${instanceSuffix}"; + user = instanceName; + group = instanceName; + nginxLogDir = "${stateDir}/${instanceName}/logs"; +in +createManagedProcess { + name = instanceName; + description = "Nginx"; + initialize = '' + mkdir -p ${nginxLogDir} + ${stdenv.lib.optionalString (!forceDisableUserChange) '' + chown ${user}:${group} ${nginxLogDir} + ''} + ''; + process = "${nginx}/bin/nginx"; + args = [ "-p" "${stateDir}/${instanceName}" ]; + foregroundProcessExtraArgs = [ "-c" foregroundConfigFile ]; + daemonExtraArgs = [ "-c" daemonConfigFile ]; + + inherit dependencies instanceName; + + credentials = { + groups = { + "${group}" = {}; + }; + users = { + "${user}" = { + inherit group; + description = "Nginx user"; + }; + }; + }; + + overrides = { + sysvinit = { + runlevels = [ 3 4 5 ]; + }; + }; +} diff --git a/examples/webapps-agnostic/processes-advanced.nix b/examples/webapps-agnostic/processes-advanced.nix new file mode 100644 index 0000000..75b9e8b --- /dev/null +++ b/examples/webapps-agnostic/processes-advanced.nix @@ -0,0 +1,96 @@ +{ pkgs ? import { inherit system; } +, system ? builtins.currentSystem +, stateDir ? "/var" +, runtimeDir ? "${stateDir}/run" +, logDir ? "${stateDir}/log" +, tmpDir ? (if stateDir == "/var" then "/tmp" else "${stateDir}/tmp") +, forceDisableUserChange ? false +, processManager ? "sysvinit" +, webappMode ? null +}: + +let + constructors = import ./constructors.nix { + inherit pkgs stateDir runtimeDir logDir tmpDir forceDisableUserChange processManager webappMode; + }; +in +rec { + webapp1 = rec { + port = 5000; + dnsName = "webapp1.local"; + + pkg = constructors.webapp { + inherit port; + instanceSuffix = "1"; + }; + }; + + webapp2 = rec { + port = 5001; + dnsName = "webapp2.local"; + + pkg = constructors.webapp { + inherit port; + instanceSuffix = "2"; + }; + }; + + webapp3 = rec { + port = 5002; + dnsName = "webapp3.local"; + + pkg = constructors.webapp { + inherit port; + instanceSuffix = "3"; + }; + }; + + webapp4 = rec { + port = 5003; + dnsName = "webapp4.local"; + + pkg = constructors.webapp { + inherit port; + instanceSuffix = "4"; + }; + }; + + nginxReverseProxy = rec { + port = 8080; + + pkg = constructors.nginxReverseProxy { + webapps = [ webapp1 webapp2 webapp3 webapp4 ]; + inherit port; + } {}; + }; + + webapp5 = rec { + port = 6002; + dnsName = "webapp5.local"; + + pkg = constructors.webapp { + inherit port; + instanceSuffix = "5"; + }; + }; + + webapp6 = rec { + port = 6003; + dnsName = "webapp6.local"; + + pkg = constructors.webapp { + inherit port; + instanceSuffix = "6"; + }; + }; + + nginxReverseProxy2 = rec { + port = 8081; + + pkg = constructors.nginxReverseProxy { + webapps = [ webapp5 webapp6 ]; + inherit port; + instanceSuffix = "2"; + } {}; + }; +} diff --git a/examples/webapps-agnostic/processes.nix b/examples/webapps-agnostic/processes.nix new file mode 100644 index 0000000..814ed4b --- /dev/null +++ b/examples/webapps-agnostic/processes.nix @@ -0,0 +1,35 @@ +{ pkgs ? import { inherit system; } +, system ? builtins.currentSystem +, stateDir ? "/var" +, runtimeDir ? "${stateDir}/run" +, logDir ? "${stateDir}/log" +, tmpDir ? (if stateDir == "/var" then "/tmp" else "${stateDir}/tmp") +, forceDisableUserChange ? false +, processManager +, webappMode ? null +}: + +let + constructors = import ./constructors.nix { + inherit pkgs stateDir runtimeDir logDir tmpDir forceDisableUserChange processManager webappMode; + }; +in +rec { + webapp = rec { + port = 5000; + dnsName = "webapp.local"; + + pkg = constructors.webapp { + inherit port; + }; + }; + + nginxReverseProxy = rec { + port = 8080; + + pkg = constructors.nginxReverseProxy { + webapps = [ webapp ]; + inherit port; + } {}; + }; +} diff --git a/examples/webapps-agnostic/services.nix b/examples/webapps-agnostic/services.nix new file mode 100644 index 0000000..bbe6eba --- /dev/null +++ b/examples/webapps-agnostic/services.nix @@ -0,0 +1,45 @@ +{ pkgs, distribution, invDistribution, system +, stateDir ? "/var" +, runtimeDir ? "${stateDir}/run" +, logDir ? "${stateDir}/log" +, tmpDir ? (if stateDir == "/var" then "/tmp" else "${stateDir}/tmp") +, forceDisableUserChange ? false +, processManager ? null # "sysvinit" +}: + +let + constructors = import ./constructors.nix { + inherit pkgs stateDir runtimeDir logDir tmpDir forceDisableUserChange processManager; + webappMode = null; + }; + + processType = + if processManager == null then "managed-process" + else if processManager == "sysvinit" then "sysvinit-script" + else if processManager == "systemd" then "systemd-unit" + else if processManager == "supervisord" then "supervisord-program" + else throw "Unknown process manager: ${processManager}"; +in +rec { + webapp = rec { + name = "webapp"; + port = 5000; + dnsName = "webapp.local"; + pkg = constructors.webapp { + inherit port; + }; + type = processType; + }; + + nginxReverseProxy = rec { + name = "nginxReverseProxy"; + port = 8080; + pkg = constructors.nginxReverseProxy { + inherit port; + }; + dependsOn = { + inherit webapp; + }; + type = processType; + }; +} diff --git a/examples/webapps-agnostic/webapp-daemon.nix b/examples/webapps-agnostic/webapp-daemon.nix new file mode 100644 index 0000000..b8bc58b --- /dev/null +++ b/examples/webapps-agnostic/webapp-daemon.nix @@ -0,0 +1,39 @@ +{createManagedProcess, runtimeDir}: +{port, instanceSuffix ? ""}: + +let + webapp = import ../webapp; + instanceName = "webapp${instanceSuffix}"; +in +createManagedProcess { + name = instanceName; + description = "Simple web application"; + inherit instanceName; + + # This expression only specifies how to run webapp in daemon mode + daemon = "${webapp}/bin/webapp"; + daemonArgs = [ "-D" ]; + + environment = { + PORT = port; + PID_FILE = "${runtimeDir}/${instanceName}.pid"; + }; + user = instanceName; + credentials = { + groups = { + "${instanceName}" = {}; + }; + users = { + "${instanceName}" = { + group = instanceName; + description = "Webapp"; + }; + }; + }; + + overrides = { + sysvinit = { + runlevels = [ 3 4 5 ]; + }; + }; +} diff --git a/examples/webapps-agnostic/webapp-fg.nix b/examples/webapps-agnostic/webapp-fg.nix new file mode 100644 index 0000000..205918f --- /dev/null +++ b/examples/webapps-agnostic/webapp-fg.nix @@ -0,0 +1,37 @@ +{createManagedProcess, runtimeDir}: +{port, instanceSuffix ? ""}: + +let + webapp = import ../webapp; + instanceName = "webapp${instanceSuffix}"; +in +createManagedProcess { + name = instanceName; + description = "Simple web application"; + inherit instanceName; + + # This expression only specifies how to run the webapp in foreground mode + foregroundProcess = "${webapp}/bin/webapp"; + + environment = { + PORT = port; + }; + user = instanceName; + credentials = { + groups = { + "${instanceName}" = {}; + }; + users = { + "${instanceName}" = { + group = instanceName; + description = "Webapp"; + }; + }; + }; + + overrides = { + sysvinit = { + runlevels = [ 3 4 5 ]; + }; + }; +} diff --git a/examples/webapps-agnostic/webapp.nix b/examples/webapps-agnostic/webapp.nix new file mode 100644 index 0000000..f0ee4d6 --- /dev/null +++ b/examples/webapps-agnostic/webapp.nix @@ -0,0 +1,40 @@ +{createManagedProcess, runtimeDir}: +{port, instanceSuffix ? ""}: + +let + webapp = import ../../webapp; + instanceName = "webapp${instanceSuffix}"; +in +createManagedProcess { + name = instanceName; + description = "Simple web application"; + inherit instanceName; + + # This expression can both run in foreground or daemon mode. + # The process manager can pick which mode it prefers. + process = "${webapp}/bin/webapp"; + daemonArgs = [ "-D" ]; + + environment = { + PORT = port; + PID_FILE = "${runtimeDir}/${instanceName}.pid"; + }; + user = instanceName; + credentials = { + groups = { + "${instanceName}" = {}; + }; + users = { + "${instanceName}" = { + group = instanceName; + description = "Webapp"; + }; + }; + }; + + overrides = { + sysvinit = { + runlevels = [ 3 4 5 ]; + }; + }; +} diff --git a/examples/webapps-sysvinit/constructors.nix b/examples/webapps-sysvinit/constructors.nix new file mode 100644 index 0000000..abdc72d --- /dev/null +++ b/examples/webapps-sysvinit/constructors.nix @@ -0,0 +1,34 @@ +{ pkgs +, stateDir +, logDir +, runtimeDir +, tmpDir +, forceDisableUserChange +}: + +let + createSystemVInitScript = import ../../nixproc/create-managed-process/sysvinit/create-sysvinit-script.nix { + inherit (pkgs) stdenv writeTextFile daemon; + inherit runtimeDir tmpDir forceDisableUserChange; + + createCredentials = import ../../nixproc/create-credentials { + inherit (pkgs) stdenv; + }; + + initFunctions = import ../../nixproc/create-managed-process/sysvinit/init-functions.nix { + basePackages = [ pkgs.coreutils pkgs.gnused pkgs.inetutils pkgs.gnugrep pkgs.sysvinit ]; + inherit (pkgs) stdenv fetchurl; + inherit runtimeDir; + }; + }; +in +{ + webapp = import ./webapp.nix { + inherit createSystemVInitScript runtimeDir; + }; + + nginxReverseProxy = import ./nginx-reverse-proxy.nix { + inherit createSystemVInitScript stateDir logDir runtimeDir; + inherit (pkgs) stdenv writeTextFile nginx; + }; +} diff --git a/examples/webapps-sysvinit/distribution-empty.nix b/examples/webapps-sysvinit/distribution-empty.nix new file mode 100644 index 0000000..f303833 --- /dev/null +++ b/examples/webapps-sysvinit/distribution-empty.nix @@ -0,0 +1,5 @@ +{infrastructure}: + +{ + +} diff --git a/examples/webapps-sysvinit/distribution.nix b/examples/webapps-sysvinit/distribution.nix new file mode 100644 index 0000000..c372a47 --- /dev/null +++ b/examples/webapps-sysvinit/distribution.nix @@ -0,0 +1,6 @@ +{infrastructure}: + +{ + webapp = [ infrastructure.test1 ]; + nginxReverseProxy = [ infrastructure.test2 ]; +} diff --git a/examples/webapps-sysvinit/errorpage/index.html b/examples/webapps-sysvinit/errorpage/index.html new file mode 100644 index 0000000..afaaead --- /dev/null +++ b/examples/webapps-sysvinit/errorpage/index.html @@ -0,0 +1,10 @@ + + + + Disnix VirtualHosts example + + + + This web application is unavailable at the moment! + + diff --git a/examples/webapps-sysvinit/network-logical.nix b/examples/webapps-sysvinit/network-logical.nix new file mode 100644 index 0000000..609106f --- /dev/null +++ b/examples/webapps-sysvinit/network-logical.nix @@ -0,0 +1,17 @@ +{ + test1 = {pkgs, ...}: + + { + services.disnix.enable = true; + services.openssh.enable = true; + networking.firewall.enable = false; + }; + + test2 = {pkgs, ...}: + + { + services.disnix.enable = true; + services.openssh.enable = true; + networking.firewall.enable = false; + }; +} diff --git a/examples/webapps-sysvinit/network-physical.nix b/examples/webapps-sysvinit/network-physical.nix new file mode 100644 index 0000000..b4928f6 --- /dev/null +++ b/examples/webapps-sysvinit/network-physical.nix @@ -0,0 +1,14 @@ +{ + test1 = {pkgs, ...}: + { + deployment.targetEnv = "virtualbox"; + deployment.virtualbox.memorySize = 4096; # megabytes + }; + + test2 = {pkgs, ...}: + { + deployment.targetEnv = "virtualbox"; + deployment.virtualbox.memorySize = 4096; # megabytes + }; + +} diff --git a/examples/webapps-sysvinit/nginx-reverse-proxy.nix b/examples/webapps-sysvinit/nginx-reverse-proxy.nix new file mode 100644 index 0000000..a1792b3 --- /dev/null +++ b/examples/webapps-sysvinit/nginx-reverse-proxy.nix @@ -0,0 +1,67 @@ +{createSystemVInitScript, stdenv, writeTextFile, nginx, runtimeDir, stateDir, logDir}: +{port ? 80, webapps ? [], instanceSuffix ? ""}: +interDeps: + +let + instanceName = "nginx${instanceSuffix}"; + nginxStateDir = "${stateDir}/${instanceName}"; + dependencies = webapps ++ (builtins.attrValues interDeps); +in +import ./nginx.nix { + inherit createSystemVInitScript nginx instanceSuffix; + stateDir = nginxStateDir; + + dependencies = map (webapp: webapp.pkg) dependencies; + + configFile = writeTextFile { + name = "nginx.conf"; + text = '' + error_log ${nginxStateDir}/logs/error.log; + pid ${runtimeDir}/${instanceName}.pid; + + events { + worker_connections 190000; + } + + http { + ${stdenv.lib.concatMapStrings (dependency: '' + upstream webapp${toString dependency.port} { + server localhost:${toString dependency.port}; + } + '') webapps} + + ${stdenv.lib.concatMapStrings (dependencyName: + let + dependency = builtins.getAttr dependencyName interDeps; + in + '' + upstream webapp${toString dependency.port} { + server ${dependency.target.properties.hostname}:${toString dependency.port}; + } + '') (builtins.attrNames interDeps)} + + # Fallback virtual host displaying an error page. This is what users see + # if they connect to a non-deployed web application. + # Without it, nginx redirects to the first available virtual host, giving + # unpredictable results. This could happen while an upgrade is in progress. + + server { + listen ${toString port}; + server_name aaaa; + root ${./errorpage}; + } + + ${stdenv.lib.concatMapStrings (dependency: '' + server { + listen ${toString port}; + server_name ${dependency.dnsName}; + + location / { + proxy_pass http://webapp${toString dependency.port}; + } + } + '') dependencies} + } + ''; + }; +} diff --git a/examples/webapps-sysvinit/nginx.nix b/examples/webapps-sysvinit/nginx.nix new file mode 100644 index 0000000..9ae4503 --- /dev/null +++ b/examples/webapps-sysvinit/nginx.nix @@ -0,0 +1,17 @@ +{createSystemVInitScript, nginx, configFile, stateDir, dependencies ? [], instanceSuffix ? ""}: + +let + instanceName = "nginx${instanceSuffix}"; +in +createSystemVInitScript { + name = instanceName; + description = "Nginx"; + initialize = '' + mkdir -p ${stateDir}/logs + ''; + process = "${nginx}/bin/nginx"; + args = [ "-c" configFile "-p" stateDir ]; + runlevels = [ 3 4 5 ]; + + inherit dependencies instanceName; +} diff --git a/examples/webapps-sysvinit/processes-advanced.nix b/examples/webapps-sysvinit/processes-advanced.nix new file mode 100644 index 0000000..ab77af6 --- /dev/null +++ b/examples/webapps-sysvinit/processes-advanced.nix @@ -0,0 +1,94 @@ +{ pkgs ? import { inherit system; } +, system ? builtins.currentSystem +, stateDir ? "/var" +, runtimeDir ? "${stateDir}/run" +, logDir ? "${stateDir}/log" +, tmpDir ? (if stateDir == "/var" then "/tmp" else "${stateDir}/tmp") +, forceDisableUserChange ? false +}: + +let + constructors = import ./constructors.nix { + inherit pkgs stateDir runtimeDir logDir tmpDir forceDisableUserChange; + }; +in +rec { + webapp1 = rec { + port = 5000; + dnsName = "webapp1.local"; + + pkg = constructors.webapp { + inherit port; + instanceSuffix = "1"; + }; + }; + + webapp2 = rec { + port = 5001; + dnsName = "webapp2.local"; + + pkg = constructors.webapp { + inherit port; + instanceSuffix = "2"; + }; + }; + + webapp3 = rec { + port = 5002; + dnsName = "webapp3.local"; + + pkg = constructors.webapp { + inherit port; + instanceSuffix = "3"; + }; + }; + + webapp4 = rec { + port = 5003; + dnsName = "webapp4.local"; + + pkg = constructors.webapp { + inherit port; + instanceSuffix = "4"; + }; + }; + + nginxReverseProxy = rec { + port = 8080; + + pkg = constructors.nginxReverseProxy { + webapps = [ webapp1 webapp2 webapp3 webapp4 ]; + inherit port; + } {}; + }; + + webapp5 = rec { + port = 6002; + dnsName = "webapp5.local"; + + pkg = constructors.webapp { + inherit port; + instanceSuffix = "5"; + }; + }; + + webapp6 = rec { + port = 6003; + dnsName = "webapp6.local"; + + pkg = constructors.webapp { + inherit port; + instanceSuffix = "6"; + }; + }; + + nginxReverseProxy2 = rec { + port = 8081; + + pkg = constructors.nginxReverseProxy { + webapps = [ webapp5 webapp6 ]; + inherit port; + instanceSuffix = "2"; + } {}; + }; +} diff --git a/examples/webapps-sysvinit/processes.nix b/examples/webapps-sysvinit/processes.nix new file mode 100644 index 0000000..4cec76d --- /dev/null +++ b/examples/webapps-sysvinit/processes.nix @@ -0,0 +1,33 @@ +{ pkgs ? import { inherit system; } +, system ? builtins.currentSystem +, stateDir ? "/var" +, runtimeDir ? "${stateDir}/run" +, logDir ? "${stateDir}/log" +, tmpDir ? (if stateDir == "/var" then "/tmp" else "${stateDir}/tmp") +, forceDisableUserChange ? false +}: + +let + constructors = import ./constructors.nix { + inherit pkgs stateDir runtimeDir logDir tmpDir forceDisableUserChange; + }; +in +rec { + webapp = rec { + port = 5000; + dnsName = "webapp.local"; + + pkg = constructors.webapp { + inherit port; + }; + }; + + nginxReverseProxy = rec { + port = 8080; + + pkg = constructors.nginxReverseProxy { + webapps = [ webapp ]; + inherit port; + } {}; + }; +} diff --git a/examples/webapps-sysvinit/services.nix b/examples/webapps-sysvinit/services.nix new file mode 100644 index 0000000..286e3ad --- /dev/null +++ b/examples/webapps-sysvinit/services.nix @@ -0,0 +1,36 @@ +{ pkgs, distribution, invDistribution, system +, stateDir ? "/var" +, runtimeDir ? "${stateDir}/run" +, logDir ? "${stateDir}/log" +, tmpDir ? (if stateDir == "/var" then "/tmp" else "${stateDir}/tmp") +, forceDisableUserChange ? true +}: + +let + constructors = import ./constructors.nix { + inherit pkgs stateDir runtimeDir logDir tmpDir forceDisableUserChange; + }; +in +rec { + webapp = rec { + name = "webapp"; + port = 5000; + dnsName = "webapp.local"; + pkg = constructors.webapp { + inherit port; + }; + type = "sysvinit-script"; + }; + + nginxReverseProxy = rec { + name = "nginxReverseProxy"; + port = 8080; + pkg = constructors.nginxReverseProxy { + inherit port; + }; + dependsOn = { + inherit webapp; + }; + type = "sysvinit-script"; + }; +} diff --git a/examples/webapps-sysvinit/webapp.nix b/examples/webapps-sysvinit/webapp.nix new file mode 100644 index 0000000..51372cf --- /dev/null +++ b/examples/webapps-sysvinit/webapp.nix @@ -0,0 +1,33 @@ +{createSystemVInitScript, runtimeDir}: +{port, instanceSuffix ? ""}: + +let + webapp = import ../../webapp; + instanceName = "webapp${instanceSuffix}"; +in +createSystemVInitScript { + name = instanceName; + inherit instanceName; + process = "${webapp}/bin/webapp"; + args = [ "-D" ]; + environment = { + PORT = port; + PID_FILE = "${runtimeDir}/${instanceName}.pid"; + }; + + runlevels = [ 3 4 5 ]; + + user = instanceName; + + credentials = { + groups = { + "${instanceName}" = {}; + }; + users = { + "${instanceName}" = { + group = instanceName; + description = "Webapp"; + }; + }; + }; +} diff --git a/nixproc/create-credentials/default.nix b/nixproc/create-credentials/default.nix new file mode 100644 index 0000000..56858a6 --- /dev/null +++ b/nixproc/create-credentials/default.nix @@ -0,0 +1,43 @@ +{stdenv}: +{groups, users}: + +stdenv.mkDerivation { + name = "credentials"; + buildCommand = '' + mkdir -p $out/dysnomia-support/groups + + ${stdenv.lib.concatMapStrings (groupname: + let + group = builtins.getAttr groupname groups; + in + '' + cat > $out/dysnomia-support/groups/${groupname} < $out/dysnomia-support/users/${username} < { inherit system; } +, stateDir ? "/var" +, runtimeDir ? "${stateDir}/run" +, tmpDir ? (if stateDir == "/var" then "/tmp" else "${stateDir}/tmp") +, forceDisableUserChange ? false +}: + +let + createManagedProcessFromConfig = configFile: + let + createManagedProcess = import ./create-managed-process-universal.nix { + inherit pkgs runtimeDir tmpDir forceDisableUserChange processManager; + }; + + properties = builtins.fromJSON (builtins.readFile configFile); + + normalizedProperties = properties // pkgs.stdenv.lib.optionalAttrs (properties ? dependencies) { + dependencies = map (dependency: createManagedProcessFromConfig "${dependency}/${builtins.substring 33 (builtins.stringLength dependency) (baseNameOf dependency)}.json") properties.dependencies; + }; + in + createManagedProcess normalizedProperties; +in +createManagedProcessFromConfig configFile diff --git a/nixproc/create-managed-process/agnostic/create-managed-process-universal.nix b/nixproc/create-managed-process/agnostic/create-managed-process-universal.nix new file mode 100644 index 0000000..8207aee --- /dev/null +++ b/nixproc/create-managed-process/agnostic/create-managed-process-universal.nix @@ -0,0 +1,98 @@ +{pkgs, runtimeDir, tmpDir, forceDisableUserChange ? false, processManager ? null}: + +let + basePackages = [ + pkgs.coreutils + pkgs.gnused + pkgs.gnugrep + pkgs.inetutils + ]; + + createCredentials = import ../../create-credentials { + inherit (pkgs) stdenv; + }; + + createSystemVInitScript = import ../sysvinit/create-sysvinit-script.nix { + inherit (pkgs) stdenv writeTextFile daemon; + inherit createCredentials runtimeDir tmpDir forceDisableUserChange; + + initFunctions = import ../sysvinit/init-functions.nix { + inherit (pkgs) stdenv fetchurl; + inherit runtimeDir; + basePackages = basePackages ++ [ pkgs.sysvinit ]; + }; + }; + + generateSystemVInitScript = import ./generate-sysvinit-script.nix { + inherit createSystemVInitScript; + inherit (pkgs) stdenv; + }; + + createSystemdService = import ../systemd/create-systemd-service.nix { + inherit (pkgs) writeTextFile stdenv; + inherit createCredentials basePackages forceDisableUserChange; + }; + + generateSystemdService = import ./generate-systemd-service.nix { + inherit createSystemdService; + inherit (pkgs) stdenv writeTextFile; + }; + + createSupervisordProgram = import ../supervisord/create-supervisord-program.nix { + inherit (pkgs) writeTextFile stdenv; + inherit (pkgs.pythonPackages) supervisor; + inherit createCredentials basePackages forceDisableUserChange runtimeDir; + }; + + generateSupervisordProgram = import ./generate-supervisord-program.nix { + inherit createSupervisordProgram runtimeDir; + inherit (pkgs) stdenv writeTextFile; + }; + + createBSDRCScript = import ../bsdrc/create-bsdrc-script.nix { + inherit (pkgs) writeTextFile stdenv; + inherit createCredentials forceDisableUserChange runtimeDir; + + rcSubr = import ../bsdrc/rcsubr.nix { + inherit (pkgs) stdenv; + inherit forceDisableUserChange; + }; + }; + + generateBSDRCScript = import ../agnostic/generate-bsdrc-script.nix { + inherit createBSDRCScript; + inherit (pkgs) stdenv; + }; + + createLaunchdDaemon = import ../launchd/create-launchd-daemon.nix { + inherit (pkgs) writeTextFile stdenv; + inherit createCredentials forceDisableUserChange; + }; + + generateLaunchdDaemon = import ../agnostic/generate-launchd-daemon.nix { + inherit (pkgs) stdenv writeTextFile; + inherit createLaunchdDaemon runtimeDir; + }; + + createCygrunsrvParams = import ../cygrunsrv/create-cygrunsrv-params.nix { + inherit (pkgs) writeTextFile stdenv; + }; + + generateCygrunsrvParams = import ../agnostic/generate-cygrunsrv-params.nix { + inherit (pkgs) stdenv writeTextFile; + inherit createCygrunsrvParams runtimeDir; + }; +in +import ./create-managed-process.nix { + inherit processManager; + inherit (pkgs) stdenv; + + generateProcessFun = + if processManager == "sysvinit" then generateSystemVInitScript + else if processManager == "systemd" then generateSystemdService + else if processManager == "supervisord" then generateSupervisordProgram + else if processManager == "bsdrc" then generateBSDRCScript + else if processManager == "launchd" then generateLaunchdDaemon + else if processManager == "cygrunsrv" then generateCygrunsrvParams + else throw "Unknown process manager: ${processManager}"; +} diff --git a/nixproc/create-managed-process/agnostic/create-managed-process.nix b/nixproc/create-managed-process/agnostic/create-managed-process.nix new file mode 100644 index 0000000..0ba8ca3 --- /dev/null +++ b/nixproc/create-managed-process/agnostic/create-managed-process.nix @@ -0,0 +1,59 @@ +{ generateProcessFun, processManager, stdenv }: + +{ +# A name that identifies the process instance +name +# A more human-readable description of the process +, description ? name +# Shell commands that specify how the state should be initialized +, initialize ? "" +# Path to a process to execute (both in foreground and daemon mode) +, process ? null +# Generic command-line parameters propagated to the process +, args ? [] +# The executable that needs to run to start the process is daemon mode +, daemon ? process +# Extra arguments appended to args when the process runs in daemon mode +, daemonExtraArgs ? [] +# Command-line arguments propagated to the daemon +, daemonArgs ? (args ++ daemonExtraArgs) +# A name that uniquely identifies each process instance. It is used to generate a unique PID file. +, instanceName ? null +# Path to a PID file that the system should use to manage the process. If null, it will use a default path. +, pidFile ? null +# The executable that needs to run to start the process in foreground mode +, foregroundProcess ? process +# Extra arguments appended to args when the process runs in foreground mode +, foregroundProcessExtraArgs ? [] +# Command-line arguments propagated to the foreground process +, foregroundProcessArgs ? (args ++ foregroundProcessExtraArgs) +# Specifies which packages need to be in the PATH +, path ? [] +# An attribute set specifying arbitrary environment variables +, environment ? {} +# If not null, the current working directory will be changed before executing any activities +, directory ? null +# If not null, the umask will be changed before executing any activities +, umask ? null +# If not null, the nice level be changed before executing any activities +, nice ? null +# Specifies as which user the process should run. If null, the user privileges will not be changed. +, user ? null +# Dependencies on other processes. Typically, this specification is used to derive the activation order. +, dependencies ? [] +# Specifies which groups and users that need to be created. +, credentials ? {} +# Specifies process manager specific properties that augmented to the generated function parameters +, overrides ? {} +}@properties: + +let + createAgnosticConfig = import ./create-agnostic-config.nix { + inherit stdenv; + }; +in +if processManager == null then createAgnosticConfig properties +else generateProcessFun { + inherit name description initialize daemon daemonArgs instanceName pidFile foregroundProcess foregroundProcessArgs path environment directory umask nice user dependencies credentials; + overrides = if builtins.hasAttr processManager overrides then builtins.getAttr processManager overrides else {}; +} diff --git a/nixproc/create-managed-process/agnostic/generate-bsdrc-script.nix b/nixproc/create-managed-process/agnostic/generate-bsdrc-script.nix new file mode 100644 index 0000000..af37dfd --- /dev/null +++ b/nixproc/create-managed-process/agnostic/generate-bsdrc-script.nix @@ -0,0 +1,37 @@ +{ createBSDRCScript, stdenv }: + +{ name +, description +, initialize +, daemon +, daemonArgs +, instanceName +, pidFile +, foregroundProcess +, foregroundProcessArgs +, path +, environment +, directory +, umask +, nice +, user +, dependencies +, credentials +, overrides +}: + +# TODO: umask + +createBSDRCScript (stdenv.lib.recursiveUpdate ({ + inherit name environment path directory nice dependencies; + inherit user instanceName credentials; + + command = if daemon != null then daemon else foregroundProcess; + commandIsDaemon = daemon != null; + commandArgs = if daemon != null then daemonArgs else foregroundProcessArgs; + +} // stdenv.lib.optionalAttrs (pidFile != null) { + inherit pidFile; +} // stdenv.lib.optionalAttrs (initialize != "") { + commands.start.pre = initialize; +}) overrides) diff --git a/nixproc/create-managed-process/agnostic/generate-cygrunsrv-params.nix b/nixproc/create-managed-process/agnostic/generate-cygrunsrv-params.nix new file mode 100644 index 0000000..3028bc5 --- /dev/null +++ b/nixproc/create-managed-process/agnostic/generate-cygrunsrv-params.nix @@ -0,0 +1,57 @@ +{ createCygrunsrvParams +, stdenv +, writeTextFile +, runtimeDir ? "/var/run" +}: + +{ name +, description +, initialize +, daemon +, daemonArgs +, instanceName +, pidFile +, foregroundProcess +, foregroundProcessArgs +, path +, environment +, directory +, umask +, nice +, user +, dependencies +, credentials +, overrides +}: + +# TODO: credentials +# TODO: directory unused +# TODO: umask unused +# TODO: nice unused +# TODO: user unused + +let + generateForegroundWrapper = import ./generate-foreground-wrapper.nix { + inherit stdenv writeTextFile; + }; +in +createCygrunsrvParams (stdenv.lib.recursiveUpdate ({ + inherit name environment dependencies; + + environmentPath = path; + + path = if foregroundProcess != null then + if initialize == "" then foregroundProcess + else generateForegroundWrapper { + wrapDaemon = false; + executable = foregroundProcess; + inherit name initialize runtimeDir pidFile stdenv; + } + else generateForegroundWrapper { + wrapDaemon = true; + executable = daemon; + inherit name initialize runtimeDir pidFile stdenv; + }; + + args = if foregroundProcess != null then foregroundProcessArgs else daemonArgs; +}) overrides) diff --git a/nixproc/create-managed-process/agnostic/generate-foreground-wrapper.nix b/nixproc/create-managed-process/agnostic/generate-foreground-wrapper.nix new file mode 100644 index 0000000..b8f9605 --- /dev/null +++ b/nixproc/create-managed-process/agnostic/generate-foreground-wrapper.nix @@ -0,0 +1,83 @@ +{stdenv, writeTextFile}: +{ name +, wrapDaemon +, initialize +, executable +, stdenv +, runtimeDir +, instanceName ? null +, pidFile ? (if instanceName == null then null else "${runtimeDir}/${instanceName}.pid") +}: + +let + _pidFile = if pidFile == null then "${runtimeDir}/$(basename ${executable}).pid" else pidFile; +in +writeTextFile { + name = "${name}-foregroundwrapper.sh"; + text = '' + #! ${stdenv.shell} -e + + ${initialize} + + ${if wrapDaemon then '' + export _TOP_PID=$$ + + # Handle to SIGTERM and SIGINT signals and forward them to the daemon process + _term() + { + trap "exit 0" TERM + kill -TERM "$pid" + kill $_TOP_PID + } + + _interrupt() + { + kill -INT "$pid" + } + + trap _term SIGTERM + trap _interrupt SIGINT + + # Start process in the background as a daemon + ${executable} "$@" + + # Wait for the PID file to become available. Useful to work with daemons that don't behave well enough. + count=0 + + while [ ! -f "${_pidFile}" ] + do + if [ $count -eq 10 ] + then + echo "It does not seem that there isn't any pid file! Giving up!" + exit 1 + fi + + echo "Waiting for ${_pidFile} to become available..." + sleep 1 + + ((count=count++)) + done + + # Determine the daemon's PID by using the PID file + pid=$(cat ${_pidFile}) + + # Wait in the background for the PID to terminate + ${if stdenv.isDarwin then '' + lsof -p $pid +r 3 &>/dev/null & + '' else if stdenv.isLinux || stdenv.isCygwin then '' + tail --pid=$pid -f /dev/null & + '' else if stdenv.isBSD || stdenv.isSunOS then '' + pwait $pid & + '' else throw "Don't know how to wait for process completion on system: ${stdenv.system}"} + + # Wait for the blocker process to complete. We use wait, so that bash can still + # handle the SIGTERM and SIGINT signals that may be sent to it by a process + # manager + blocker_pid=$! + wait $blocker_pid + '' else '' + exec "${executable}" "$@" + ''} + ''; + executable = true; +} diff --git a/nixproc/create-managed-process/agnostic/generate-launchd-daemon.nix b/nixproc/create-managed-process/agnostic/generate-launchd-daemon.nix new file mode 100644 index 0000000..c2af2a2 --- /dev/null +++ b/nixproc/create-managed-process/agnostic/generate-launchd-daemon.nix @@ -0,0 +1,73 @@ +{ createLaunchdDaemon +, stdenv +, writeTextFile +, runtimeDir ? "/var/run" +}: + +{ name +, description +, initialize +, daemon +, daemonArgs +, instanceName +, pidFile +, foregroundProcess +, foregroundProcessArgs +, path +, environment +, directory +, umask +, nice +, user +, dependencies +, credentials +, overrides +}: + +let + generateForegroundWrapper = import ./generate-foreground-wrapper.nix { + inherit stdenv writeTextFile; + }; + + Program = if foregroundProcess != null then + if initialize == "" then foregroundProcess + else generateForegroundWrapper ({ + wrapDaemon = false; + executable = foregroundProcess; + inherit name initialize runtimeDir stdenv; + } // stdenv.lib.optionalAttrs (instanceName != null) { + inherit instanceName; + } // stdenv.lib.optionalAttrs (pidFile != null) { + inherit pidFile; + }) + else generateForegroundWrapper ({ + wrapDaemon = true; + executable = daemon; + inherit name initialize runtimeDir stdenv; + } // stdenv.lib.optionalAttrs (instanceName != null) { + inherit instanceName; + } // stdenv.lib.optionalAttrs (pidFile != null) { + inherit pidFile; + }); + ProgramArguments = [ Program ] ++ (if foregroundProcess != null then foregroundProcessArgs else daemonArgs); + + daemonConfig = createLaunchdDaemon (stdenv.lib.recursiveUpdate ({ + inherit name credentials Program; + } // stdenv.lib.optionalAttrs (ProgramArguments != [ Program ]) { + inherit ProgramArguments; + } // stdenv.lib.optionalAttrs (environment != {}) { + EnvironmentVariables = environment; + } // stdenv.lib.optionalAttrs (path != []) { + inherit path; + } // stdenv.lib.optionalAttrs (directory != null) { + WorkingDirectory = directory; + } // stdenv.lib.optionalAttrs (umask != null) { + Umask = umask; + } // stdenv.lib.optionalAttrs (nice != null) { + Nice = nice; + } // stdenv.lib.optionalAttrs (user != null) { + UserName = user; + }) overrides); +in +if dependencies == [] then daemonConfig else +builtins.trace "WARNING: dependencies have been specified for process: ${name}, but launchd has no notion of process dependencies. Proper activation ordering cannot be guaranteed!" daemonConfig diff --git a/nixproc/create-managed-process/agnostic/generate-prestart-script.nix b/nixproc/create-managed-process/agnostic/generate-prestart-script.nix new file mode 100644 index 0000000..aa4c47f --- /dev/null +++ b/nixproc/create-managed-process/agnostic/generate-prestart-script.nix @@ -0,0 +1,11 @@ +{ stdenv, writeTextFile }: +{ name, initialize }: + +writeTextFile { + name = "${name}-prestart"; + executable = true; + text = '' + #! ${stdenv.shell} -e + ${initialize} + ''; +} diff --git a/nixproc/create-managed-process/agnostic/generate-supervisord-program.nix b/nixproc/create-managed-process/agnostic/generate-supervisord-program.nix new file mode 100644 index 0000000..cda05fe --- /dev/null +++ b/nixproc/create-managed-process/agnostic/generate-supervisord-program.nix @@ -0,0 +1,60 @@ +{ createSupervisordProgram, stdenv, writeTextFile, runtimeDir }: + +{ name +, description +, initialize +, daemon +, daemonArgs +, instanceName +, pidFile +, foregroundProcess +, foregroundProcessArgs +, path +, environment +, directory +, umask +, nice +, user +, dependencies +, credentials +, overrides +}: + +let + generateForegroundWrapper = import ./generate-foreground-wrapper.nix { + inherit stdenv writeTextFile; + }; + + command = if foregroundProcess != null then + (if initialize == "" + then foregroundProcess + else generateForegroundWrapper ({ + wrapDaemon = false; + executable = foregroundProcess; + inherit name initialize runtimeDir stdenv; + } // stdenv.lib.optionalAttrs (instanceName != null) { + inherit instanceName; + } // stdenv.lib.optionalAttrs (pidFile != null) { + inherit pidFile; + })) + " ${toString foregroundProcessArgs}" + else (generateForegroundWrapper ({ + wrapDaemon = true; + executable = daemon; + inherit name initialize runtimeDir stdenv; + } // stdenv.lib.optionalAttrs (instanceName != null) { + inherit instanceName; + } // stdenv.lib.optionalAttrs (pidFile != null) { + inherit pidFile; + })) + " ${toString daemonArgs}"; +in +createSupervisordProgram (stdenv.lib.recursiveUpdate ({ + inherit name command path environment dependencies credentials; +} // stdenv.lib.optionalAttrs (umask != null) { + inherit umask; +} // stdenv.lib.optionalAttrs (nice != null) { + inherit nice; +} // stdenv.lib.optionalAttrs (pidFile != null) { + inherit pidFile; +} // stdenv.lib.optionalAttrs (user != null) { + inherit user; +}) overrides) diff --git a/nixproc/create-managed-process/agnostic/generate-systemd-service.nix b/nixproc/create-managed-process/agnostic/generate-systemd-service.nix new file mode 100644 index 0000000..74b2438 --- /dev/null +++ b/nixproc/create-managed-process/agnostic/generate-systemd-service.nix @@ -0,0 +1,52 @@ +{ createSystemdService, stdenv, writeTextFile }: + +{ name +, description +, initialize +, daemon +, daemonArgs +, instanceName +, pidFile +, foregroundProcess +, foregroundProcessArgs +, path +, environment +, directory +, umask +, nice +, user +, dependencies +, credentials +, overrides +}: + +let + generatePreStartScript = import ./generate-prestart-script.nix { + inherit stdenv writeTextFile; + }; +in +createSystemdService (stdenv.lib.recursiveUpdate { + inherit name path environment dependencies credentials; + + Unit = { + Description = description; + }; + Service = { + ExecStart = if foregroundProcess != null then "${foregroundProcess} ${toString foregroundProcessArgs}" else "${daemon} ${toString daemonArgs}"; + Type = if foregroundProcess != null then "simple" else "forking"; + } // stdenv.lib.optionalAttrs (initialize != "") { + ExecStartPre = stdenv.lib.optionalString (user != null) "+" + generatePreStartScript { + inherit name initialize; + }; + } // stdenv.lib.optionalAttrs (directory != null) { + WorkingDirectory = directory; + } // stdenv.lib.optionalAttrs (umask != null) { + UMask = umask; + } // stdenv.lib.optionalAttrs (nice != null) { + Nice = nice; + } // stdenv.lib.optionalAttrs (foregroundProcess == null && pidFile != null) { + PIDFile = pidFile; + } // stdenv.lib.optionalAttrs (user != null) { + User = user; + }; +} overrides) diff --git a/nixproc/create-managed-process/agnostic/generate-sysvinit-script.nix b/nixproc/create-managed-process/agnostic/generate-sysvinit-script.nix new file mode 100644 index 0000000..f9c54c1 --- /dev/null +++ b/nixproc/create-managed-process/agnostic/generate-sysvinit-script.nix @@ -0,0 +1,32 @@ +{ createSystemVInitScript, stdenv }: + +{ name +, description +, initialize +, daemon +, daemonArgs +, instanceName +, pidFile +, foregroundProcess +, foregroundProcessArgs +, path +, environment +, directory +, umask +, nice +, user +, dependencies +, credentials +, overrides +}: + +createSystemVInitScript (stdenv.lib.recursiveUpdate ({ + inherit name description path environment directory umask nice dependencies credentials; + inherit instanceName initialize user; + + process = if daemon != null then daemon else foregroundProcess; + processIsDaemon = daemon != null; + args = if daemon != null then daemonArgs else foregroundProcessArgs; +} // stdenv.lib.optionalAttrs (pidFile != null) { + inherit pidFile; +}) overrides) diff --git a/nixproc/create-managed-process/bsdrc/build-bsdrc-env.nix b/nixproc/create-managed-process/bsdrc/build-bsdrc-env.nix new file mode 100644 index 0000000..21343a1 --- /dev/null +++ b/nixproc/create-managed-process/bsdrc/build-bsdrc-env.nix @@ -0,0 +1,30 @@ +{ pkgs ? import { inherit system; } +, system ? builtins.currentSystem +, stateDir ? "/var" +, runtimeDir ? "${stateDir}/run" +, logDir ? "${stateDir}/log" +, tmpDir ? (if stateDir == "/var" then "/tmp" else "${stateDir}/tmp") +, forceDisableUserChange ? false +, exprFile +}@args: + +let + processesFun = import exprFile; + + processesFormalArgs = builtins.functionArgs processesFun; + + processesArgs = builtins.intersectAttrs processesFormalArgs (args // { + processManager = "bsdrc"; + }); + + processes = processesFun processesArgs; +in +pkgs.buildEnv { + name = "rc.d"; + paths = map (processName: + let + process = builtins.getAttr processName processes; + in + process.pkg + ) (builtins.attrNames processes); +} diff --git a/nixproc/create-managed-process/bsdrc/create-bsdrc-script.nix b/nixproc/create-managed-process/bsdrc/create-bsdrc-script.nix new file mode 100644 index 0000000..d764df8 --- /dev/null +++ b/nixproc/create-managed-process/bsdrc/create-bsdrc-script.nix @@ -0,0 +1,237 @@ +{ writeTextFile +, stdenv +, createCredentials + +# Path to the rc.subr script +, rcSubr ? "/etc/rc.subr" +# Specifies which command are builtin. This is to determine which extra commands may have been provided. +, builtinCommands ? [ "start" "stop" "reload" "restart" "status" "poll" "rcvar" ] +# Specifies the default signal used for reloading a process +, defaultReloadSignal ? "HUP" +# Specifies the default signal used for stopping a process +, defaultStopSignal ? "TERM" +# Default run time directory where PID files are stored +, runtimeDir ? "/var/run" +# Specifies whether user changing functionality should be disabled or not +, forceDisableUserChange ? false +}: + +{ +# A name that identifies the process instance +name +# The variable suffix that indicates whether a service has been enabled or not. +, rcvar ? "enabled" +# An attribute set defining default values for configuration environment variables +, rcvarsDefaults ? {} +# The command that executes a daemon +, command ? null +# The command-line parameters passed to the command +, commandArgs ? [] +# Specifies whether the command daemonizes or not. If the command is not a daemon, it gets daemonized by the generator +, commandIsDaemon ? true +# Specifies the signal that needs to be sent to reload a process +, reloadSignal ? "HUP" +# Specifies the signal that needs to be sent to stop a process +, stopSignal ? "TERM" +# Specifies which packages need to be in the PATH +, environment ? {} +# An attribute set specifying arbitrary environment variables +, path ? [] +# A name that uniquely identifies each process instance. It is used to generate a unique PID file. +, instanceName ? null +# Path to a PID file that the system should use to manage the process. If null, it will use a default path. +, pidFile ? (if instanceName == null then null else "${runtimeDir}/${instanceName}.pid") +# If not null, the nice level be changed before executing any activities +, nice ? null +# If not null, the current working directory will be changed before executing any activities +, directory ? null +# Specifies as which user the process should run. If null, the user privileges will not be changed. +, user ? null +# Defines files that must be readable before running the start method +, requiredFiles ? [] +# Defines directories that must exist before running the start method +, requiredDirs ? [] +# Defines external environment variables this script depends on +, requiredVars ? [] +# Perform checkyesno on each of the list variables before running the start method. +, requiredModules ? [] +# Specifies the implementation of arbitrary commands +, commands ? {} +# If set to true the rc script accepts an arbitrary number of parameters. If set to false, it only accepts one. +, flexibleParameters ? false +# Specifies which feature the script requires. This is used by the rc init system to determine the proper activation order +, requires ? [] +# Specifies which features the script provides. By default, it simply considers it name a feature. +, provides ? [ "${name}" ] +# Keywords to be display in the comments section +, keywords ? [] +# A list of bsd rc scripts that this script depends on +, dependencies ? [] +# Specifies which groups and users that need to be created. +, credentials ? {} +}: + +# TODO: +# umask +# other properties. see rc.subr manpage + +assert command == null -> commands ? start && commands ? stop; + +let + extraCommands = builtins.attrNames (removeAttrs commands builtinCommands); + + _user = if forceDisableUserChange then null else user; + + _command = if commandIsDaemon then command else "daemon"; + _commandArgs = if commandIsDaemon then commandArgs else + stdenv.lib.optionals (pidFile != null) [ "-p" pidFile ] + ++ [ command ] + ++ commandArgs; + + _requires = map (dependency: dependency.name) dependencies ++ requires; + + envFile = if environment == {} then null else writeTextFile { + name = "${name}-envfile"; + text = stdenv.lib.concatMapStrings (name: + let + value = builtins.getAttr name environment; + in + ''${name}=${stdenv.lib.escapeShellArg value} + '' + ) (builtins.attrNames environment); + }; + + rcScript = writeTextFile { + inherit name; + executable = true; + text = '' + #!/bin/sh + '' + + stdenv.lib.optionalString (provides != []) '' + # PROVIDE: ${toString provides} + '' + + stdenv.lib.optionalString (_requires != []) '' + # REQUIRE: ${toString _requires} + '' + + stdenv.lib.optionalString (keywords != []) '' + # KEYWORD: ${toString keywords} + '' + + '' + . ${rcSubr} + + name="${name}" + '' + stdenv.lib.optionalString (rcvar != null) '' + rcvar=''${name}_${rcvar} + '' + + '' + + load_rc_config $name + ${stdenv.lib.concatMapStrings (rcvarName: '' + : ''${name}_${rcvarName}:=${toString builtins.getAttr rcvarName rcvarsDefaults} + '') (builtins.attrNames rcvarsDefaults)} + + '' + + stdenv.lib.optionalString (_command != null) '' + command=${_command} + '' + + stdenv.lib.optionalString (_commandArgs != []) '' + command_args="${toString _commandArgs}" + '' + + stdenv.lib.optionalString (requiredDirs != []) '' + required_dirs="${toString requiredDirs}" + '' + + stdenv.lib.optionalString (requiredFiles != []) '' + required_files="${toString requiredFiles}" + '' + + stdenv.lib.optionalString (requiredVars != []) '' + required_vars="${toString requiredVars}" + '' + + stdenv.lib.optionalString (requiredModules != []) '' + required_modules="${toString requiredModules}" + '' + + stdenv.lib.optionalString (pidFile != null) '' + pidfile="${pidFile}" + '' + + stdenv.lib.optionalString (reloadSignal != defaultReloadSignal) '' + sig_reload="${reloadSignal}" + '' + + stdenv.lib.optionalString (stopSignal != defaultStopSignal) '' + sig_stop="${stopSignal}" + '' + + stdenv.lib.optionalString (nice != null) '' + ${name}_nice=${toString nice} + '' + + stdenv.lib.optionalString (directory != null) '' + ${name}_chdir=${directory} + '' + + stdenv.lib.optionalString (_user != null) '' + ${name}_user=${_user} + '' + + stdenv.lib.optionalString (envFile != null) '' + ${name}_env_file=${envFile} + '' + + stdenv.lib.optionalString (extraCommands != []) '' + extra_commands="${toString extraCommands}" + '' + + stdenv.lib.concatMapStrings (commandName: + let + command = builtins.getAttr commandName commands; + in + stdenv.lib.optionalString (command ? pre) ''${commandName}_precmd=''${name}_pre${commandName} + '' + + stdenv.lib.optionalString (command ? implementation) ''${commandName}_cmd=''${name}_${commandName} + '' + + stdenv.lib.optionalString (command ? post) ''${commandName}_postcmd=''${name}_post${commandName} + '' + ) (builtins.attrNames commands) + + stdenv.lib.optionalString (path != []) '' + + PATH="${builtins.concatStringsSep ":" (map(package: "${package}/bin") path)}:$PATH" + export PATH + '' + + "\n" + + stdenv.lib.concatMapStrings (commandName: + let + command = builtins.getAttr commandName commands; + in + '' + ${stdenv.lib.optionalString (command ? pre) '' + ${name}_pre${commandName}() + { + ${command.pre} + } + ''} + ${stdenv.lib.optionalString (command ? implementation) '' + ${name}_${commandName}() + { + ${command.implementation} + } + ''} + ${stdenv.lib.optionalString (command ? post) '' + ${name}_post${commandName}() + { + ${command.post} + } + ''} + '' + ) (builtins.attrNames commands) + + '' + run_rc_command "${if flexibleParameters then "$@" else "$1"}" + ''; + }; + + credentialsSpec = if credentials == {} || forceDisableUserChange then null else createCredentials credentials; +in +stdenv.mkDerivation { + inherit name; + + buildCommand = '' + mkdir -p $out/etc/rc.d + cd $out/etc/rc.d + ln -s ${rcScript} ${name} + + ${stdenv.lib.optionalString (credentialsSpec != null) '' + ln -s ${credentialsSpec}/dysnomia-support $out/dysnomia-support + ''} + ''; +} diff --git a/nixproc/create-managed-process/bsdrc/rcsubr.nix b/nixproc/create-managed-process/bsdrc/rcsubr.nix new file mode 100644 index 0000000..4819cd3 --- /dev/null +++ b/nixproc/create-managed-process/bsdrc/rcsubr.nix @@ -0,0 +1,12 @@ +{stdenv, forceDisableUserChange}: + +stdenv.mkDerivation { + name = "rc.subr"; + src = /etc/rc.subr; + # Disable the limits command when we want to deploy processes as an unprivileged user + buildCommand = if forceDisableUserChange then '' + sed -e 's|limits -C $_login_class $_limits||' $src > $out + '' else '' + cp $src $out + ''; +} diff --git a/nixproc/create-managed-process/cygrunsrv/build-cygrunsrv-env.nix b/nixproc/create-managed-process/cygrunsrv/build-cygrunsrv-env.nix new file mode 100644 index 0000000..a32132b --- /dev/null +++ b/nixproc/create-managed-process/cygrunsrv/build-cygrunsrv-env.nix @@ -0,0 +1,30 @@ +{ pkgs ? import { inherit system; } +, system ? builtins.currentSystem +, stateDir ? "/var" +, runtimeDir ? "${stateDir}/run" +, logDir ? "${stateDir}/log" +, tmpDir ? (if stateDir == "/var" then "/tmp" else "${stateDir}/tmp") +, forceDisableUserChange ? false +, exprFile +}@args: + +let + processesFun = import exprFile; + + processesFormalArgs = builtins.functionArgs processesFun; + + processesArgs = builtins.intersectAttrs processesFormalArgs (args // { + processManager = "cygrunsrv"; + }); + + processes = processesFun processesArgs; +in +pkgs.buildEnv { + name = "cygrunsrv-env"; + paths = map (processName: + let + process = builtins.getAttr processName processes; + in + process.pkg + ) (builtins.attrNames processes); +} diff --git a/nixproc/create-managed-process/cygrunsrv/create-cygrunsrv-params.nix b/nixproc/create-managed-process/cygrunsrv/create-cygrunsrv-params.nix new file mode 100644 index 0000000..5551bb7 --- /dev/null +++ b/nixproc/create-managed-process/cygrunsrv/create-cygrunsrv-params.nix @@ -0,0 +1,112 @@ +{ stdenv +, writeTextFile + +# Prefix that is in front of all Windows services generated by this function +, prefix ? "nix-process-" +}: + +{ +# A name that identifies the process instance +name +# A more human readable name that identifies the process +, displayName ? "${prefix}${name}" +# Path to the executable to run +, path +# Command-line arguments propagated to the executable +, args ? [] +# An attribute set specifying arbitrary environment variables +, environment ? {} +# Specifies whether this service needs to be automatically started or not. +# 'manual' indicates manual start, 'auto' indicates automatic start +, type ? "auto" +# Specifies as which user the process should run. If null, the user privileges will not be changed. +, user ? null +# The password of the user so that the user privileges can be changed +, password ? null +# File where the stdin should read from. null indicates that no file should be read +, stdin ? null +# File where the stdout should write to. null discards output +, stdout ? null +# File where the stderr should write to. null discards output +, stderr ? null +# The signal that needs to be sent to the process to terminate it +, terminateSignal ? "TERM" +# Indicates whether the process should be terminated on shutdown +, terminateOnShutdown ? false +# Dependencies on other Windows services. The service manager makes sure that dependencies are activated first. +, dependencies ? [] +# Specifies which packages need to be in the PATH +, environmentPath ? [] +}: + +let + _environment = stdenv.lib.optionalAttrs (environmentPath != []) { + PATH = builtins.concatStringsSep ":" (map (package: "${package}/bin") environmentPath); # Augment path environment variable, if applicable + } // environment; + + cygrunsrvConfig = writeTextFile { + name = "${prefix}${name}-cygrunsrv-params"; + text = '' + --path + ${path} + --disp + ${displayName} + '' + + stdenv.lib.optionalString (type != "auto") '' + --type + ${type} + '' + + stdenv.lib.optionalString (args != []) '' + --args + ${builtins.concatStringsSep " " (map (arg: stdenv.lib.escapeShellArg arg) args)} + '' + + + stdenv.lib.concatMapStrings (variableName: + let + value = builtins.getAttr variableName _environment; + in + '' + --env + '${variableName}=${stdenv.lib.escape [ "'" ] (toString value)}' + '') (builtins.attrNames _environment) + + stdenv.lib.optionalString (user != null) '' + --user + ${user} + '' + + stdenv.lib.optionalString (password != null) '' + --passwd + ${password} + '' + + stdenv.lib.optionalString (stdin != null) '' + --stdin + ${stdin} + '' + + stdenv.lib.optionalString (stdout != null) '' + --stdout + ${stdout} + '' + + stdenv.lib.optionalString (stderr != null) '' + --stderr + ${stderr} + '' + + stdenv.lib.optionalString (terminateSignal != "TERM") '' + --termsig + ${terminateSignal} + '' + + stdenv.lib.optionalString terminateOnShutdown '' + --shutdown + '' + + stdenv.lib.concatMapStrings (dependency: '' + --dep + ${dependency.name} + '') dependencies; + }; +in +stdenv.mkDerivation { + name = "${prefix}${name}"; + + buildCommand = '' + mkdir -p $out + ln -s ${cygrunsrvConfig} $out/${prefix}${name}-cygrunsrvparams + ''; +} diff --git a/nixproc/create-managed-process/launchd/build-launchd-env.nix b/nixproc/create-managed-process/launchd/build-launchd-env.nix new file mode 100644 index 0000000..78a4bcf --- /dev/null +++ b/nixproc/create-managed-process/launchd/build-launchd-env.nix @@ -0,0 +1,30 @@ +{ pkgs ? import { inherit system; } +, system ? builtins.currentSystem +, stateDir ? "/var" +, runtimeDir ? "${stateDir}/run" +, logDir ? "${stateDir}/log" +, tmpDir ? (if stateDir == "/var" then "/tmp" else "${stateDir}/tmp") +, forceDisableUserChange ? false +, exprFile +}@args: + +let + processesFun = import exprFile; + + processesFormalArgs = builtins.functionArgs processesFun; + + processesArgs = builtins.intersectAttrs processesFormalArgs (args // { + processManager = "launchd"; + }); + + processes = processesFun processesArgs; +in +buildEnv { + name = "launchd"; + paths = map (processName: + let + process = builtins.getAttr processName processes; + in + process.pkg + ) (builtins.attrNames processes); +} diff --git a/nixproc/create-managed-process/launchd/create-launchd-daemon.nix b/nixproc/create-managed-process/launchd/create-launchd-daemon.nix new file mode 100644 index 0000000..3dab59c --- /dev/null +++ b/nixproc/create-managed-process/launchd/create-launchd-daemon.nix @@ -0,0 +1,96 @@ +{ writeTextFile +, stdenv +, createCredentials + +# Specifies whether user changing functionality should be disabled or not +, forceDisableUserChange ? false +# Prefix that is in front of all launchd plist files generated by this function +, prefix ? "org.nixos." +}: + +{ +# A name that identifies the process instance +name +# Specifies which packages need to be in the PATH +, path ? [] +# Specifies which groups and users that need to be created. +, credentials ? {} +# The remaining parameters are directly translated to plist XML properties. +# Possible configuration options can be found here: https://www.launchd.info +, ... +}@args: + +let + environment = stdenv.lib.optionalAttrs (path != []) { + PATH = builtins.concatStringsSep ":" (map (package: "${package}/bin") path); # Augment path environment variable, if applicable + } // stdenv.lib.mapAttrs (name: value: toString value) args.EnvironmentVariables or {}; # Convert all environment variables to strings + + label = if args ? Label then args.Label else "${prefix}${name}"; + + properties = { + Label = label; + } // removeAttrs args ([ "name" "path" "credentials" ] ++ stdenv.lib.optional forceDisableUserChange "UserName") // stdenv.lib.optionalAttrs (environment != {}) { + EnvironmentVariables = environment; + }; + + attrsToPList = attrs: + "\n" + + stdenv.lib.concatMapStrings (name: + let + value = builtins.getAttr name attrs; + in + '' + ${name} + ${exprToPList value} + '' + ) (builtins.attrNames attrs) + + "\n"; + + listToPList = list: + "\n" + + stdenv.lib.concatMapStrings (value: exprToPList value + "\n") list + + "\n"; + + exprToPList = expr: + let + exprType = builtins.typeOf expr; + in + if exprType == "bool" then + if expr then "" else "" + else if exprType == "int" then "${toString expr}" + else if exprType == "float" then "${toString expr}" + else if exprType == "string" then "${expr}" + else if exprType == "set" then + if stdenv.lib.isDerivation expr + then "${expr}" + else attrsToPList expr + else if exprType == "list" then listToPList expr + else if exprType == "null" then "" + else if exprType == "lambda" then throw "Cannot convert a lambda to a plist property" + else "${expr}"; + + launchdDaemonConfig = writeTextFile { + name = "${label}.plist"; + text = '' + + + + + ${exprToPList properties} + + ''; + }; + + credentialsSpec = if credentials == {} || forceDisableUserChange then null else createCredentials credentials; +in +stdenv.mkDerivation { + inherit name; + buildCommand = '' + mkdir -p $out/Library/LaunchDaemons + ln -s ${launchdDaemonConfig} $out/Library/LaunchDaemons/${label}.plist + + ${stdenv.lib.optionalString (credentialsSpec != null) '' + ln -s ${credentialsSpec}/dysnomia-support $out/dysnomia-support + ''} + ''; +} diff --git a/nixproc/create-managed-process/supervisord/build-supervisord-env.nix b/nixproc/create-managed-process/supervisord/build-supervisord-env.nix new file mode 100644 index 0000000..8f9b1fb --- /dev/null +++ b/nixproc/create-managed-process/supervisord/build-supervisord-env.nix @@ -0,0 +1,33 @@ +{ pkgs ? import { inherit system; } +, system ? builtins.currentSystem +, stateDir ? "/var" +, runtimeDir ? "${stateDir}/run" +, logDir ? "${stateDir}/log" +, tmpDir ? (if stateDir == "/var" then "/tmp" else "${stateDir}/tmp") +, forceDisableUserChange ? false +, exprFile +}@args: + +let + processesFun = import exprFile; + + processesFormalArgs = builtins.functionArgs processesFun; + + processesArgs = builtins.intersectAttrs processesFormalArgs (args // { + processManager = "supervisord"; + }); + + processes = processesFun processesArgs; +in +pkgs.buildEnv { + name = "supervisord.d"; + paths = map (processName: + let + process = builtins.getAttr processName processes; + in + process.pkg + ) (builtins.attrNames processes); + postBuild = '' + cp ${./supervisord.conf} $out/supervisord.conf + ''; +} diff --git a/nixproc/create-managed-process/supervisord/create-supervisord-program.nix b/nixproc/create-managed-process/supervisord/create-supervisord-program.nix new file mode 100644 index 0000000..c9a0343 --- /dev/null +++ b/nixproc/create-managed-process/supervisord/create-supervisord-program.nix @@ -0,0 +1,71 @@ +{writeTextFile, stdenv, createCredentials, supervisor, basePackages, forceDisableUserChange ? false, runtimeDir}: + +{ +# A name that identifies the process instance +name +# Indicates whether we want to use the pidproxy +, useProxy ? false +# Command line instruction to execute +, command ? null +# Name of the PID file that contains the PID of the running process +, pidFile ? "${name}.pid" +# Specifies which packages need to be in the PATH +, path ? [] +# An attribute set specifying arbitrary environment variables +, environment ? {} +# List of supervisord programs that this configuration depends on. This is used to derive the activation order. +, dependencies ? [] +# Specifies which groups and users that need to be created. +, credentials ? {} +# The remainder of the parameters directly translate to the properties described in: http://supervisord.org/configuration.html +, ... +}@params: + +let + properties = removeAttrs params ([ "name" "command" "useProxy" "pidFile" "path" "environment" "dependencies" "credentials" ] ++ stdenv.lib.optional forceDisableUserChange "user"); + + priority = if dependencies == [] then 1 + else builtins.head (builtins.sort (a: b: a > b) (map (dependency: dependency.priority) dependencies)) + 1; + + _command = (stdenv.lib.optionalString useProxy "${supervisor}/bin/pidproxy ${runtimeDir}/${pidFile} ") + command; + + _environment = { + PATH = builtins.concatStringsSep ":" (map (package: "${package}/bin") (basePackages ++ path)); + } // environment; + + confFile = writeTextFile { + name = "${name}.conf"; + text = '' + [program:${name}] + command=${_command} + priority=${toString priority} + '' + + (if _environment == {} then "" else "environment=" + stdenv.lib.concatMapStringsSep "," (name: + let + value = builtins.getAttr name _environment; + in + "${name}=\"${stdenv.lib.escape [ "\"" ] (toString value)}\"" + ) (builtins.attrNames _environment)) + + "\n" + + stdenv.lib.concatMapStrings (name: + let + value = builtins.getAttr name properties; + in + ''${name}=${toString value} + '' + ) (builtins.attrNames properties); + }; + + credentialsSpec = if credentials == {} || forceDisableUserChange then null else createCredentials credentials; +in +stdenv.mkDerivation { + inherit name priority; + buildCommand = '' + mkdir -p $out/conf.d + ln -s ${confFile} $out/conf.d/${name}.conf + + ${stdenv.lib.optionalString (credentialsSpec != null) '' + ln -s ${credentialsSpec}/dysnomia-support $out/dysnomia-support + ''} + ''; +} diff --git a/nixproc/create-managed-process/supervisord/supervisord.conf b/nixproc/create-managed-process/supervisord/supervisord.conf new file mode 100644 index 0000000..910a7df --- /dev/null +++ b/nixproc/create-managed-process/supervisord/supervisord.conf @@ -0,0 +1,10 @@ +[supervisord] + +[include] +files=conf.d/* + +[inet_http_server] +port = 127.0.0.1:9001 + +[rpcinterface:supervisor] +supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface diff --git a/nixproc/create-managed-process/systemd/build-systemd-env.nix b/nixproc/create-managed-process/systemd/build-systemd-env.nix new file mode 100644 index 0000000..ebef262 --- /dev/null +++ b/nixproc/create-managed-process/systemd/build-systemd-env.nix @@ -0,0 +1,30 @@ +{ pkgs ? import { inherit system; } +, system ? builtins.currentSystem +, stateDir ? "/var" +, runtimeDir ? "${stateDir}/run" +, logDir ? "${stateDir}/log" +, tmpDir ? (if stateDir == "/var" then "/tmp" else "${stateDir}/tmp") +, forceDisableUserChange ? false +, exprFile +}@args: + +let + processesFun = import exprFile; + + processesFormalArgs = builtins.functionArgs processesFun; + + processesArgs = builtins.intersectAttrs processesFormalArgs (args // { + processManager = "systemd"; + }); + + processes = processesFun processesArgs; +in +pkgs.buildEnv { + name = "systemd"; + paths = map (processName: + let + process = builtins.getAttr processName processes; + in + process.pkg + ) (builtins.attrNames processes); +} diff --git a/nixproc/create-managed-process/systemd/create-systemd-service.nix b/nixproc/create-managed-process/systemd/create-systemd-service.nix new file mode 100644 index 0000000..304a288 --- /dev/null +++ b/nixproc/create-managed-process/systemd/create-systemd-service.nix @@ -0,0 +1,117 @@ +{ writeTextFile +, stdenv +, createCredentials +, basePackages + +# Specifies whether user changing functionality should be disabled or not +, forceDisableUserChange ? false +# Prefix that is in front of all systemd units generated by this function +, prefix ? "nix-process-" +}: + +{ +# A name that identifies the process instance +name +# An attribute set specifying arbitrary environment variables +, environment ? {} +# List of supervisord services that this configuration depends on. +# These properties are translated to Wants= and After= properties to ensure +# proper activation ordering and that the dependencies are started first +, dependencies ? [] +# Specifies which packages need to be in the PATH +, path ? [] +# Specifies which groups and users that need to be created. +, credentials ? {} +# The remainder of the parameters directly get translated to sections and properties +# See: https://www.freedesktop.org/software/systemd/man/systemd.unit.html +# and: https://www.freedesktop.org/software/systemd/man/systemd.service.html +, ... +}@args: + +let + sections = removeAttrs args [ "name" "environment" "dependencies" "path" "credentials" ]; + + _environment = { + PATH = builtins.concatStringsSep ":" (map (package: "${package}/bin") (basePackages ++ path)); + } // environment; + + generateEnvironmentVariables = environment: + stdenv.lib.concatMapStrings (name: + let + value = builtins.getAttr name _environment; + in + ''Environment=${name}=${toString value} + '' + ) (builtins.attrNames _environment); + + mapDependencies = dependencies: + if dependencies == [] then "" + else + '' + Wants=${toString (map (dependency: "${dependency.name}.service") dependencies)} + After=${toString (map (dependency: "${dependency.name}.service") dependencies)} + ''; + + generateSection = {title, properties}: + '' + + [${title}] + ${stdenv.lib.concatMapStrings (name: + let + value = builtins.getAttr name properties; + in + if forceDisableUserChange && (name == "User" || name == "Group") then "" else # Don't change user privileges when we force it to be disabled + ''${name}=${toString value} + '' + ) (builtins.attrNames properties)}'' + + (if title == "Service" then generateEnvironmentVariables _environment else "") + + (if title == "Unit" then mapDependencies dependencies else ""); + + generateSections = sections: + stdenv.lib.concatMapStrings (title: + let + properties = builtins.getAttr title sections; + in + generateSection { + inherit title properties; + } + ) (builtins.attrNames sections); + + service = writeTextFile { + name = "${name}.service"; + text = '' + ${generateSections sections} + ${stdenv.lib.optionalString (!(sections ? Service) && _environment != {}) '' + [Service] + + ${generateEnvironmentVariables _environment}''} + ${stdenv.lib.optionalString (!(sections ? Unit) && dependencies != []) '' + + [Unit] + ${mapDependencies dependencies} + ''} + ''; + }; + + credentialsSpec = if credentials == {} || forceDisableUserChange then null else createCredentials credentials; +in +stdenv.mkDerivation { + name = "${prefix}${name}"; + + buildCommand = '' + mkdir -p $out/etc/systemd/system + ln -s ${service} $out/etc/systemd/system/${prefix}${name}.service + + ${stdenv.lib.optionalString (dependencies != []) '' + mkdir -p $out/etc/systemd/system/${prefix}${name}.service.wants + + ${stdenv.lib.concatMapStrings (dependency: '' + ln -s ${dependency}/etc/systemd/system/${dependency.name}.service $out/etc/systemd/system/${prefix}${name}.service.wants + '') dependencies} + ''} + + ${stdenv.lib.optionalString (credentialsSpec != null) '' + ln -s ${credentialsSpec}/dysnomia-support $out/dysnomia-support + ''} + ''; +} diff --git a/nixproc/create-managed-process/sysvinit/README.md b/nixproc/create-managed-process/sysvinit/README.md new file mode 100644 index 0000000..f6eee43 --- /dev/null +++ b/nixproc/create-managed-process/sysvinit/README.md @@ -0,0 +1,457 @@ +A Nix and sysvinit-based process management framework +===================================================== +This sub project contains a function abstraction that makes it possible to +generate sysvinit scripts with Nix. With this function it is possible to easily +manage *process deployments* -- Nix takes care of deploying the executable in +isolation in the Nix store, and the sysvinit script is used to manage the +lifecycle of the process. + +Advantages of this approach: +* Works on any Linux system with the Nix package manager installed +* Can be used for unprivileged process deployments +* No additional process dependencies required -- Nix arranges all package + dependencies, and that is all you need. + +Usage +===== +This sub project has a variety of use cases. + +Composing the createSystemVInitScript function +---------------------------------------------- +To construct sysvinit scripts, you must first compose the +`createSystemVInitScript` function and provide it some global settings. +These global settings apply to all sysvinit scripts that are generated with it. + +The following partial Nix expression shows how to compose this function with +the most common settings: + +```nix +{ pkgs ? import { inherit system; } +, system ? builtins.currentSystem +, stateDir ? "/var" +, runtimeDir ? "${stateDir}/run" +, tmpDir ? (if stateDir == "/var" then "/tmp" else "${stateDir}/tmp") +, forceDisableUserChange ? false +}: + +let + createSystemVInitScript = import ./create-sysvinit-script.nix { + inherit (stdenv) stdenv writeTextFile daemon; + inherit runtimeDir tmpDir forceDisableUserChange; + + initFunctions = import ./init-functions.nix { + basePackages = [ pkgs.coreutils pkgs.gnused pkgs.inetutils pkgs.gnugrep pkgs.sysvinit ]; + inherit (pkgs) stdenv; + inherit runtimeDir; + }; + + createCredentials = import ./create-credentials.nix { + inherit (pkgs) stdenv; + }; + }; +in +... +``` + +In the above Nix code fragment, we provide the following global configuration +settings: + +* We need to provide a number of mandatory package dependencies, such as + `stdenv`, `writeTextFile` and `daemon`. +* `runtimeDir` specifies the location where all PID files reside +* `tmpDir` specifies the location of the temp directory +* `forceDisableUserChange` globally disables user switches. For production + environments, this should be disabled so that processes can run more securely + as an unprivileged user. For development environments it may be useful to + enable this feature so that you manage all processes without having super user + privileges. +* The `initFunctions` parameter refers to a function invocation that deploys + a package with the `init-functions` script. This script provides standard + LSB functionality to manage processes. +* The `createCredentials` composes the function that can be used to configure + a Dysnomia configuration file so that users and groups can be created on + activation and discarded on deactivation. + +In addition to the common settings shown above, there are also a number of +unconventional parameters. For common use scenarios, the default values suffice. +You only need to adjust them in special circumstances: + +* `initialInstructions` specify the global initial instructions added to any +sysvinit script. +* `startDaemon` specifies the command-line instruction to start a daemon. +* `startProcessAsDaemon` specifies the command-line instruction to start a + foreground process as a daemon. +* `startDaemon` specifies the command-line instruction to stop a daemon. +* `reloadDaemon` specifies the command-line instruction to reload a daemon. +* `evaluateCommand` specifies the command-line instruction that displays the + status of the previously executed shell instruction. +* `statusCommand` specifies the command that retrieves the status of the daemon +* `restartActivity` specifies the implementation of the restart activity, that + is common to all process-oriented sysvinit scripts. +* `supportedRunLevels` referts to a list that iterates all supported runlevels. +* `minSequence` specifies the minimum start sequence number. +* `maxSequence` specifies the maximum start sequence number. + +Creating a sysvinit script +-------------------------- +After composing the `createSystemVInitScript` function, we can write Nix +expressions that build sysvinit scripts. + +### Specifying activities + +The following Nix expression is a straight forward example demonstrating how we +can manage the Nginx web server: + +```nix +{createSystemVInitScript, nginx, configFile, stateDir}: + +createSystemVInitScript { + name = "nginx"; + description = "Nginx"; + activities = { + start = '' + mkdir -p ${stateDir}/logs + log_info_msg "Starting Nginx..." + loadproc ${nginx}/bin/nginx -c ${configFile} -p ${stateDir} + evaluate_retval + ''; + stop = '' + log_info_msg "Stopping Nginx..." + killproc ${nginx}/bin/nginx + evaluate_retval + ''; + reload = '' + log_info_msg "Reloading Nginx..." + killproc ${nginx}/bin/nginx -HUP + evaluate_retval + ''; + restart = '' + $0 stop + sleep 1 + $0 start + ''; + status = "statusproc ${nginx}/bin/nginx"; + }; + runlevels = [ 3 4 5 ]; +} +``` + +The above Nix expression composes a sysvinit script to manage the life-cycle of +the Nginx server: + +* The `name` parameter specifies the name of the sysvinit script +* The `description` specifies the description field shown in the meta + information section. +* The `activities` parameter refers to an attribute set that specifies the + implementation of each activity in bash code. +* The `runlevels` parameter specifies in which runlevels we want to start this + script. It will automatically compose symlinks with the appropriate start + sequence numbers in rc.d directories for the corresponding runlevels. An + implication is that this function will automatically compose stop rc.d + symlinks for the remaining runlevels. + It will stop sysvinit scripts in exactly the opposite of the start order. + +### Specifying instructions + +Many sysvinit scripts implement activities that consist of a description line, +followed by a command, followed by displaying the status, e.g.: + +```bash +log_info_msg "Starting Nginx..." +loadproc ${nginx}/bin/nginx -c ${configFile} -p ${stateDir} +evaluate_retval +``` + +It is possible to reduce this boilerplate code by using the instructions +facility: + +```nix +{createSystemVInitScript, nginx, configFile, stateDir}: + +createSystemVInitScript { + name = "nginx"; + description = "Nginx"; + instructions = { + start = { + activity = "Starting"; + instruction = '' + mkdir -p ${stateDir}/logs + loadproc ${nginx}/bin/nginx -c ${configFile} -p ${stateDir} + ''; + }; + stop = { + activity = "Stopping"; + instruction = "killproc ${nginx}/bin/nginx"; + }; + reload = { + activity = "Reloading"; + instruction = "killproc ${nginx}/bin/nginx -HUP"; + }; + }; + activities = { + status = "statusproc ${nginx}/bin/nginx"; + }; + runlevels = [ 3 4 5 ]; +} +``` + +In the above Nix expression we have replaced the `start`, `stop`, and `reload` +reload activities, with `instructions`. For these instructions, the description +line is automatically derived from the `description` parameter and augmented with +the instruction displaying the status. + +### Specifying daemons to manage + +It is possible to reduce the amount of boilerplate code even further -- +in many scenarios we want to manage a process. The kind of activities that you +need are typically the same -- `start`, `stop`, `reload` (if applicable), +`status` and `restart`: + +```nix +{createSystemVInitScript, nginx, configFile, stateDir}: + +createSystemVInitScript { + name = "nginx"; + description = "Nginx"; + initialize = '' + mkdir -p ${stateDir}/logs + ''; + process = "${nginx}/bin/nginx"; + args = [ "-c" configFile "-p" stateDir ]; + runlevels = [ 3 4 5 ]; +} +``` + +In the above example, we do not specify any activities or instructions. +Instead, we specify the `process` we want to run and the command-line +instructions (`args`). From these properties, the generator will automatically +generate all relevant activities. + +### Managing foreground processes + +```nix +{createSystemVInitScript, port ? 5000}: + +let + webapp = (import ./webapp {}).package; +in +createSystemVInitScript { + name = "webapp"; + process = "${webapp}/lib/node_modules/webapp/app.js"; + processIsDaemon = false; + runlevels = [ 3 4 5 ]; + environment = { + PORT = port; + }; +} +``` + +By default, sysvinit scripts expect processes to daemonize -- a process forks +another process that keeps running in the background and then the parent process +terminates. Most common system software, e.g. web servers, DBMS servers, +provides this kind of functionality. + +Application software, however, that are typically implemented in more +"higher-level languages" than C, do not have this ability out of the box. + +It is also possible to invoke libslack's +[daemon](http://www.libslack.org/daemon/) command to let a foreground process +daemonize -- when setting `processIsDaemon` to `false` (the default is: `true`), +the generator will automatically invoke the `daemon` command to accomplish this. + +The `environment` parameter can be used to set additional environment variables. +In the above example, it is used to specify to which TCP port the process should +bind to. + +### Specifying process dependencies + +Processes may also communicate with other processes by using some kind of IPC +mechanism, such as Unix domain sockets. In such cases, a process might depend on +the activation of another process. + +For example, the `nginx` server could act as a reverse proxy for the `webapp`. +This means that the `webapp` process should be activated first, otherwise users +might get a 502 bad gateway error. + +We can augment the Nginx reverse proxy with a dependency parameter: + +```nix +{createSystemVInitScript, nginx, configFile, stateDir, webapp}: + +createSystemVInitScript { + name = "nginx"; + description = "Nginx"; + initialize = '' + mkdir -p ${stateDir}/logs + ''; + process = "${nginx}/bin/nginx"; + args = [ "-c" configFile "-p" stateDir ]; + runlevels = [ 3 4 5 ]; + dependencies = [ webapp ]; +} +``` + +The above example passed the `webapp` process as a dependency (through the +`dependencies` parameter) to the Nginx process. The generator makes sure that +the Nginx process gets a higher start sequence number and a lower stop sequence +number than the `webapp` process, ensuring that it starts in the right order +and stops in the reverse order. + +### Managing multiple process instances + +In addition to deploying single instances of processes, it is also possible to +have multiple instances of processes. For example, the Nginx reverse proxy can +forward incoming requests to multiple instances of the `webapp`. + +sysvinit scripts use PID files to control daemons. Normally, PID files have the +same name as the executable. When running multiple instances, we must make sure +that every instance gets a unique PID, otherwise all instances might get +killed. + +We can adjust the Nix expression of `webapp` with an `instanceName` parameter: + +```nix +{createSystemVInitScript}: +{instanceSuffix ? "", port ? 5000}: + +let + webapp = (import ./webapp {}).package; + instanceName = "webapp${instanceSuffix}"; +in +createSystemVInitScript { + name = instanceName; + inherit instanceName; + process = "${webapp}/lib/node_modules/webapp/app.js"; + processIsDaemon = false; + runlevels = [ 3 4 5 ]; + environment = { + PORT = port; + }; +} +``` + +In the above example, we define a nested function in which the outer function +header (first line) refers to configuration properties applying to all process +instances. + +The inner function header (second line) refers to instance properties: +* `instanceSuffix` can be used to construct a unique name for each `webapp` + instance called in a variable called `instanceName`. +* `port` is a parameter that specifies to which TCP port the webapp should + listen to. This port value should be unique for each `webapp` instance. + +The `instanceName` parameter instructs the sysvinit script generator to create +a unique PID file for each running instance making it possible to control +instances individually. + +### Managing user credentials + +When deploying processes as system administrator, it is typically +insecure/unsafe to spawn processes as a root user. + +The following expression instructs the generator to run the `webapp` as a unique +unprivileged user: + +```nix +{createSystemVInitScript}: +{port, instanceSuffix ? ""}: + +let + webapp = (import ./webapp {}).package; + instanceName = "webapp${instanceSuffix}"; +in +createSystemVInitScript { + name = instanceName; + inherit instanceName; + process = "${webapp}/lib/node_modules/webapp/app.js"; + processIsDaemon = false; + runlevels = [ 3 4 5 ]; + environment = { + PORT = port; + }; + user = instanceName; + + credentials = { + groups = { + "${instanceName}" = {}; + }; + users = { + "${instanceName}" = { + group = instanceName; + description = "Webapp"; + }; + }; + }; +} +``` + +In the above example, the `credentials` parameter generates a configuration file +that Dysnomia can use to create or discard groups and users. The `user` parameter +instructs the script to switch privileges to the given user. + +### Disable user switching + +For production deployments, switching user privileges is useful, but for +experimentation as an unprivileged user it is not. + +It is also possible to globally disable user switching, by setting the +`forceDisableUserChange` to `true` in the example that composes the +`createSystemVInitScript` function. + +### Other settings + +The `createSystemVInitScript` has a variety of other configuration properties +not shown here, such as: + +* Specifying the `umask` for default file permissions +* Specifying the nice level of the process with `nice` +* Change the current working directory with `directory` +* Adding additional packages to the sysvinit script's PATH, via `paths` +* Removing generated sysvinit script activities with `removeActivities` + +Deploying a collection of processes +----------------------------------- +We can use the `nixproc-sysvinit-switch` command to build and activate all +processes in a processes expression: + +```bash +$ nixproc-sysvinit-switch processes.nix +``` + +The `nixproc-sysvinit-switch` command will activate the processes in the right +order (this means it will ensure that `webapp` gets activated before `nginx`). + +If we have already deployed a collection of processes then it does a comparison +with the previous deployment, and only deactivates obsolete processes and +activates new processes: + +```bash +$ nixproc-sysvinit-switch processes.nix +``` + +In the above case, `rcswitch` will only deactivate obsolete processes and +activate new processes, making redeployments significantly faster. + +Running activities on a collection of processes +----------------------------------------------- +In addition to deploying a collection of processes or upgrading a collection of +processes, it is also possible to run a certain activity on all sysvinit +scripts in the last deployed Nix profile: + +```bash +$ nixproc-sysvinit-runactivity status +``` + +The above command will show the statuses of all processes in the provided Nix +profile. + +By default, `nixproc-sysvinit-runactivity` will examine scripts in start +activation order. + +We can also specify that we want to examine all scripts in the reverse order. +This is particularly useful to stop all services without breaking process +dependencies: + +```bash +$ nixproc-sysvinit-runactivity -r stop +``` diff --git a/nixproc/create-managed-process/sysvinit/build-sysvinit-env.nix b/nixproc/create-managed-process/sysvinit/build-sysvinit-env.nix new file mode 100644 index 0000000..2ceb530 --- /dev/null +++ b/nixproc/create-managed-process/sysvinit/build-sysvinit-env.nix @@ -0,0 +1,30 @@ +{ pkgs ? import { inherit system; } +, system ? builtins.currentSystem +, stateDir ? "/var" +, runtimeDir ? "${stateDir}/run" +, logDir ? "${stateDir}/log" +, tmpDir ? (if stateDir == "/var" then "/tmp" else "${stateDir}/tmp") +, forceDisableUserChange ? false +, exprFile +}@args: + +let + processesFun = import exprFile; + + processesFormalArgs = builtins.functionArgs processesFun; + + processesArgs = builtins.intersectAttrs processesFormalArgs (args // { + processManager = "sysvinit"; + }); + + processes = processesFun processesArgs; +in +pkgs.buildEnv { + name = "rc.d"; + paths = map (processName: + let + process = builtins.getAttr processName processes; + in + process.pkg + ) (builtins.attrNames processes); +} diff --git a/nixproc/create-managed-process/sysvinit/create-sysvinit-script.nix b/nixproc/create-managed-process/sysvinit/create-sysvinit-script.nix new file mode 100644 index 0000000..cd8f96e --- /dev/null +++ b/nixproc/create-managed-process/sysvinit/create-sysvinit-script.nix @@ -0,0 +1,247 @@ +{ stdenv +, writeTextFile +, daemon +, initFunctions +, createCredentials + +# Instructions that are carried out before anything else gets executed +, initialInstructions ? ". ${initFunctions}" +# Command that specifies how to start a daemon +, startDaemon ? "start_daemon" +# Path to the executable that daemonizes a foreground process +, startProcessAsDaemon ? "${daemon}/bin/daemon" +# Command that specifies how to stop a daemon +, stopDaemon ? "killproc" +# Command that specifies how to reload a daemon +, reloadDaemon ? "killproc" +# Command that evaluates the return value of the previous command +, evaluateCommand ? "evaluate_retval" +# Command that shows the status of a daemon +, statusCommand ? "statusproc" +# Default run time directory where PID files are stored +, runtimeDir ? "/var/run" +# Directory in which temp files are stored +, tmpDir ? "/tmp" +# Specifies the implementation of the restart activity that is used for all sysvinit scripts. Typically, there is little reason to change it. +, restartActivity ? '' + $0 stop + sleep 1 + $0 start +'' +# Specifies which runlevels are supported +, supportedRunlevels ? stdenv.lib.range 0 6 +# The minimum start/stop sequence number +, minSequence ? 0 +# The maximum start/stop sequence number +, maxSequence ? 99 +# Specifies whether user changing functionality should be disabled or not +, forceDisableUserChange ? false +}: + +{ +# A name that identifies the process instance +name +# A name that uniquely identifies each process instance. It is used to generate a unique PID file. +, instanceName ? null +# A description that is added to the comments section +, description ? name +# Global instructions that are executed before any activity gets executed +, globalInstructions ? "" +# If not null, the umask will be changed before executing any activities +, umask ? null +# If not null, the nice level be changed before executing any activities +, nice ? null +# If not null, the current working directory will be changed before executing any activities +, directory ? null +# Specifies which packages need to be in the PATH +, path ? [] +# An attribute set specifying arbitrary environment variables +, environment ? {} +# Shell instructions that specify how the state of the process should be initialized +, initialize ? "" +# A high level property to specify what process needs to be managed. From this property, the start, stop, reload, status and restart activities are derived. +, process ? null +# Specifies that the process is a daemon. If a process is not a daemon, then the generator will automatically daemonize it. +, processIsDaemon ? true +# Command-line arguments passed to the process. +, args ? [] +# Path to a PID file that the system should use to manage the process. If null, it will use a default path. +, pidFile ? (if instanceName == null then null else if user == null || user == "root" || forceDisableUserChange then "${runtimeDir}/${instanceName}.pid" else "${tmpDir}/${instanceName}.pid") +# Specifies as which user the process should run. If null, the user privileges will not be changed. +, user ? null +# Specifies which signal should be send to the process to reload its configuration +, reloadSignal ? "-HUP" +# Specifies arbitrary instructions to carry out. Before each instruction the activity will be printed, and after the execution it will evaluate the return status +, instructions ? {} +# Specifies the raw implementation of each activity. +, activities ? {} +# Specifies activities to remove from the generated activities attribute set +, removeActivities ? [] +# Specifies in which runlevels the script should be started. From this value, the script will automatically be stopped in the remaining runlevels +, runlevels ? [] +# Specifies in which runlevels the script should be started. +, defaultStart ? [] +# Specifies in which runlevels the script should be stopped. +, defaultStop ? [] +# A list of sysvinit scripts that this scripts depends on. This is used to automatically derive the start and stop sequence. Dependencies will be started first and stopped last. +, dependencies ? [] +# Specifies which groups and users that need to be created. +, credentials ? {} +}: + +let + # Enumerates the activities in a logical order -- the common activities first, then the remaining activities in alphabetical order + enumerateActivities = activities: + stdenv.lib.optional (activities ? start) "start" + ++ stdenv.lib.optional (activities ? stop) "stop" + ++ stdenv.lib.optional (activities ? reload) "reload" + ++ stdenv.lib.optional (activities ? restart) "restart" + ++ stdenv.lib.optional (activities ? status) "status" + ++ builtins.filter (activityName: activityName != "start" && activityName != "stop" && activityName != "reload" && activityName != "restart" && activityName != "status" && activityName != "*") (builtins.attrNames activities) + ++ stdenv.lib.optional (activities ? "*") "*"; + + _user = if forceDisableUserChange then null else user; + + _instructions = (stdenv.lib.optionalAttrs (process != null) { + start = { + activity = "Starting"; + instruction = + initialize + + (if processIsDaemon then "${startDaemon} ${stdenv.lib.optionalString (pidFile != null) "-f -p ${pidFile}"} ${stdenv.lib.optionalString (nice != null) "-n ${nice}"} ${stdenv.lib.optionalString (_user != null) "su ${_user} -c '"} ${process} ${toString args} ${stdenv.lib.optionalString (_user != null) "'"}" + else "${startProcessAsDaemon} -U -i ${if pidFile == null then "-P ${runtimeDir} -n $(basename ${process})" else "-F ${pidFile}"} ${stdenv.lib.optionalString (_user != null) "-u ${_user}"} -- ${process} ${toString args}"); + }; + stop = { + activity = "Stopping"; + instruction = "${stopDaemon} ${stdenv.lib.optionalString (pidFile != null) "-p ${pidFile}"} ${process}"; + }; + reload = { + activity = "Reloading"; + instruction = "${reloadDaemon} ${stdenv.lib.optionalString (pidFile != null) "-p ${pidFile}"} ${process} ${reloadSignal}"; + }; + }) // instructions; + + _activities = + let + convertedInstructions = stdenv.lib.mapAttrs (name: instruction: + '' + log_info_msg "${instruction.activity} ${description}..." + ${instruction.instruction} + ${evaluateCommand} + '' + ) _instructions; + + defaultActivities = stdenv.lib.optionalAttrs (process != null) { + status = "${statusCommand} ${stdenv.lib.optionalString (pidFile != null) "-p ${pidFile}"} ${process}"; + restart = restartActivity; + } // { + "*" = '' + echo "Usage: $0 {${builtins.concatStringsSep "|" (builtins.filter (activityName: activityName != "*") (enumerateActivities _activities))}}" + exit 1 + ''; + }; + in + removeAttrs (convertedInstructions // defaultActivities // activities) removeActivities; + + _defaultStart = if runlevels != [] then stdenv.lib.intersectLists runlevels supportedRunlevels + else defaultStart; + + _defaultStop = if runlevels != [] then stdenv.lib.subtractLists _defaultStart supportedRunlevels + else defaultStop; + + _environment = stdenv.lib.optionalAttrs (path != []) { + PATH = "${builtins.concatStringsSep ":" (map(package: "${package}/bin" ) path)}:$PATH"; + } // environment; + + initdScript = writeTextFile { + inherit name; + executable = true; + text = '' + #! ${stdenv.shell} + + ## BEGIN INIT INFO + # Provides: ${name} + '' + + stdenv.lib.optionalString (_defaultStart != []) "# Default-Start: ${toString _defaultStart}\n" + + stdenv.lib.optionalString (_defaultStop != []) "# Default-Stop: ${toString _defaultStop}\n" + + stdenv.lib.optionalString (dependencies != []) '' + # Should-Start: ${toString (map (dependency: dependency.name) dependencies)} + # Should-Stop: ${toString (map (dependency: dependency.name) dependencies)} + '' + + '' + # Description: ${description} + ## END INIT INFO + + ${initialInstructions} + ${globalInstructions} + '' + + stdenv.lib.optionalString (umask != null) '' + umask ${umask} + '' + + stdenv.lib.optionalString (directory != null) '' + cd ${directory} + '' + + stdenv.lib.concatMapStrings (name: + let + value = builtins.getAttr name _environment; + in + '' + export ${name}=${stdenv.lib.escapeShellArg value} + '' + ) (builtins.attrNames _environment) + + '' + + case "$1" in + ${stdenv.lib.concatMapStrings (activityName: + let + instructions = builtins.getAttr activityName _activities; + in + '' + ${activityName}) + ${instructions} + ;; + + '' + ) (enumerateActivities _activities)} + esac + ''; + }; + + startSequenceNumber = + if dependencies == [] then minSequence + else builtins.head (builtins.sort (a: b: a > b) (map (dependency: dependency.sequence) dependencies)) + 1; + + stopSequenceNumber = maxSequence - startSequenceNumber + minSequence; + + sequenceNumberToString = number: + if number < 10 then "0${toString number}" + else toString number; + + credentialsSpec = if credentials == {} || forceDisableUserChange then null else createCredentials credentials; +in +stdenv.mkDerivation { + inherit name; + + sequence = startSequenceNumber; + + buildCommand = '' + mkdir -p $out/etc/rc.d + cd $out/etc/rc.d + + mkdir -p init.d + ln -s ${initdScript} init.d/${name} + + ${stdenv.lib.concatMapStrings (runlevel: '' + mkdir -p rc${toString runlevel}.d + ln -s ../init.d/${name} rc${toString runlevel}.d/S${sequenceNumberToString startSequenceNumber}${name} + '') _defaultStart} + + ${stdenv.lib.concatMapStrings (runlevel: '' + mkdir -p rc${toString runlevel}.d + ln -s ../init.d/${name} rc${toString runlevel}.d/K${sequenceNumberToString stopSequenceNumber}${name} + '') _defaultStop} + + ${stdenv.lib.optionalString (credentialsSpec != null) '' + ln -s ${credentialsSpec}/dysnomia-support $out/dysnomia-support + ''} + ''; +} diff --git a/nixproc/create-managed-process/sysvinit/init-functions.nix b/nixproc/create-managed-process/sysvinit/init-functions.nix new file mode 100644 index 0000000..c6ca0db --- /dev/null +++ b/nixproc/create-managed-process/sysvinit/init-functions.nix @@ -0,0 +1,26 @@ +{stdenv, fetchurl, basePackages, runtimeDir}: + +let + basePath = builtins.concatStringsSep ":" (map (package: "${package}/bin") basePackages); + + src = fetchurl { + url = http://www.linuxfromscratch.org/lfs/downloads/9.0/lfs-bootscripts-20190524.tar.xz; + sha256 = "0975wmghhh7j5qify0m170ba2d7vl0km7sw05kclnmwpgivimb38"; + }; +in +stdenv.mkDerivation { + name = "init-functions"; + + buildCommand = '' + tar xfv ${src} lfs-bootscripts-20190524/lfs/lib/services/init-functions + + sed \ + -e "s|/bin:/usr/bin:/sbin:/usr/sbin|${basePath}|" \ + -e "s|/bin/sh|${stdenv.shell}|" \ + -e "s|/bin/echo|echo|" \ + -e "s|/bin/head|head|" \ + -e "s|/var/run|${runtimeDir}|" \ + -e "s|/run/bootlog|${runtimeDir}/bootlog|" \ + lfs-bootscripts-20190524/lfs/lib/services/init-functions > $out + ''; +} diff --git a/tools/bsdrc/default.nix b/tools/bsdrc/default.nix new file mode 100644 index 0000000..62b302b --- /dev/null +++ b/tools/bsdrc/default.nix @@ -0,0 +1,21 @@ +{stdenv, getopt}: + +stdenv.mkDerivation { + name = "nixproc-bsdrc-tools"; + buildCommand = '' + mkdir -p $out/bin + + sed -e "s|/bin/bash|$SHELL|" \ + -e "s|@getopt@|${getopt}/bin/getopt|" \ + -e "s|@sed@|$(type -p sed)|" \ + -e "s|@commonchecks@|${../commonchecks}|" \ + ${./nixproc-bsdrc-switch.in} > $out/bin/nixproc-bsdrc-switch + chmod +x $out/bin/nixproc-bsdrc-switch + + sed -e "s|/bin/bash|$SHELL|" \ + -e "s|@getopt@|${getopt}/bin/getopt|" \ + -e "s|@commonchecks@|${../commonchecks}|" \ + ${./nixproc-bsdrc-runactivity.in} > $out/bin/nixproc-bsdrc-runactivity + chmod +x $out/bin/nixproc-bsdrc-runactivity + ''; +} diff --git a/tools/bsdrc/nixproc-bsdrc-runactivity.in b/tools/bsdrc/nixproc-bsdrc-runactivity.in new file mode 100644 index 0000000..d039ee3 --- /dev/null +++ b/tools/bsdrc/nixproc-bsdrc-runactivity.in @@ -0,0 +1,91 @@ +#!/bin/bash +set -e +shopt -s nullglob + +# Shows the usage of this command to the user + +showUsage() +{ + cat <&2 + exit 1 +fi + +source @commonchecks@ + +checkNixStateDir +checkProfile +composeOldProfilePath + +# Execute the activities + +rcpath="$oldProfilePath/etc/rc.d" + +if [ "$reverse" = "1" ] +then + for i in $(rcorder $rcpath/* | tail -r) + do + $i $activity + done +else + for i in $(rcorder $rcpath/*) + do + $i $activity + done +fi diff --git a/tools/bsdrc/nixproc-bsdrc-switch.in b/tools/bsdrc/nixproc-bsdrc-switch.in new file mode 100644 index 0000000..f7fb24e --- /dev/null +++ b/tools/bsdrc/nixproc-bsdrc-switch.in @@ -0,0 +1,197 @@ +#!/bin/bash +set -e +shopt -s nullglob + +# Shows the usage of this command to the user + +showUsage() +{ + cat <&2 + + for i in $(rcorder $rcold/* | tail -r) + do + currentPath=$(readlink -f $i) + oldscripts+=($currentPath) + done +fi + +# Determine paths of new scripts + +newscripts=() + +for i in $(rcorder $rcnew/*) +do + currentPath=$(readlink -f $i) + newscripts+=($currentPath) +done + +# Create new groups and users +createNewGroups +createNewUsers + +# Stop and remove obsolete scripts + +for i in $(rcorder $rcold/* | tail -r) +do + if ! containsElement "$(readlink -f "$i")" "${newscripts[@]}" + then + if [ "$enableAtBoot" = "1" ] + then + scriptName="$(basename $i)" + /usr/local/etc/$scriptName stop || true + @sed@ -i -e "/^${scriptName}_enabled=YES"'$'"/d" /etc/rc.conf + rm -f /usr/local/etc/$scriptName + else + "$i" onestop + fi + fi +done + +# Install and start new scripts + +for i in $(rcorder $rcnew/*) +do + if ! containsElement "$(readlink -f "$i")" "${oldscripts[@]}" + then + if [ "$enableAtBoot" = "1" ] + then + scriptName="$(basename $i)" + ln -sfn $rcnew/$scriptName /usr/local/etc + echo "${scriptName}_enabled=YES" >> /etc/rc.conf + /usr/local/etc/$scriptName start + else + "$i" onestart + fi + fi +done + +# Delete obsolete users and groups +deleteObsoleteUsers +deleteObsoleteGroups + +# Set new profile +setNixProfile diff --git a/tools/build/default.nix b/tools/build/default.nix new file mode 100644 index 0000000..8c3bb1d --- /dev/null +++ b/tools/build/default.nix @@ -0,0 +1,15 @@ +{stdenv, getopt}: + +stdenv.mkDerivation { + name = "nixproc-build-tools"; + buildCommand = '' + mkdir -p $out/bin + + sed -e "s|/bin/bash|$SHELL|" \ + -e "s|@getopt@|${getopt}/bin/getopt|" \ + -e "s|@readlink@|$(type -p readlink)|" \ + -e "s|@NIXPROC@|${../../nixproc}|" \ + ${./nixproc-build.in} > $out/bin/nixproc-build + chmod +x $out/bin/nixproc-build + ''; +} diff --git a/tools/build/nixproc-build.in b/tools/build/nixproc-build.in new file mode 100644 index 0000000..6fca94d --- /dev/null +++ b/tools/build/nixproc-build.in @@ -0,0 +1,138 @@ +#!/bin/bash -e + +# Shows the usage of this command to the user + +showUsage() +{ + cat <&2 + exit 1 +else + processManagerArg="--argstr processManager $processManager" +fi + +if [ "$1" = "" ] +then + echo "No processes expression provided!" >&2 + exit 1 +else + exprFile=$(@readlink@ -f "$1") +fi + +# Validate the given options + +if [ "$NIXPROC_STATE_DIR" != "" ] +then + stateDirArg="--argstr stateDir $NIXPROC_STATE_DIR" +fi + +if [ "$NIXPROC_RUNTIME_DIR" != "" ] +then + runtimeDirArg="--argstr stateDir $NIXPROC_RUNTIME_DIR" +fi + +if [ "$NIXPROC_LOG_DIR" != "" ] +then + logDirArg="--argstr logDir $NIXPROC_LOG_DIR" +fi + +if [ "$NIXPROC_TMP_DIR" != "" ] +then + tmpDirArg="--argstr tmpDir $NIXPROC_TMP_DIR" +fi + +if [ "$NIXPROC_FORCE_DISABLE_USER_CHANGE" = "1" ] +then + forceDisableUserChangeArg="--arg forceDisableUserChange true" +fi + +NIXPROC=${NIXPROC:-@NIXPROC@} + +# Build the profile +nix-build $stateDirArg $runtimeDirArg $logDirArg $tmpDirArg $forceDisableUserChangeArg $noOutLinkArg $showTraceArg $processManagerArg --argstr exprFile "$exprFile" $NIXPROC/create-managed-process/$processManager/build-$processManager-env.nix diff --git a/tools/commonchecks b/tools/commonchecks new file mode 100644 index 0000000..e8069b3 --- /dev/null +++ b/tools/commonchecks @@ -0,0 +1,96 @@ +#!/bin/bash +set -e +shopt -s nullglob + +containsElement() +{ + local element match="$1" + shift + + for element + do + [[ "$element" == "$match" ]] && return 0 + done + return 1 +} + +checkNixStateDir() +{ + NIX_STATE_DIR=${NIX_STATE_DIR:-/nix/var/nix} +} + +checkProfile() +{ + profile=${profile:-processes} +} + +buildProfile() +{ + local processManager="$1" + profilePath=$(nixproc-build --process-manager $processManager $stateDirArg $runtimeDirArg $logDirArg $tmpDirArg $forceDisableUserChangeArg $showTraceArg --no-out-link "$path") +} + +composeOldProfilePath() +{ + if [ "$oldProfilePath" = "" ] + then + oldProfilePath="$NIX_STATE_DIR/profiles/$profile" + fi +} + +setNixProfile() +{ + nix-env -p "$NIX_STATE_DIR/profiles/$profile" --set "$profilePath" +} + +createNewGroups() +{ + for groupfile in $profilePath/dysnomia-support/groups/* + do + local groupname="$(basename $groupfile)" + + if [ ! -f "$oldProfilePath/dysnomia-support/groups/$groupname" ] + then + echo dysnomia-addgroups "$profilePath/dysnomia-support/groups/$groupname" + fi + done +} + +createNewUsers() +{ + for userfile in $profilePath/dysnomia-support/users/* + do + local username="$(basename $userfile)" + + if [ ! -f "$oldProfilePath/dysnomia-support/users/$username" ] + then + echo dysnomia-addusers "$profilePath/dysnomia-support/users/$username" + fi + done +} + +deleteObsoleteUsers() +{ + for userfile in $oldProfilePath/dysnomia-support/users/* + do + local username="$(basename $userfile)" + + if [ ! -f "$profilePath/dysnomia-support/users/$username" ] + then + echo dysnomia-delusers "$oldProfilePath/dysnomia-support/users/$username" + fi + done +} + +deleteObsoleteGroups() +{ + for groupfile in $oldProfilePath/dysnomia-support/groups/* + do + local groupname="$(basename $groupfile)" + + if [ ! -f "$profilePath/dysnomia-support/groups/$groupname" ] + then + echo dysnomia-delgroups "$oldProfilePath/dysnomia-support/groups/$groupname" + fi + done +} diff --git a/tools/cygrunsrv/default.nix b/tools/cygrunsrv/default.nix new file mode 100644 index 0000000..3c9bfab --- /dev/null +++ b/tools/cygrunsrv/default.nix @@ -0,0 +1,14 @@ +{stdenv, getopt}: + +stdenv.mkDerivation { + name = "nixproc-cygrunsrv-tools"; + buildCommand = '' + mkdir -p $out/bin + + sed -e "s|/bin/bash|$SHELL|" \ + -e "s|@getopt@|${getopt}/bin/getopt|" \ + -e "s|@commonchecks@|${../commonchecks}|" \ + ${./nixproc-cygrunsrv-switch.in} > $out/bin/nixproc-cygrunsrv-switch + chmod +x $out/bin/nixproc-cygrunsrv-switch + ''; +} diff --git a/tools/cygrunsrv/nixproc-cygrunsrv-switch.in b/tools/cygrunsrv/nixproc-cygrunsrv-switch.in new file mode 100644 index 0000000..65a99e6 --- /dev/null +++ b/tools/cygrunsrv/nixproc-cygrunsrv-switch.in @@ -0,0 +1,167 @@ +#!/bin/bash +set -e +shopt -s nullglob + +showUsage() +{ + cat < { inherit system; } +, system ? builtins.currentSystem +}: + +rec { + build = import ./build { + inherit (pkgs) stdenv getopt; + }; + + generate-config = import ./generate-config { + inherit (pkgs) stdenv getopt; + }; + + bsdrc = import ./bsdrc { + inherit (pkgs) stdenv getopt; + }; + + cygrunsrv = import ./cygrunsrv { + inherit (pkgs) stdenv getopt; + }; + + launchd = import ./launchd { + inherit (pkgs) stdenv getopt; + }; + + supervisord = import ./supervisord { + inherit (pkgs) stdenv getopt; + }; + + systemd = import ./systemd { + inherit (pkgs) stdenv getopt; + }; + + sysvinit = import ./sysvinit { + inherit (pkgs) stdenv getopt; + }; +} diff --git a/tools/generate-config/default.nix b/tools/generate-config/default.nix new file mode 100644 index 0000000..fd05828 --- /dev/null +++ b/tools/generate-config/default.nix @@ -0,0 +1,14 @@ +{stdenv, getopt}: + +stdenv.mkDerivation { + name = "nixproc-generate-config"; + buildCommand = '' + mkdir -p $out/bin + + sed -e "s|/bin/bash|$SHELL|" \ + -e "s|@getopt@|${getopt}/bin/getopt|" \ + -e "s|@NIXPROC@|${../../nixproc}|" \ + ${./nixproc-generate-config.in} > $out/bin/nixproc-generate-config + chmod +x $out/bin/nixproc-generate-config + ''; +} diff --git a/tools/generate-config/nixproc-generate-config.in b/tools/generate-config/nixproc-generate-config.in new file mode 100644 index 0000000..2b0d1f7 --- /dev/null +++ b/tools/generate-config/nixproc-generate-config.in @@ -0,0 +1,111 @@ +#!/bin/bash -e + +# Shows the usage of this command to the user + +showUsage() +{ + cat <&2 + exit 1 +fi + +if [ "$processManager" = "" ] +then + echo "No process manager was specified!" >&2 + exit 1 +fi + +NIXPROC=${NIXPROC:-@NIXPROC@} + +# Build the configuration +nix-build $NIXPROC/create-managed-process/agnostic/create-managed-process-from-config.nix --arg configFile $configFile --argstr processManager $processManager $stateDirArg $runtimeDirArg $logDirArg $tmpDirArg $forceDisableUserChangeArg $noOutLinkArg $showTraceArg diff --git a/tools/launchd/default.nix b/tools/launchd/default.nix new file mode 100644 index 0000000..9fa07a1 --- /dev/null +++ b/tools/launchd/default.nix @@ -0,0 +1,15 @@ +{stdenv, getopt}: + +stdenv.mkDerivation { + name = "nixproc-launchd-tools"; + buildCommand = '' + mkdir -p $out/bin + + sed -e "s|/bin/bash|$SHELL|" \ + -e "s|@getopt@|${getopt}/bin/getopt|" \ + -e "s|@commonchecks@|${../commonchecks}|" \ + -e "s|@readlink@|$(type -p readlink)|" \ + ${./nixproc-launchd-switch.in} > $out/bin/nixproc-launchd-switch + chmod +x $out/bin/nixproc-launchd-switch + ''; +} diff --git a/tools/launchd/nixproc-launchd-switch.in b/tools/launchd/nixproc-launchd-switch.in new file mode 100644 index 0000000..fcb1464 --- /dev/null +++ b/tools/launchd/nixproc-launchd-switch.in @@ -0,0 +1,173 @@ +#!/bin/bash +set -e +shopt -s nullglob + +showUsage() +{ + cat < $out/bin/nixproc-supervisord-start + chmod +x $out/bin/nixproc-supervisord-start + + sed -e "s|/bin/bash|$SHELL|" \ + -e "s|@getopt@|${getopt}/bin/getopt|" \ + -e "s|@readlink@|$(type -p readlink)|" \ + -e "s|@commonchecks@|${../commonchecks}|" \ + -e "s|@supervisordchecks@|${./supervisordchecks}|" \ + ${./nixproc-supervisord-switch.in} > $out/bin/nixproc-supervisord-switch + chmod +x $out/bin/nixproc-supervisord-switch + + sed -e "s|/bin/bash|$SHELL|" \ + -e "s|@getopt@|${getopt}/bin/getopt|" \ + -e "s|@readlink@|$(type -p readlink)|" \ + -e "s|@commonchecks@|${../commonchecks}|" \ + -e "s|@supervisordchecks@|${./supervisordchecks}|" \ + ${./nixproc-supervisord-deploy-stateless.in} > $out/bin/nixproc-supervisord-deploy-stateless + chmod +x $out/bin/nixproc-supervisord-deploy-stateless + ''; +} diff --git a/tools/supervisord/nixproc-supervisord-deploy-stateless.in b/tools/supervisord/nixproc-supervisord-deploy-stateless.in new file mode 100644 index 0000000..ae1e12f --- /dev/null +++ b/tools/supervisord/nixproc-supervisord-deploy-stateless.in @@ -0,0 +1,95 @@ +#!/bin/bash -e + +showUsage() +{ + cat <&2 + exit 1 + fi +} diff --git a/tools/systemd/default.nix b/tools/systemd/default.nix new file mode 100644 index 0000000..2c9150f --- /dev/null +++ b/tools/systemd/default.nix @@ -0,0 +1,14 @@ +{stdenv, getopt}: + +stdenv.mkDerivation { + name = "nixproc-systemd-tools"; + buildCommand = '' + mkdir -p $out/bin + + sed -e "s|/bin/bash|$SHELL|" \ + -e "s|@getopt@|${getopt}/bin/getopt|" \ + -e "s|@commonchecks@|${../commonchecks}|" \ + ${./nixproc-systemd-switch.in} > $out/bin/nixproc-systemd-switch + chmod +x $out/bin/nixproc-systemd-switch + ''; +} diff --git a/tools/systemd/nixproc-systemd-switch.in b/tools/systemd/nixproc-systemd-switch.in new file mode 100644 index 0000000..dd556d0 --- /dev/null +++ b/tools/systemd/nixproc-systemd-switch.in @@ -0,0 +1,198 @@ +#!/bin/bash +set -e +shopt -s nullglob + +showUsage() +{ + cat < $out/bin/nixproc-sysvinit-switch + chmod +x $out/bin/nixproc-sysvinit-switch + + sed -e "s|/bin/bash|$SHELL|" \ + -e "s|@getopt@|${getopt}/bin/getopt|" \ + -e "s|@commonchecks@|${../commonchecks}|" \ + -e "s|@sysvinitchecks@|${./sysvinitchecks}|" \ + ${./nixproc-sysvinit-runactivity.in} > $out/bin/nixproc-sysvinit-runactivity + chmod +x $out/bin/nixproc-sysvinit-runactivity + ''; +} diff --git a/tools/sysvinit/nixproc-sysvinit-runactivity.in b/tools/sysvinit/nixproc-sysvinit-runactivity.in new file mode 100644 index 0000000..3122e8d --- /dev/null +++ b/tools/sysvinit/nixproc-sysvinit-runactivity.in @@ -0,0 +1,100 @@ +#!/bin/bash +set -e +shopt -s nullglob + +# Shows the usage of this command to the user + +showUsage() +{ + cat <&2 + exit 1 +fi + +source @commonchecks@ + +checkNixStateDir +checkProfile +composeOldProfilePath + +source @sysvinitchecks@ + +checkRunlevel + +# Execute the activities + +rcpath="$oldProfilePath/etc/rc.d/rc${runlevel}.d" + +if [ "$reverse" = "1" ] +then + for i in $(ls $rcpath/S* | sort -r) + do + $i $activity + done +else + for i in $(ls $rcpath/S*) + do + $i $activity + done +fi diff --git a/tools/sysvinit/nixproc-sysvinit-switch.in b/tools/sysvinit/nixproc-sysvinit-switch.in new file mode 100644 index 0000000..d6f0945 --- /dev/null +++ b/tools/sysvinit/nixproc-sysvinit-switch.in @@ -0,0 +1,181 @@ +#!/bin/bash +set -e +shopt -s nullglob + +# Shows the usage of this command to the user + +showUsage() +{ + cat <&2 + + for i in $(ls $rcold/S* | sort -r) + do + currentPath=$(readlink -f $i) + oldscripts+=($currentPath) + done +fi + +# Determine paths of new scripts + +newscripts=() + +for i in $(ls $rcnew/S*) +do + currentPath=$(readlink -f $i) + newscripts+=($currentPath) +done + +# Create new groups and users +createNewGroups +createNewUsers + +# Stop obsolete scripts + +for i in ${oldscripts[@]} +do + if ! containsElement "$i" "${newscripts[@]}" + then + $i stop + fi +done + +# Start new scripts + +for i in ${newscripts[@]} +do + if ! containsElement "$i" "${oldscripts[@]}" + then + $i start + fi +done + +# Delete obsolete users and groups +deleteObsoleteUsers +deleteObsoleteGroups + +# Set new profile +setNixProfile diff --git a/tools/sysvinit/sysvinitchecks b/tools/sysvinit/sysvinitchecks new file mode 100644 index 0000000..313f9af --- /dev/null +++ b/tools/sysvinit/sysvinitchecks @@ -0,0 +1,9 @@ +#!/bin/bash -e + +checkRunlevel() +{ + if [ "$runlevel" = "" ] + then + runlevel=$(runlevel | cut -d ' ' -f2) + fi +} diff --git a/webapp/Makefile b/webapp/Makefile new file mode 100644 index 0000000..147b2d1 --- /dev/null +++ b/webapp/Makefile @@ -0,0 +1,14 @@ +CC = cc + +all: + $(CC) $(CFLAGS) -c daemonize.c + $(CC) $(CFLAGS) -c service.c + $(CC) $(CFLAGS) $(LDFLAGS) daemonize.o service.o main.c -lmicrohttpd -o webapp + +install: all + install -d -m755 $(PREFIX)/bin + install -m755 webapp $(PREFIX)/bin + +clean: + rm -f *.o + rm -f webapp diff --git a/webapp/README.md b/webapp/README.md new file mode 100644 index 0000000..782390c --- /dev/null +++ b/webapp/README.md @@ -0,0 +1,18 @@ +Test web application +==================== +This is a very simple test web application that can run in foreground mode and +daemon mode. Its only purpose is to return a very simple static HTML page. + +The most interesting part of this example is probably the daemonize +infrastructure (`daemonize.h`, `daemonize.c`) -- I have been trying to closely +follow systemd's recommendations for implementing traditional SysV daemons +(more info:`man 7 daemon`) sticking myself to POSIX standards as much as +possible. + +To keep the code as clear as possible, I have encapsulated each recommended +step into a function abstraction, and every failure yields a distinct error +code so that we can easily trace the origins of the error. + +The daemonize infrastructure is very generic -- you only need to provide a +pointer to a function that initializes the daemon's state and a pointer to +a function that runs the main loop. diff --git a/webapp/daemonize.c b/webapp/daemonize.c new file mode 100644 index 0000000..e40849d --- /dev/null +++ b/webapp/daemonize.c @@ -0,0 +1,259 @@ +#include "daemonize.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define NULL_DEV_FILE "/dev/null" +#define TRUE 1 +#define FALSE 0 +#define BUFFER_SIZE 10 + +static int close_non_standard_file_descriptors(void) +{ + unsigned int i; + + struct rlimit rlim; + int num_of_fds = getrlimit(RLIMIT_NOFILE, &rlim); + + if(num_of_fds == -1) + return FALSE; + + for(i = 3; i < num_of_fds; i++) + close(i); + + return TRUE; +} + +static int reset_signal_handlers_to_default(void) +{ +#if defined _NSIG + unsigned int i; + + for(i = 1; i < _NSIG; i++) + { + if(i != SIGKILL && i != SIGSTOP) + signal(i, SIG_DFL); + } +#endif + return TRUE; +} + +static int clear_signal_mask(void) +{ + sigset_t set; + + return((sigemptyset(&set) == 0) + && (sigprocmask(SIG_SETMASK, &set, NULL) == 0)); +} + +static int create_pid_file(const char *pid_file) +{ + pid_t my_pid = getpid(); + char my_pid_str[10]; + int fd; + + sprintf(my_pid_str, "%d", my_pid); + + if((fd = open(pid_file, O_CREAT | O_EXCL | O_WRONLY, S_IRUSR | S_IWUSR)) == -1) + return FALSE; + + if(write(fd, my_pid_str, strlen(my_pid_str)) == -1) + return FALSE; + + close(fd); + + return TRUE; +} + +static int attach_standard_file_descriptors_to_null(void) +{ + int null_fd_read, null_fd_write; + + return(((null_fd_read = open(NULL_DEV_FILE, O_RDONLY)) != -1) + && (dup2(null_fd_read, STDIN_FILENO) != -1) + && ((null_fd_write = open(NULL_DEV_FILE, O_WRONLY)) != -1) + && (dup2(null_fd_write, STDOUT_FILENO) != -1) + && (dup2(null_fd_write, STDERR_FILENO) != -1)); +} + +static void notify_parent_process(int writefd, DaemonStatus message) +{ + char byte = (char)message; + while(write(writefd, &byte, 1) == 0); + close(writefd); +} + +static pid_t fork_daemon_process(int writefd, const char *pid_file, void *data, int (*initialize_daemon) (void *data), int (*run_main_loop) (void *data)) +{ + pid_t pid = fork(); + + if(pid == 0) + { + if(!attach_standard_file_descriptors_to_null()) + { + notify_parent_process(writefd, STATUS_CANNOT_ATTACH_STD_FDS_TO_NULL); + exit(STATUS_CANNOT_ATTACH_STD_FDS_TO_NULL); + } + + umask(0); + + if(chdir("/") == -1) + { + notify_parent_process(writefd, STATUS_CANNOT_CHDIR); + exit(STATUS_CANNOT_CHDIR); + } + + if(!create_pid_file(pid_file)) + { + notify_parent_process(writefd, STATUS_CANNOT_CREATE_PID_FILE); + exit(STATUS_CANNOT_CREATE_PID_FILE); + } + + if(!initialize_daemon(data)) + { + notify_parent_process(writefd, STATUS_CANNOT_INIT_DAEMON); + unlink(pid_file); + exit(STATUS_CANNOT_INIT_DAEMON); + } + + notify_parent_process(writefd, STATUS_INIT_SUCCESS); + + int exit_status = run_main_loop(data); + + if(unlink(pid_file) == 0) + exit(STATUS_CANNOT_UNLINK_PID_FILE); + + exit(exit_status); + } + + return pid; +} + +static pid_t fork_helper_process(int pipefd[2], const char *pid_file, void *data, int (*initialize_daemon) (void *data), int (*run_main_loop) (void *data)) +{ + pid_t pid = fork(); + + if(pid == 0) + { + close(pipefd[0]); /* Close unneeded read-end */ + + if(setsid() == -1) + { + notify_parent_process(pipefd[1], STATUS_CANNOT_SET_SID); + exit(STATUS_CANNOT_SET_SID); + } + + /* Fork again, so that the terminal can not be acquired again */ + if(fork_daemon_process(pipefd[1], pid_file, data, initialize_daemon, run_main_loop) == -1) + { + notify_parent_process(pipefd[1], STATUS_CANNOT_FORK_DAEMON_PROCESS); + exit(STATUS_CANNOT_FORK_DAEMON_PROCESS); + } + + exit(0); /* Exit the helper process, so that the daemon process gets adopted by PID 1 */ + } + + return pid; +} + +static DaemonStatus wait_for_notification_message(int readfd) +{ + char buf[BUFFER_SIZE]; + ssize_t bytes_read = read(readfd, buf, 1); + + if(bytes_read == -1) + return STATUS_CANNOT_READ_FROM_PIPE; + else if(bytes_read == 0) + return STATUS_UNKNOWN_DAEMON_ERROR; + else + return buf[0]; +} + +DaemonStatus daemonize(const char *pid_file, void *data, int (*initialize_daemon) (void *data), int (*run_main_loop) (void *data)) +{ + int pipefd[2]; + + if(!close_non_standard_file_descriptors()) + return STATUS_CANNOT_CLOSE_NON_STD_FDS; + + if(!reset_signal_handlers_to_default()) + return STATUS_CANNOT_RESET_SIGNAL_HANDLERS; + + if(!clear_signal_mask()) + return STATUS_CANNOT_CLEAR_SIGNAL_MASK; + + if(pipe(pipefd) == -1) + return STATUS_CANNOT_CREATE_PIPE; + else + { + if(fork_helper_process(pipefd, pid_file, data, initialize_daemon, run_main_loop) == -1) + return STATUS_CANNOT_FORK_HELPER_PROCESS; + else + { + DaemonStatus exit_status; + + close(pipefd[1]); /* Close unneeded write end */ + exit_status = wait_for_notification_message(pipefd[0]); + close(pipefd[0]); + return exit_status; + } + } +} + +void print_daemon_status(DaemonStatus status, FILE *file) +{ + switch(status) + { + case STATUS_INIT_SUCCESS: + fprintf(file, "Daemon initialized successfully!\n"); + break; + case STATUS_CANNOT_ATTACH_STD_FDS_TO_NULL: + fprintf(file, "Cannot attach standard file descriptors to " NULL_DEV_FILE " !\n"); + break; + case STATUS_CANNOT_CHDIR: + fprintf(file, "Cannot change current working directory to /\n"); + break; + case STATUS_CANNOT_CREATE_PID_FILE: + fprintf(file, "Cannot create PID file!\n"); + break; + case STATUS_CANNOT_INIT_DAEMON: + fprintf(file, "Cannot initialize the daemon!\n"); + break; + case STATUS_CANNOT_UNLINK_PID_FILE: + fprintf(file, "Cannot unlink PID file!\n"); + break; + case STATUS_CANNOT_CLOSE_NON_STD_FDS: + fprintf(file, "Cannot close the non standard file descriptors!\n"); + break; + case STATUS_CANNOT_RESET_SIGNAL_HANDLERS: + fprintf(file, "Cannot reset signal handlers!\n"); + break; + case STATUS_CANNOT_CLEAR_SIGNAL_MASK: + fprintf(file, "Cannot clear signal mask!\n"); + break; + case STATUS_CANNOT_CREATE_PIPE: + fprintf(file, "Cannot create pipe!\n"); + break; + case STATUS_CANNOT_FORK_HELPER_PROCESS: + fprintf(file, "Cannot fork helper process!\n"); + break; + case STATUS_CANNOT_READ_FROM_PIPE: + fprintf(file, "Cannot read from pipe!\n"); + break; + case STATUS_CANNOT_SET_SID: + fprintf(file, "Cannot set session ID!\n"); + break; + case STATUS_CANNOT_FORK_DAEMON_PROCESS: + fprintf(file, "Cannot fork daemon process!\n"); + break; + case STATUS_UNKNOWN_DAEMON_ERROR: + fprintf(file, "Unknown daemon error!\n"); + break; + } +} diff --git a/webapp/daemonize.h b/webapp/daemonize.h new file mode 100644 index 0000000..123b8f7 --- /dev/null +++ b/webapp/daemonize.h @@ -0,0 +1,29 @@ +#ifndef __DAEMONIZE_H +#define __DAEMONIZE_H +#include + +typedef enum +{ + STATUS_INIT_SUCCESS = 0x0, + STATUS_CANNOT_ATTACH_STD_FDS_TO_NULL = 0x1, + STATUS_CANNOT_CHDIR = 0x2, + STATUS_CANNOT_CREATE_PID_FILE = 0x3, + STATUS_CANNOT_INIT_DAEMON = 0x4, + STATUS_CANNOT_UNLINK_PID_FILE = 0x5, + STATUS_CANNOT_CLOSE_NON_STD_FDS = 0x6, + STATUS_CANNOT_RESET_SIGNAL_HANDLERS = 0x7, + STATUS_CANNOT_CLEAR_SIGNAL_MASK = 0x8, + STATUS_CANNOT_CREATE_PIPE = 0x9, + STATUS_CANNOT_FORK_HELPER_PROCESS = 0xa, + STATUS_CANNOT_READ_FROM_PIPE = 0xb, + STATUS_CANNOT_SET_SID = 0xc, + STATUS_CANNOT_FORK_DAEMON_PROCESS = 0xd, + STATUS_UNKNOWN_DAEMON_ERROR = 0xe +} +DaemonStatus; + +DaemonStatus daemonize(const char *pid_file, void *data, int (*initialize_daemon) (void *data), int (*run_main_loop) (void *data)); + +void print_daemon_status(DaemonStatus status, FILE *file); + +#endif diff --git a/webapp/default.nix b/webapp/default.nix new file mode 100644 index 0000000..2484e06 --- /dev/null +++ b/webapp/default.nix @@ -0,0 +1,11 @@ +with import {}; + +stdenv.mkDerivation { + name = "webapp"; + src = ./.; + buildInputs = [ libmicrohttpd ]; + CFLAGS = "-I${libmicrohttpd.dev}/include"; + LDFLAGS = "-L${libmicrohttpd}/lib"; + makeFlags = "PREFIX=$(out)"; + dontStrip = true; +} diff --git a/webapp/main.c b/webapp/main.c new file mode 100644 index 0000000..8caad38 --- /dev/null +++ b/webapp/main.c @@ -0,0 +1,44 @@ +#include +#include +#include +#include "service.h" + +#define TRUE 1 +#define FALSE 0 + +int main(int argc, char *argv[]) +{ + unsigned int i; + int run_as_daemon = FALSE; + int port; + char *port_value = getenv("PORT"); + + if(port_value == NULL) + { + fprintf(stderr, "We need a PORT environment variable that specifies to which port the HTTP service should bind to!\n"); + return 1; + } + + port = atoi(port_value); + + for(i = 1; i < argc; i++) + { + if(strcmp(argv[i], "-D") == 0) + run_as_daemon = TRUE; + } + + if(run_as_daemon) + { + char *pid_file = getenv("PID_FILE"); + + if(pid_file == NULL) + { + fprintf(stderr, "We need the PID_FILE environment variable that specifies the path to a file storing the PID of the daemon process!\n"); + return 1; + } + + return run_daemon(port, pid_file); + } + else + return run_foreground_process(port); +} diff --git a/webapp/service.c b/webapp/service.c new file mode 100644 index 0000000..ec33741 --- /dev/null +++ b/webapp/service.c @@ -0,0 +1,158 @@ +#include "service.h" +#include +#include +#include +#include + +#include +#include +#include +#include + +#define TRUE 1 +#define FALSE 0 + +#define PAGE_TEMPLATE "\n"\ + "\n"\ + " \n"\ + " Simple test webapp\n"\ + " \n"\ + " \n"\ + " Simple test webapp listening on port: %d\n"\ + " \n"\ + "\n" + +static int ahc_echo(void *cls, struct MHD_Connection *connection, const char *url, const char *method, const char *version, const char *upload_data, size_t *upload_data_size, void **ptr) +{ + static int dummy; + const char *page = cls; + struct MHD_Response *response; + int ret; + + if(strcmp(method, "GET") != 0) + return MHD_NO; /* unexpected method */ + + if(&dummy != *ptr) + { + /* The first time only the headers are valid, + do not respond in the first round... */ + *ptr = &dummy; + return MHD_YES; + } + + if (*upload_data_size != 0) + return MHD_NO; /* upload data in a GET!? */ + + *ptr = NULL; /* clear context pointer */ + + response = MHD_create_response_from_buffer(strlen(page), (void*)page, MHD_RESPMEM_PERSISTENT); + ret = MHD_queue_response(connection, MHD_HTTP_OK, response); + + MHD_destroy_response(response); + return ret; +} + +volatile int terminated = FALSE; + +static void handle_shutdown(int signum) +{ + terminated = TRUE; +} + +typedef struct +{ + char *page; + struct MHD_Daemon *daemon; +} +DaemonWrapper; + +static DaemonWrapper *create_daemon_wrapper(int port) +{ + DaemonWrapper *wrapper = (DaemonWrapper*)malloc(sizeof(DaemonWrapper)); + wrapper->page = (char*)malloc(sizeof(PAGE_TEMPLATE) + 10); + + sprintf(wrapper->page, PAGE_TEMPLATE, port); + + wrapper->daemon = MHD_start_daemon(MHD_USE_THREAD_PER_CONNECTION, port, NULL, NULL, &ahc_echo, wrapper->page, MHD_OPTION_END); + + if(wrapper->daemon == NULL) + { + free(wrapper->page); + free(wrapper); + return NULL; + } + else + { + signal(SIGINT, handle_shutdown); + signal(SIGTERM, handle_shutdown); + + return wrapper; + } +} + +static void delete_daemon_wrapper(DaemonWrapper *wrapper) +{ + if(wrapper != NULL) + { + MHD_stop_daemon(wrapper->daemon); + free(wrapper->page); + free(wrapper); + } +} + +static int run_main_loop(void) +{ + /* Loop until termination request was received */ + while(!terminated) + sleep(1); + + return 0; +} + +int run_foreground_process(int port) +{ + DaemonWrapper *wrapper = create_daemon_wrapper(port); + + if(wrapper == NULL) + { + fprintf(stderr, "Cannot start HTTP service!\n"); + return 1; + } + else + { + int exit_status = run_main_loop(); + delete_daemon_wrapper(wrapper); + return exit_status; + } +} + +typedef struct +{ + int port; + DaemonWrapper *wrapper; +} +DaemonConfig; + +static int init_http_service(void *data) +{ + DaemonConfig *config = (DaemonConfig*)data; + config->wrapper = create_daemon_wrapper(config->port); + + return (config->wrapper != NULL); +} + +static int run_http_service(void *data) +{ + return run_main_loop(); +} + +DaemonStatus run_daemon(int port, const char *pid_file) +{ + DaemonConfig config; + config.port = port; + + DaemonStatus exit_status = daemonize(pid_file, &config, init_http_service, run_http_service); + if(exit_status != STATUS_INIT_SUCCESS) + print_daemon_status(exit_status, stderr); + return exit_status; +} diff --git a/webapp/service.h b/webapp/service.h new file mode 100644 index 0000000..6c31ff3 --- /dev/null +++ b/webapp/service.h @@ -0,0 +1,9 @@ +#ifndef __SERVICE_H +#define __SERVICE_H +#include "daemonize.h" + +int run_foreground_process(int port); + +DaemonStatus run_daemon(int port, const char *pid_file); + +#endif