src: add detailed embedder process initialization API

So far, process initialization has been a bit all over the place
in Node.js. `InitializeNodeWithArgs()` is our main public API
for this, but inclusion of items in it vs. `InitializeOncePerProcess()`
and `PlatformInit()` has been random at best. Likewise,
some pieces of initialization have been guarded by
`NODE_SHARED_MODE`, but also fairly randomly and without
any meaningful connection to shared library usage.

This leaves embedders in a position to cherry-pick some of
the initialization code into their own code to make their
application behave like typical Node.js applications to the
degree to which they desire it.

Electron takes an alternative route and makes direct use of
`InitializeOncePerProcess()` already while it is a private
API, with a `TODO` to add it to the public API in Node.js.

This commit addresses that `TODO`, and `TODO`s around the
`NODE_SHARED_MODE` usage. Specifically:

- `InitializeOncePerProcess()` and `TearDownOncePerProcess()`
  are added to the public API.
- The `flags` option of these functions are merged with the
  `flags` option for `InitializeNodeWithArgs()`, since they
  essentially share the same semantics.
- The return value of the function is made an abstract class,
  rather than a struct, for easier API/ABI stability.
- Initialization code from `main()` is brought into these
  functions (since that makes sense in general).
- Add a `TODO` for turning `InitializeNodeWithArgs()` into
  a small wrapper around `InitializeOncePerProcess()` and
  eventually removing it (at least one major release cycle
  each, presumably).
- Remove `NODE_SHARED_MODE` guards and replace them with
  runtime options.

