[bootsvc] Mark bootsvc as critical to root job

Set bootsvc as critical to the root job. This will cause the root job to
be terminated if bootsvc terminates.

This CL also:
* Changes the default action for when root job terminates, so that the
system reboots. This seems like the most sensible default for
production.
* Adds the ability to print a notice when the root job terminates. This
allows us to write ZBI tests for this behaviour.

Change-Id: I08658e0bee2e92481fec90dfd1cd14f8f9112277
diff --git a/docs/reference/kernel/kernel_cmdline.md b/docs/reference/kernel/kernel_cmdline.md
index c08cebb..720ae1b 100644
--- a/docs/reference/kernel/kernel_cmdline.md
+++ b/docs/reference/kernel/kernel_cmdline.md
@@ -41,11 +41,6 @@
 between the program and individual arguments. For example,
 'bootsvc.next=bin/mybin,arg1,arg2'.
 
-## bootsvc.on_next_process_exit=reboot|shutdown
-
-What bootsvc should do when the "next process" (see
-[bootsvc.next](#bootsvc_next_bootfs-path)) exits. This defaults to 'reboot'.
-
 ## clock\.backstop=\<seconds\>
 
 Sets the initial offset (from the Unix epoch, in seconds) for the UTC clock.
@@ -372,19 +367,22 @@
 The `k oom info` command will show the current value of this and other
 parameters.
 
-## kernel.root-job.reboot=\<bool>
+## kernel.root-job.behavior=\<string>
 
-This option specifies whether the kernel should reboot the system when the root
-job is either: terminated, or has no jobs and no processes.
+This option specifies what action the kernel should take when the root job is
+either terminated, or has no jobs and no processes. This can take one of the
+following values:
 
-By default, the kernel will halt the system, stopping any further execution.
+* halt -- Halt the system
+* reboot -- Reboot the system (the default)
+* bootloader -- Reboot into the bootloader
+* recovery -- Reboot into the recovery partition
+* shutdown -- Shut down the system
 
-## kernel.root-job.shutdown=\<bool>
+## kernel.root-job.notice=\<string>
 
-This option specifies whether the kernel should shutdown the system when the
-root job is either: terminated, or has no jobs and no processes.
-
-By default, the kernel will halt the system, stopping any further execution.
+The option allows a notice to be printed when the root job is either:
+terminated, or has no jobs and no processes.
 
 ## kernel.x86.disable_spec_mitigations=\<bool>
 
@@ -447,7 +445,7 @@
 If false, this option leaves PCI devices running when calling mexec. Defaults
 to true.
 
-## kernel.serial=\<string\>
+## kernel.serial=\<string>
 
 This controls what serial port is used.  If provided, it overrides the serial
 port described by the system's bootdata.  The kernel debug serial port is
diff --git a/zircon/kernel/object/glue.cc b/zircon/kernel/object/glue.cc
index 092f355..f3f28a6 100644
--- a/zircon/kernel/object/glue.cc
+++ b/zircon/kernel/object/glue.cc
@@ -12,6 +12,7 @@
 #include <inttypes.h>
 #include <lib/cmdline.h>
 #include <lib/crashlog.h>
+#include <lib/debuglog.h>
 #include <zircon/syscalls/object.h>
 #include <zircon/types.h>
 
@@ -52,18 +53,40 @@
     if (HasChild(state)) {
       return 0;
     }
-    if (gCmdline.GetBool("kernel.root-job.reboot", false)) {
-      printf("root-job: rebooting\n");
-      platform_halt(HALT_ACTION_REBOOT, HALT_REASON_SW_RESET);
-    } else if (gCmdline.GetBool("kernel.root-job.shutdown", true)) {
-      printf("root-job: shutting down\n");
-      platform_halt(HALT_ACTION_SHUTDOWN, HALT_REASON_SW_RESET);
-    } else {
-      printf("root-job: halting\n");
-      platform_halt(HALT_ACTION_HALT, HALT_REASON_SW_RESET);
-    }
+    // We may be in an interrupt context, e.g. thread_process_pending_signals(),
+    // so we schedule a DPC.
+    dpc_queue(&dpc_, true);
     return kNeedRemoval;
   }
+
+  static void Halt(dpc_t* dpc) {
+    const char* notice = gCmdline.GetString("kernel.root-job.notice");
+    if (notice != nullptr) {
+      printf("root-job: notice: %s\n", notice);
+    }
+
+    const char* behavior = gCmdline.GetString("kernel.root-job.behavior");
+    if (behavior == nullptr) {
+      behavior = "reboot";
+    }
+
+    printf("root-job: taking %s action\n", behavior);
+    dlog_shutdown();
+
+    if (!strcmp(behavior, "halt")) {
+      platform_halt(HALT_ACTION_HALT, HALT_REASON_SW_RESET);
+    } else if (!strcmp(behavior, "bootloader")) {
+      platform_halt(HALT_ACTION_REBOOT_BOOTLOADER, HALT_REASON_SW_RESET);
+    } else if (!strcmp(behavior, "recovery")) {
+      platform_halt(HALT_ACTION_REBOOT_RECOVERY, HALT_REASON_SW_RESET);
+    } else if (!strcmp(behavior, "shutdown")) {
+      platform_halt(HALT_ACTION_SHUTDOWN, HALT_REASON_SW_RESET);
+    } else {
+      platform_halt(HALT_ACTION_REBOOT, HALT_REASON_SW_RESET);
+    }
+  }
+
+  dpc_t dpc_{LIST_INITIAL_CLEARED_VALUE, &Halt, nullptr};
 };
 
 static ktl::unique_ptr<RootJobObserver> root_job_observer;
