|
@@ -0,0 +1,453 @@
|
|
|
|
|
+using Invercargill;
|
|
|
|
|
+using Invercargill.DataStructures;
|
|
|
|
|
+
|
|
|
|
|
+namespace Usm.Rad {
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * USM system integration for the rapid-deployment supervisor.
|
|
|
|
|
+ *
|
|
|
|
|
+ * The cheap queries ({@link Usm.ResourceFinder}, {@link Usm.Manifest}
|
|
|
|
|
+ * parsing, {@link Usm.SystemPackageManager}) run in-process against
|
|
|
|
|
+ * libusm, while every stateful operation (enrolment, repository wiring,
|
|
|
|
|
+ * updates, installs) spawns the `usm` CLI with inherited fds so its
|
|
|
|
|
+ * progress and errors land in the container log stream verbatim.
|
|
|
|
|
+ */
|
|
|
|
|
+public class System : Object {
|
|
|
|
|
+
|
|
|
|
|
+ // The enrolled managed-state root; src/cli/Enroll.vala keeps its copy
|
|
|
|
|
+ // private, so the default is replicated here for the snapshot fallback
|
|
|
|
|
+ private const string DEFAULT_STATE_PATH = "/var/usm";
|
|
|
|
|
+
|
|
|
|
|
+ private Config config;
|
|
|
|
|
+ private string usm_binary;
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * Creates an integration bound to the supervisor's {@link Config} and
|
|
|
|
|
+ * the resolved `usm` executable, which every spawned operation uses.
|
|
|
|
|
+ */
|
|
|
|
|
+ public System(Config config, string usm_binary) {
|
|
|
|
|
+ this.config = config;
|
|
|
|
|
+ this.usm_binary = usm_binary;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * Ensures this system is managed by usm by spawning
|
|
|
|
|
+ * `usm enroll -y`. Enrolment is idempotent on the usm side (the image
|
|
|
|
|
+ * may already be pre-enrolled), so this is safe to run at every
|
|
|
|
|
+ * startup; a non-zero exit throws {@link RadError.DEPLOY} carrying the
|
|
|
|
|
+ * exit status.
|
|
|
|
|
+ */
|
|
|
|
|
+ public void ensure_enrolled() throws RadError {
|
|
|
|
|
+ Log.info("Enrolling the system with usm");
|
|
|
|
|
+ run_usm(new string[] { "enroll", "-y" });
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * Wires every {@link Config.usm_repos} entry into usm's repos.d by
|
|
|
|
|
+ * spawning `usm add-repo -y <entry>` per repository. The first failing
|
|
|
|
|
+ * entry throws {@link RadError.DEPLOY} naming it — whether that is
|
|
|
|
|
+ * fatal is the caller's decision (the supervisor warns and continues).
|
|
|
|
|
+ */
|
|
|
|
|
+ public void wire_repos() throws RadError {
|
|
|
|
|
+ foreach(var entry in config.usm_repos) {
|
|
|
|
|
+ Log.info(@"Wiring USM repository \"$entry\"");
|
|
|
|
|
+ try {
|
|
|
|
|
+ run_usm(new string[] { "add-repo", "-y", entry });
|
|
|
|
|
+ }
|
|
|
|
|
+ catch(RadError e) {
|
|
|
|
|
+ throw new RadError.DEPLOY(@"Could not wire USM repository \"$entry\": $(e.message)");
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * Refreshes repository listings and updates installed packages by
|
|
|
|
|
+ * spawning `usm update -y`, then reports whether the set of installed
|
|
|
|
|
+ * USM packages changed (the supervisor's trigger for redeploying
|
|
|
|
|
+ * behind an unchanged commit).
|
|
|
|
|
+ *
|
|
|
|
|
+ * The snapshot names and versions every installed package through
|
|
|
|
|
+ * {@link Usm.SystemState}; an unenrolled or unreadable state falls
|
|
|
|
|
+ * back to listing the managed `installed` marks directory, and an
|
|
|
|
|
+ * unreadable directory degrades to an empty snapshot with a warning —
|
|
|
|
|
+ * a failed comparison at worst triggers one redundant deployment.
|
|
|
|
|
+ */
|
|
|
|
|
+ public async bool refresh_and_update() throws RadError {
|
|
|
|
|
+ var before = installed_packages_snapshot();
|
|
|
|
|
+ Log.info("Refreshing USM repositories and updating installed packages");
|
|
|
|
|
+ yield run_usm_async(new string[] { "update", "-y" });
|
|
|
|
|
+ var after = installed_packages_snapshot();
|
|
|
|
|
+ return before != after;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * Satisfies the dependencies of the manifest in {@link repo_dir}
|
|
|
|
|
+ * before `usm manifest install` runs (that command exits 252 on
|
|
|
|
|
+ * unmet dependencies). Every ref from the manifest's build, manage
|
|
|
|
|
+ * and runtime phases is checked with one {@link Usm.ResourceFinder};
|
|
|
|
|
+ * missing refs are resolved through one batched
|
|
|
|
|
+ * {@link Usm.SystemPackageManager.query_sync} against the configured
|
|
|
|
|
+ * helper, then through `usm provides` per remaining ref, and the
|
|
|
|
|
+ * collected packages installed with `usm install -y` (USM names) and
|
|
|
|
|
+ * {@link Usm.SystemPackageManager.install} (native names). A fresh
|
|
|
|
|
+ * finder re-checks afterwards; refs still missing throw
|
|
|
|
|
+ * {@link RadError.DEPENDENCY}.
|
|
|
|
|
+ *
|
|
|
|
|
+ * Grouped candidate phases contribute every alternative ref, so an
|
|
|
|
|
+ * unchosen alternative is installed rather than treated as optional.
|
|
|
|
|
+ *
|
|
|
|
|
+ * Limitation: version-constrained refs (for example `lib:foo>=2`)
|
|
|
|
|
+ * are stripped to their names for provider lookup — SPM helpers and
|
|
|
|
|
+ * `usm provides` match names exactly — so the constraint holds only
|
|
|
|
|
+ * insofar as `usm install`'s own resolution enforces it.
|
|
|
|
|
+ */
|
|
|
|
|
+ public async void ensure_deps(string repo_dir) throws RadError {
|
|
|
|
|
+ var manifest_path = Path.build_filename(repo_dir, "MANIFEST.usm");
|
|
|
|
|
+ Usm.Manifest manifest;
|
|
|
|
|
+ try {
|
|
|
|
|
+ manifest = new Usm.Manifest.from_file(manifest_path);
|
|
|
|
|
+ }
|
|
|
|
|
+ catch(Error e) {
|
|
|
|
|
+ throw new RadError.DEPLOY(@"Could not read \"$manifest_path\": $(e.message)");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ var seen = new HashSet<Usm.ResourceRef>();
|
|
|
|
|
+ var refs = new Vector<Usm.ResourceRef>();
|
|
|
|
|
+ var dependencies = manifest.dependencies ?? new Usm.Dependencies();
|
|
|
|
|
+ collect_phase_refs(dependencies.manage, seen, refs);
|
|
|
|
|
+ collect_phase_refs(dependencies.build, seen, refs);
|
|
|
|
|
+ collect_phase_refs(dependencies.runtime, seen, refs);
|
|
|
|
|
+
|
|
|
|
|
+ var finder = new Usm.ResourceFinder();
|
|
|
|
|
+ var missing = refs.where(r => !finder.has_resource(r)).to_vector();
|
|
|
|
|
+ if(missing.length == 0) {
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ Log.info(@"$(missing.length) dependency ref(s) of \"$(manifest.name)\" missing");
|
|
|
|
|
+
|
|
|
|
|
+ var spm = load_system_package_manager();
|
|
|
|
|
+ var spm_names = new Vector<string>();
|
|
|
|
|
+ var unresolved = missing;
|
|
|
|
|
+ if(spm != null && spm.enabled) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ var result = spm.query_sync(missing);
|
|
|
|
|
+ if(result != null) {
|
|
|
|
|
+ var remaining = new Vector<Usm.ResourceRef>();
|
|
|
|
|
+ foreach(var dependency in missing) {
|
|
|
|
|
+ if(!covered_by_system_packages(dependency, result)) {
|
|
|
|
|
+ remaining.add(dependency);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ foreach(var candidate in result.packages) {
|
|
|
|
|
+ if(!spm_names.contains(candidate.name)) {
|
|
|
|
|
+ spm_names.add(candidate.name);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ unresolved = remaining;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ catch(Error e) {
|
|
|
|
|
+ Log.warning(@"System package manager query failed: $(e.message); continuing with USM repositories");
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ else {
|
|
|
|
|
+ Log.warning("No system package manager is configured; resolving missing dependencies from USM repositories alone");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ var usm_names = new Vector<string>();
|
|
|
|
|
+ if(unresolved.length > 0) {
|
|
|
|
|
+ foreach(var dependency in unresolved) {
|
|
|
|
|
+ usm_names.add_all(usm_providers(@"$(dependency.resource_type.to_string()):$(dependency.base_name)"));
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if(usm_names.length > 0) {
|
|
|
|
|
+ Log.info(@"Installing USM packages: $(string.joinv(" ", usm_names.to_array()))");
|
|
|
|
|
+ var arguments = new Vector<string>();
|
|
|
|
|
+ arguments.add("install");
|
|
|
|
|
+ arguments.add("-y");
|
|
|
|
|
+ arguments.add_all(usm_names);
|
|
|
|
|
+ yield run_usm_async(arguments.to_array());
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if(spm_names.length > 0) {
|
|
|
|
|
+ Log.info(@"Installing system packages: $(string.joinv(" ", spm_names.to_array()))");
|
|
|
|
|
+ var system_installed = false;
|
|
|
|
|
+ try {
|
|
|
|
|
+ system_installed = yield spm.install(spm_names, event => log_install_progress(event));
|
|
|
|
|
+ }
|
|
|
|
|
+ catch(Error e) {
|
|
|
|
|
+ throw new RadError.DEPLOY(@"Could not run the system package manager install: $(e.message)");
|
|
|
|
|
+ }
|
|
|
|
|
+ if(!system_installed) {
|
|
|
|
|
+ throw new RadError.DEPLOY(@"The system package manager failed to install: $(string.joinv(" ", spm_names.to_array()))");
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ var recheck = new Usm.ResourceFinder();
|
|
|
|
|
+ var unmet = missing.where(r => !recheck.has_resource(r)).to_vector();
|
|
|
|
|
+ if(unmet.length > 0) {
|
|
|
|
|
+ var listing = string.joinv(", ", unmet.select<string>(r => r.to_string()).to_array());
|
|
|
|
|
+ throw new RadError.DEPENDENCY(@"Dependencies not satisfied after installation: $listing");
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * Spawns `usm` with inherited fds and waits for it, throwing
|
|
|
|
|
+ * {@link RadError.DEPLOY} with the exit-status (or signal) detail on
|
|
|
|
|
+ * failure.
|
|
|
|
|
+ */
|
|
|
|
|
+ private void run_usm(string[] arguments) throws RadError {
|
|
|
|
|
+ var argv = usm_argv(arguments);
|
|
|
|
|
+ var command = string.joinv(" ", argv);
|
|
|
|
|
+ Subprocess process;
|
|
|
|
|
+ try {
|
|
|
|
|
+ process = new Subprocess.newv(argv, SubprocessFlags.INHERIT_FDS);
|
|
|
|
|
+ }
|
|
|
|
|
+ catch(Error e) {
|
|
|
|
|
+ throw new RadError.DEPLOY(@"Could not spawn \"$command\": $(e.message)");
|
|
|
|
|
+ }
|
|
|
|
|
+ process.wait();
|
|
|
|
|
+ check_usm_exit(command, process);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /** The asynchronous twin of {@link run_usm} for long-running updates. */
|
|
|
|
|
+ private async void run_usm_async(string[] arguments) throws RadError {
|
|
|
|
|
+ var argv = usm_argv(arguments);
|
|
|
|
|
+ var command = string.joinv(" ", argv);
|
|
|
|
|
+ Subprocess process;
|
|
|
|
|
+ try {
|
|
|
|
|
+ process = new Subprocess.newv(argv, SubprocessFlags.INHERIT_FDS);
|
|
|
|
|
+ }
|
|
|
|
|
+ catch(Error e) {
|
|
|
|
|
+ throw new RadError.DEPLOY(@"Could not spawn \"$command\": $(e.message)");
|
|
|
|
|
+ }
|
|
|
|
|
+ try {
|
|
|
|
|
+ yield process.wait_async(null);
|
|
|
|
|
+ }
|
|
|
|
|
+ catch(Error e) {
|
|
|
|
|
+ throw new RadError.DEPLOY(@"Could not wait for \"$command\": $(e.message)");
|
|
|
|
|
+ }
|
|
|
|
|
+ check_usm_exit(command, process);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /** Prepends the resolved usm executable to a command's arguments. */
|
|
|
|
|
+ private string[] usm_argv(string[] arguments) {
|
|
|
|
|
+ var argv = new string[arguments.length + 1];
|
|
|
|
|
+ argv[0] = usm_binary;
|
|
|
|
|
+ for(int index = 0; index < arguments.length; index++) {
|
|
|
|
|
+ argv[index + 1] = arguments[index];
|
|
|
|
|
+ }
|
|
|
|
|
+ return argv;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /** Throws {@link RadError.DEPLOY} naming {@link command} unless it exited cleanly. */
|
|
|
|
|
+ private static void check_usm_exit(string command, Subprocess process) throws RadError {
|
|
|
|
|
+ if(process.get_if_signaled()) {
|
|
|
|
|
+ throw new RadError.DEPLOY(@"\"$command\" was terminated by signal $(process.get_term_sig())");
|
|
|
|
|
+ }
|
|
|
|
|
+ if(process.get_exit_status() != 0) {
|
|
|
|
|
+ throw new RadError.DEPLOY(@"\"$command\" exited with status $(process.get_exit_status())");
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * Collects every ref of one dependency phase (the union across
|
|
|
|
|
+ * candidate groups for grouped phases) into {@link refs}, skipping
|
|
|
|
|
+ * refs already collected through an earlier phase.
|
|
|
|
|
+ */
|
|
|
|
|
+ private static void collect_phase_refs(Usm.DependencyPhase? phase, HashSet<Usm.ResourceRef> seen, Vector<Usm.ResourceRef> refs) {
|
|
|
|
|
+ if(phase == null) {
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ foreach(var resource in phase.all_refs()) {
|
|
|
|
|
+ if(seen.contains(resource)) {
|
|
|
|
|
+ continue;
|
|
|
|
|
+ }
|
|
|
|
|
+ seen.add(resource);
|
|
|
|
|
+ refs.add(resource);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * The system package manager for the usm configuration, or null when
|
|
|
|
|
+ * the configuration cannot be read — dependency resolution then runs
|
|
|
|
|
+ * against USM repositories alone.
|
|
|
|
|
+ */
|
|
|
|
|
+ private static Usm.SystemPackageManager? load_system_package_manager() {
|
|
|
|
|
+ try {
|
|
|
|
|
+ return new Usm.SystemPackageManager(new Usm.Configuration.from_paths(new Usm.Paths()));
|
|
|
|
|
+ }
|
|
|
|
|
+ catch(Error e) {
|
|
|
|
|
+ Log.warning(@"Could not read the usm configuration, continuing without a system package manager: $(e.message)");
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * Whether any package of a query {@link result} provides
|
|
|
|
|
+ * {@link dependency}: {@link Usm.ResourceRef.satisfied_by} against
|
|
|
|
|
+ * each candidate's resources, with an exact string match as fallback
|
|
|
|
|
+ * for constrained refs whose helper echoed the constraint back.
|
|
|
|
|
+ */
|
|
|
|
|
+ private static bool covered_by_system_packages(Usm.ResourceRef dependency, Usm.SystemQueryResult result) {
|
|
|
|
|
+ foreach(var candidate in result.packages) {
|
|
|
|
|
+ foreach(var resource in candidate.resources) {
|
|
|
|
|
+ if(dependency.satisfied_by(resource) || dependency.to_string() == resource.to_string()) {
|
|
|
|
|
+ return true;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * The package names providing {@link resource_ref}, collected from
|
|
|
|
|
+ * `usm provides` stdout lines of the form
|
|
|
|
|
+ * `name-version (origin) provides ref` (see src/cli/Provides.vala).
|
|
|
|
|
+ * Exit status 1 (nothing provides the ref) is an expected miss; other
|
|
|
|
|
+ * failures warn and return what was read — the closing re-check
|
|
|
|
|
+ * turns silent gaps into a {@link RadError.DEPENDENCY}.
|
|
|
|
|
+ */
|
|
|
|
|
+ private Vector<string> usm_providers(string resource_ref) {
|
|
|
|
|
+ var names = new Vector<string>();
|
|
|
|
|
+ var argv = usm_argv(new string[] { "provides", resource_ref });
|
|
|
|
|
+ var command = string.joinv(" ", argv);
|
|
|
|
|
+ Subprocess process;
|
|
|
|
|
+ try {
|
|
|
|
|
+ // Stdout is captured for parsing; stderr inherits by default,
|
|
|
|
|
+ // so repository refresh chatter reaches the container log
|
|
|
|
|
+ process = new Subprocess.newv(argv, SubprocessFlags.STDOUT_PIPE);
|
|
|
|
|
+ }
|
|
|
|
|
+ catch(Error e) {
|
|
|
|
|
+ Log.warning(@"Could not spawn \"$command\": $(e.message)");
|
|
|
|
|
+ return names;
|
|
|
|
|
+ }
|
|
|
|
|
+ try {
|
|
|
|
|
+ var lines = new DataInputStream(process.get_stdout_pipe());
|
|
|
|
|
+ string line = null;
|
|
|
|
|
+ while((line = lines.read_line()) != null) {
|
|
|
|
|
+ var name = provides_package_name(line);
|
|
|
|
|
+ if(name != null && !names.contains(name)) {
|
|
|
|
|
+ names.add(name);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ catch(Error e) {
|
|
|
|
|
+ Log.warning(@"Could not read the output of \"$command\": $(e.message)");
|
|
|
|
|
+ }
|
|
|
|
|
+ process.wait();
|
|
|
|
|
+ var status = process.get_exit_status();
|
|
|
|
|
+ if(status != 0 && status != 1) {
|
|
|
|
|
+ Log.warning(@"\"$command\" exited with status $status");
|
|
|
|
|
+ }
|
|
|
|
|
+ return names;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * The package name of one `usm provides` result line, or null for
|
|
|
|
|
+ * lines that do not match the `name-version (origin) provides ref`
|
|
|
|
|
+ * shape. Package names may contain dashes; usm versions never do, so
|
|
|
|
|
+ * the name ends at the final dash before the origin parenthesis.
|
|
|
|
|
+ */
|
|
|
|
|
+ private static string? provides_package_name(string line) {
|
|
|
|
|
+ var origin = line.index_of(" (");
|
|
|
|
|
+ var provides = line.index_of(") provides ");
|
|
|
|
|
+ if(origin < 1 || provides < origin) {
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+ var name_and_version = line.substring(0, origin);
|
|
|
|
|
+ var separator = name_and_version.last_index_of("-");
|
|
|
|
|
+ if(separator < 1) {
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+ return name_and_version.substring(0, separator);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * One line per streamed system package install event; the per-package
|
|
|
|
|
+ * progress fraction events stay unlogged to keep the supervisor's
|
|
|
|
|
+ * output readable.
|
|
|
|
|
+ */
|
|
|
|
|
+ private static void log_install_progress(Usm.SystemInstallEvent event) {
|
|
|
|
|
+ switch(event.event_type) {
|
|
|
|
|
+ case Usm.SystemInstallEventType.BEGIN:
|
|
|
|
|
+ Log.info(@"System package transaction started ($(event.total) packages)");
|
|
|
|
|
+ break;
|
|
|
|
|
+ case Usm.SystemInstallEventType.PACKAGE_COMPLETE:
|
|
|
|
|
+ Log.info(@"Installed system package \"$(event.name ?? "")\"");
|
|
|
|
|
+ break;
|
|
|
|
|
+ case Usm.SystemInstallEventType.COMPLETE:
|
|
|
|
|
+ Log.info(@"System package transaction completed ($(event.installed) packages installed)");
|
|
|
|
|
+ break;
|
|
|
|
|
+ case Usm.SystemInstallEventType.ERROR:
|
|
|
|
|
+ Log.warning(@"System package install failed: $(event.message ?? "")");
|
|
|
|
|
+ break;
|
|
|
|
|
+ default:
|
|
|
|
|
+ break;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * A deterministic fingerprint of the installed-package set: sorted
|
|
|
|
|
+ * `name-version` lines. An unreadable managed state degrades to
|
|
|
|
|
+ * {@link installed_marks_snapshot}.
|
|
|
|
|
+ */
|
|
|
|
|
+ private static string installed_packages_snapshot() {
|
|
|
|
|
+ var entries = new Vector<string>();
|
|
|
|
|
+ try {
|
|
|
|
|
+ var state = new Usm.SystemState(new Usm.Paths());
|
|
|
|
|
+ foreach(var installed in state.get_installed_packages()) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ var manifest = installed.get_manifest();
|
|
|
|
|
+ entries.add(@"$(manifest.name)-$(manifest.version.to_string())");
|
|
|
|
|
+ }
|
|
|
|
|
+ catch(Error e) {
|
|
|
|
|
+ Log.warning(@"Skipping unreadable installed package \"$(installed.package_name)\": $(e.message)");
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ catch(Error e) {
|
|
|
|
|
+ return installed_marks_snapshot();
|
|
|
|
|
+ }
|
|
|
|
|
+ return string.joinv("\n", entries.sort((a, b) => a.collate(b)).to_array());
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * The snapshot fallback for an unenrolled or unreadable state: the
|
|
|
|
|
+ * names of the `installed` marks directory, which are themselves
|
|
|
|
|
+ * `name-version`. An unreadable directory warns and yields an empty
|
|
|
|
|
+ * snapshot.
|
|
|
|
|
+ */
|
|
|
|
|
+ private static string installed_marks_snapshot() {
|
|
|
|
|
+ var entries = new Vector<string>();
|
|
|
|
|
+ try {
|
|
|
|
|
+ foreach(var mark in Iterate.directory(Path.build_filename(managed_state_path(), "installed"))) {
|
|
|
|
|
+ entries.add(mark);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ catch(Error e) {
|
|
|
|
|
+ Log.warning(@"Could not list the usm installed-packages directory, treating the snapshot as empty: $(e.message)");
|
|
|
|
|
+ return "";
|
|
|
|
|
+ }
|
|
|
|
|
+ return string.joinv("\n", entries.sort((a, b) => a.collate(b)).to_array());
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * The managed state root from usm.config's managed section, or the
|
|
|
|
|
+ * enrolment default when no readable configuration carries one.
|
|
|
|
|
+ */
|
|
|
|
|
+ private static string managed_state_path() {
|
|
|
|
|
+ try {
|
|
|
|
|
+ var configuration = new Usm.Configuration.from_paths(new Usm.Paths());
|
|
|
|
|
+ if(configuration.managed_config != null && configuration.managed_config.state_path != null) {
|
|
|
|
|
+ return configuration.managed_config.state_path;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ catch(Error e) {
|
|
|
|
|
+ // Unenrolled or unreadable: the enrolment default below
|
|
|
|
|
+ }
|
|
|
|
|
+ return DEFAULT_STATE_PATH;
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+}
|