// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "content/browser/child_process_launcher_helper.h"

#import <BrowserEngineKit/BrowserEngineKit.h>

#include <atomic>
#include <list>
#include <string_view>

#include "base/apple/mach_port_rendezvous_ios.h"
#include "base/files/file.h"
#include "base/files/file_util.h"
#include "base/files/scoped_temp_dir.h"
#include "base/no_destructor.h"
#include "base/path_service.h"
#include "base/strings/sys_string_conversions.h"
#include "base/threading/platform_thread.h"
#include "content/browser/child_process_launcher.h"
#include "content/browser/child_process_launcher_helper_posix.h"
#include "content/public/browser/child_process_launcher_utils.h"
#include "content/public/common/content_switches.h"
#include "gpu/ipc/common/surface_handle.h"
#include "services/network/public/mojom/network_service.mojom.h"
#include "ui/accelerated_widget_mac/ca_layer_frame_sink_provider.h"

namespace content {
namespace internal {

static base::NoDestructor<base::Lock> g_process_table_lock_;
static base::NoDestructor<
    std::map<pid_t, scoped_refptr<ChildProcessLauncherHelper>>>
    g_process_table_;

void InvalidateProcess(NSObject* process) {
  if ([process isKindOfClass:[BEWebContentProcess class]]) {
    [(BEWebContentProcess*)process invalidate];
  } else if ([process isKindOfClass:[BENetworkingProcess class]]) {
    [(BENetworkingProcess*)process invalidate];
  } else if ([process isKindOfClass:[BERenderingProcess class]]) {
    [(BERenderingProcess*)process invalidate];
  }
}

void OnChildProcessTerminatedOnAnyThread(pid_t process_id) {
  base::AutoLock guard(*g_process_table_lock_);
  auto it = g_process_table_->find(process_id);
  if (it != g_process_table_->end()) {
    it->second->ClearProcessStorage();
    g_process_table_->erase(it);
  }
}

bool WaitForExit(pid_t process_id, int* exit_code, base::TimeDelta timeout) {
  base::TimeTicks wakeup_time = base::TimeTicks::Now() + timeout;
  constexpr uint32_t kMaxSleepInMicroseconds = 1 << 18;  // ~256 ms.
  uint32_t max_sleep_time_usecs = 1 << 10;               // ~1 ms.
  int double_sleep_time = 0;

  while (true) {
    {
      base::AutoLock guard(*g_process_table_lock_);
      auto it = g_process_table_->find(process_id);
      if (it != g_process_table_->end()) {
        if (it->second->GetProcess() == nullptr) {
          if (exit_code) {
            *exit_code = it->second->GetExitCode().value_or(0);
          }
          return true;
        }
      } else {
        return true;
      }
    }
    base::TimeTicks now = base::TimeTicks::Now();
    if (now > wakeup_time) {
      return false;
    }

    const uint32_t sleep_time_usecs = static_cast<uint32_t>(
        std::min(static_cast<uint64_t>((wakeup_time - now).InMicroseconds()),
                 uint64_t{max_sleep_time_usecs}));
    base::PlatformThread::Sleep(base::Microseconds(sleep_time_usecs));
    if ((max_sleep_time_usecs < kMaxSleepInMicroseconds) &&
        (double_sleep_time++ % 4 == 0)) {
      max_sleep_time_usecs *= 2;
    }
  }
}

bool TerminateNow(pid_t process_id, int exit_code, bool wait) {
  NSObject* process = nullptr;
  {
    base::AutoLock guard(*g_process_table_lock_);
    auto it = g_process_table_->find(process_id);
    if (it != g_process_table_->end()) {
      it->second->SetExitCode(exit_code);
      process = it->second->GetProcess();
    }
  }

  if (!process) {
    return false;
  }
  InvalidateProcess(process);
  if (wait) {
    return WaitForExit(process_id, nullptr, base::Seconds(60));
  }
  return true;
}

// Object used to pass the result of the launch from the async
// dispatch_queue to the LauncherThread.
class LaunchResult {
 public:
  void Invalidate() { InvalidateProcess(process); }