diff --git a/zircon/public/gn/test/zbi_test.gni b/zircon/public/gn/test/zbi_test.gni
index e4f9854..917882d 100644
--- a/zircon/public/gn/test/zbi_test.gni
+++ b/zircon/public/gn/test/zbi_test.gni
@@ -12,7 +12,7 @@
 # zero, but shutting down.  This string includes some random data that
 # shouldn't appear elsewhere, to avoid false-positive matches.
 zbi_test_success_string =
-    "*** ZBI test successful! MDd7/O65SuVZ23yGAaQG4CedYQGH9E1/58r73pSAVK0= ***"
+    "***ZBI-test-successful!-MDd7/O65SuVZ23yGAaQG4CedYQGH9E1/58r73pSAVK0=***"
 
 # Build a ZBI file to be run as a standalone ZBI test.
 #
diff --git a/zircon/system/core/bootsvc/main.cc b/zircon/system/core/bootsvc/main.cc
index a054abc..6b9d1ad 100644
--- a/zircon/system/core/bootsvc/main.cc
+++ b/zircon/system/core/bootsvc/main.cc
@@ -159,13 +159,13 @@
 //     - fuchsia.boot.RootJob, which provides root job handles
 //     - fuchsia.boot.RootResource, which provides root resource handles
 // - A loader that can load libraries from /boot, hosted by bootsvc
