cmd_doctor.v

7.9 KB · raw

1//! `koh doctor` — integrity check and cleanup.2//!3//! One source of truth means there is nothing to rebuild; what is left to check4//! is that the file is sound and that every object a save names is really5//! there. Doctor also tidies the things that are safe to lose: the file cache6//! and, with --yes, old peek folders.78module cli910import json211import os12import time13import src.fmt14import src.history15import src.storage1617pub fn cmd_doctor(raw []string) int {18	args := parse('doctor', raw, Options{19		values: ['backup']20		flags:  ['json', 'yes', 'offline']21	}) or { return usage_error(err.msg()) }22	if args.positional.len > 0 {23		return usage_error('doctor takes no positional arguments')24	}25	store := open_store() or {26		report := DoctorJson{27			health:   'unhealthy'28			root:     storage.find_root(os.getwd()) or { os.getwd() }29			problems: [err.msg()]30			checks:   [DoctorCheck{'database open', 'fail', err.msg()}]31			scope:    'database could not be opened; further checks could not run'32		}33		if args.json() { emit_json(json2.encode(report)) } else { print_doctor(report) }34		return 135	}36	defer { store.close() }37	mut report := DoctorJson{38		root:           store.root39		database:       os.join_path(store.root, storage.file_name)40		schema_version: storage.schema_version41		checked_at:     time.now().unix()42		koh_version:    version43	}44	working('checking database, objects and all stored history')45	store.db.exec('BEGIN') or { return refuse(args.json(), err.msg()) }46	report.check('SQLite integrity', store.integrity_check() or { [err.msg()] })47	report.check('object hashes and sizes', store.check_objects() or { [err.msg()] })48	report.check('save object references', store.check_save_files() or { [err.msg()] })49	doctor_local(store, mut report) or { report.check('local metadata', [err.msg()]) }50	store.end() or { report.check('read transaction', [err.msg()]) }51	working('reading working files without cached hashes')52	doctor_working_tree(store, mut report) or { report.check('working tree', [err.msg()]) }53	doctor_build(store.root, mut report)54	if args.flag('yes') {55		report.peek_folders_removed = sweep_peek_folders()56	}57	if path := args.value_or_none('backup') {58		mut backed_up := true59		store.backup(path) or {60			report.check('backup', [err.msg()])61			backed_up = false62		}63		if backed_up {64			report.backup = path65		}66	}67	report.size_bytes = os.file_size(report.database)68	report.wal_bytes = os.file_size('${report.database}-wal')69	doctor_remote_config(mut report)70	if !args.flag('offline') {71		working('auditing remote history and releases (read-only)')72		doctor_remote(store, mut report) or { report.check('remote audit', [err.msg()]) }73	} else {74		report.checks << DoctorCheck{'remote audit', 'skipped', '--offline requested; remote content not checked'}75	}76	report.scope = 'repository data and configuration; application tests, backup recovery and remote write permissions are not verified'77	report.health = if report.problems.len > 0 {78		'unhealthy'79	} else if report.warnings.len > 0 { 'warning' } else { 'healthy' }80	if args.json() { emit_json(json2.encode(report)) } else { print_doctor(report) }81	return if report.problems.len > 0 { 1 } else { 0 }82}8384fn (mut report DoctorJson) check(name string, problems []string) {85	report.checks << DoctorCheck{name, if problems.len == 0 { 'pass' } else { 'fail' }, if problems.len == 0 {86		'verified'87	} else {88		problems.join('; ')89	}}90	for problem in problems {91		if problem !in report.problems { report.problems << problem }92	}93}9495fn print_doctor(report DoctorJson) {96	if report.problems.len == 0 {97		ok('${report.root} is sound · local health: ${report.health}')98	} else {99		warn('${report.root} has ${fmt.plural(report.problems.len, 'problem')}')100	}101	note('database: ${fmt.human_bytes(i64(report.size_bytes))} + ${fmt.human_bytes(i64(report.wal_bytes))} WAL · schema ${report.schema_version}')102	if report.metadata_available {103		if report.head.id != '' {104			note('head [${report.head.id}] ${report.head.message.all_before('\n')} · ${fmt.stamp(report.head.created_at)}')105			note('${report.head_files} files at head · agent: ${report.head.agent} · session: ${report.head.session}')106		} else {107			note('head: ${if report.saves == 0 { 'no saves yet' } else { report.head_id }}')108		}109		note('${report.saves} stored saves · ${report.line_saves} on line · ${report.off_line_saves} off line · ${report.checkpoints} checkpoints')110		note('${report.objects} objects · ${fmt.human_bytes(report.object_bytes)} content · ${fmt.human_bytes(report.packed_bytes)} compressed')111		note('${report.events} events · ${report.flagged_saves} flagged saves · ${report.file_entries} file entries')112	} else {113		note('repository metadata unavailable; see failed checks')114	}115	for check in report.checks {116		println('  ${check.status}: ${check.name} · ${check.detail}')117	}118	for warning in report.warnings {119		warn(warning)120	}121	if report.remote_target != '' { note('remote: ${report.remote_target}') }122	if report.source != '' { note('read-only source: ${report.source}') }123	if report.last_offered_head != '' {124		note('last successful offer: [${report.last_offered_head}] (stored value)')125	}126	if report.project_version != '' {127		note('project ${report.project_version} · ${report.build_platforms.join(', ')}')128	}129	if report.cache_cleared {130		note('cleared ${report.cached_files} cached hashes; working files read again')131	}132	if report.peek_folders_removed > 0 {133		note('removed ${report.peek_folders_removed} old peek folders')134	}135	if report.backup != '' { ok('backed up to ${report.backup}') }136	if report.problems.len > 0 {137		note('keep .koh intact before recovering history; use a known-good backup or another verified copy')138	}139	note('scope: ${report.scope}')140}141142fn shell_quote(text string) string {143	return "'${text.replace("'", "'\\''")}'"144}145146// peek_age is how old a peek folder must be before doctor clears it.147const peek_age = 86400148149// sweep_peek_folders clears the scratch trees `koh peek` leaves in the system150// temp directory, once they are a day old.151fn sweep_peek_folders() int {152	mut removed := 0153	for name in os.ls(os.temp_dir()) or { return 0 } {154		path := os.join_path(os.temp_dir(), name)155		info := os.lstat(path) or { continue }156		if !name.starts_with(peek_prefix) || os.is_link(path) || !os.is_dir(path)157			|| info.uid != u32(os.getuid()) || !peek_is_old(os.file_last_mod_unix(path), time.now().unix()) {158			continue159		}160		os.rmdir_all(path) or { continue }161		removed++162	}163	return removed164}165166// peek_is_old is true once a peek is more than a day old. A younger one may167// still be open in somebody's editor.168fn peek_is_old(modified i64, now i64) bool {169	return modified < now - peek_age170}171172struct DoctorCheck {173	name   string174	status string175	detail string176}177178struct DoctorJson {179mut:180	health               string181	scope                string182	metadata_available   bool183	ignore_files         map[string]int184	checks               []DoctorCheck185	warnings             []string186	problems             []string187	root                 string188	database             string189	schema_version       int190	checked_at           i64191	koh_version          string192	head_id              string193	head                 history.Save194	head_files           int195	objects              int196	saves                int197	checkpoints          int198	line_saves           int199	off_line_saves       int200	events               int201	flagged_saves        int202	head_flagged         bool203	file_entries         i64204	size_bytes           u64205	wal_bytes            u64206	object_bytes         i64207	packed_bytes         i64208	unreferenced_objects i64209	restore_unfinished   bool210	pending_strips       []string211	changes              Changes212	ignored_saved_files  []string213	build_platforms      []string214	project_version      string215	source               string216	remote_repo          string217	remote_target        string218	last_offered_head    string219	remote_head          string220	remote_objects       int221	remote_saves         int222	remote_releases      int223	cached_files         int224	cache_cleared        bool225	peek_folders_removed int226	backup               string227}