PR-URL: https://github.com/nodejs/node/pull/44121
Reviewed-By: Joyee Cheung <joyeec9h3@gmail.com>
Reviewed-By: Michael Dawson <midawson@redhat.com>
diff --git a/doc/api/embedding.md b/doc/api/embedding.md
index d07bec7..9f831b3 100644
--- a/doc/api/embedding.md
+++ b/doc/api/embedding.md
@@ -37,15 +37,18 @@
 int main(int argc, char** argv) {
   argv = uv_setup_args(argc, argv);
   std::vector<std::string> args(argv, argv + argc);
-  std::vector<std::string> exec_args;
-  std::vector<std::string> errors;
   // Parse Node.js CLI options, and print any errors that have occurred while
   // trying to parse them.
-  int exit_code = node::InitializeNodeWithArgs(&args, &exec_args, &errors);
-  for (const std::string& error : errors)
+  std::unique_ptr<node::InitializationResult> result =
+      node::InitializeOncePerProcess(args, {
+        node::ProcessInitializationFlags::kNoInitializeV8,
+        node::ProcessInitializationFlags::kNoInitializeNodeV8Platform
+      });
+
+  for (const std::string& error : result->errors())
     fprintf(stderr, "%s: %s\n", args[0].c_str(), error.c_str());
-  if (exit_code != 0) {
-    return exit_code;
+  if (result->early_return() != 0) {
+    return result->exit_code();
   }
 
   // Create a v8::Platform instance. `MultiIsolatePlatform::Create()` is a way
@@ -58,10 +61,13 @@
   V8::Initialize();
 
   // See below for the contents of this function.
-  int ret = RunNodeInstance(platform.get(), args, exec_args);
+  int ret = RunNodeInstance(
+      platform.get(), result->args(), result->exec_args());
 
   V8::Dispose();
   V8::DisposePlatform();
+
+  node::TearDownOncePerProcess();
   return ret;
 }
 ```
diff --git a/src/node.cc b/src/node.cc
index f1cd377..a40ac14 100644
--- a/src/node.cc
+++ b/src/node.cc
@@ -567,37 +567,8 @@
 } stdio[1 + STDERR_FILENO];
 #endif  // __POSIX__
 
-
-inline void PlatformInit() {
+void ResetSignalHandlers() {
 #ifdef __POSIX__
-#if HAVE_INSPECTOR
-  sigset_t sigmask;
-  sigemptyset(&sigmask);
-  sigaddset(&sigmask, SIGUSR1);
-  const int err = pthread_sigmask(SIG_SETMASK, &sigmask, nullptr);
-#endif  // HAVE_INSPECTOR
-
-  // Make sure file descriptors 0-2 are valid before we start logging anything.
-  for (auto& s : stdio) {
-    const int fd = &s - stdio;
-    if (fstat(fd, &s.stat) == 0)
-      continue;
-    // Anything but EBADF means something is seriously wrong.  We don't
-    // have to special-case EINTR, fstat() is not interruptible.
-    if (errno != EBADF)
-      ABORT();
-    if (fd != open("/dev/null", O_RDWR))
-      ABORT();
-    if (fstat(fd, &s.stat) != 0)
-      ABORT();
-  }
-
-#if HAVE_INSPECTOR
-  CHECK_EQ(err, 0);
-#endif  // HAVE_INSPECTOR
-
-  // TODO(addaleax): NODE_SHARED_MODE does not really make sense here.
-#ifndef NODE_SHARED_MODE
   // Restore signal dispositions, the parent process may have changed them.
   struct sigaction act;
   memset(&act, 0, sizeof(act));
@@ -611,94 +582,150 @@
     act.sa_handler = (nr == SIGPIPE || nr == SIGXFSZ) ? SIG_IGN : SIG_DFL;
     CHECK_EQ(0, sigaction(nr, &act, nullptr));
   }
-#endif  // !NODE_SHARED_MODE
+#endif  // __POSIX__
+}
 
-  // Record the state of the stdio file descriptors so we can restore it
-  // on exit.  Needs to happen before installing signal handlers because
-  // they make use of that information.
-  for (auto& s : stdio) {
-    const int fd = &s - stdio;
-    int err;
+static std::atomic<uint64_t> init_process_flags = 0;
 
-    do
-      s.flags = fcntl(fd, F_GETFL);
-    while (s.flags == -1 && errno == EINTR);  // NOLINT
-    CHECK_NE(s.flags, -1);
+static void PlatformInit(ProcessInitializationFlags::Flags flags) {
+  // init_process_flags is accessed in ResetStdio(),
+  // which can be called from signal handlers.
+  CHECK(init_process_flags.is_lock_free());
+  init_process_flags.store(flags);
 
-    if (uv_guess_handle(fd) != UV_TTY) continue;
-    s.isatty = true;
-
-    do
-      err = tcgetattr(fd, &s.termios);
-    while (err == -1 && errno == EINTR);  // NOLINT
-    CHECK_EQ(err, 0);
+  if (!(flags & ProcessInitializationFlags::kNoStdioInitialization)) {
+    atexit(ResetStdio);
   }
 
-  RegisterSignalHandler(SIGINT, SignalExit, true);
-  RegisterSignalHandler(SIGTERM, SignalExit, true);
+#ifdef __POSIX__
+  if (!(flags & ProcessInitializationFlags::kNoStdioInitialization)) {
+    // Disable stdio buffering, it interacts poorly with printf()
+    // calls elsewhere in the program (e.g., any logging from V8.)
+    setvbuf(stdout, nullptr, _IONBF, 0);
+    setvbuf(stderr, nullptr, _IONBF, 0);
+
+    // Make sure file descriptors 0-2 are valid before we start logging
+    // anything.
+    for (auto& s : stdio) {
+      const int fd = &s - stdio;
+      if (fstat(fd, &s.stat) == 0) continue;
+      // Anything but EBADF means something is seriously wrong.  We don't
+      // have to special-case EINTR, fstat() is not interruptible.
+      if (errno != EBADF) ABORT();
+      if (fd != open("/dev/null", O_RDWR)) ABORT();
+      if (fstat(fd, &s.stat) != 0) ABORT();
+    }
+  }
+
+  if (!(flags & ProcessInitializationFlags::kNoDefaultSignalHandling)) {
+#if HAVE_INSPECTOR
+    sigset_t sigmask;
+    sigemptyset(&sigmask);
+    sigaddset(&sigmask, SIGUSR1);
+    const int err = pthread_sigmask(SIG_SETMASK, &sigmask, nullptr);
+    CHECK_EQ(err, 0);
+#endif  // HAVE_INSPECTOR
+
+    ResetSignalHandlers();
+  }
+
+  if (!(flags & ProcessInitializationFlags::kNoStdioInitialization)) {
+    // Record the state of the stdio file descriptors so we can restore it
+    // on exit.  Needs to happen before installing signal handlers because
+    // they make use of that information.
+    for (auto& s : stdio) {
+      const int fd = &s - stdio;
+      int err;
+
+      do {
+        s.flags = fcntl(fd, F_GETFL);
+      } while (s.flags == -1 && errno == EINTR);  // NOLINT
+      CHECK_NE(s.flags, -1);
+
+      if (uv_guess_handle(fd) != UV_TTY) continue;
+      s.isatty = true;
+
+      do {
+        err = tcgetattr(fd, &s.termios);
+      } while (err == -1 && errno == EINTR);  // NOLINT
+      CHECK_EQ(err, 0);
+    }
+  }
+
+  if (!(flags & ProcessInitializationFlags::kNoDefaultSignalHandling)) {
+    RegisterSignalHandler(SIGINT, SignalExit, true);
+    RegisterSignalHandler(SIGTERM, SignalExit, true);
 
 #if NODE_USE_V8_WASM_TRAP_HANDLER
 #if defined(_WIN32)
-  {
-    constexpr ULONG first = TRUE;
-    per_process::old_vectored_exception_handler =
-        AddVectoredExceptionHandler(first, TrapWebAssemblyOrContinue);
-  }
-#else
-  // Tell V8 to disable emitting WebAssembly
-  // memory bounds checks. This means that we have
-  // to catch the SIGSEGV in TrapWebAssemblyOrContinue
-  // and pass the signal context to V8.
-  {
-    struct sigaction sa;
-    memset(&sa, 0, sizeof(sa));
-    sa.sa_sigaction = TrapWebAssemblyOrContinue;
-    sa.sa_flags = SA_SIGINFO;
-    CHECK_EQ(sigaction(SIGSEGV, &sa, nullptr), 0);
-  }
-#endif  // defined(_WIN32)
-  V8::EnableWebAssemblyTrapHandler(false);
-#endif  // NODE_USE_V8_WASM_TRAP_HANDLER
-
-  // Raise the open file descriptor limit.
-  struct rlimit lim;
-  if (getrlimit(RLIMIT_NOFILE, &lim) == 0 && lim.rlim_cur != lim.rlim_max) {
-    // Do a binary search for the limit.
-    rlim_t min = lim.rlim_cur;
-    rlim_t max = 1 << 20;
-    // But if there's a defined upper bound, don't search, just set it.
-    if (lim.rlim_max != RLIM_INFINITY) {
-      min = lim.rlim_max;
-      max = lim.rlim_max;
+    {
+      constexpr ULONG first = TRUE;
+      per_process::old_vectored_exception_handler =
+          AddVectoredExceptionHandler(first, TrapWebAssemblyOrContinue);
     }
-    do {
-      lim.rlim_cur = min + (max - min) / 2;
-      if (setrlimit(RLIMIT_NOFILE, &lim)) {
-        max = lim.rlim_cur;
-      } else {
-        min = lim.rlim_cur;
+#else
+    // Tell V8 to disable emitting WebAssembly
+    // memory bounds checks. This means that we have
+    // to catch the SIGSEGV in TrapWebAssemblyOrContinue
+    // and pass the signal context to V8.
+    {
+      struct sigaction sa;
+      memset(&sa, 0, sizeof(sa));
+      sa.sa_sigaction = TrapWebAssemblyOrContinue;
+      sa.sa_flags = SA_SIGINFO;
+      CHECK_EQ(sigaction(SIGSEGV, &sa, nullptr), 0);
+    }
+#endif  // defined(_WIN32)
+    V8::EnableWebAssemblyTrapHandler(false);
+#endif  // NODE_USE_V8_WASM_TRAP_HANDLER
+  }
+
+  if (!(flags & ProcessInitializationFlags::kNoAdjustResourceLimits)) {
+    // Raise the open file descriptor limit.
+    struct rlimit lim;
+    if (getrlimit(RLIMIT_NOFILE, &lim) == 0 && lim.rlim_cur != lim.rlim_max) {
+      // Do a binary search for the limit.
+      rlim_t min = lim.rlim_cur;
+      rlim_t max = 1 << 20;
+      // But if there's a defined upper bound, don't search, just set it.
+      if (lim.rlim_max != RLIM_INFINITY) {
+        min = lim.rlim_max;
+        max = lim.rlim_max;
       }
-    } while (min + 1 < max);
+      do {
+        lim.rlim_cur = min + (max - min) / 2;
+        if (setrlimit(RLIMIT_NOFILE, &lim)) {
+          max = lim.rlim_cur;
+        } else {
+          min = lim.rlim_cur;
+        }
+      } while (min + 1 < max);
+    }
   }
 #endif  // __POSIX__
 #ifdef _WIN32
-  for (int fd = 0; fd <= 2; ++fd) {
-    auto handle = reinterpret_cast<HANDLE>(_get_osfhandle(fd));
-    if (handle == INVALID_HANDLE_VALUE ||
-        GetFileType(handle) == FILE_TYPE_UNKNOWN) {
-      // Ignore _close result. If it fails or not depends on used Windows
-      // version. We will just check _open result.
-      _close(fd);
-      if (fd != _open("nul", _O_RDWR))
-        ABORT();
+  if (!(flags & ProcessInitializationFlags::kNoStdioInitialization)) {
+    for (int fd = 0; fd <= 2; ++fd) {
+      auto handle = reinterpret_cast<HANDLE>(_get_osfhandle(fd));
+      if (handle == INVALID_HANDLE_VALUE ||
+          GetFileType(handle) == FILE_TYPE_UNKNOWN) {
+        // Ignore _close result. If it fails or not depends on used Windows
+        // version. We will just check _open result.
+        _close(fd);
+        if (fd != _open("nul", _O_RDWR)) ABORT();
+      }
     }
   }
 #endif  // _WIN32
 }
 
-
 // Safe to call more than once and from signal handlers.
 void ResetStdio() {
+  if (init_process_flags.load() &
+      ProcessInitializationFlags::kNoStdioInitialization) {
+    return;
+  }
+
   uv_tty_reset_mode();
 #ifdef __POSIX__
   for (auto& s : stdio) {
@@ -834,10 +861,12 @@
 
 static std::atomic_bool init_called{false};
 
+// TODO(addaleax): Turn this into a wrapper around InitializeOncePerProcess()
+// (with the corresponding additional flags set), then eventually remove this.
 int InitializeNodeWithArgs(std::vector<std::string>* argv,
                            std::vector<std::string>* exec_argv,
                            std::vector<std::string>* errors,
-                           ProcessFlags::Flags flags) {
+                           ProcessInitializationFlags::Flags flags) {
   // Make sure InitializeNodeWithArgs() is called only once.
   CHECK(!init_called.exchange(true));
 
@@ -848,8 +877,10 @@
   binding::RegisterBuiltinModules();
 
   // Make inherited handles noninheritable.
-  if (!(flags & ProcessFlags::kEnableStdioInheritance))
+  if (!(flags & ProcessInitializationFlags::kEnableStdioInheritance) &&
+      !(flags & ProcessInitializationFlags::kNoStdioInitialization)) {
     uv_disable_stdio_inheritance();
+  }
 
   // Cache the original command line to be
   // used in diagnostic reports.
@@ -865,7 +896,7 @@
   HandleEnvOptions(per_process::cli_options->per_isolate->per_env);
 
 #if !defined(NODE_WITHOUT_NODE_OPTIONS)
-  if (!(flags & ProcessFlags::kDisableNodeOptionsEnv)) {
+  if (!(flags & ProcessInitializationFlags::kDisableNodeOptionsEnv)) {
     std::string node_options;
 
     if (credentials::SafeGetenv("NODE_OPTIONS", &node_options)) {
@@ -886,7 +917,7 @@
   }
 #endif
 
-  if (!(flags & ProcessFlags::kDisableCLIOptions)) {
+  if (!(flags & ProcessInitializationFlags::kDisableCLIOptions)) {
     const int exit_code = ProcessGlobalArgs(argv,
                                             exec_argv,
                                             errors,
@@ -899,7 +930,7 @@
     uv_set_process_title(per_process::cli_options->title.c_str());
 
 #if defined(NODE_HAVE_I18N_SUPPORT)
-  if (!(flags & ProcessFlags::kNoICU)) {
+  if (!(flags & ProcessInitializationFlags::kNoICU)) {
     // If the parameter isn't given, use the env variable.
     if (per_process::cli_options->icu_data_dir.empty())
       credentials::SafeGetenv("NODE_ICU_DATA",
@@ -949,82 +980,78 @@
   return 0;
 }
 
-InitializationResult InitializeOncePerProcess(int argc, char** argv) {
-  return InitializeOncePerProcess(argc, argv, kDefaultInitialization);
-}
+std::unique_ptr<InitializationResult> InitializeOncePerProcess(
+    const std::vector<std::string>& args,
+    ProcessInitializationFlags::Flags flags) {
+  auto result = std::make_unique<InitializationResultImpl>();
+  result->args_ = args;
 
-InitializationResult InitializeOncePerProcess(
-  int argc,
-  char** argv,
-  InitializationSettingsFlags flags,
-  ProcessFlags::Flags process_flags) {
-  uint64_t init_flags = flags;
-  if (init_flags & kDefaultInitialization) {
-    init_flags = init_flags | kInitializeV8 | kInitOpenSSL | kRunPlatformInit;
+  if (!(flags & ProcessInitializationFlags::kNoParseGlobalDebugVariables)) {
+    // Initialized the enabled list for Debug() calls with system
+    // environment variables.
+    per_process::enabled_debug_list.Parse();
   }
 
-  // Initialized the enabled list for Debug() calls with system
-  // environment variables.
-  per_process::enabled_debug_list.Parse();
-
-  atexit(ResetStdio);
-
-  if (init_flags & kRunPlatformInit)
-    PlatformInit();
-
-  CHECK_GT(argc, 0);
-
-  // Hack around with the argv pointer. Used for process.title = "blah".
-  argv = uv_setup_args(argc, argv);
-
-  InitializationResult result;
-  result.args = std::vector<std::string>(argv, argv + argc);
-  std::vector<std::string> errors;
+  PlatformInit(flags);
 
   // This needs to run *before* V8::Initialize().
   {
-    result.exit_code = InitializeNodeWithArgs(
-        &(result.args), &(result.exec_args), &errors, process_flags);
-    for (const std::string& error : errors)
-      fprintf(stderr, "%s: %s\n", result.args.at(0).c_str(), error.c_str());
-    if (result.exit_code != 0) {
-      result.early_return = true;
+    result->exit_code_ = InitializeNodeWithArgs(
+        &result->args_, &result->exec_args_, &result->errors_, flags);
+    if (result->exit_code() != 0) {
+      result->early_return_ = true;
       return result;
     }
   }
 
-  if (per_process::cli_options->use_largepages == "on" ||
-      per_process::cli_options->use_largepages == "silent") {
-    int result = node::MapStaticCodeToLargePages();
-    if (per_process::cli_options->use_largepages == "on" && result != 0) {
-      fprintf(stderr, "%s\n", node::LargePagesError(result));
+  if (!(flags & ProcessInitializationFlags::kNoUseLargePages) &&
+      (per_process::cli_options->use_largepages == "on" ||
+       per_process::cli_options->use_largepages == "silent")) {
+    int lp_result = node::MapStaticCodeToLargePages();
+    if (per_process::cli_options->use_largepages == "on" && lp_result != 0) {
+      result->errors_.emplace_back(node::LargePagesError(lp_result));
     }
   }
 
-  if (per_process::cli_options->print_version) {
-    printf("%s\n", NODE_VERSION);
-    result.exit_code = 0;
-    result.early_return = true;
-    return result;
+  if (!(flags & ProcessInitializationFlags::kNoPrintHelpOrVersionOutput)) {
+    if (per_process::cli_options->print_version) {
+      printf("%s\n", NODE_VERSION);
+      result->exit_code_ = 0;
+      result->early_return_ = true;
+      return result;
+    }
+
+    if (per_process::cli_options->print_bash_completion) {
+      std::string completion = options_parser::GetBashCompletion();
+      printf("%s\n", completion.c_str());
+      result->exit_code_ = 0;
+      result->early_return_ = true;
+      return result;
+    }
+
+    if (per_process::cli_options->print_v8_help) {
+      V8::SetFlagsFromString("--help", static_cast<size_t>(6));
+      result->exit_code_ = 0;
+      result->early_return_ = true;
+      return result;
+    }
   }
 
-  if (per_process::cli_options->print_bash_completion) {
-    std::string completion = options_parser::GetBashCompletion();
-    printf("%s\n", completion.c_str());
-    result.exit_code = 0;
-    result.early_return = true;
-    return result;
-  }
-
-  if (per_process::cli_options->print_v8_help) {
-    V8::SetFlagsFromString("--help", static_cast<size_t>(6));
-    result.exit_code = 0;
-    result.early_return = true;
-    return result;
-  }
-
-  if (init_flags & kInitOpenSSL) {
+  if (!(flags & ProcessInitializationFlags::kNoInitOpenSSL)) {
 #if HAVE_OPENSSL && !defined(OPENSSL_IS_BORINGSSL)
+    auto GetOpenSSLErrorString = []() -> std::string {
+      std::string ret;
+      ERR_print_errors_cb(
+          [](const char* str, size_t len, void* opaque) {
+            std::string* ret = static_cast<std::string*>(opaque);
+            ret->append(str, len);
+            ret->append("\n");
+            return 0;
+          },
+          static_cast<void*>(&ret));
+      return ret;
+    };
+
     {
       std::string extra_ca_certs;
       if (credentials::SafeGetenv("NODE_EXTRA_CA_CERTS", &extra_ca_certs))
@@ -1080,10 +1107,12 @@
     OPENSSL_INIT_free(settings);
 
     if (ERR_peek_error() != 0) {
-      result.exit_code = ERR_GET_REASON(ERR_peek_error());
-      result.early_return = true;
-      fprintf(stderr, "OpenSSL configuration error:\n");
-      ERR_print_errors_fp(stderr);
+      // XXX: ERR_GET_REASON does not return something that is
+      // useful as an exit code at all.
+      result->exit_code_ = ERR_GET_REASON(ERR_peek_error());
+      result->early_return_ = true;
+      result->errors_.emplace_back("OpenSSL configuration error:\n" +
+                                   GetOpenSSLErrorString());
       return result;
     }
 #else  // OPENSSL_VERSION_MAJOR < 3
@@ -1091,22 +1120,30 @@
       OPENSSL_init();
     }
 #endif
-  if (!crypto::ProcessFipsOptions()) {
-      result.exit_code = ERR_GET_REASON(ERR_peek_error());
-      result.early_return = true;
-      fprintf(stderr, "OpenSSL error when trying to enable FIPS:\n");
-      ERR_print_errors_fp(stderr);
+    if (!crypto::ProcessFipsOptions()) {
+      // XXX: ERR_GET_REASON does not return something that is
+      // useful as an exit code at all.
+      result->exit_code_ = ERR_GET_REASON(ERR_peek_error());
+      result->early_return_ = true;
+      result->errors_.emplace_back(
+          "OpenSSL error when trying to enable FIPS:\n" +
+          GetOpenSSLErrorString());
       return result;
+    }
+
+    // V8 on Windows doesn't have a good source of entropy. Seed it from
+    // OpenSSL's pool.
+    V8::SetEntropySource(crypto::EntropySource);
+#endif  // HAVE_OPENSSL && !defined(OPENSSL_IS_BORINGSSL)
   }
 
-  // V8 on Windows doesn't have a good source of entropy. Seed it from
-  // OpenSSL's pool.
-  V8::SetEntropySource(crypto::EntropySource);
-#endif  // HAVE_OPENSSL && !defined(OPENSSL_IS_BORINGSSL)
-}
-  per_process::v8_platform.Initialize(
-      static_cast<int>(per_process::cli_options->v8_thread_pool_size));
-  if (init_flags & kInitializeV8) {
+  if (!(flags & ProcessInitializationFlags::kNoInitializeNodeV8Platform)) {
+    per_process::v8_platform.Initialize(
+        static_cast<int>(per_process::cli_options->v8_thread_pool_size));
+    result->platform_ = per_process::v8_platform.Platform();
+  }
+
+  if (!(flags & ProcessInitializationFlags::kNoInitializeV8)) {
     V8::Initialize();
   }
 
@@ -1117,30 +1154,47 @@
 }
 
 void TearDownOncePerProcess() {
+  const uint64_t flags = init_process_flags.load();
+  ResetStdio();
+  if (!(flags & ProcessInitializationFlags::kNoDefaultSignalHandling)) {
+    ResetSignalHandlers();
+  }
+
   per_process::v8_initialized = false;
-  V8::Dispose();
+  if (!(flags & ProcessInitializationFlags::kNoInitializeV8)) {
+    V8::Dispose();
+  }
 
 #if NODE_USE_V8_WASM_TRAP_HANDLER && defined(_WIN32)
-  RemoveVectoredExceptionHandler(per_process::old_vectored_exception_handler);
+  if (!(flags & ProcessInitializationFlags::kNoDefaultSignalHandling)) {
+    RemoveVectoredExceptionHandler(per_process::old_vectored_exception_handler);
+  }
 #endif
 
-  // uv_run cannot be called from the time before the beforeExit callback
-  // runs until the program exits unless the event loop has any referenced
-  // handles after beforeExit terminates. This prevents unrefed timers
-  // that happen to terminate during shutdown from being run unsafely.
-  // Since uv_run cannot be called, uv_async handles held by the platform
-  // will never be fully cleaned up.
-  per_process::v8_platform.Dispose();
+  if (!(flags & ProcessInitializationFlags::kNoInitializeNodeV8Platform)) {
+    V8::DisposePlatform();
+    // uv_run cannot be called from the time before the beforeExit callback
+    // runs until the program exits unless the event loop has any referenced
+    // handles after beforeExit terminates. This prevents unrefed timers
+    // that happen to terminate during shutdown from being run unsafely.
+    // Since uv_run cannot be called, uv_async handles held by the platform
+    // will never be fully cleaned up.
+    per_process::v8_platform.Dispose();
+  }
 }
 
+InitializationResult::~InitializationResult() {}
+InitializationResultImpl::~InitializationResultImpl() {}
+
 int GenerateAndWriteSnapshotData(const SnapshotData** snapshot_data_ptr,
-                                 InitializationResult* result) {
+                                 const InitializationResult* result) {
+  int exit_code = result->exit_code();
   // nullptr indicates there's no snapshot data.
   DCHECK_NULL(*snapshot_data_ptr);
 
   // node:embedded_snapshot_main indicates that we are using the
   // embedded snapshot and we are not supposed to clean it up.
-  if (result->args[1] == "node:embedded_snapshot_main") {
+  if (result->args()[1] == "node:embedded_snapshot_main") {
     *snapshot_data_ptr = SnapshotBuilder::GetEmbeddedSnapshotData();
     if (*snapshot_data_ptr == nullptr) {
       // The Node.js binary is built without embedded snapshot
@@ -1148,19 +1202,19 @@
               "node:embedded_snapshot_main was specified as snapshot "
               "entry point but Node.js was built without embedded "
               "snapshot.\n");
-      result->exit_code = 1;
-      return result->exit_code;
+      exit_code = 1;
+      return exit_code;
     }
   } else {
     // Otherwise, load and run the specified main script.
     std::unique_ptr<SnapshotData> generated_data =
         std::make_unique<SnapshotData>();
-    result->exit_code = node::SnapshotBuilder::Generate(
-        generated_data.get(), result->args, result->exec_args);
-    if (result->exit_code == 0) {
+    exit_code = node::SnapshotBuilder::Generate(
+        generated_data.get(), result->args(), result->exec_args());
+    if (exit_code == 0) {
       *snapshot_data_ptr = generated_data.release();
     } else {
-      return result->exit_code;
+      return exit_code;
     }
   }
 
@@ -1181,13 +1235,14 @@
     fprintf(stderr,
             "Cannot open %s for writing a snapshot.\n",
             snapshot_blob_path.c_str());
-    result->exit_code = 1;
+    exit_code = 1;
   }
-  return result->exit_code;
+  return exit_code;
 }
 
 int LoadSnapshotDataAndRun(const SnapshotData** snapshot_data_ptr,
-                           InitializationResult* result) {
+                           const InitializationResult* result) {
+  int exit_code = result->exit_code();
   // nullptr indicates there's no snapshot data.
   DCHECK_NULL(*snapshot_data_ptr);
   // --snapshot-blob indicates that we are reading a customized snapshot.
@@ -1196,8 +1251,8 @@
     FILE* fp = fopen(filename.c_str(), "rb");
     if (fp == nullptr) {
       fprintf(stderr, "Cannot open %s", filename.c_str());
-      result->exit_code = 1;
-      return result->exit_code;
+      exit_code = 1;
+      return exit_code;
     }
     std::unique_ptr<SnapshotData> read_data = std::make_unique<SnapshotData>();
     SnapshotData::FromBlob(read_data.get(), fp);
@@ -1215,19 +1270,28 @@
   NodeMainInstance main_instance(*snapshot_data_ptr,
                                  uv_default_loop(),
                                  per_process::v8_platform.Platform(),
-                                 result->args,
-                                 result->exec_args);
-  result->exit_code = main_instance.Run();
-  return result->exit_code;
+                                 result->args(),
+                                 result->exec_args());
+  exit_code = main_instance.Run();
+  return exit_code;
 }
 
 int Start(int argc, char** argv) {
-  InitializationResult result = InitializeOncePerProcess(argc, argv);
-  if (result.early_return) {
-    return result.exit_code;
+  CHECK_GT(argc, 0);
+
+  // Hack around with the argv pointer. Used for process.title = "blah".
+  argv = uv_setup_args(argc, argv);
+
+  std::unique_ptr<InitializationResult> result =
+      InitializeOncePerProcess(std::vector<std::string>(argv, argv + argc));
+  for (const std::string& error : result->errors()) {
+    FPrintF(stderr, "%s: %s\n", result->args().at(0), error);
+  }
+  if (result->early_return()) {
+    return result->exit_code();
   }
 
-  DCHECK_EQ(result.exit_code, 0);
+  DCHECK_EQ(result->exit_code(), 0);
   const SnapshotData* snapshot_data = nullptr;
 
   auto cleanup_process = OnScopeLeave([&]() {
@@ -1243,17 +1307,17 @@
 
   // --build-snapshot indicates that we are in snapshot building mode.
   if (per_process::cli_options->build_snapshot) {
-    if (result.args.size() < 2) {
+    if (result->args().size() < 2) {
       fprintf(stderr,
               "--build-snapshot must be used with an entry point script.\n"
               "Usage: node --build-snapshot /path/to/entry.js\n");
       return 9;
     }
-    return GenerateAndWriteSnapshotData(&snapshot_data, &result);
+    return GenerateAndWriteSnapshotData(&snapshot_data, result.get());
   }
 
   // Without --build-snapshot, we are in snapshot loading mode.
-  return LoadSnapshotDataAndRun(&snapshot_data, &result);
+  return LoadSnapshotDataAndRun(&snapshot_data, result.get());
 }
 
 int Stop(Environment* env) {
diff --git a/src/node.h b/src/node.h
index 3191be7..f8afff5 100644
--- a/src/node.h
+++ b/src/node.h
@@ -223,11 +223,14 @@
 
 class IsolateData;
 class Environment;
+class MultiIsolatePlatform;
+class InitializationResultImpl;
 
 namespace ProcessFlags {
 enum Flags : uint64_t {
   kNoFlags = 0,
   // Enable stdio inheritance, which is disabled by default.
+  // This flag is also implied by kNoStdioInitialization.
   kEnableStdioInheritance = 1 << 0,
   // Disable reading the NODE_OPTIONS environment variable.
   kDisableNodeOptionsEnv = 1 << 1,
@@ -235,8 +238,67 @@
   kDisableCLIOptions = 1 << 2,
   // Do not initialize ICU.
   kNoICU = 1 << 3,
+  // Do not modify stdio file descriptor or TTY state.
+  kNoStdioInitialization = 1 << 4,
+  // Do not register Node.js-specific signal handlers
+  // and reset other signal handlers to default state.
+  kNoDefaultSignalHandling = 1 << 5,
+  // Do not perform V8 initialization.
+  kNoInitializeV8 = 1 << 6,
+  // Do not initialize a default Node.js-provided V8 platform instance.
+  kNoInitializeNodeV8Platform = 1 << 7,
+  // Do not initialize OpenSSL config.
+  kNoInitOpenSSL = 1 << 8,
+  // Do not initialize Node.js debugging based on environment variables.
+  kNoParseGlobalDebugVariables = 1 << 9,
+  // Do not adjust OS resource limits for this process.
+  kNoAdjustResourceLimits = 1 << 10,
+  // Do not map code segments into large pages for this process.
+  kNoUseLargePages = 1 << 11,
+  // Skip printing output for --help, --version, --v8-options.
+  kNoPrintHelpOrVersionOutput = 1 << 12,
+
+  // Emulate the behavior of InitializeNodeWithArgs() when passing
+  // a flags argument to the InitializeOncePerProcess() replacement
+  // function.
+  kLegacyInitializeNodeWithArgsBehavior =
+      kNoStdioInitialization | kNoDefaultSignalHandling | kNoInitializeV8 |
+      kNoInitializeNodeV8Platform | kNoInitOpenSSL |
+      kNoParseGlobalDebugVariables | kNoAdjustResourceLimits |
+      kNoUseLargePages | kNoPrintHelpOrVersionOutput,
 };
 }  // namespace ProcessFlags
+// TODO(addaleax): Make this the canonical name, as it is more descriptive.
+namespace ProcessInitializationFlags = ProcessFlags;
+
+class NODE_EXTERN InitializationResult {
+ public:
+  virtual ~InitializationResult();
+
+  // Returns a suggested process exit code.
+  virtual int exit_code() const = 0;
+
+  // Returns 'true' if initialization was aborted early due to errors.
+  virtual bool early_return() const = 0;
+
+  // Returns the parsed list of non-Node.js arguments.
+  virtual const std::vector<std::string>& args() const = 0;
+
+  // Returns the parsed list of Node.js arguments.
+  virtual const std::vector<std::string>& exec_args() const = 0;
+
+  // Returns an array of errors. Note that these may be warnings
+  // whose existence does not imply a non-zero exit code.
+  virtual const std::vector<std::string>& errors() const = 0;
+
+  // If kNoInitializeNodeV8Platform was not specified, the global Node.js
+  // platform instance.
+  virtual MultiIsolatePlatform* platform() const = 0;
+
+ private:
+  InitializationResult() = default;
+  friend class InitializationResultImpl;
+};
 
 // TODO(addaleax): Officially deprecate this and replace it with something
 // better suited for a public embedder API.
@@ -250,11 +312,40 @@
 // from argv, fill exec_argv, and possibly add errors resulting from parsing
 // the arguments to `errors`. The return value is a suggested exit code for the
 // program; If it is 0, then initializing Node.js succeeded.
-NODE_EXTERN int InitializeNodeWithArgs(
-    std::vector<std::string>* argv,
-    std::vector<std::string>* exec_argv,
-    std::vector<std::string>* errors,
-    ProcessFlags::Flags flags = ProcessFlags::kNoFlags);
+// This runs a subset of the initialization performed by
+// InitializeOncePerProcess(), which supersedes this function.
+// The subset is roughly equivalent to the one given by
+// `ProcessInitializationFlags::kLegacyInitializeNodeWithArgsBehavior`.
+NODE_DEPRECATED("Use InitializeOncePerProcess() instead",
+                NODE_EXTERN int InitializeNodeWithArgs(
+                    std::vector<std::string>* argv,
+                    std::vector<std::string>* exec_argv,
+                    std::vector<std::string>* errors,
+                    ProcessInitializationFlags::Flags flags =
+                        ProcessInitializationFlags::kNoFlags));
+
+// Set up per-process state needed to run Node.js. This will consume arguments
+// from args, and return information about the initialization success,
+// including the arguments split into argv/exec_argv, a list of potential
+// errors encountered during initialization, and a potential suggested
+// exit code.
+NODE_EXTERN std::unique_ptr<InitializationResult> InitializeOncePerProcess(
+    const std::vector<std::string>& args,
+    ProcessInitializationFlags::Flags flags =
+        ProcessInitializationFlags::kNoFlags);
+// Undoes the initialization performed by InitializeOncePerProcess(),
+// where cleanup is necessary.
+NODE_EXTERN void TearDownOncePerProcess();
+// Convenience overload for specifying multiple flags without having
+// to worry about casts.
+inline std::unique_ptr<InitializationResult> InitializeOncePerProcess(
+    const std::vector<std::string>& args,
+    std::initializer_list<ProcessInitializationFlags::Flags> list) {
+  uint64_t flags_accum = ProcessInitializationFlags::kNoFlags;
+  for (const auto flag : list) flags_accum |= static_cast<uint64_t>(flag);
+  return InitializeOncePerProcess(
+      args, static_cast<ProcessInitializationFlags::Flags>(flags_accum));
+}
 
 enum OptionEnvvarSettings {
   kAllowedInEnvironment,
diff --git a/src/node_credentials.cc b/src/node_credentials.cc
index 458fa13..ccc7749 100644
--- a/src/node_credentials.cc
+++ b/src/node_credentials.cc
@@ -13,6 +13,7 @@
 #endif
 #ifdef __linux__
 #include <linux/capability.h>
+#include <sys/auxv.h>
 #include <sys/syscall.h>
 #endif  // __linux__
 
@@ -31,9 +32,18 @@
 using v8::Uint32;
 using v8::Value;
 
-namespace per_process {
-bool linux_at_secure = false;
-}  // namespace per_process
+bool linux_at_secure() {
+  // This could reasonably be a static variable, but this way
+  // we can guarantee that this function is always usable
+  // and returns the correct value,  e.g. even in static
+  // initialization code in other files.
+#ifdef __linux__
+  static const bool value = getauxval(AT_SECURE);
+  return value;
+#else
+  return false;
+#endif
+}
 
 namespace credentials {
 
@@ -70,11 +80,10 @@
                 v8::Isolate* isolate) {
 #if !defined(__CloudABI__) && !defined(_WIN32)
 #if defined(__linux__)
-  if ((!HasOnly(CAP_NET_BIND_SERVICE) && per_process::linux_at_secure) ||
+  if ((!HasOnly(CAP_NET_BIND_SERVICE) && linux_at_secure()) ||
       getuid() != geteuid() || getgid() != getegid())
 #else
-  if (per_process::linux_at_secure || getuid() != geteuid() ||
-      getgid() != getegid())
+  if (linux_at_secure() || getuid() != geteuid() || getgid() != getegid())
 #endif
     goto fail;
 #endif
diff --git a/src/node_internals.h b/src/node_internals.h
index 890d77f..3ec50cf 100644
--- a/src/node_internals.h
+++ b/src/node_internals.h
@@ -311,29 +311,24 @@
     std::vector<v8::Local<v8::Value>>* arguments);
 void MarkBootstrapComplete(const v8::FunctionCallbackInfo<v8::Value>& args);
 
-struct InitializationResult {
-  int exit_code = 0;
-  std::vector<std::string> args;
-  std::vector<std::string> exec_args;
-  bool early_return = false;
+class InitializationResultImpl final : public InitializationResult {
+ public:
+  ~InitializationResultImpl();
+  int exit_code() const { return exit_code_; }
+  bool early_return() const { return early_return_; }
+  const std::vector<std::string>& args() const { return args_; }
+  const std::vector<std::string>& exec_args() const { return exec_args_; }
+  const std::vector<std::string>& errors() const { return errors_; }
+  MultiIsolatePlatform* platform() const { return platform_; }
+
+  int exit_code_ = 0;
+  std::vector<std::string> args_;
+  std::vector<std::string> exec_args_;
+  std::vector<std::string> errors_;
+  bool early_return_ = false;
+  MultiIsolatePlatform* platform_ = nullptr;
 };
 
-enum InitializationSettingsFlags : uint64_t {
-  kDefaultInitialization = 1 << 0,
-  kInitializeV8 = 1 << 1,
-  kRunPlatformInit = 1 << 2,
-  kInitOpenSSL = 1 << 3
-};
-
-// TODO(codebytere): eventually document and expose to embedders.
-InitializationResult NODE_EXTERN_PRIVATE InitializeOncePerProcess(int argc,
-                                                                  char** argv);
-InitializationResult NODE_EXTERN_PRIVATE InitializeOncePerProcess(
-    int argc,
-    char** argv,
-    InitializationSettingsFlags flags,
-    ProcessFlags::Flags process_flags = ProcessFlags::kNoFlags);
-void NODE_EXTERN_PRIVATE TearDownOncePerProcess();
 void SetIsolateErrorHandlers(v8::Isolate* isolate, const IsolateSettings& s);
 void SetIsolateMiscHandlers(v8::Isolate* isolate, const IsolateSettings& s);
 void SetIsolateCreateParamsForNode(v8::Isolate::CreateParams* params);
@@ -424,6 +419,8 @@
 std::ostream& operator<<(std::ostream& output,
                          const PerformanceState::SerializeInfo& d);
 }
+
+bool linux_at_secure();
 }  // namespace node
 
 #endif  // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS
diff --git a/src/node_main.cc b/src/node_main.cc
index 6bac107..d486bbc 100644
--- a/src/node_main.cc
+++ b/src/node_main.cc
@@ -88,42 +88,8 @@
 }
 #else
 // UNIX
-#ifdef __linux__
-#include <sys/auxv.h>
-#endif  // __linux__
-#if defined(__POSIX__) && defined(NODE_SHARED_MODE)
-#include <string.h>
-#include <signal.h>
-#endif
-
-namespace node {
-namespace per_process {
-extern bool linux_at_secure;
-}  // namespace per_process
-}  // namespace node
 
 int main(int argc, char* argv[]) {
-#if defined(__POSIX__) && defined(NODE_SHARED_MODE)
-  // In node::PlatformInit(), we squash all signal handlers for non-shared lib
-  // build. In order to run test cases against shared lib build, we also need
-  // to do the same thing for shared lib build here, but only for SIGPIPE for
-  // now. If node::PlatformInit() is moved to here, then this section could be
-  // removed.
-  {
-    struct sigaction act;
-    memset(&act, 0, sizeof(act));
-    act.sa_handler = SIG_IGN;
-    sigaction(SIGPIPE, &act, nullptr);
-  }
-#endif
-
-#if defined(__linux__)
-  node::per_process::linux_at_secure = getauxval(AT_SECURE);
-#endif
-  // Disable stdio buffering, it interacts poorly with printf()
-  // calls elsewhere in the program (e.g., any logging from V8.)
-  setvbuf(stdout, nullptr, _IONBF, 0);
-  setvbuf(stderr, nullptr, _IONBF, 0);
   return node::Start(argc, argv);
 }
 #endif
diff --git a/src/node_main_instance.cc b/src/node_main_instance.cc
index 1287e79..984ebfc 100644
--- a/src/node_main_instance.cc
+++ b/src/node_main_instance.cc
@@ -139,21 +139,6 @@
     *exit_code = SpinEventLoop(env).FromMaybe(1);
   }
 
-  ResetStdio();
-
-  // TODO(addaleax): Neither NODE_SHARED_MODE nor HAVE_INSPECTOR really
-  // make sense here.
-#if HAVE_INSPECTOR && defined(__POSIX__) && !defined(NODE_SHARED_MODE)
-  struct sigaction act;
-  memset(&act, 0, sizeof(act));
-  for (unsigned nr = 1; nr < kMaxSignal; nr += 1) {
-    if (nr == SIGKILL || nr == SIGSTOP || nr == SIGPROF)
-      continue;
-    act.sa_handler = (nr == SIGPIPE) ? SIG_IGN : SIG_DFL;
-    CHECK_EQ(0, sigaction(nr, &act, nullptr));
-  }
-#endif
-
 #if defined(LEAK_SANITIZER)
   __lsan_do_leak_check();
 #endif
diff --git a/test/embedding/embedtest.cc b/test/embedding/embedtest.cc
index ee1e2af..2ad8afd 100644
--- a/test/embedding/embedtest.cc
+++ b/test/embedding/embedtest.cc
@@ -23,13 +23,16 @@
 int main(int argc, char** argv) {
   argv = uv_setup_args(argc, argv);
   std::vector<std::string> args(argv, argv + argc);
-  std::vector<std::string> exec_args;
-  std::vector<std::string> errors;
-  int exit_code = node::InitializeNodeWithArgs(&args, &exec_args, &errors);
-  for (const std::string& error : errors)
+  std::unique_ptr<node::InitializationResult> result =
+      node::InitializeOncePerProcess(
+          args,
+          {node::ProcessInitializationFlags::kNoInitializeV8,
+           node::ProcessInitializationFlags::kNoInitializeNodeV8Platform});
+
+  for (const std::string& error : result->errors())
     fprintf(stderr, "%s: %s\n", args[0].c_str(), error.c_str());
-  if (exit_code != 0) {
-    return exit_code;
+  if (result->early_return() != 0) {
+    return result->exit_code();
   }
 
   std::unique_ptr<MultiIsolatePlatform> platform =
@@ -37,10 +40,13 @@
   V8::InitializePlatform(platform.get());
   V8::Initialize();
 
-  int ret = RunNodeInstance(platform.get(), args, exec_args);
+  int ret =
+      RunNodeInstance(platform.get(), result->args(), result->exec_args());
 
   V8::Dispose();
   V8::DisposePlatform();
+
+  node::TearDownOncePerProcess();
   return ret;
 }
 
diff --git a/tools/snapshot/node_mksnapshot.cc b/tools/snapshot/node_mksnapshot.cc
index 15f9607..0b8b7a6 100644
--- a/tools/snapshot/node_mksnapshot.cc
+++ b/tools/snapshot/node_mksnapshot.cc
@@ -64,17 +64,18 @@
     return 1;
   }
 
-  node::InitializationResult result =
-      node::InitializeOncePerProcess(argc, argv);
+  std::unique_ptr<node::InitializationResult> result =
+      node::InitializeOncePerProcess(
+          std::vector<std::string>(argv, argv + argc));
 
-  CHECK(!result.early_return);
-  CHECK_EQ(result.exit_code, 0);
+  CHECK(!result->early_return());
+  CHECK_EQ(result->exit_code(), 0);
 
   std::string out_path;
   if (node::per_process::cli_options->build_snapshot) {
-    out_path = result.args[2];
+    out_path = result->args()[2];
   } else {
-    out_path = result.args[1];
+    out_path = result->args()[1];
   }
 
   std::ofstream out(out_path, std::ios::out | std::ios::binary);
@@ -85,8 +86,8 @@
 
   int exit_code = 0;
   {
-    exit_code =
-        node::SnapshotBuilder::Generate(out, result.args, result.exec_args);
+    exit_code = node::SnapshotBuilder::Generate(
+        out, result->args(), result->exec_args());
     if (exit_code == 0) {
       if (!out) {
         std::cerr << "Failed to write " << out_path << "\n";