|
|
@@ -159,12 +159,22 @@ namespace Usm {
|
|
|
*/
|
|
|
public Vector<RebuildEntry> rebuilds { get; private set; default = new Vector<RebuildEntry>(); }
|
|
|
|
|
|
+ /**
|
|
|
+ * Emitted when a Ctrl+C lands mid-transaction: handlers answer
|
|
|
+ * whether to keep going (true — the interrupted operation re-runs)
|
|
|
+ * or roll the journaled transaction back (false). With no handler
|
|
|
+ * connected the signal's default return is false — the rollback
|
|
|
+ * behaviour non-interactive contexts want.
|
|
|
+ */
|
|
|
+ public signal bool interrupt_decided(string subject, string phase_verb);
|
|
|
+
|
|
|
public void run() throws TransactionError {
|
|
|
|
|
|
// 1. Verify the transaction is valid
|
|
|
strategise();
|
|
|
|
|
|
journal = new Journal(journal_backup_root());
|
|
|
+ TransactionInterrupt.install();
|
|
|
try {
|
|
|
journal.begin();
|
|
|
|
|
|
@@ -216,6 +226,7 @@ namespace Usm {
|
|
|
throw new TransactionError.UNKNOWN_ERROR(@"Error running transaction: $(e.message)");
|
|
|
}
|
|
|
finally {
|
|
|
+ TransactionInterrupt.restore();
|
|
|
journal = null;
|
|
|
}
|
|
|
}
|
|
|
@@ -238,6 +249,7 @@ namespace Usm {
|
|
|
current_task_type = TransactionTask.REBUILDING;
|
|
|
|
|
|
journal = new Journal(journal_backup_root());
|
|
|
+ TransactionInterrupt.install();
|
|
|
try {
|
|
|
journal.begin();
|
|
|
|
|
|
@@ -258,6 +270,7 @@ namespace Usm {
|
|
|
throw new TransactionError.UNKNOWN_ERROR(@"Error rebuilding package $(package.package_name): $(e.message)");
|
|
|
}
|
|
|
finally {
|
|
|
+ TransactionInterrupt.restore();
|
|
|
journal = null;
|
|
|
}
|
|
|
}
|
|
|
@@ -470,21 +483,40 @@ namespace Usm {
|
|
|
report_progress(TransactionTask.STRATEGISING, (float)strategise_current_task / (float)strategise_worst_case_task_count);
|
|
|
try {
|
|
|
var manifest = package.get_manifest();
|
|
|
+ // Resources provided by packages this transaction
|
|
|
+ // REMOVES are gone before the first build runs —
|
|
|
+ // stale on disk at planning time, they satisfy
|
|
|
+ // nothing (an update pair's replacement counts only
|
|
|
+ // from the lot that installs it, so the dependency
|
|
|
+ // lands in an earlier lot and the dependent builds
|
|
|
+ // against the fresh artifact)
|
|
|
+ PredicateDelegate<ResourceRef> removed_by_plan = d => planned_removal.any(p => {
|
|
|
+ try {
|
|
|
+ return p.get_manifest().satisfies(d);
|
|
|
+ }
|
|
|
+ catch(Error e) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ });
|
|
|
// Build-phase refs must be satisfied by resources
|
|
|
- // already present or installed by an EARLIER lot: a
|
|
|
+ // that SURVIVE the removal phase and are already
|
|
|
+ // present or installed by an EARLIER lot: a
|
|
|
// same-lot provide is not installed yet when this
|
|
|
// package builds, so a build dependency on it defers
|
|
|
// the package to the next lot
|
|
|
- PredicateDelegate<ResourceRef> buildtime_satisfied = d => resource_finder.has_resource(d)
|
|
|
+ PredicateDelegate<ResourceRef> buildtime_satisfied = d => !removed_by_plan(d) && (
|
|
|
+ resource_finder.has_resource(d)
|
|
|
|| preinstalled_resources.any(r => d.satisfied_by(r))
|
|
|
- || installed_by_earlier_lots.any(r => d.satisfied_by(r));
|
|
|
+ || installed_by_earlier_lots.any(r => d.satisfied_by(r)));
|
|
|
// Manage-phase executables run at install time, when
|
|
|
// everything this lot has already admitted (in
|
|
|
- // topological order) is installed
|
|
|
- PredicateDelegate<ResourceRef> installtime_satisfied = d => resource_finder.has_resource(d)
|
|
|
+ // topological order) is installed — but nothing the
|
|
|
+ // removal phase takes away
|
|
|
+ PredicateDelegate<ResourceRef> installtime_satisfied = d => !removed_by_plan(d) && (
|
|
|
+ resource_finder.has_resource(d)
|
|
|
|| preinstalled_resources.any(r => d.satisfied_by(r))
|
|
|
|| installed_by_earlier_lots.any(r => d.satisfied_by(r))
|
|
|
- || installed_by_this_lot.any(r => d.satisfied_by(r));
|
|
|
+ || installed_by_this_lot.any(r => d.satisfied_by(r)));
|
|
|
if(manifest.dependencies.manage.is_satisfied(installtime_satisfied) &&
|
|
|
manifest.dependencies.build.is_satisfied(buildtime_satisfied)) {
|
|
|
lot.add(package);
|
|
|
@@ -838,23 +870,57 @@ namespace Usm {
|
|
|
|
|
|
private void do_for(Enumerable<CachedPackage> packages, PackageDelegate func, TransactionTask task_type) throws TransactionError {
|
|
|
foreach (var package in packages) {
|
|
|
- try {
|
|
|
- begin_action(task_type, package.package_name, current_task);
|
|
|
- report_progress(task_type, 0.0f);
|
|
|
- func(package);
|
|
|
- lock(progress_lock) {
|
|
|
- current_task++;
|
|
|
- }
|
|
|
- }
|
|
|
- catch (TransactionError e) {
|
|
|
- throw e;
|
|
|
+ // A Ctrl+C that arrived between operations resolves before
|
|
|
+ // this one starts: keep going (clearing the flag) or roll
|
|
|
+ // the transaction back
|
|
|
+ if(TransactionInterrupt.requested && !resolve_interrupt(package.package_name, task_type)) {
|
|
|
+ throw new TransactionError.INTERRUPTED(@"Interrupted before $(task_type.get_verb()) $(package.package_name)");
|
|
|
}
|
|
|
- catch(Error e) {
|
|
|
- throw new TransactionError.UNKNOWN_ERROR(@"Error $(task_type.get_verb()) package $(package.package_name): $(e.message)");
|
|
|
+ while(true) {
|
|
|
+ try {
|
|
|
+ begin_action(task_type, package.package_name, current_task);
|
|
|
+ report_progress(task_type, 0.0f);
|
|
|
+ func(package);
|
|
|
+ lock(progress_lock) {
|
|
|
+ current_task++;
|
|
|
+ }
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ catch (TransactionError e) {
|
|
|
+ // The operation failed with a Ctrl+C pending — the
|
|
|
+ // terminal signalled the child too. Ask: roll back,
|
|
|
+ // or clear the flag and re-run this operation
|
|
|
+ if(TransactionInterrupt.requested && resolve_interrupt(package.package_name, task_type)) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ throw e;
|
|
|
+ }
|
|
|
+ catch(Error e) {
|
|
|
+ if(TransactionInterrupt.requested && resolve_interrupt(package.package_name, task_type)) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ throw new TransactionError.UNKNOWN_ERROR(@"Error $(task_type.get_verb()) package $(package.package_name): $(e.message)");
|
|
|
+ }
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * Resolves a pending interrupt for {@link subject} in the phase
|
|
|
+ * {@link verb}: emits {@link interrupt_decided}. A "keep going"
|
|
|
+ * answer clears the flag so the retried operation runs
|
|
|
+ * uninterrupted; a rollback answer (or no handler — the signal's
|
|
|
+ * default) leaves it set and the caller unwinds to the journaled
|
|
|
+ * rollback.
|
|
|
+ */
|
|
|
+ private bool resolve_interrupt(string subject, TransactionTask task_type) {
|
|
|
+ bool keep_going = interrupt_decided(subject, task_type.get_verb());
|
|
|
+ if(keep_going) {
|
|
|
+ TransactionInterrupt.requested = false;
|
|
|
+ }
|
|
|
+ return keep_going;
|
|
|
+ }
|
|
|
+
|
|
|
private void report_progress(TransactionTask task, float progress) {
|
|
|
lock(progress_lock) {
|
|
|
progress_updated(task, current_subject, current_task, task_count, progress);
|
|
|
@@ -1169,7 +1235,9 @@ namespace Usm {
|
|
|
INVALID_TRANSACTION,
|
|
|
UNKNOWN_ERROR,
|
|
|
BUILD_ERROR,
|
|
|
- INSTALL_ERROR
|
|
|
+ INSTALL_ERROR,
|
|
|
+ /** A Ctrl+C the interrupt decision answered with "roll back". */
|
|
|
+ INTERRUPTED
|
|
|
}
|
|
|
|
|
|
|