| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423 |
- /**
- * usmrad — USM Rapid Application Deployment supervisor.
- *
- * Tracks a git branch, builds and installs the branch's manifest into an
- * A/B slot layout under the application directory, and keeps an instance of
- * the application's executable running from the active slot. A deployment
- * failure never disturbs a running instance; a successful deployment
- * terminates the old instance and launches the new one. When the supervised
- * executable exits, usmrad exits with its exit code.
- *
- * Configuration is entirely environmental; see {@link Config}.
- */
- namespace Usm.Rad {
- /**
- * Failure modes of the rapid-deployment supervisor.
- */
- public errordomain RadError {
- /** A USMRAD_* environment variable is missing or malformed */
- CONFIGURATION,
- /** A git operation (clone, fetch, reset) failed */
- GIT,
- /** A build or install step failed */
- DEPLOY,
- /** A dependency resource could not be satisfied by the SPM or USM repositories */
- DEPENDENCY
- }
- /**
- * Bracketed-origin logging for the usmrad supervisor, per STYLE.md.
- */
- public class Log {
- /**
- * Prints an informational line to stdout with the {@link Log} origin
- * prefix.
- */
- public static void info(string message) {
- print("[usmrad] %s\n", message);
- }
- /**
- * Prints a warning line to stderr with the {@link Log} origin prefix.
- */
- public static void warning(string message) {
- printerr("[usmrad] %s\n", message);
- }
- }
- /**
- * argv construction for spawned commands and joined argument strings.
- *
- * Invercargill's {@link Invercargill.DataStructures.Vector.to_array} returns
- * exactly-sized arrays without the NULL sentinel GLib's spawn helpers and
- * {@link string.joinv} require — an unterminated argv crashes inside
- * g_subprocess_newv's g_strdupv. Every vector-built argument list therefore
- * flows through {@link from_vector}, whose trailing zeroed slot terminates
- * the array the same way a Vala literal or explicit `new string[n]` fill
- * does.
- */
- public class Argv {
- /**
- * A NULL-terminated copy of {@link command}: one slot longer than the
- * vector, with the final slot left zeroed as the sentinel.
- */
- public static string[] from_vector(Invercargill.DataStructures.Vector<string> command) {
- var size = (int)command.length;
- var argv = new string[size + 1];
- var index = 0;
- foreach(var argument in command) {
- argv[index++] = argument;
- }
- return argv;
- }
- }
- /**
- * usmrad configuration, parsed from the USMRAD_* environment.
- */
- public class Config : Object {
- /** Git repository URL to deploy (USMRAD_REPO, required). */
- public string repo { get; set; }
- /** Branch to track (USMRAD_BRANCH, default "master"). */
- public string branch { get; set; default = "master"; }
- /** Poll interval in minutes; 0 disables polling (USMRAD_POLL). */
- public double poll_minutes { get; set; default = 0; }
- /** Application directory (USMRAD_APPDIR, default "/app"). */
- public string appdir { get; set; default = "/app"; }
- /** USMR URIs wired into usm at startup (USMRAD_USM_REPOS, semicolon-delimited). */
- public string[] usm_repos { get; set; default = new string[] {}; }
- /** Seconds between SIGTERM and SIGKILL when terminating an instance (USMRAD_TERM_GRACE). */
- public int term_grace_seconds { get; set; default = 10; }
- /**
- * Poll interval in whole seconds; 0 when polling is disabled. Fractional
- * minutes round down, so USMRAD_POLL=0.25 polls every 15 seconds.
- */
- public uint poll_interval_seconds {
- get {
- return (uint)(poll_minutes * 60.0);
- }
- }
- /**
- * Reads the USMRAD_* environment applying defaults, throwing
- * {@link RadError.CONFIGURATION} with a user-facing message when a
- * variable is missing or malformed.
- */
- public static Config from_environment() throws RadError {
- var config = new Config();
- var repo = Environment.get_variable("USMRAD_REPO");
- if(repo == null || repo.strip().length == 0) {
- throw new RadError.CONFIGURATION("USMRAD_REPO is required (a git repository URL)");
- }
- config.repo = repo.strip();
- var branch = Environment.get_variable("USMRAD_BRANCH");
- if(branch != null && branch.strip().length > 0) {
- config.branch = branch.strip();
- }
- var poll = Environment.get_variable("USMRAD_POLL");
- if(poll != null && poll.strip().length > 0) {
- double parsed_poll;
- if(!double.try_parse(poll.strip(), out parsed_poll) || parsed_poll < 0) {
- throw new RadError.CONFIGURATION(@"USMRAD_POLL must be a non-negative number of minutes, got \"$poll\"");
- }
- config.poll_minutes = parsed_poll;
- }
- var appdir = Environment.get_variable("USMRAD_APPDIR");
- if(appdir != null && appdir.strip().length > 0) {
- config.appdir = appdir.strip();
- }
- var repos = Environment.get_variable("USMRAD_USM_REPOS");
- if(repos != null && repos.strip().length > 0) {
- var entries = new Invercargill.DataStructures.Vector<string>();
- foreach(var entry in repos.split(";")) {
- if(entry.strip().length > 0) {
- entries.add(entry.strip());
- }
- }
- config.usm_repos = entries.to_array();
- }
- var grace = Environment.get_variable("USMRAD_TERM_GRACE");
- if(grace != null && grace.strip().length > 0) {
- int64 parsed_grace;
- if(!int64.try_parse(grace.strip(), out parsed_grace) || parsed_grace < 0) {
- throw new RadError.CONFIGURATION(@"USMRAD_TERM_GRACE must be a non-negative number of seconds, got \"$grace\"");
- }
- config.term_grace_seconds = (int)parsed_grace;
- }
- return config;
- }
- }
- Config rad_config;
- Slots rad_slots;
- Source rad_source;
- System rad_system;
- Deployer rad_deployer;
- Supervisor rad_supervisor;
- MainLoop rad_loop;
- string rad_exec_name;
- string[] rad_exec_args;
- int rad_exit_code = 0;
- bool rad_cycle_busy = false;
- bool rad_instance_running = false;
- // The test target links every rad source for pure-logic coverage; only the
- // real executable may own the process entry point
- #if !USMRAD_TESTS
- public static int main(string[] args) {
- if(args.length < 2 || args[1].has_prefix("-")) {
- usage();
- return 255;
- }
- rad_exec_name = args[1];
- rad_exec_args = args[2:args.length];
- try {
- rad_config = Config.from_environment();
- }
- catch(RadError e) {
- Log.warning(e.message);
- usage();
- return 255;
- }
- rad_slots = new Slots(rad_config.appdir);
- try {
- rad_slots.ensure_layout();
- }
- catch(Error e) {
- Log.warning(@"Could not create the appdir layout in \"$(rad_config.appdir)\": $(e.message)");
- return 255;
- }
- var usm_binary = find_usm_binary();
- if(usm_binary == null) {
- Log.warning("Could not locate the usm executable next to usmrad or on PATH");
- return 255;
- }
- rad_source = new Source(rad_config.repo, rad_config.branch, rad_slots.repo_dir);
- rad_system = new System(rad_config, usm_binary);
- rad_deployer = new Deployer(rad_config, rad_slots, rad_source, rad_system, usm_binary);
- rad_supervisor = new Supervisor(rad_slots.pwd, rad_config.term_grace_seconds);
- rad_loop = new MainLoop();
- rad_supervisor.exited.connect(status => {
- rad_exit_code = status;
- rad_loop.quit();
- });
- rad_supervisor.shutdown.connect(signal_number => {
- rad_exit_code = 128 + signal_number;
- rad_loop.quit();
- });
- rad_supervisor.run();
- // Launch the previous good instance first so a running deployment cycle
- // never adds downtime for an instance that already works
- rad_instance_running = launch_from_current();
- if(rad_instance_running) {
- Log.info(@"Launched previous instance from slot \"$(rad_slots.read_current_slot())\"");
- }
- Idle.add(() => {
- startup_cycle.begin();
- return false;
- });
- if(rad_config.poll_interval_seconds > 0) {
- Timeout.add_seconds(rad_config.poll_interval_seconds, () => {
- run_cycle.begin();
- return true;
- });
- }
- rad_loop.run();
- return rad_exit_code;
- }
- /**
- * Runs the startup check cycle: enrollment, repository wiring and one
- * deployment check. With no previous instance a failed first deployment is
- * fatal (exit 251); with one, failures merely warn.
- */
- private static async void startup_cycle() {
- try {
- rad_system.ensure_enrolled();
- }
- catch(RadError e) {
- Log.warning(@"usm enroll failed: $(e.message)");
- rad_exit_code = 245;
- rad_loop.quit();
- return;
- }
- try {
- rad_system.wire_repos();
- }
- catch(RadError e) {
- Log.warning(@"Could not wire USMRAD_USM_REPOS repositories: $(e.message); continuing");
- }
- var deployed = yield run_cycle();
- if(!deployed && !rad_instance_running) {
- Log.warning("First deployment failed and no previous instance exists");
- rad_exit_code = 251;
- rad_loop.quit();
- }
- }
- /**
- * Runs one check cycle, guarded against re-entry: synchronise the source
- * repository, refresh and update USM packages, and when new commits or
- * package updates are available, deploy into the inactive slot and swap.
- * Returns true when a deployment went through and a new instance launched.
- */
- private static async bool run_cycle() {
- if(rad_cycle_busy) {
- return false;
- }
- rad_cycle_busy = true;
- var deployed = false;
- try {
- deployed = yield run_cycle_inner();
- }
- finally {
- rad_cycle_busy = false;
- }
- return deployed;
- }
- private static async bool run_cycle_inner() {
- try {
- rad_source.ensure_cloned();
- rad_source.fetch();
- }
- catch(RadError e) {
- Log.warning(@"Could not synchronise \"$(rad_config.repo)\": $(e.message); skipping cycle");
- return false;
- }
- var deployed_commit = rad_slots.read_deployed_commit();
- string remote_head = null;
- try {
- remote_head = rad_source.remote_head();
- }
- catch(RadError e) {
- Log.warning(@"Could not resolve the remote head: $(e.message); skipping cycle");
- return false;
- }
- var new_commits = deployed_commit == null || deployed_commit != remote_head;
- var deps_changed = false;
- try {
- deps_changed = yield rad_system.refresh_and_update();
- }
- catch(RadError e) {
- Log.warning(@"usm update failed: $(e.message); continuing with the commit check");
- }
- if(!new_commits && !deps_changed) {
- return false;
- }
- Log.info(new_commits
- ? @"New commits on \"$(rad_config.branch)\" ($remote_head)"
- : "Upstream USM package updates detected");
- var inactive_slot = rad_slots.inactive_slot() ?? "a";
- try {
- yield rad_deployer.deploy(inactive_slot);
- }
- catch(RadError e) {
- Log.warning(@"Deployment into slot \"$inactive_slot\" failed: $(e.message)");
- return false;
- }
- if(rad_instance_running) {
- Log.info("Terminating previous instance");
- yield rad_supervisor.terminate();
- rad_instance_running = false;
- }
- if(launch_from_current()) {
- rad_instance_running = true;
- Log.info(@"Launched new instance from slot \"$(rad_slots.read_current_slot())\" ($remote_head)");
- return true;
- }
- // The deployment succeeded but nothing launchable came out of it; the
- // container supervisor is better placed to recover than a dead poll loop
- Log.warning(@"\"$(rad_exec_name)\" does not resolve or launch from the deployed slot");
- rad_exit_code = 251;
- rad_loop.quit();
- return true;
- }
- /**
- * Launches the configured exec from the current slot. Returns false without
- * ceremony when no slot is current; warns when a current slot exists but the
- * exec does not resolve or launch within it.
- */
- private static bool launch_from_current() {
- var current_slot = rad_slots.read_current_slot();
- if(current_slot == null) {
- return false;
- }
- var executable = rad_slots.resolve_exec(current_slot, rad_exec_name);
- if(executable == null) {
- Log.warning(@"\"$(rad_exec_name)\" does not resolve in slot \"$current_slot\" (PATH-style search within the slot)");
- return false;
- }
- try {
- rad_supervisor.launch(executable, rad_exec_args);
- }
- catch(Error e) {
- Log.warning(@"Could not launch \"$executable\": $(e.message)");
- return false;
- }
- return true;
- }
- /**
- * Locates the usm executable: a sibling of usmrad's own executable first
- * (both install into the same bindir), then the PATH.
- */
- private static string? find_usm_binary() {
- try {
- var own_path = FileUtils.read_link("/proc/self/exe");
- var candidate = Path.build_filename(Path.get_dirname(own_path), "usm");
- if(FileUtils.test(candidate, FileTest.IS_EXECUTABLE)) {
- return candidate;
- }
- }
- catch(Error e) {
- // Fall through to the PATH search
- }
- return Environment.find_program_in_path("usm");
- }
- private static void usage() {
- printerr("USAGE:\n\tusmrad <exec> [args...]\n\nEnvironment:\n");
- printerr("\tUSMRAD_REPO (required) git repository URL\n");
- printerr("\tUSMRAD_BRANCH branch to track (default: master)\n");
- printerr("\tUSMRAD_POLL poll interval in minutes, 0 disables (default: 0)\n");
- printerr("\tUSMRAD_APPDIR application directory (default: /app)\n");
- printerr("\tUSMRAD_USM_REPOS semicolon-delimited .usmr URIs\n");
- printerr("\tUSMRAD_TERM_GRACE seconds between SIGTERM and SIGKILL (default: 10)\n");
- }
- #endif
- }
|