Journal.vala 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393
  1. using Invercargill;
  2. using Invercargill.DataStructures;
  3. namespace Usm {
  4. /**
  5. * Rollback journal for a {@link Transaction}: every install and
  6. * remove records what it is about to change ({@link entry_install}
  7. * and {@link entry_remove}), every file that would be overwritten or
  8. * deleted is copied into the journal's backup area first
  9. * ({@link backup_file}), and each operation that succeeded marks
  10. * itself done ({@link complete}). When the transaction then fails,
  11. * {@link rollback} undoes the recorded operations in reverse —
  12. * deleting installed files, restoring overwritten and removed files
  13. * from their backups, and re-creating `installed/` marks — while
  14. * {@link describe_rollback} narrates what was undone.
  15. *
  16. * The journal owns `<backup-root>/<transaction-id>/` (with the
  17. * default root `/var/usm/backup`, rooted like every other USM
  18. * destination through {@link Paths}), holding `journal.json` plus
  19. * each backed-up file mirrored at `<package-name>/<original-path>`.
  20. * {@link finish} removes the whole directory on the success path; a
  21. * rollback deliberately keeps it for inspection.
  22. *
  23. * What a journal cannot undo:
  24. *
  25. * * post-install and remove scripts may change the system beyond
  26. * their manifest's resources — system users, enabled services,
  27. * databases — and those side effects persist through a rollback;
  28. * * system packages installed through the system package manager
  29. * are not journalable, they belong to that manager;
  30. * * parent directories created for new files are left behind, and
  31. * empty `dir` provides are neither backed up nor re-created;
  32. * * the `.usmc` downloads and build caches a transaction made stay
  33. * in the cache — harmless, and reusable by the retry;
  34. * * a rollback step that itself fails (a missing or unreadable
  35. * backup, for instance) is reported and skipped, never retried.
  36. */
  37. public class Journal {
  38. private const string OPERATION_INSTALL = "install";
  39. private const string OPERATION_REMOVE = "remove";
  40. /** Extension of the sidecar recording a backed-up symlink's target. */
  41. private const string LINK_SIDECAR_SUFFIX = ".usm-link-target";
  42. /** Directory holding every transaction's backup area, e.g. `/var/usm/backup`. */
  43. public string backup_root { get; private set; }
  44. /** This journal's unique directory name beneath {@link backup_root}. */
  45. public string transaction_id { get; private set; }
  46. /** The backup area `<backup-root>/<transaction-id>`, created by {@link begin}. */
  47. public string directory { get; private set; }
  48. private Vector<JournalEntry> entries = new Vector<JournalEntry>();
  49. private Vector<string> undone = new Vector<string>();
  50. public Journal(string backup_root) {
  51. this.backup_root = backup_root;
  52. transaction_id = @"$(new DateTime.now_utc().format_iso8601())-$(Uuid.string_random())";
  53. }
  54. /**
  55. * Creates the backup directory and writes an empty journal: the
  56. * first call of a transaction, before any operation runs.
  57. */
  58. public void begin() throws Error {
  59. directory = Path.build_filename(backup_root, transaction_id);
  60. File.new_for_path(directory).make_directory_with_parents();
  61. write();
  62. }
  63. /**
  64. * Records what installing {@link pkg} will do: the
  65. * {@link files_created} destination paths do not exist yet, the
  66. * {@link files_overwritten} paths do and will have their content
  67. * replaced — back those up with {@link backup_file} before the
  68. * install executes. {@link state_mark_path} is the `installed/`
  69. * symlink {@link SystemState.mark_installed} will create and
  70. * {@link previous_state_target} the target that symlink held
  71. * before, null when nothing was marked (see
  72. * {@link SystemState.read_installed_target}); a rollback
  73. * re-links the previous target, or removes a fresh mark.
  74. */
  75. public void entry_install(CachedPackage pkg, string[] files_created, string[] files_overwritten,
  76. string? state_mark_path = null, string? previous_state_target = null) throws Error {
  77. var entry = new JournalEntry();
  78. entry.operation = OPERATION_INSTALL;
  79. entry.package_name = pkg.package_name;
  80. entry.state_mark_path = state_mark_path;
  81. entry.previous_state_target = previous_state_target;
  82. foreach(var path in files_created) {
  83. entry.files_created.add(path);
  84. }
  85. foreach(var path in files_overwritten) {
  86. entry.files_overwritten.add(path);
  87. }
  88. entries.add(entry);
  89. write();
  90. }
  91. /**
  92. * Records what removing {@link pkg} will do: every
  93. * {@link files_removed} path is deleted by the removal — back
  94. * them up with {@link backup_file} before it executes.
  95. * {@link state_mark_path} is the `installed/` symlink
  96. * {@link SystemState.unmark_installed} will delete and
  97. * {@link state_target} the target it held (null when nothing was
  98. * marked); a rollback re-links it.
  99. */
  100. public void entry_remove(CachedPackage pkg, string[] files_removed,
  101. string? state_mark_path = null, string? state_target = null) throws Error {
  102. var entry = new JournalEntry();
  103. entry.operation = OPERATION_REMOVE;
  104. entry.package_name = pkg.package_name;
  105. entry.state_mark_path = state_mark_path;
  106. entry.state_target = state_target;
  107. foreach(var path in files_removed) {
  108. entry.files_removed.add(path);
  109. }
  110. entries.add(entry);
  111. write();
  112. }
  113. /**
  114. * Copies {@link file_path} into {@link package_name}'s backup
  115. * area, preserving mode and ownership metadata. A symbolic link
  116. * is recorded as a sidecar holding its target, so restoration
  117. * re-creates the link itself rather than a copy of whatever it
  118. * pointed at.
  119. */
  120. public void backup_file(string file_path, string package_name) throws Error {
  121. var backup_path = backup_path_for(file_path, package_name);
  122. var parent = File.new_for_path(backup_path).get_parent();
  123. if(parent != null && !parent.query_exists()) {
  124. parent.make_directory_with_parents();
  125. }
  126. var info = File.new_for_path(file_path)
  127. .query_info(FileAttribute.STANDARD_TYPE + "," + FileAttribute.STANDARD_SYMLINK_TARGET, FileQueryInfoFlags.NOFOLLOW_SYMLINKS);
  128. if(info.get_file_type() == FileType.SYMBOLIC_LINK) {
  129. FileUtils.set_contents(backup_path + LINK_SIDECAR_SUFFIX, info.get_symlink_target() ?? "");
  130. return;
  131. }
  132. File.new_for_path(file_path).copy(File.new_for_path(backup_path), FileCopyFlags.ALL_METADATA);
  133. }
  134. /**
  135. * Marks {@link package_name}'s latest uncompleted entry done: the
  136. * operation succeeded, so a rollback has to undo it.
  137. */
  138. public void complete(string package_name) throws Error {
  139. for(int i = (int)entries.length - 1; i >= 0; i--) {
  140. if(entries[i].package_name == package_name && !entries[i].completed) {
  141. entries[i].completed = true;
  142. break;
  143. }
  144. }
  145. write();
  146. }
  147. /**
  148. * Undoes every recorded operation in reverse order — completed
  149. * ones plus a trailing half-applied one whose own operation
  150. * failed — removing again what was installed and installing
  151. * again what was removed. Steps that fail are reported and
  152. * skipped, never retried; the backup directory is kept
  153. * afterwards for inspection. Follow with
  154. * {@link describe_rollback}.
  155. */
  156. public void rollback() {
  157. undone = new Vector<string>();
  158. for(int i = (int)entries.length - 1; i >= 0; i--) {
  159. var entry = entries[i];
  160. try {
  161. if(entry.operation == OPERATION_INSTALL) {
  162. rollback_install(entry);
  163. }
  164. else {
  165. rollback_remove(entry);
  166. }
  167. }
  168. catch(Error e) {
  169. warning(@"[Usm] Could not roll back the $(entry.operation) of \"$(entry.package_name)\": $(e.message)");
  170. }
  171. }
  172. }
  173. /**
  174. * A human-readable transcript of what the last {@link rollback}
  175. * undid, one line per operation; when nothing was ever recorded
  176. * the transaction had changed nothing, and the description says
  177. * so instead.
  178. */
  179. public string describe_rollback() {
  180. if(undone.length == 0) {
  181. return "Nothing had been changed when the transaction failed, so there was nothing to roll back.";
  182. }
  183. var builder = new StringBuilder("Rolled back after the failure:\n");
  184. foreach(var line in undone) {
  185. builder.append(" ").append(line).append("\n");
  186. }
  187. return builder.str;
  188. }
  189. /**
  190. * Removes the backup directory entirely — the success path. The
  191. * {@link backup_root} goes with it when no other transaction is
  192. * leaving a directory behind.
  193. */
  194. public void finish() throws Error {
  195. Util.delete_tree(directory);
  196. try {
  197. File.new_for_path(backup_root).delete();
  198. }
  199. catch(Error e) {
  200. // a non-empty root belongs to a concurrent transaction
  201. }
  202. }
  203. private void rollback_install(JournalEntry entry) throws Error {
  204. for(int i = (int)entry.files_created.length - 1; i >= 0; i--) {
  205. delete_if_present(entry.files_created[i]);
  206. }
  207. for(int i = (int)entry.files_overwritten.length - 1; i >= 0; i--) {
  208. restore_file(entry.files_overwritten[i], entry.package_name);
  209. }
  210. if(entry.state_mark_path != null) {
  211. if(entry.previous_state_target == null) {
  212. delete_if_present((!)entry.state_mark_path);
  213. }
  214. else {
  215. relink((!)entry.state_mark_path, (!)entry.previous_state_target);
  216. }
  217. }
  218. var parts = new Vector<string>();
  219. parts.add(@"removed $(entry.files_created.length) created file(s)");
  220. if(entry.files_overwritten.length > 0) {
  221. parts.add(@"restored $(entry.files_overwritten.length) overwritten file(s)");
  222. }
  223. if(entry.state_mark_path != null) {
  224. parts.add(entry.previous_state_target == null ? "removed the installed mark" : "restored the previous installed mark");
  225. }
  226. undone.add(@"uninstalled \"$(entry.package_name)\": $(string.joinv(", ", parts.to_array()))");
  227. }
  228. private void rollback_remove(JournalEntry entry) throws Error {
  229. for(int i = (int)entry.files_removed.length - 1; i >= 0; i--) {
  230. restore_file(entry.files_removed[i], entry.package_name);
  231. }
  232. if(entry.state_mark_path != null && entry.state_target != null) {
  233. relink((!)entry.state_mark_path, (!)entry.state_target);
  234. }
  235. var parts = new Vector<string>();
  236. parts.add(@"restored $(entry.files_removed.length) file(s)");
  237. if(entry.state_mark_path != null && entry.state_target != null) {
  238. parts.add("re-created the installed mark");
  239. }
  240. undone.add(@"reinstalled \"$(entry.package_name)\": $(string.joinv(", ", parts.to_array()))");
  241. }
  242. private void delete_if_present(string path) throws Error {
  243. try {
  244. File.new_for_path(path).delete();
  245. }
  246. catch(IOError.NOT_FOUND e) {
  247. }
  248. }
  249. private void relink(string mark_path, string target) throws Error {
  250. delete_if_present(mark_path);
  251. File.new_for_path(mark_path).make_symbolic_link(target);
  252. }
  253. private void restore_file(string file_path, string package_name) throws Error {
  254. var backup_path = backup_path_for(file_path, package_name);
  255. var sidecar_path = backup_path + LINK_SIDECAR_SUFFIX;
  256. if(File.new_for_path(sidecar_path).query_exists()) {
  257. string target;
  258. FileUtils.get_contents(sidecar_path, out target);
  259. delete_if_present(file_path);
  260. File.new_for_path(file_path).make_symbolic_link(target);
  261. return;
  262. }
  263. var dest = File.new_for_path(file_path);
  264. var parent = dest.get_parent();
  265. if(parent != null && !parent.query_exists()) {
  266. parent.make_directory_with_parents();
  267. }
  268. delete_if_present(file_path);
  269. File.new_for_path(backup_path).copy(dest, FileCopyFlags.ALL_METADATA);
  270. }
  271. /** The backup location mirroring {@link file_path} under this journal's area. */
  272. private string backup_path_for(string file_path, string package_name) {
  273. var mirrored = file_path.has_prefix(Path.DIR_SEPARATOR_S) ? file_path.substring(1) : file_path;
  274. return Path.build_filename(directory, package_name, mirrored);
  275. }
  276. private void write() throws Error {
  277. var builder = new StringBuilder("[");
  278. for(int i = 0; i < entries.length; i++) {
  279. if(i > 0) {
  280. builder.append(",");
  281. }
  282. entries[i].write(builder);
  283. }
  284. builder.append("]");
  285. FileUtils.set_contents(Path.build_filename(directory, "journal.json"), builder.str);
  286. }
  287. /** One recorded operation: everything a rollback needs to undo it. */
  288. private class JournalEntry {
  289. public string operation = "";
  290. public string package_name = "";
  291. public bool completed = false;
  292. public Vector<string> files_created = new Vector<string>();
  293. public Vector<string> files_overwritten = new Vector<string>();
  294. public Vector<string> files_removed = new Vector<string>();
  295. public string? state_mark_path = null;
  296. public string? state_target = null;
  297. public string? previous_state_target = null;
  298. public void write(StringBuilder builder) {
  299. var fields = new Vector<string>();
  300. fields.add(json_field("operation", json_string(operation)));
  301. fields.add(json_field("package", json_string(package_name)));
  302. fields.add(json_field("status", json_string(completed ? "completed" : "in-progress")));
  303. fields.add(json_field("files_created", json_string_array(files_created)));
  304. fields.add(json_field("files_overwritten", json_string_array(files_overwritten)));
  305. fields.add(json_field("files_removed", json_string_array(files_removed)));
  306. if(state_mark_path != null) {
  307. fields.add(json_field("state_entry", json_string((!)state_mark_path)));
  308. }
  309. builder.append("{").append(string.joinv(",", fields.to_array())).append("}");
  310. }
  311. private static string json_field(string key, string value) {
  312. return json_string(key) + ":" + value;
  313. }
  314. private static string json_string(string value) {
  315. var builder = new StringBuilder("\"");
  316. for(int i = 0; i < value.length; i++) {
  317. char c = value[i];
  318. switch(c) {
  319. case '"':
  320. builder.append("\\\"");
  321. break;
  322. case '\\':
  323. builder.append("\\\\");
  324. break;
  325. case '\b':
  326. builder.append("\\b");
  327. break;
  328. case '\f':
  329. builder.append("\\f");
  330. break;
  331. case '\n':
  332. builder.append("\\n");
  333. break;
  334. case '\r':
  335. builder.append("\\r");
  336. break;
  337. case '\t':
  338. builder.append("\\t");
  339. break;
  340. default:
  341. if(c < 0x20) {
  342. builder.append_printf("\\u%04x", (int)c);
  343. }
  344. else {
  345. builder.append_c(c);
  346. }
  347. break;
  348. }
  349. }
  350. return builder.append("\"").str;
  351. }
  352. private static string json_string_array(Vector<string> values) {
  353. var escaped = new Vector<string>();
  354. foreach(var value in values) {
  355. escaped.add(json_string(value));
  356. }
  357. return "[" + string.joinv(",", escaped.to_array()) + "]";
  358. }
  359. }
  360. }
  361. }