-// If set to true `shutdown` will cause the system to power off when the next
-// process exits. If set to false, the system will reboot insted of powering
-// off.
+//
+// If the next process terminates, bootsvc will quit.
 void LaunchNextProcess(fbl::RefPtr<bootsvc::BootfsService> bootfs,
                        fbl::RefPtr<bootsvc::SvcfsService> svcfs,
-                       fbl::RefPtr<bootsvc::BootfsLoaderService> loader_svc, bool shutdown,
-                       const zx::resource& root_rsrc, const zx::debuglog& log) {
+                       fbl::RefPtr<bootsvc::BootfsLoaderService> loader_svc,
+                       const zx::resource& root_rsrc, const zx::debuglog& log,
+                       async::Loop& loop) {
   const char* bootsvc_next = getenv("bootsvc.next");
   if (bootsvc_next == nullptr) {
     bootsvc_next =
@@ -255,23 +255,24 @@
   printf("bootsvc: Launched %s\n", next_program);
 
   // wait for termination and then reboot or power off the system
-  zx_signals_t observed = ZX_SIGNAL_NONE;
+  zx_signals_t observed;
   zx_status_t termination_result =
       proc_handle.wait_one(ZX_TASK_TERMINATED, zx::time::basic_time::infinite(), &observed);
   if (termination_result != ZX_OK) {
     printf("bootsvc: failure waiting for next process termination %i\n", termination_result);
-    return;
   }
-  if (shutdown) {
-    zx_system_powerctl(root_rsrc.get(), ZX_SYSTEM_POWERCTL_SHUTDOWN, NULL);
-  } else {
-    zx_system_powerctl(root_rsrc.get(), ZX_SYSTEM_POWERCTL_REBOOT, NULL);
-  }
+
+  // If the next process terminated, quit the main loop.
+  loop.Quit();
 }
 
 }  // namespace
 
 int main(int argc, char** argv) {
+  // Close the loader-service channel so the service can go away.
+  // We won't use it any more (no dlopen calls in this process).
+  zx_handle_close(dl_set_loader_service(ZX_HANDLE_INVALID));
+
   // NOTE: This will be the only source of zx::debuglog in the system.
   // Eventually, we will receive this through a startup handle from userboot.
   zx::debuglog log;
@@ -283,9 +284,9 @@
 
   printf("bootsvc: Starting...\n");
 
-  // Close the loader-service channel so the service can go away.
-  // We won't use it any more (no dlopen calls in this process).
-  zx_handle_close(dl_set_loader_service(ZX_HANDLE_INVALID));
+  status = zx::job::default_job()->set_critical(0, *zx::process::self());
+  ZX_ASSERT_MSG(status == ZX_OK, "Failed to set bootsvc as critical to root-job: %s\n",
+                zx_status_get_string(status));
 
   async::Loop loop(&kAsyncLoopConfigNoAttachToCurrentThread);
 
@@ -299,7 +300,7 @@
   ZX_ASSERT_MSG(status == ZX_OK, "BootfsService creation failed: %s\n",
                 zx_status_get_string(status));
   status = bootfs_svc->AddBootfs(std::move(bootfs_vmo));
-  ZX_ASSERT_MSG(status == ZX_OK, "bootfs add failed: %s\n", zx_status_get_string(status));
+  ZX_ASSERT_MSG(status == ZX_OK, "Bootfs add failed: %s\n", zx_status_get_string(status));
 
   // Process the ZBI boot image
   printf("bootsvc: Retrieving boot image...\n");
@@ -351,24 +352,16 @@
   ZX_ASSERT_MSG(status == ZX_OK, "BootfsLoaderService creation failed: %s\n",
                 zx_status_get_string(status));
 
-  const char* suspend_behavior = getenv("bootsvc.on_next_process_exit");
-  bool shutdown = false;
-  if (suspend_behavior != nullptr) {
-    if (strlen(suspend_behavior) == 8 && strcmp("shutdown", suspend_behavior) == 0) {
-      shutdown = true;
-    }
-  }
-
   // Launch the next process in the chain.  This must be in a thread, since
   // it may issue requests to the loader, which runs in the async loop that
   // starts running after this.
   printf("bootsvc: Launching next process...\n");
-  std::thread(LaunchNextProcess, bootfs_svc, svcfs_svc, loader_svc, shutdown,
-              std::cref(root_resource), std::cref(log))
+  std::thread(LaunchNextProcess, bootfs_svc, svcfs_svc, loader_svc,
+              std::cref(root_resource), std::cref(log), std::ref(loop))
       .detach();
 
   // Begin serving the bootfs fileystem and loader
   loop.Run();
-  printf("bootsvc: exiting\n");
+  printf("bootsvc: Exiting\n");
   return 0;
 }
diff --git a/zircon/system/core/bootsvc/test/BUILD.gn b/zircon/system/core/bootsvc/test/BUILD.gn
index 4b37a56..ad644f6e 100644
--- a/zircon/system/core/bootsvc/test/BUILD.gn
+++ b/zircon/system/core/bootsvc/test/BUILD.gn
@@ -8,6 +8,7 @@
   testonly = true
   deps = [
     ":bootsvc-integration-tests",
+    ":bootsvc-root-job-test",
     ":bootsvc-unit-test",
   ]
 }
@@ -117,6 +118,7 @@
   args = [
     "userboot=bin/bootsvc",
     "bootsvc.next=bin/bootsvc-integration-test,testargument",
+    "kernel.root-job.behavior=shutdown",
   ]
   deps = [
     # We need a zircon kernel to get off the ground at all.
@@ -138,3 +140,25 @@
   output_dir = root_build_dir
   output_name = "$target_name-$current_cpu"
 }
+
+zbi_test("bootsvc-root-job-test") {
+  args = [
+    "userboot=bin/bootsvc",
+    "bootsvc.next=test/core/zbi-child-test",
+    "kernel.root-job.behavior=shutdown",
+    "kernel.root-job.notice=$zbi_test_success_string",
+  ]
+  deps = [
+    # We need a zircon kernel to get off the ground at all.
+    "$zx/kernel",
+
+    # Include bootsvc itself, since that's what we're testing here.  Note
+    # that this uses the package() target for bootsvc, which comes with its
+    # own data_deps to exercise the `userboot.root` option to find bootsvc
+    # and its libraries inside a package directory in the BOOTFS.
+    "..",
+
+    # Include the integration test binary, which bootsvc will launch.
+    "$zx/system/utest/mexec:zbi-child",
+  ]
+}