///|
priv struct RecoveryFinding {
path : String
kind : String
state : String
} derive(ToJson)
///|
priv struct RecoveryReport {
schema : Int32
read_only : Bool
point_in_time_only : Bool
requires_attention : Bool
truncated : Bool
visited : Int32
findings : Array[RecoveryFinding]
automatic_recovery : Bool
journal_replay : Bool
gc : Bool
power_loss_verified : Bool
} derive(ToJson)
///|
priv struct RecoveryInspection {
findings : Array[RecoveryFinding]
mut visited : Int32
mut requires_attention : Bool
mut truncated : Bool
}
///|
fn RecoveryInspection::record(
self : RecoveryInspection,
path : String,
kind : String,
state : String,
attention : Bool,
) -> Unit {
if self.findings.length() >= 1024 {
self.truncated = true
self.requires_attention = true
return
}
self.findings.push({ path, kind, state, })
if attention {
self.requires_attention = true
}
}
///|
async fn inspect_advisory_lock(path : String) -> Bool {
let file = @fs.open(path, mode=ReadOnly)
defer file.close()
if file.kind() != Regular {
raise VcsError::InvalidPath(
"recovery lock inspection expected a regular file",
)
}
if file.try_lock(Exclusive) {
file.unlock()
false
} else {
true
}
}
///|
async fn inspect_recovery_directory(
root : String,
relative : String,
inspection : RecoveryInspection,
depth : Int32,
) -> Unit {
if depth > 64 || inspection.visited >= 20000 {
inspection.truncated = true
inspection.requires_attention = true
return
}
for name in @fs.readdir(root + "/" + relative, include_hidden=true) {
inspection.visited = inspection.visited + 1
if inspection.visited > 20000 {
inspection.truncated = true
inspection.requires_attention = true
return
}
let child = relative + "/" + name
let path = root + "/" + child
try {
let kind = @fs.kind(path, follow_symlink=false)
if relative == ".moonvcs" && name == "switch-transaction" {
let state = if kind == Directory {
switch_recovery_summary(root, child) catch {
error => {
if @async.is_cancellation_error(error) {
raise error
}
"unknown_or_incomplete_manifest; preserve all files; see BRANCH_SWITCH.md"
}
}
} else {
"unsupported_pending_entry_preserved; see BRANCH_SWITCH.md"
}
inspection.record(child, "switch_transaction", state, true)
} else if relative == ".moonvcs/switch-history" {
let state = if kind == Directory {
switch_recovery_summary(root, child) catch {
error => {
if @async.is_cancellation_error(error) {
raise error
}
"unknown_switch_archive_preserved"
}
}
} else {
"unsupported_switch_archive_preserved"
}
inspection.record(
child,
"switch_archive",
state,
!state.has_prefix("phase=completed;"),
)
} else if relative == ".moonvcs/quarantine" {
if kind == Directory {
let lease = path + "/active.lock"
if @fs.exists(lease) &&
@fs.kind(lease, follow_symlink=false) == Regular {
let active = inspect_advisory_lock(lease)
inspection.record(
child,
"quarantine",
if active {
"active"
} else {
"inactive_lease_requires_inspection"
},
!active,
)
} else {
inspection.record(
child, "quarantine", "unknown_owner_or_activity", true,
)
}
} else {
inspection.record(
child, "quarantine_entry", "unsupported_kind_preserved", true,
)
}
} else if kind == Directory {
inspect_recovery_directory(root, child, inspection, depth + 1)
} else if kind != Regular {
// Never follow metadata symlinks or read their targets.
inspection.record(
child, "metadata_entry", "unsupported_kind_preserved", true,
)
} else if relative == ".moonvcs" &&
["staging-transaction.lock", "lazy-fetch.lock", "receive-publish.lock"].contains(
name,
) {
let active = inspect_advisory_lock(path)
inspection.record(
child,
"advisory_lock",
if active {
"active"
} else {
"idle_file_not_a_stale_lock"
},
false,
)
} else if name.has_suffix(".lock") {
inspection.record(
child, "exclusive_lock_or_pending_write", "unknown_owner_or_activity",
true,
)
} else if relative.has_prefix(".moonvcs/objects/") &&
name.contains(".tmp-") {
inspection.record(
child, "temporary_object", "requires_inspection", true,
)
}
} catch {
error => {
if @async.is_cancellation_error(error) {
raise error
}
inspection.record(
child, "inspection_error", "concurrent_change_or_io_error", true,
)
}
}
}
}
///|
async fn recovery_report(root : String) -> String {
let inspection : RecoveryInspection = {
findings: [],
visited: 0,
requires_attention: false,
truncated: false,
}
if @fs.kind(root + "/.moonvcs", follow_symlink=false) == Directory {
inspect_recovery_directory(root, ".moonvcs", inspection, 0)
} else {
inspection.record(
".moonvcs", "metadata_root", "unsupported_kind_preserved", true,
)
}
@json.to_json(RecoveryReport::{
schema: 1,
read_only: true,
point_in_time_only: true,
requires_attention: inspection.requires_attention,
truncated: inspection.truncated,
visited: inspection.visited,
findings: inspection.findings,
automatic_recovery: false,
journal_replay: false,
gc: false,
power_loss_verified: false,
}).stringify()
}