  id<BEProcessCapabilityGrant> GrantForeground(NSError** error) {
    id<BEProcessCapabilityGrant> grant;
    BEProcessCapability* cap = [BEProcessCapability foreground];
    if ([process isKindOfClass:[BEWebContentProcess class]]) {
      grant = [(BEWebContentProcess*)process grantCapability:cap error:error];
    } else if ([process isKindOfClass:[BENetworkingProcess class]]) {
      grant = [(BENetworkingProcess*)process grantCapability:cap error:error];
    } else if ([process isKindOfClass:[BERenderingProcess class]]) {
      grant = [(BERenderingProcess*)process grantCapability:cap error:error];
    }
    return grant;
  }

  xpc_connection_t CreateXPCConnection(NSError** error) {
    if ([process isKindOfClass:[BEWebContentProcess class]]) {
      return [(BEWebContentProcess*)process makeLibXPCConnectionError:error];
    } else if ([process isKindOfClass:[BENetworkingProcess class]]) {
      return [(BENetworkingProcess*)process makeLibXPCConnectionError:error];
    } else if ([process isKindOfClass:[BERenderingProcess class]]) {
      return [(BERenderingProcess*)process makeLibXPCConnectionError:error];
    }
    return {};
  }

  NSObject* process;
  NSError* launch_error;
};

// Object to store the process handles.
class ProcessStorage : public ProcessStorageBase {
 public:
  ProcessStorage(NSObject* process,
                 xpc_connection_t connection,
                 id<BEProcessCapabilityGrant> grant)
      : process_(process), ipc_channel_(connection), grant_(grant) {}

  ~ProcessStorage() override { [grant_ invalidate]; }

  void ReleaseProcess() override {
    terminated_ = true;
    process_ = nullptr;
  }

  NSObject* Process() { return process_; }

  bool IsTerminated() const { return terminated_; }

