launcher.vala 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244
  1. /*
  2. * Copyright (c) 2012-2025 Daniele Bartolini et al.
  3. * SPDX-License-Identifier: GPL-3.0-or-later
  4. */
  5. namespace Crown
  6. {
  7. public static Gee.ArrayList<GLib.Subprocess?> subprocesses = null;
  8. [DBus (name = "org.crownengine.SubprocessLauncherError")]
  9. public errordomain SubprocessLauncherError
  10. {
  11. SUBPROCESS_NOT_FOUND,
  12. SUBPROCESS_CANCELLED
  13. }
  14. [DBus (name = "org.crownengine.SubprocessLauncher")]
  15. public class SubprocessLauncherServer : Object
  16. {
  17. public uint32 _subprocess_id;
  18. public SubprocessLauncherServer()
  19. {
  20. _subprocess_id = 0;
  21. }
  22. /// Spawns a subprocess and returns its ID which is *not* its PID.
  23. public uint32 spawnv_async(GLib.SubprocessFlags flags, string[] argv, string working_dir) throws GLib.SpawnError, GLib.Error
  24. {
  25. GLib.SubprocessLauncher sl = new GLib.SubprocessLauncher(flags);
  26. sl.set_cwd(working_dir);
  27. try {
  28. GLib.Subprocess subproc = sl.spawnv(argv);
  29. subproc.set_data<uint32>("id", _subprocess_id);
  30. subprocesses.add(subproc);
  31. logi("Created suprocess ID %u PID %s".printf(_subprocess_id
  32. , (string)subproc.get_identifier())
  33. );
  34. return _subprocess_id++;
  35. } catch (GLib.Error e) {
  36. throw e;
  37. }
  38. }
  39. /// Waits for @a process_id to terminate and returns its exit status.
  40. public int wait(uint32 process_id) throws GLib.Error
  41. {
  42. int ii;
  43. for (ii = 0; ii < subprocesses.size; ++ii) {
  44. uint32 id = subprocesses[ii].get_data<uint32>("id");
  45. if (id == process_id)
  46. break;
  47. }
  48. if (ii == subprocesses.size)
  49. throw new SubprocessLauncherError.SUBPROCESS_NOT_FOUND("Process ID %u not found".printf(process_id));
  50. try {
  51. if (!subprocesses[ii].wait())
  52. throw new SubprocessLauncherError.SUBPROCESS_CANCELLED("Process ID %u was cancelled".printf(process_id));
  53. if (subprocesses[ii].get_if_exited()) {
  54. int exit_status = subprocesses[ii].get_exit_status();
  55. subprocesses.remove_at(ii);
  56. return exit_status;
  57. }
  58. } catch (GLib.Error e) {
  59. throw e;
  60. }
  61. return int.MAX;
  62. }
  63. }
  64. [DBus (name = "org.crownengine.SubprocessLauncher")]
  65. public interface SubprocessLauncher : Object
  66. {
  67. public abstract uint32 spawnv_async(GLib.SubprocessFlags flags, string[] argv, string working_dir) throws GLib.SpawnError, GLib.Error;
  68. public abstract int wait(uint32 process_id) throws GLib.Error;
  69. }
  70. public static void on_bus_acquired(GLib.DBusConnection conn)
  71. {
  72. try {
  73. conn.register_object("/org/crownengine/subprocess_launcher", new SubprocessLauncherServer());
  74. } catch (GLib.IOError e) {
  75. logi("Could not register DBus service.");
  76. }
  77. }
  78. public static void on_name_acquired(GLib.DBusConnection conn, string name)
  79. {
  80. logi("DBus name acquired: %s".printf(name));
  81. }
  82. public static void on_name_lost(GLib.DBusConnection conn, string name)
  83. {
  84. logi("DBus name lost: %s".printf(name));
  85. }
  86. public static GLib.MainLoop loop = null;
  87. public static void wait_async_ready_callback(Object? source_object, AsyncResult res)
  88. {
  89. GLib.Subprocess subproc = (GLib.Subprocess)source_object;
  90. try {
  91. subproc.wait_async.end(res);
  92. uint32 subproc_id = subproc.get_data<uint32>("id");
  93. if (subproc.get_if_exited()) {
  94. int ec = subproc.get_exit_status();
  95. logi("Process ID %u exited with status %d".printf(subproc_id, ec));
  96. } else {
  97. logi("Process ID %u exited abnormally".printf(subproc_id));
  98. }
  99. } catch (GLib.Error e) {
  100. if (e.code == 19) {
  101. subproc.force_exit();
  102. // Assume subproc is dead now.
  103. } else {
  104. loge(e.message);
  105. }
  106. }
  107. // The last subprocess to exit quits the app.
  108. subprocesses.remove(subproc);
  109. if (subprocesses.size == 0) {
  110. loop.quit();
  111. }
  112. }
  113. public static void child_watch_function(GLib.Pid pid, int wait_status)
  114. {
  115. // When the monitored child terminates, terminate all the
  116. // processes it spawned.
  117. try {
  118. if (GLib.Process.check_exit_status(wait_status)) {
  119. int exit_status;
  120. #if CROWN_PLATFORM_WINDOWS
  121. exit_status = wait_status;
  122. #else
  123. exit_status = Process.exit_status(wait_status);
  124. #endif
  125. logi("Child PID %s terminated with status %d".printf(pid.to_string(), exit_status));
  126. }
  127. } catch (GLib.Error e) {
  128. loge(e.message);
  129. }
  130. GLib.Process.close_pid(pid);
  131. uint wait_timer_id = 0;
  132. GLib.Cancellable cancellable = new GLib.Cancellable();
  133. if (subprocesses.size > 0) {
  134. // Spawn a watchdog to cancel all wait_async() after a while.
  135. uint interval_ms = 500;
  136. wait_timer_id = GLib.Timeout.add(interval_ms, () => {
  137. cancellable.cancel();
  138. return GLib.Source.REMOVE;
  139. });
  140. logi("Waiting %ums for %d subprocesses to terminate...".printf(interval_ms, subprocesses.size));
  141. }
  142. if (wait_timer_id > 0) {
  143. // Wait asynchronously for children to terminate.
  144. foreach (var subproc in subprocesses)
  145. subproc.wait_async.begin(cancellable, wait_async_ready_callback);
  146. } else {
  147. // Force children to exit.
  148. foreach (var subproc in subprocesses)
  149. subproc.force_exit();
  150. loop.quit();
  151. }
  152. }
  153. public static int launcher_main(string[] args)
  154. {
  155. loop = new GLib.MainLoop(null, false);
  156. subprocesses = new Gee.ArrayList<GLib.Subprocess?>();
  157. GLib.Pid child_pid = 0;
  158. #if CROWN_PLATFORM_LINUX
  159. // Signal handlers.
  160. GLib.Unix.signal_add(Posix.Signal.INT, () => {
  161. if (child_pid != 0)
  162. Posix.kill(child_pid, Posix.Signal.INT);
  163. // Try to not terminate prior to the child.
  164. return GLib.Source.CONTINUE;
  165. });
  166. GLib.Unix.signal_add(Posix.Signal.TERM, () => {
  167. if (child_pid != 0)
  168. Posix.kill(child_pid, Posix.Signal.TERM);
  169. // Try to not terminate prior to the child.
  170. return GLib.Source.CONTINUE;
  171. });
  172. #endif
  173. // Connect to DBus.
  174. GLib.Bus.own_name(GLib.BusType.SESSION
  175. , "org.crownengine.SubprocessLauncher"
  176. , GLib.BusNameOwnerFlags.NONE
  177. , on_bus_acquired
  178. , on_name_acquired
  179. , on_name_lost
  180. );
  181. // Spawn child process with the same args plus --child.
  182. try {
  183. string[] child_args = args;
  184. child_args += "--child";
  185. GLib.Process.spawn_async(null
  186. , child_args
  187. , null
  188. , GLib.SpawnFlags.DO_NOT_REAP_CHILD
  189. , null
  190. , out child_pid
  191. );
  192. } catch (GLib.SpawnError e) {
  193. loge("%s".printf(e.message));
  194. return 1;
  195. }
  196. // Monitor child for termination.
  197. // This requires one of GLib.Process.spawn*().
  198. uint event_source = GLib.ChildWatch.add(child_pid, child_watch_function);
  199. if (event_source <= 0) {
  200. loge("Failed to create child watch");
  201. return -1;
  202. }
  203. loop.run();
  204. logi("Bye");
  205. return 0;
  206. }
  207. } /* namespace Crown */