Transaction.vala 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308
  1. using Invercargill;
  2. namespace Usm {
  3. public class Transaction {
  4. public Paths paths { get; set; }
  5. public SystemState state { get; set; }
  6. public Set<CachedPackage> to_install { get; set; }
  7. public Set<CachedPackage> to_remove { get; set; }
  8. public signal void progress_updated(TransactionTask task_type, string subject, int current_task, int total_tasks, float task_progress);
  9. private int task_count = 0;
  10. private int current_task = 0;
  11. private string current_subject = "transaction";
  12. private Vector<Vector<CachedPackage>> install_lots;
  13. private Vector<CachedPackage> remove_order;
  14. public void run() throws TransactionError {
  15. // 1. Verify the transaction is valid
  16. strategise();
  17. var all_packages = to_remove.concat(to_install);
  18. // 2. Unpack packages
  19. do_for(all_packages, unpack_package, TransactionTask.UNPACKING);
  20. // 3. Remove packages
  21. do_for(remove_order, remove_package, TransactionTask.REMOVING);
  22. foreach (var lot in install_lots) {
  23. // 3. Build packages
  24. do_for(lot, build_package, TransactionTask.BUILDING);
  25. // 5. Install packages
  26. do_for(lot, install_package, TransactionTask.INSTALLING);
  27. }
  28. // 6. Clean up
  29. do_for(all_packages, cleanup_package, TransactionTask.CLEANING_UP);
  30. }
  31. private string previous_key = "";
  32. public void print_progress(TransactionTask task_type, string subject, int current_task, int total_tasks, float task_progress) {
  33. var verb = task_type.get_verb();
  34. verb = verb[0].toupper().to_string() + verb.substring(1);
  35. var percent = (int)(task_progress * 100.0f);
  36. var key = @"$(task_type.get_verb())_$(subject)_$current_task";
  37. var prefix = previous_key == key ? "\x1b[1F\x1b[2K" : "";
  38. previous_key = key;
  39. printerr(@"$prefix[$(current_task+1)/$total_tasks] $verb $subject ($percent%)\n");
  40. }
  41. public void print_progress_simple(TransactionTask task_type, string subject, int current_task, int total_tasks, float task_progress) {
  42. var verb = task_type.get_verb();
  43. verb = verb[0].toupper().to_string() + verb.substring(1);
  44. var percent = (int)(task_progress * 100.0f);
  45. printerr(@"[$(current_task+1)/$total_tasks] $verb $subject ($percent%)\n");
  46. }
  47. public void strategise() throws TransactionError {
  48. task_count = (to_install.count() * 4) + (to_remove.count() * 3) + 1;
  49. var strategise_worst_case_task_count = (to_remove.count() * to_remove.count()) + (to_install.count() * to_install.count());
  50. var strategise_current_task = 0;
  51. report_progress(TransactionTask.STRATEGISING, 0.0f);
  52. // Installation strategy
  53. install_lots = new Vector<Vector<CachedPackage>>();
  54. var touched = new Set<CachedPackage>();
  55. var available_resources = new Set<ResourceRef>();
  56. var round = 0;
  57. while(true) {
  58. strategise_current_task = round * to_install.count();
  59. var lot = new Vector<CachedPackage>();
  60. var remaining = to_install.difference(touched);
  61. if(remaining.count() == 0) {
  62. break;
  63. }
  64. foreach (var package in to_install.difference(touched)) {
  65. report_progress(TransactionTask.STRATEGISING, (float)strategise_current_task / (float)strategise_worst_case_task_count);
  66. try {
  67. var manifest = package.get_manifest();
  68. var installtime_dependencies = manifest.dependencies.manage.concat(manifest.dependencies.build);
  69. if(installtime_dependencies.all(d => d.is_satisfied() || available_resources.any(r => d.satisfied_by(r)))) {
  70. lot.add(package);
  71. touched.add(package);
  72. available_resources.add_all(manifest.provides.select<ResourceRef>(p => p.key));
  73. }
  74. strategise_current_task++;
  75. }
  76. catch(Error e) {
  77. throw new TransactionError.UNKNOWN_ERROR(@"Failed to read manifest for package \"$(package.package_name)\": $(e.message)");
  78. }
  79. }
  80. if(lot.count() == 0) {
  81. var packages = to_install.difference(touched).to_string(p => p.package_name, ", ");
  82. throw new TransactionError.INVALID_TRANSACTION(@"Could not build a transaction strategy, packages $(packages) have unmet or cyclical dependencies");
  83. }
  84. install_lots.add(lot);
  85. round++;
  86. }
  87. var current_task_baseline = (to_install.count() * to_install.count());
  88. strategise_current_task = current_task_baseline;
  89. report_progress(TransactionTask.STRATEGISING, (float)strategise_current_task / (float)strategise_worst_case_task_count);
  90. // Removal strategy
  91. remove_order = new Vector<CachedPackage>();
  92. touched = new Set<CachedPackage>();
  93. Set<CachedPackageManifest> remaining_to_remove;
  94. try {
  95. remaining_to_remove = to_remove
  96. .attempt_select<CachedPackageManifest>(p => new CachedPackageManifest(p))
  97. .to_set();
  98. }
  99. catch(Error e) {
  100. throw new TransactionError.UNKNOWN_ERROR(@"Failed to read manifest: $(e.message)");
  101. }
  102. round = 0;
  103. while(true) {
  104. strategise_current_task = current_task_baseline + (round * to_remove.count());
  105. if(remaining_to_remove.count() == 0) {
  106. break;
  107. }
  108. foreach (var package in remaining_to_remove) {
  109. report_progress(TransactionTask.STRATEGISING, (float)strategise_current_task / (float)strategise_worst_case_task_count);
  110. if(remaining_to_remove.no(p => p.manifest.dependencies.manage.any(d => package.manifest.provides.any(r => d.satisfied_by(r.key))))) {
  111. remove_order.add(package.package);
  112. touched.add(package.package);
  113. remaining_to_remove.remove(package);
  114. strategise_current_task++;
  115. round++;
  116. break;
  117. }
  118. strategise_current_task++;
  119. }
  120. var packages = remaining_to_remove.to_string(p => p.package.package_name, ", ");
  121. throw new TransactionError.INVALID_TRANSACTION(@"Could not build a transaction strategy, packages $(packages) have unmet or cyclical dependencies");
  122. }
  123. report_progress(TransactionTask.STRATEGISING, 1.0f);
  124. current_task++;
  125. }
  126. private delegate void PackageDelegate(CachedPackage package) throws Error;
  127. private void do_for(Enumerable<CachedPackage> packages, PackageDelegate func, TransactionTask task_type) throws TransactionError {
  128. foreach (var package in packages) {
  129. try {
  130. current_subject = package.package_name;
  131. report_progress(task_type, 0.0f);
  132. func(package);
  133. current_task++;
  134. }
  135. catch (TransactionError e) {
  136. throw e;
  137. }
  138. catch(Error e) {
  139. throw new TransactionError.UNKNOWN_ERROR(@"Error $(task_type.get_verb()) package $(package.package_name): $(e.message)");
  140. }
  141. }
  142. }
  143. private void report_progress(TransactionTask task, float progress) {
  144. progress_updated(task, current_subject, current_task, task_count, progress);
  145. }
  146. public void unpack_package(CachedPackage package) throws Error {
  147. var will_remove = to_remove.has(package);
  148. // Get a clean copy of the sources
  149. package.clean_source();
  150. report_progress(TransactionTask.UNPACKING, will_remove ? 0.25f : 0.5f);
  151. package.get_source_directory();
  152. report_progress(TransactionTask.UNPACKING, will_remove ? 0.5f : 1.0f);
  153. if(will_remove) {
  154. // Get a clean copy of the build artifact
  155. package.clean_build_directory();
  156. report_progress(TransactionTask.UNPACKING, 0.75f);
  157. package.get_build_directory();
  158. report_progress(TransactionTask.UNPACKING, 1.0f);
  159. }
  160. }
  161. private void build_package(CachedPackage package) throws Error {
  162. // Get source directory, and create build directory
  163. var source_dir = package.get_source_directory();
  164. var build_dir = package.create_build_directory();
  165. // Change directory to sources
  166. Environment.set_current_dir(source_dir);
  167. var manifest = new Usm.Manifest.from_file("MANIFEST.usm");
  168. // Build package
  169. var build_proc = manifest.run_build(build_dir, paths, SubprocessFlags.STDOUT_SILENCE);
  170. build_proc.wait_check();
  171. report_progress(TransactionTask.BUILDING, 1.0f);
  172. }
  173. private void remove_package(CachedPackage package) throws Error {
  174. // Get source and build directories
  175. var source_dir = package.get_source_directory();
  176. // "cd" into the source directory and read the manifest
  177. Environment.set_current_dir(source_dir);
  178. var manifest = new Usm.Manifest.from_file("MANIFEST.usm");
  179. // Run remove process if present
  180. var build_proc = manifest.run_remove(RemoveType.FINAL, SubprocessFlags.STDOUT_SILENCE);
  181. if(build_proc != null)
  182. build_proc.wait_check();
  183. manifest.remove_resources( paths, (r, cr, tr, f) => report_progress(TransactionTask.INSTALLING, ((float)cr + (float)f) / (float)tr));
  184. state.unmark_installed(package);
  185. }
  186. private void install_package(CachedPackage package) throws Error {
  187. var source_dir = package.get_source_directory();
  188. var build_dir = package.get_build_directory();
  189. // "cd" into the source directory and read the manifest
  190. Environment.set_current_dir(source_dir);
  191. var manifest = new Usm.Manifest.from_file("MANIFEST.usm");
  192. report_progress(TransactionTask.INSTALLING, 0.0f);
  193. string? install_dir = null;
  194. // Run install process if present
  195. if(manifest.executables.install != null) {
  196. install_dir = package.create_install_directory();
  197. var install_proc = manifest.run_install(build_dir, install_dir, paths, InstallType.FRESH, SubprocessFlags.STDOUT_SILENCE);
  198. install_proc.wait_check();
  199. }
  200. // Install the package's resources
  201. manifest.install_resources(source_dir, build_dir, install_dir, paths, (r, cr, tr, f) => report_progress(TransactionTask.INSTALLING, ((float)cr + (float)f) / (float)tr));
  202. // Run post install process if present
  203. report_progress(TransactionTask.INSTALLING, 1.0f);
  204. var post_install_proc = manifest.run_post_install(build_dir, InstallType.FRESH, SubprocessFlags.STDOUT_SILENCE);
  205. if(post_install_proc != null)
  206. post_install_proc.wait_check();
  207. // Update the system state, and cleanup
  208. state.mark_installed(package);
  209. }
  210. private void cleanup_package(CachedPackage package) throws Error {
  211. package.archive_build();
  212. report_progress(TransactionTask.CLEANING_UP, 0.33334f);
  213. package.clean_source();
  214. report_progress(TransactionTask.CLEANING_UP, 0.66667f);
  215. package.clean_install();
  216. report_progress(TransactionTask.CLEANING_UP, 1.0f);
  217. }
  218. }
  219. public enum TransactionTask {
  220. STRATEGISING,
  221. UNPACKING,
  222. BUILDING,
  223. REMOVING,
  224. INSTALLING,
  225. CLEANING_UP;
  226. public string get_verb() {
  227. switch (this) {
  228. case STRATEGISING:
  229. return "preparing a strategy for";
  230. case UNPACKING:
  231. return "unpacking";
  232. case BUILDING:
  233. return "building";
  234. case REMOVING:
  235. return "removing";
  236. case INSTALLING:
  237. return "installing";
  238. case CLEANING_UP:
  239. return "cleaning up";
  240. default:
  241. assert_not_reached();
  242. }
  243. }
  244. }
  245. public errordomain TransactionError {
  246. INVALID_TRANSACTION,
  247. UNKNOWN_ERROR,
  248. BUILD_ERROR,
  249. INSTALL_ERROR
  250. }
  251. }