summaryrefslogtreecommitdiff
path: root/source/settings/ChangeQueue.mc
diff options
context:
space:
mode:
Diffstat (limited to 'source/settings/ChangeQueue.mc')
-rw-r--r--source/settings/ChangeQueue.mc553
1 files changed, 430 insertions, 123 deletions
diff --git a/source/settings/ChangeQueue.mc b/source/settings/ChangeQueue.mc
index 5de74e0..dc6facd 100644
--- a/source/settings/ChangeQueue.mc
+++ b/source/settings/ChangeQueue.mc
@@ -4,15 +4,25 @@
// Anyone who controls the phone can send settings, so edits, new secrets,
// "Don't require PIN" and deletes of locked entries are not applied when
// they arrive: they wait here until someone confirms them with the PIN on
-// the watch (or rejects them). Each item is one ManageAction sealed on its
-// own with the device key (aad "OTPW-Q-1"), so a damaged item costs only
-// itself. A new secret in an item is no more exposed than it was in the
-// settings it came from, as with pending entries (README 7.7).
+// the watch (or rejects them). A new secret in an item is no more exposed
+// than it was in the settings it came from, as with pending entries
+// (README 7.7).
+//
+// Each item is [action code, box] or, for a REPLACE, [action code, box,
+// material number]: one ManageAction sealed on its own with the device key
+// (aad "OTPW-Q-2" and the code byte, so a changed code doesn't open), so a
+// damaged item costs only itself. Counting reads only the codes. The new
+// secret or contents of a REPLACE (checked before queueing, up to 4 KB) are
+// a storage value of their own, "qm.<n>", sealed with an aad naming the
+// number and the entry, opened only when that action is applied, and
+// deleted with its item. Items from 0.9.0 are a bare box (aad "OTPW-Q-1",
+// material inside) and stay readable; they count as "to confirm".
//
// "Require PIN" waits here only while the watch is locked (it needs the
-// master key). It only adds protection, so applyLocks() applies it at the
-// next correct (or newly set) PIN, without "Confirm all", the way pending
-// entries are promoted.
+// master key). It only adds protection, so LockJob applies it at the next
+// correct (or newly set) PIN, without "Confirm all", the way pending
+// entries are promoted. Every walk that opens items is a job, one item per
+// step (the watchdog: opening an item is a few byte loops).
import Toybox.Application;
import Toybox.Lang;
@@ -21,78 +31,202 @@ module ChangeQueue {
const KEY = "q";
const MAX_ITEMS = 16;
- const AAD = "OTPW-Q-1";
+ const AAD = "OTPW-Q-2";
+ // Items queued by 0.9.0.
+ const AAD_V1 = "OTPW-Q-1";
+ const MATERIAL_KEY = "qm.";
+ const MATERIAL_AAD = "OTPW-QM-1";
- // What applyLocks() did at the last PIN, for the results of a "Confirm
- // all" that follows it (ChangeQueueJob). Replaced at every PIN.
+ // What LockJob did at this PIN, for the results of a "Confirm all" that
+ // follows it (ChangeQueueJob). A PIN's first LockJob starts them afresh,
+ // a legacy PIN's second one (the new PIN) adds to them, and the PIN
+ // screen takes them when it closes, however it closes (they are in the
+ // results by then).
var _lockLines as Array<String> = [] as Array<String>;
// countToConfirm()'s cached result and the vault it is for.
var _toConfirm as Number? = null;
var _toConfirmFor as Vault? = null;
- function count(vault as Vault) as Number {
+ // The stored items (not opened).
+ function items(vault as Vault) as Array<Application.Storage.ValueType> {
var stored = vault.storage().getValue(KEY);
- return stored instanceof Array ? stored.size() : 0;
+ return stored instanceof Array ? stored as Array<Application.Storage.ValueType> : [] as Array<Application.Storage.ValueType>;
+ }
+
+ function count(vault as Vault) as Number {
+ return items(vault).size();
}
- // Append an action. Throws VaultError LIMIT (too many waiting) or FULL.
+ // Append an action (a REPLACE's material goes to a value of its own).
+ // Throws VaultError LIMIT (too many waiting) or FULL.
function add(vault as Vault, action as ManageAction) as Void {
- var stored = vault.storage().getValue(KEY);
- var items = stored instanceof Array ? stored as Array<Application.Storage.ValueType> : [] as Array<Application.Storage.ValueType>;
- if (items.size() >= MAX_ITEMS) {
+ addWith(vault, action, null);
+ }
+
+ // As add(); the copy sealing makes of up to 4 KB of material goes to
+ // `wiper` when given (ManageRun wipes it in steps).
+ function addWith(vault as Vault, action as ManageAction, wiper as Jobs.Wiper?) as Void {
+ var stored = items(vault);
+ if (stored.size() >= MAX_ITEMS) {
throw new VaultError(VaultError.LIMIT, MAX_ITEMS + " changes already wait for your PIN. Confirm or reject them on the watch first");
}
+ var keys = vault.keyManager.deviceKeys();
+ var material = action.material;
+ var ref = material != null ? freeRef(stored) : -1;
+ if (material != null) {
+ try {
+ var leftovers = [] as Array<ByteArray>;
+ vault.storage().setValue(MATERIAL_KEY + ref, SealedBox.sealKeep(keys, materialAad(ref, action.id), material,
+ wiper != null ? leftovers : null));
+ if (wiper != null) {
+ wiper.addAll(leftovers);
+ }
+ } catch (e instanceof Lang.Exception) {
+ throw new VaultError(VaultError.FULL, "storage full");
+ }
+ }
+ action.material = null;
+ action.ref = ref;
var plain = action.toBytes();
- items.add(SealedBox.seal(vault.keyManager.deviceKeys(), Bytes.fromUtf8(AAD), plain));
+ action.material = material;
+ action.ref = -1;
+ var code = action.action;
+ var item = [code, SealedBox.seal(keys, itemAad(code), plain)] as Array<Application.Storage.ValueType>;
+ if (ref >= 0) {
+ item.add(ref);
+ }
Bytes.wipe(plain);
+ stored.add(item as Application.Storage.ValueType);
changed();
try {
- vault.storage().setValue(KEY, items);
+ vault.storage().setValue(KEY, stored);
} catch (e instanceof Lang.Exception) {
+ if (ref >= 0) {
+ vault.storage().deleteValue(MATERIAL_KEY + ref);
+ }
throw new VaultError(VaultError.FULL, "storage full");
}
}
- // The waiting actions in arrival order; items that don't verify are
- // left out (and dropped by the next clear()).
- function load(vault as Vault) as Array<ManageAction> {
- var out = [] as Array<ManageAction>;
- var stored = vault.storage().getValue(KEY);
- if (!(stored instanceof Array)) {
- return out;
+ // The action code of a stored item, read without opening it; -1 for a
+ // 0.9.0 item or a malformed one.
+ function codeOf(item as Application.Storage.ValueType) as Number {
+ if (item instanceof Array && item.size() >= 2 && item[0] instanceof Number) {
+ return item[0] as Number;
+ }
+ return -1;
+ }
+
+ // The material number of a stored item, -1 for none.
+ function refOf(item as Application.Storage.ValueType) as Number {
+ if (item instanceof Array && item.size() >= 3 && item[2] instanceof Number) {
+ return item[2] as Number;
}
+ return -1;
+ }
+
+ // Open one stored item: the action, or null when it doesn't verify.
+ // With `material`, a REPLACE's material is opened too (null when it
+ // doesn't verify); without, it isn't (descriptions, locks). A 0.9.0 item
+ // always has its material inside (release() hands it to the wiper).
+ // Kilobyte buffers to zero go to `wiper`, for the caller's next steps.
+ function open(vault as Vault, item as Application.Storage.ValueType, wiper as Jobs.Wiper, material as Boolean) as ManageAction? {
var keys = vault.keyManager.deviceKeys();
- var aad = Bytes.fromUtf8(AAD);
+ var leftovers = [] as Array<ByteArray>;
+ if (item instanceof ByteArray) {
+ var old = SealedBox.openKeep(keys, Bytes.fromUtf8(AAD_V1), item, leftovers);
+ wiper.addAll(leftovers);
+ if (old == null) {
+ return null;
+ }
+ var a = ManageAction.fromBytes(old);
+ wiper.add(old);
+ return a;
+ }
+ var code = codeOf(item);
+ var box = code >= 0 ? (item as Array<Application.Storage.ValueType>)[1] : null;
+ var plain = box instanceof ByteArray ? SealedBox.open(keys, itemAad(code), box) : null;
+ if (plain == null) {
+ return null;
+ }
+ var action = ManageAction.fromBytes(plain);
+ Bytes.wipe(plain); // small: names and an ID
+ if (action == null || action.action != code || action.ref != refOf(item)) {
+ return null;
+ }
+ if (action.ref >= 0 && material) {
+ var stored = vault.storage().getValue(MATERIAL_KEY + action.ref);
+ var opened = stored instanceof ByteArray ? SealedBox.openKeep(keys, materialAad(action.ref, action.id), stored, leftovers)
+ : null;
+ wiper.addAll(leftovers);
+ if (opened == null) {
+ return null;
+ }
+ action.material = opened;
+ }
+ return action;
+ }
+
+ // Done with an action: its material goes to the wiper.
+ function release(action as ManageAction, wiper as Jobs.Wiper) as Void {
+ wiper.add(action.material);
+ action.material = null;
+ action.text = null;
+ }
+
+ // An item's identity while it waits: its box's random IV, as hex ("-"
+ // for a malformed item). The menu, "Confirm all", "Reject all" and
+ // LockJob act on items by it, so changes that arrive meanwhile (or
+ // items another job removed) are never mixed up with the ones read.
+ function keyOf(item as Application.Storage.ValueType) as String {
+ var box = item instanceof ByteArray ? item : (codeOf(item) >= 0 ? (item as Array<Application.Storage.ValueType>)[1] : null);
+ if (!(box instanceof ByteArray) || box.size() < SealedBox.MIN_SIZE) {
+ return "-";
+ }
+ return Bytes.toHex(box.slice(1, 1 + SealedBox.IV_BYTES));
+ }
+
+ // Remove the items with these keys (and their material); the others
+ // keep waiting, in order. Returns how many are left.
+ function remove(vault as Vault, keys as Array<String>) as Number {
+ changed();
+ var stored = items(vault);
+ var kept = [] as Array<Application.Storage.ValueType>;
for (var i = 0; i < stored.size(); i++) {
- var box = stored[i];
- var plain = box instanceof ByteArray ? SealedBox.open(keys, aad, box) : null;
- if (plain != null) {
- var action = ManageAction.fromBytes(plain);
- Bytes.wipe(plain);
- if (action != null) {
- out.add(action);
- }
+ if (keys.indexOf(keyOf(stored[i])) >= 0) {
+ deleteMaterial(vault, stored[i]);
+ } else {
+ kept.add(stored[i]);
}
}
- return out;
+ if (kept.size() == stored.size()) {
+ return kept.size();
+ }
+ if (kept.size() == 0) {
+ vault.storage().deleteValue(KEY);
+ return 0;
+ }
+ try {
+ vault.storage().setValue(KEY, kept);
+ } catch (e instanceof Lang.Exception) {
+ // Can't happen when shrinking; if it did, the items stay (an
+ // applied one again is harmless: "already requires the PIN").
+ }
+ return kept.size();
}
- // Short descriptions for the confirmation screen ("Delete: K7F2 Bank").
+ // Short descriptions for the confirmation screen ("Delete: K7F2 Bank"),
+ // in one event: for tests (the menu runs DescribeJob).
function descriptions(vault as Vault) as Array<String> {
- var actions = load(vault);
- var out = [] as Array<String>;
- for (var i = 0; i < actions.size(); i++) {
- out.add(actions[i].describe());
- actions[i].wipe();
- }
- return out;
+ var job = new DescribeJob(vault);
+ Jobs.runToCompletion(job, 1);
+ return job.lines;
}
// The number of waiting changes that need "Confirm all": all but
- // "Require PIN", which applies at the next PIN by itself. For the list's
- // title and the menu; cached until the queue changes, because the list
- // asks at every draw and counting opens every item.
+ // "Require PIN", which applies at the next PIN by itself. Cached until
+ // the queue changes, because the list asks at every draw.
function countToConfirm(vault as Vault) as Number {
var n = _toConfirm;
if (n == null || _toConfirmFor != vault) {
@@ -109,67 +243,27 @@ module ChangeQueue {
_toConfirm = null;
}
- // The number of waiting "Require PIN" actions (applied at the next PIN).
+ // The number of waiting "Require PIN" actions (applied at the next PIN),
+ // from the item codes alone.
function countLocks(vault as Vault) as Number {
- var actions = load(vault);
+ var stored = items(vault);
var n = 0;
- for (var i = 0; i < actions.size(); i++) {
- if (actions[i].action == ManageAction.LOCK) {
+ for (var i = 0; i < stored.size(); i++) {
+ if (codeOf(stored[i]) == ManageAction.LOCK) {
n++;
}
- actions[i].wipe();
}
return n;
}
- // After a correct or new PIN (session unlocked): apply the waiting
- // "Require PIN" actions and remove them from the queue; the other items
- // keep waiting, in order. Each is one re-seal (ManageRun, no code
- // check), like promotePending. One that fails (the entry is gone or
- // already locked) is dropped too.
- function applyLocks(vault as Vault) as Void {
- _lockLines = [] as Array<String>;
+ // LockJob's lines (added to those of the PIN's first job, for a legacy
+ // PIN's second); the queue changed.
+ function addLockLines(lines as Array<String>) as Void {
+ _lockLines.addAll(lines);
changed();
- var stored = vault.storage().getValue(KEY);
- if (!(stored instanceof Array) || !vault.session.isUnlocked()) {
- return;
- }
- var keys = vault.keyManager.deviceKeys();
- var aad = Bytes.fromUtf8(AAD);
- var kept = [] as Array<Application.Storage.ValueType>;
- for (var i = 0; i < stored.size(); i++) {
- var box = stored[i];
- var plain = box instanceof ByteArray ? SealedBox.open(keys, aad, box) : null;
- var action = plain != null ? ManageAction.fromBytes(plain) : null;
- Bytes.wipe(plain);
- if (action != null && action.action == ManageAction.LOCK) {
- var run = new ManageRun(vault, action, true);
- Jobs.runToCompletion(run, 1);
- _lockLines.add(run.failed ? "Not applied: " + action.describe() + ". " + run.message : run.message);
- } else {
- if (action != null) {
- action.wipe();
- }
- kept.add(box as Application.Storage.ValueType); // damaged items are dropped by the next clear()
- }
- }
- Diag.data("locks_applied", "count=" + _lockLines.size()); // for UI scripts: no names
- if (kept.size() == stored.size()) {
- return;
- }
- if (kept.size() == 0) {
- clear(vault);
- return;
- }
- try {
- vault.storage().setValue(KEY, kept);
- } catch (e instanceof Lang.Exception) {
- // Can't happen when shrinking; if it did, the locks just apply
- // again at the next PIN ("already requires the PIN").
- }
}
- // The lines of the last applyLocks(), once (then forgotten).
+ // The lines of the last LockJob, once (then forgotten).
function takeLockLines() as Array<String> {
var lines = _lockLines;
_lockLines = [] as Array<String>;
@@ -178,68 +272,281 @@ module ChangeQueue {
function clear(vault as Vault) as Void {
changed();
+ var stored = items(vault);
+ for (var i = 0; i < stored.size(); i++) {
+ deleteMaterial(vault, stored[i]);
+ }
vault.storage().deleteValue(KEY);
}
- // Remove the first n items (the ones just applied), keeping any that
- // arrived from the phone meanwhile.
- function drop(vault as Vault, n as Number) as Void {
- changed();
- var stored = vault.storage().getValue(KEY);
- if (!(stored instanceof Array) || stored.size() <= n) {
- clear(vault);
- return;
+ function deleteMaterial(vault as Vault, item as Application.Storage.ValueType) as Void {
+ var ref = refOf(item);
+ if (ref >= 0) {
+ vault.storage().deleteValue(MATERIAL_KEY + ref);
+ }
+ }
+
+ // The lowest material number no stored item uses.
+ function freeRef(stored as Array<Application.Storage.ValueType>) as Number {
+ var ref = 0;
+ var used = true;
+ while (used) {
+ used = false;
+ for (var i = 0; i < stored.size(); i++) {
+ if (refOf(stored[i]) == ref) {
+ used = true;
+ ref++;
+ break;
+ }
+ }
+ }
+ return ref;
+ }
+
+ function itemAad(code as Number) as ByteArray {
+ var aad = Bytes.fromUtf8(AAD);
+ aad.add(code & 0xFF);
+ return aad;
+ }
+
+ function materialAad(ref as Number, id as String) as ByteArray {
+ return Bytes.fromUtf8(MATERIAL_AAD + "." + ref + "." + id);
+ }
+}
+
+// The queue's descriptions ("Delete: K7F2 Bank"), one item per step, and
+// the keys of every item read (damaged ones too): "Confirm all" and
+// "Reject all" act on exactly these.
+class DescribeJob extends Jobs.Job {
+
+ var lines as Array<String> = [] as Array<String>;
+ var keys as Array<String> = [] as Array<String>;
+
+ private var _vault as Vault;
+ private var _items as Array<Application.Storage.ValueType>;
+ private var _at as Number = 0;
+ private var _wiper as Jobs.Wiper = new Jobs.Wiper();
+
+ function initialize(vault as Vault) {
+ Job.initialize();
+ _vault = vault;
+ _items = ChangeQueue.items(vault);
+ }
+
+ function step(budget as Number) as Boolean {
+ if (_wiper.left() > 0) {
+ // A 0.9.0 item's contents: zeroed in steps of their own.
+ return _wiper.step(Jobs.Wiper.STEP_BYTES) && _at >= _items.size();
+ }
+ if (_at < _items.size()) {
+ var item = _items[_at];
+ keys.add(ChangeQueue.keyOf(item));
+ var action = ChangeQueue.open(_vault, item, _wiper, false);
+ if (action != null) {
+ lines.add(action.describe());
+ ChangeQueue.release(action, _wiper);
+ }
+ _at++;
+ }
+ return _at >= _items.size() && _wiper.left() == 0;
+ }
+}
+
+// After a correct or new PIN (PinView, while it shows its busy state):
+// promotes the pending locked entries (Vault.promoteNext, a few per step),
+// then applies the waiting "Require PIN" actions, one re-seal (ManageRun,
+// no code check) per item, and removes them from the queue; the other
+// items keep waiting, in order, unopened. One that fails (the entry is gone
+// or already locked) is dropped too, except for a full store: then it waits
+// for the next PIN. The queue is written once at the end, so a stop midway
+// re-applies the locks already done at the next PIN, reported as "already
+// requires the PIN"; promotion saves after each entry.
+class LockJob extends Jobs.Job {
+
+ // Record bytes promoted per step: one large code, or a few OTP entries
+ // (each is an open, a re-seal and the metadata).
+ static const PROMOTE_BYTES = 600;
+
+ // One line per lock applied or failed (also kept for takeLockLines).
+ var lines as Array<String> = [] as Array<String>;
+
+ private var _vault as Vault;
+ // The items when promotion ended; null before.
+ private var _items as Array<Application.Storage.ValueType>? = null;
+ // Keys of the items applied or dropped.
+ private var _done as Array<String> = [] as Array<String>;
+ private var _index as Number = 0;
+ private var _run as ManageRun? = null;
+ private var _action as ManageAction? = null;
+ private var _wiper as Jobs.Wiper = new Jobs.Wiper();
+
+ // first: the PIN's first job, which starts the lock lines afresh (a
+ // legacy PIN's second job, for the new PIN, adds to them).
+ function initialize(vault as Vault, first as Boolean) {
+ Job.initialize();
+ _vault = vault;
+ if (first) {
+ ChangeQueue.takeLockLines();
+ }
+ ChangeQueue.changed();
+ }
+
+ function step(budget as Number) as Boolean {
+ if (_wiper.left() > Jobs.Wiper.INLINE_BYTES) {
+ _wiper.step(Jobs.Wiper.STEP_BYTES);
+ return false;
+ }
+ _wiper.step(Jobs.Wiper.INLINE_BYTES);
+ var items = _items;
+ if (!_vault.session.isUnlocked()) {
+ // Locked meanwhile: what is left waits for the next PIN.
+ if (items != null) {
+ finish();
+ }
+ return true;
+ }
+ if (items == null) {
+ promote();
+ return false;
+ }
+ var run = _run;
+ if (run != null) {
+ if (!run.step(budget)) {
+ return false;
+ }
+ var action = _action as ManageAction;
+ lines.add(run.failed ? "Not applied: " + action.describe() + ". " + run.message : run.message);
+ if (!run.failed || !run.message.equals("storage full")) {
+ _done.add(ChangeQueue.keyOf(items[_index])); // a full store: it waits for the next PIN
+ }
+ ChangeQueue.release(action, _wiper);
+ _run = null;
+ _action = null;
+ _index++;
+ return false;
+ }
+ if (_index >= items.size()) {
+ finish();
+ return true;
}
+ var item = items[_index];
+ var code = ChangeQueue.codeOf(item);
+ // A 0.9.0 item must be opened to know what it is.
+ var action = code == ManageAction.LOCK || item instanceof ByteArray ? ChangeQueue.open(_vault, item, _wiper, false) : null;
+ if (action != null && action.action == ManageAction.LOCK) {
+ _action = action;
+ _run = new ManageRun(_vault, action, true);
+ return false;
+ }
+ if (action != null) {
+ ChangeQueue.release(action, _wiper);
+ }
+ _index++; // damaged items stay until confirmed or rejected
+ return false;
+ }
+
+ // Promote up to PROMOTE_BYTES of pending entries. A full store stops
+ // promotion (the rest wait for the next PIN) but not the locks.
+ private function promote() as Void {
+ var bytes = 0;
try {
- vault.storage().setValue(KEY, stored.slice(n, null) as Array<Application.Storage.ValueType>);
- } catch (e instanceof Lang.Exception) {
- clear(vault); // can't happen when shrinking; never re-apply
+ while (bytes < PROMOTE_BYTES) {
+ var size = _vault.promoteNext(_wiper);
+ if (size == 0) {
+ _items = ChangeQueue.items(_vault);
+ return;
+ }
+ bytes += size > 0 ? size : 1;
+ }
+ } catch (e instanceof VaultError) {
+ var left = _vault.pendingIds().size();
+ lines.add((e.code == VaultError.FULL ? "Storage full: " : "Not promoted: ") + left
+ + (left == 1 ? " locked entry still waits" : " locked entries still wait") + " for your PIN");
+ _items = ChangeQueue.items(_vault);
+ }
+ }
+
+ // Remove the locks just applied (and the failed ones) from the queue.
+ private function finish() as Void {
+ Diag.data("locks_applied", "count=" + lines.size()); // for UI scripts: no names
+ ChangeQueue.addLockLines(lines);
+ if (_done.size() > 0) {
+ ChangeQueue.remove(_vault, _done);
}
}
}
-// Applies every waiting change after the PIN was entered, one per step
-// (each re-seals a record; a code may need its CodeCheck). The applied
-// items leave the queue only at the end, so a crash midway loses nothing;
+// Applies the waiting changes after the PIN was entered: per step, it opens
+// one item or advances its ManageRun (each re-seals a record; a code may
+// need its CodeCheck). Only the items the menu showed (their keys) are
+// applied; changes that arrived meanwhile keep waiting. The applied items
+// leave the queue only at the end, so a crash midway loses nothing;
// applying one again is harmless (a deleted entry is then just missing).
class ChangeQueueJob extends Jobs.Job {
var lines as Array<String> = [] as Array<String>;
private var _vault as Vault;
- private var _actions as Array<ManageAction>;
- private var _loaded as Number = 0;
+ private var _keys as Array<String>?;
+ private var _items as Array<Application.Storage.ValueType> = [] as Array<Application.Storage.ValueType>;
+ private var _done as Array<String> = [] as Array<String>;
private var _index as Number = 0;
+ private var _action as ManageAction? = null;
private var _run as ManageRun? = null;
+ private var _wiper as Jobs.Wiper = new Jobs.Wiper();
- function initialize(vault as Vault) {
+ // keys: the items to apply (DescribeJob.keys), or null for all.
+ function initialize(vault as Vault, keys as Array<String>?) {
Job.initialize();
_vault = vault;
- var stored = vault.storage().getValue(ChangeQueue.KEY);
- _loaded = stored instanceof Array ? stored.size() : 0;
- _actions = ChangeQueue.load(vault);
- // "Require PIN" changes the PIN just applied (ChangeQueue.applyLocks).
+ _keys = keys;
+ var stored = ChangeQueue.items(vault);
+ for (var i = 0; i < stored.size(); i++) {
+ if (keys == null || keys.indexOf(ChangeQueue.keyOf(stored[i])) >= 0) {
+ _items.add(stored[i]);
+ }
+ }
+ // "Require PIN" changes the PIN just applied (LockJob).
lines.addAll(ChangeQueue.takeLockLines());
}
function step(budget as Number) as Boolean {
+ if (_wiper.left() > Jobs.Wiper.INLINE_BYTES) {
+ _wiper.step(Jobs.Wiper.STEP_BYTES);
+ return false;
+ }
+ _wiper.step(Jobs.Wiper.INLINE_BYTES);
var run = _run;
if (run == null) {
- if (_index >= _actions.size()) {
- ChangeQueue.drop(_vault, _loaded);
+ if (_index >= _items.size()) {
+ var left = ChangeQueue.remove(_vault, _done);
+ if (_keys != null && left > 0) {
+ lines.add(left + (left == 1 ? " change" : " changes") + " arrived meanwhile and still "
+ + (left == 1 ? "waits" : "wait") + " (menu, Phone changes)");
+ }
if (lines.size() == 0) {
lines.add("No changes were waiting.");
}
return true;
}
- run = new ManageRun(_vault, _actions[_index], true);
- _run = run;
+ _done.add(ChangeQueue.keyOf(_items[_index]));
+ var action = ChangeQueue.open(_vault, _items[_index], _wiper, true);
+ if (action == null) {
+ lines.add("Not applied: a change was damaged");
+ _index++;
+ return false;
+ }
+ _action = action;
+ _run = new ManageRun(_vault, action, true);
+ return false;
}
if (run.step(budget)) {
- var problem = run.failed ? "Not applied: " + _actions[_index].describe() + ". " : "";
+ var action = _action as ManageAction;
+ var problem = run.failed ? "Not applied: " + action.describe() + ". " : "";
lines.add(problem + run.message);
lines.addAll(run.notes);
- _actions[_index].wipe();
+ ChangeQueue.release(action, _wiper);
+ _action = null;
_index++;
_run = null;
}