 private:
  NSObject* process_;
  std::atomic<bool> terminated_ = false;
  [[maybe_unused]] xpc_connection_t ipc_channel_;
  id<BEProcessCapabilityGrant> grant_;
};

std::optional<mojo::NamedPlatformChannel>
ChildProcessLauncherHelper::CreateNamedPlatformChannelOnLauncherThread() {
  DCHECK(CurrentlyOnProcessLauncherTaskRunner());
  return std::nullopt;
}

void ChildProcessLauncherHelper::BeforeLaunchOnClientThread() {
  DCHECK(client_task_runner_->RunsTasksInCurrentSequence());
}

std::unique_ptr<PosixFileDescriptorInfo>
ChildProcessLauncherHelper::GetFilesToMap() {
  DCHECK(CurrentlyOnProcessLauncherTaskRunner());
  return CreateDefaultPosixFilesToMap(
      child_process_id(), mojo_channel_->remote_endpoint(),
      /*files_to_preload=*/{}, GetProcessType(), command_line());
}

bool ChildProcessLauncherHelper::BeforeLaunchOnLauncherThread(
    FileMappedForLaunch& files_to_register,
    base::LaunchOptions* options) {
  mojo::PlatformHandle endpoint =
      mojo_channel_->TakeRemoteEndpoint().TakePlatformHandle();
  DCHECK(endpoint.is_valid_mach_receive());
  options->mach_ports_for_rendezvous.insert(std::make_pair(
      'mojo', base::MachRendezvousPort(endpoint.TakeMachReceiveRight())));
  return true;
}

ChildProcessLauncherHelper::Process
ChildProcessLauncherHelper::LaunchProcessOnLauncherThread(
    const base::LaunchOptions* options,
    std::unique_ptr<PosixFileDescriptorInfo> files_to_register,
    bool* is_synchronous_launch,
    int* launch_result) {
  DCHECK(options);
  *is_synchronous_launch = false;
  rendezvous_server_ = std::make_unique<base::MachPortRendezvousServerIOS>(
      options->mach_ports_for_rendezvous);

  // We need to hand out unique "process ids" just use a static counter
  // for now. There should only be one launcher thread so this is
  // synchronous access it doesn't need to be an atomic.
  static pid_t g_pid = 0;
  pid_t process_id = ++g_pid;

  static bool g_hooks_registered = false;
  if (!g_hooks_registered) {
    base::Process::SetTerminationHooks(&TerminateNow, &WaitForExit);
    g_hooks_registered = true;
  }

  void (^process_terminated)() = ^void() {
    OnChildProcessTerminatedOnAnyThread(process_id);
  };
  std::string process_type = GetProcessType();
  std::string utility_sub_type =
      command_line()->GetSwitchValueASCII(switches::kUtilitySubType);
  if (process_type == switches::kUtilityProcess) {
    void (^process_launch_complete)(BENetworkingProcess* process,
                                    NSError* error) =
        ^void(BENetworkingProcess* process, NSError* error) {
          auto result = std::make_unique<LaunchResult>(process, error);
          GetProcessLauncherTaskRunner()->PostTask(
              FROM_HERE,
              base::BindOnce(&ChildProcessLauncherHelper::OnChildProcessStarted,
                             this, process_id, std::move(result)));
        };

    [BENetworkingProcess
        networkProcessWithInterruptionHandler:process_terminated
                                   completion:process_launch_complete];

  } else if (process_type == switches::kGpuProcess) {
    void (^process_launch_complete)(BERenderingProcess* process,
                                    NSError* error) =
        ^void(BERenderingProcess* process, NSError* error) {
          auto result = std::make_unique<LaunchResult>(process, error);
          GetProcessLauncherTaskRunner()->PostTask(
              FROM_HERE,
              base::BindOnce(&ChildProcessLauncherHelper::OnChildProcessStarted,
                             this, process_id, std::move(result)));
        };

    [BERenderingProcess
        renderingProcessWithInterruptionHandler:process_terminated
                                     completion:process_launch_complete];
  } else {
    // This can be both kUtility and kRenderProcess.
    void (^process_launch_complete)(BEWebContentProcess* process,
                                    NSError* error) =
        ^void(BEWebContentProcess* process, NSError* error) {
          auto result = std::make_unique<LaunchResult>(process, error);
          GetProcessLauncherTaskRunner()->PostTask(
              FROM_HERE,
              base::BindOnce(&ChildProcessLauncherHelper::OnChildProcessStarted,
                             this, process_id, std::move(result)));
        };

    [BEWebContentProcess
        webContentProcessWithInterruptionHandler:process_terminated
                                      completion:process_launch_complete];
  }
  AddRef();
  return Process();
}

void ChildProcessLauncherHelper::OnChildProcessStarted(
    pid_t process_id,
    std::unique_ptr<LaunchResult> launch_result) {
  DCHECK(CurrentlyOnProcessLauncherTaskRunner());
  scoped_refptr<ChildProcessLauncherHelper> ref(this);
  Release();  // Balances with LaunchProcessOnLauncherThread.

  int launch_result_code = LAUNCH_RESULT_FAILURE;

  if (!launch_result->launch_error) {
    NSError* error = nil;

    // TODO(dtapuska): For now we grant everything foreground capability. We
    // need to hook this grant up to the
    // `RenderProcessHostImpl::UpdateProcessPriority()`.
    id<BEProcessCapabilityGrant> grant = launch_result->GrantForeground(&error);

    xpc_connection_t xpc_connection =
        launch_result->CreateXPCConnection(&error);
    if (xpc_connection) {
      scoped_refptr<base::SequencedTaskRunner> client_task_runner =
          client_task_runner_;
      bool is_gpu_process = GetProcessType() == switches::kGpuProcess;

      xpc_connection_set_event_handler(xpc_connection, ^(xpc_object_t event) {
        if (event == XPC_ERROR_CONNECTION_INTERRUPTED ||
            event == XPC_ERROR_CONNECTION_INVALID) {
          OnChildProcessTerminatedOnAnyThread(process_id);
          return;
        }

        const char* message_type = xpc_dictionary_get_string(event, "message");
        if (message_type && std::string_view(message_type) == "layerHandle") {
          // We only expect this message from the GPU process.
          if (!is_gpu_process) {
            xpc_connection_cancel(xpc_connection);
            return;
          }
          xpc_object_t ca_layer_handle =
              xpc_dictionary_get_value(event, "layer");
          gpu::SurfaceHandle view_handle =
              xpc_dictionary_get_uint64(event, "handle");

          // Validate arguments.
          if (!ca_layer_handle || !view_handle) {
            xpc_connection_cancel(xpc_connection);
            return;
          }

          client_task_runner->PostTask(
              FROM_HERE,
              base::BindOnce(
                  [](gpu::SurfaceHandle view_handle,
                     xpc_object_t ca_layer_handle) {
                    NSError* error = nullptr;
                    BELayerHierarchyHandle* be_handle = [BELayerHierarchyHandle
                        handleWithXPCRepresentation:ca_layer_handle
                                              error:&error];
                    CALayerFrameSinkProvider* sink =
                        [CALayerFrameSinkProvider lookupByHandle:view_handle];
                    if (sink) {
                      sink.handle = be_handle;
                    }
                  },
                  view_handle, ca_layer_handle));
        }
      });
      xpc_connection_resume(xpc_connection);
      xpc_object_t message = xpc_dictionary_create(nil, nil, 0);
      xpc_object_t args_array = xpc_array_create_empty();
      for (const auto& arg : command_line()->argv()) {
        xpc_object_t value = xpc_string_create(arg.c_str());
        xpc_array_append_value(args_array, value);
      }
      xpc_dictionary_set_value(message, "args", args_array);
      xpc_dictionary_set_fd(message, "stdout", STDOUT_FILENO);
      xpc_dictionary_set_fd(message, "stderr", STDERR_FILENO);

      // We create a scoped temporary directory for the child process.
      // In order to share this unique directory with the child process
      // we need to create bookmark data and then pass this over XPC
      // to the child process. The child process will then deserialize
      // it before then assigning the TMPDIR environment variable. We
      // do this via XPC so that it is done early enough in the process
      // creation so TMPDIR is set before any real Chromium code runs.
      scoped_temp_dir_ = std::make_unique<base::ScopedTempDir>();
      CHECK(scoped_temp_dir_->CreateUniqueTempDir());

      NSURL* temp_dir_url = [[NSURL alloc]
          initFileURLWithPath:base::SysUTF8ToNSString(
                                  scoped_temp_dir_->GetPath().value())];
      NSData* bookmark_temp_dir = [temp_dir_url
                 bookmarkDataWithOptions:NSURLBookmarkCreationMinimalBookmark
          includingResourceValuesForKeys:nil
                           relativeToURL:nil
                                   error:&error];
      CHECK(error == nil) << base::SysNSStringToUTF8(
          [error localizedDescription]);

      xpc_dictionary_set_data(message, "tmp_dir", bookmark_temp_dir.bytes,
                              bookmark_temp_dir.length);

      if (is_gpu_process) {
        // This should match the bundle ID that we set for the GPU process which
        // for content_shell is in //content/shell/app/BUILD.gn and for chrome
        // is in //ios/chrome/app/chrome_app.gni. In both cases we append the
        // ".GPUProcessExtension" suffix to the main bundle ID.
        NSString* gpu_bundle_id = [[[NSBundle mainBundle] bundleIdentifier]
            stringByAppendingString:@".GPUProcessExtension"];

        // Create a cache directory for the GPU extension process.
        base::FilePath gpu_cache_path =
            base::PathService::CheckedGet(base::DIR_CACHE)
                .AppendUTF8(base::SysNSStringToUTF8(gpu_bundle_id));

        base::File::Error file_error = base::File::FILE_OK;
        CHECK(base::CreateDirectoryAndGetError(gpu_cache_path, &file_error))
            << base::File::ErrorToString(file_error);

        NSURL* gpu_cache_url = [NSURL
            fileURLWithPath:base::SysUTF8ToNSString(gpu_cache_path.value())
                isDirectory:TRUE];

        // Put the cache directory in a bookmark, similar to TMP_DIR.
        NSData* gpu_cache_bookmark = [gpu_cache_url
                   bookmarkDataWithOptions:NSURLBookmarkCreationMinimalBookmark
            includingResourceValuesForKeys:nil
                             relativeToURL:nil
                                     error:&error];
        CHECK(error == nil)
            << base::SysNSStringToUTF8([error localizedDescription]);

        xpc_dictionary_set_data(message, "gpu_cache_dir",
                                gpu_cache_bookmark.bytes,
                                gpu_cache_bookmark.length);

        // Per Apple, this needs to be propagated to the extension process to
        // setenv the same value there.
        const char* container_home_path = getenv("CFFIXED_USER_HOME");
        CHECK_NE(container_home_path, nullptr);
        xpc_dictionary_set_string(message, "browser_container_home",
                                  container_home_path);
      }

      xpc_dictionary_set_mach_send(
          message, "port", rendezvous_server_->GetMachSendRight().get());
      xpc_connection_send_message(xpc_connection, message);
      launch_result_code = LAUNCH_RESULT_SUCCESS;

      // Keep reference to process, xpc_connection and the grant for the process
      // life.
      process_storage_ = std::make_unique<ProcessStorage>(
          launch_result->process, xpc_connection, grant);

      // Add the process to the global table.
      {
        base::AutoLock guard(*g_process_table_lock_);
        CHECK(!g_process_table_->contains(process_id));
        g_process_table_->emplace(process_id, this);
      }

    } else {
      [grant invalidate];
      launch_result->Invalidate();
    }
  }

  ChildProcessLauncherHelper::Process process;
  process.process = base::Process(process_id);
#if TARGET_OS_SIMULATOR
  process.process.SetIsContentProcess();
#endif
  PostLaunchOnLauncherThread(std::move(process), launch_result_code);
}

bool ChildProcessLauncherHelper::IsUsingLaunchOptions() {
  return true;
}

void ChildProcessLauncherHelper::AfterLaunchOnLauncherThread(
    const ChildProcessLauncherHelper::Process& process,
    const base::LaunchOptions* options) {}

ChildProcessTerminationInfo ChildProcessLauncherHelper::GetTerminationInfo(
    const ChildProcessLauncherHelper::Process& process,
    bool known_dead) {
  ChildProcessTerminationInfo info;
  if (!process_storage_) {
    info.status = base::TERMINATION_STATUS_LAUNCH_FAILED;
  } else if (static_cast<ProcessStorage*>(process_storage_.get())
                 ->IsTerminated()) {
    if (exit_code_.has_value()) {
      if (exit_code_.value() == RESULT_CODE_NORMAL_EXIT) {
        info.status = base::TERMINATION_STATUS_NORMAL_TERMINATION;
      } else {
        info.status = base::TERMINATION_STATUS_PROCESS_WAS_KILLED;
      }
    } else {
      info.status = base::TERMINATION_STATUS_PROCESS_CRASHED;
    }
  } else {
    info.status = base::TERMINATION_STATUS_STILL_RUNNING;
  }
  return info;
}

void ChildProcessLauncherHelper::ClearProcessStorage() {
  if (process_storage_) {
    process_storage_->ReleaseProcess();
  }
}

void ChildProcessLauncherHelper::SetExitCode(int exit_code) {
  exit_code_ = exit_code;
}

std::optional<int> ChildProcessLauncherHelper::GetExitCode() {
  return exit_code_;
}

NSObject* ChildProcessLauncherHelper::GetProcess() {
  if (process_storage_) {
    return static_cast<ProcessStorage*>(process_storage_.get())->Process();
  }
  return nullptr;
}

// static
bool ChildProcessLauncherHelper::TerminateProcess(const base::Process& process,
                                                  int exit_code) {
  // TODO(crbug.com/40565504): Determine whether we should also call
  // EnsureProcessTerminated() to make sure of process-exit, and reap it.
  return process.Terminate(exit_code, false);
}

// static
void ChildProcessLauncherHelper::ForceNormalProcessTerminationSync(
    ChildProcessLauncherHelper::Process process) {
  DCHECK(CurrentlyOnProcessLauncherTaskRunner());
  // Client has gone away, so just kill the process.  Using exit code 0 means
  // that UMA won't treat this as a crash.
  process.process.Terminate(RESULT_CODE_NORMAL_EXIT, false);
  base::EnsureProcessTerminated(std::move(process.process));
}

void ChildProcessLauncherHelper::SetProcessPriorityOnLauncherThread(
    base::Process process,
    base::Process::Priority priority) {}

// static
base::File OpenFileToShare(const base::FilePath& path,
                           base::MemoryMappedFile::Region* region) {
  // Not used yet (until required files are described in the service manifest on
  // iOS).
  NOTREACHED();
}

}  //  namespace internal
}  // namespace content
