//===--- Subprocess.swift -------------------------------------------------===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2014 - 2017 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See https://swift.org/LICENSE.txt for license information
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
//
//===----------------------------------------------------------------------===//

import SwiftPrivate
#if os(macOS) || os(iOS) || os(watchOS) || os(tvOS)
import Darwin
#elseif os(Linux) || os(FreeBSD) || os(OpenBSD) || os(PS4) || os(Android) || os(Cygwin) || os(Haiku) || os(WASI)
import Glibc
#elseif os(Windows)
import MSVCRT
import WinSDK
#endif

#if !os(WASI)
// No signals support on WASI yet, see https://github.com/WebAssembly/WASI/issues/166.
internal func _signalToString(_ signal: Int) -> String {
#if os(WASI)
  return "unsupported"
#else
  switch CInt(signal) {
  case SIGILL:  return "SIGILL"
  case SIGABRT: return "SIGABRT"
  case SIGFPE:  return "SIGFPE"
  case SIGSEGV: return "SIGSEGV"
#if !os(Windows)
  case SIGTRAP: return "SIGTRAP"
  case SIGBUS:  return "SIGBUS"
  case SIGSYS:  return "SIGSYS"
#endif
  default:      return "SIG???? (\(signal))"
  }
#endif // os(WASI)
}
#endif

public enum ProcessTerminationStatus : CustomStringConvertible {
  case exit(Int)
  case signal(Int)

  public var description: String {
    switch self {
    case .exit(let status):
      return "Exit(\(status))"
    case .signal(let signal):
#if os(WASI)
      // No signals support on WASI yet, see https://github.com/WebAssembly/WASI/issues/166.
      fatalError("Signals are not supported on WebAssembly/WASI")
#else
      return "Signal(\(_signalToString(signal)))"
#endif
    }
  }
}


#if os(Windows)
public func spawnChild(_ args: [String])
    -> (process: HANDLE, stdin: HANDLE, stdout: HANDLE, stderr: HANDLE) {
  var _stdin: (read: HANDLE?, write: HANDLE?)
  var _stdout: (read: HANDLE?, write: HANDLE?)
  var _stderr: (read: HANDLE?, write: HANDLE?)

  var saAttributes: SECURITY_ATTRIBUTES = SECURITY_ATTRIBUTES()
  saAttributes.nLength = DWORD(MemoryLayout<SECURITY_ATTRIBUTES>.size)
  saAttributes.bInheritHandle = true
  saAttributes.lpSecurityDescriptor = nil

  if !CreatePipe(&_stdin.read, &_stdin.write, &saAttributes, 0) {
    fatalError("CreatePipe() failed")
  }
  if !SetHandleInformation(_stdin.write, HANDLE_FLAG_INHERIT, 0) {
    fatalError("SetHandleInformation() failed")
  }

  if !CreatePipe(&_stdout.read, &_stdout.write, &saAttributes, 0) {
    fatalError("CreatePipe() failed")
  }
  if !SetHandleInformation(_stdout.read, HANDLE_FLAG_INHERIT, 0) {
    fatalError("SetHandleInformation() failed")
  }

  if !CreatePipe(&_stderr.read, &_stderr.write, &saAttributes, 0) {
    fatalError("CreatePipe() failed")
  }
  if !SetHandleInformation(_stderr.read, HANDLE_FLAG_INHERIT, 0) {
    fatalError("SetHandleInformation() failed")
  }

  var siStartupInfo: STARTUPINFOW = STARTUPINFOW()
  siStartupInfo.cb = DWORD(MemoryLayout<STARTUPINFOW>.size)
  siStartupInfo.hStdError = _stderr.write
  siStartupInfo.hStdOutput = _stdout.write
  siStartupInfo.hStdInput = _stdin.read
  siStartupInfo.dwFlags |= STARTF_USESTDHANDLES

  var piProcessInfo: PROCESS_INFORMATION = PROCESS_INFORMATION()

  // TODO(compnerd): properly quote the command line being invoked here.  See
  // https://blogs.msdn.microsoft.com/twistylittlepassagesallalike/2011/04/23/everyone-quotes-command-line-arguments-the-wrong-way/
  // for more details on how to properly quote the command line for Windows.
  let command: String =
      ([CommandLine.arguments[0]] + args).joined(separator: " ")
  command.withCString(encodedAs: UTF16.self) { cString in
    if !CreateProcessW(nil, UnsafeMutablePointer<WCHAR>(mutating: cString),
                       nil, nil, true, 0, nil, nil,
                       &siStartupInfo, &piProcessInfo) {
      let dwError: DWORD = GetLastError()
      fatalError("CreateProcessW() failed \(dwError)")
    }
  }

  if !CloseHandle(_stdin.read) {
    fatalError("CloseHandle() failed")
  }
  if !CloseHandle(_stdout.write) {
    fatalError("CloseHandle() failed")
  }
  if !CloseHandle(_stderr.write) {
    fatalError("CloseHandle() failed")
  }

  // CloseHandle(piProcessInfo.hProcess)
  CloseHandle(piProcessInfo.hThread)

  return (piProcessInfo.hProcess,
          _stdin.write ?? INVALID_HANDLE_VALUE,
          _stdout.read ?? INVALID_HANDLE_VALUE,
          _stderr.read ?? INVALID_HANDLE_VALUE)
}

public func waitProcess(_ process: HANDLE) -> ProcessTerminationStatus {
  let result = WaitForSingleObject(process, INFINITE)
  if result != WAIT_OBJECT_0 {
    fatalError("WaitForSingleObject() failed")
  }

  var status: DWORD = 0
  if !GetExitCodeProcess(process, &status) {
    fatalError("GetExitCodeProcess() failed")
  }

  if status & DWORD(0x80000000) == DWORD(0x80000000) {
    return .signal(Int(status))
  }
  return .exit(Int(status))
}
#elseif os(WASI)
// WASI doesn't support child processes
public func spawnChild(_ args: [String])
  -> (pid: pid_t, stdinFD: CInt, stdoutFD: CInt, stderrFD: CInt) {
  fatalError("\(#function) is not supported on WebAssembly/WASI")
}
public func posixWaitpid(_ pid: pid_t) -> ProcessTerminationStatus {
  fatalError("\(#function) is not supported on WebAssembly/WASI")
}
#else
// posix_spawn is not available on Android.
// posix_spawn is not available on Haiku.
#if !os(Android) && !os(Haiku)
// posix_spawn isn't available in the public watchOS SDK, we sneak by the
// unavailable attribute declaration here of the APIs that we need.

// FIXME: Come up with a better way to deal with APIs that are pointers on some
// platforms but not others.
#if os(Linux)
typealias _stdlib_posix_spawn_file_actions_t = posix_spawn_file_actions_t
#else
typealias _stdlib_posix_spawn_file_actions_t = posix_spawn_file_actions_t?
#endif

@_silgen_name("_stdlib_posix_spawn_file_actions_init")
internal func _stdlib_posix_spawn_file_actions_init(
  _ file_actions: UnsafeMutablePointer<_stdlib_posix_spawn_file_actions_t>
) -> CInt

@_silgen_name("_stdlib_posix_spawn_file_actions_destroy")
internal func _stdlib_posix_spawn_file_actions_destroy(
  _ file_actions: UnsafeMutablePointer<_stdlib_posix_spawn_file_actions_t>
) -> CInt

@_silgen_name("_stdlib_posix_spawn_file_actions_addclose")
internal func _stdlib_posix_spawn_file_actions_addclose(
  _ file_actions: UnsafeMutablePointer<_stdlib_posix_spawn_file_actions_t>,
  _ filedes: CInt) -> CInt

@_silgen_name("_stdlib_posix_spawn_file_actions_adddup2")
internal func _stdlib_posix_spawn_file_actions_adddup2(
  _ file_actions: UnsafeMutablePointer<_stdlib_posix_spawn_file_actions_t>,
  _ filedes: CInt,
  _ newfiledes: CInt) -> CInt

@_silgen_name("_stdlib_posix_spawn")
internal func _stdlib_posix_spawn(
  _ pid: UnsafeMutablePointer<pid_t>?,
  _ file: UnsafePointer<Int8>,
  _ file_actions: UnsafePointer<_stdlib_posix_spawn_file_actions_t>?,
  _ attrp: UnsafePointer<posix_spawnattr_t>?,
  _ argv: UnsafePointer<UnsafeMutablePointer<Int8>?>,
  _ envp: UnsafePointer<UnsafeMutablePointer<Int8>?>?) -> CInt
#endif

/// Calls POSIX `pipe()`.
func posixPipe() -> (readFD: CInt, writeFD: CInt) {
  var fds: [CInt] = [ -1, -1 ]
  if pipe(&fds) != 0 {
    preconditionFailure("pipe() failed")
  }
  return (fds[0], fds[1])
}

/// Start the same executable as a child process, redirecting its stdout and
/// stderr.
public func spawnChild(_ args: [String])
  -> (pid: pid_t, stdinFD: CInt, stdoutFD: CInt, stderrFD: CInt) {
  // The stdout, stdin, and stderr from the child process will be redirected
  // to these pipes.
  let childStdout = posixPipe()
  let childStdin = posixPipe()
  let childStderr = posixPipe()

#if os(Android) || os(Haiku)
  // posix_spawn isn't available on Android. Instead, we fork and exec.
  // To correctly communicate the exit status of the child process to this
  // (parent) process, we'll use this pipe.
  let childToParentPipe = posixPipe()

  let pid = fork()
  precondition(pid >= 0, "fork() failed")
  if pid == 0 {
    // pid of 0 means we are now in the child process.
    // Capture the output before executing the program.
    close(childStdout.readFD)
    close(childStdin.writeFD)
    close(childStderr.readFD)
    close(childToParentPipe.readFD)
    dup2(childStdout.writeFD, STDOUT_FILENO)
    dup2(childStdin.readFD, STDIN_FILENO)
    dup2(childStderr.writeFD, STDERR_FILENO)

    // Set the "close on exec" flag on the parent write pipe. This will
    // close the pipe if the execve() below successfully executes a child
    // process.
    let closeResult = fcntl(childToParentPipe.writeFD, F_SETFD, FD_CLOEXEC)
    let closeErrno = errno
    precondition(
      closeResult == 0,
      "Could not set the close behavior of the child-to-parent pipe; " +
      "errno: \(closeErrno)")

    // Start the executable. If execve() does not encounter an error, the
    // code after this block will never be executed, and the parent write pipe
    // will be closed.
    withArrayOfCStrings([CommandLine.arguments[0]] + args) {
      execve(CommandLine.arguments[0], $0, environ)
    }

    // If execve() encountered an error, we write the errno encountered to the
    // parent write pipe.
    let errnoSize = MemoryLayout.size(ofValue: errno)
    var execveErrno = errno
    let writtenBytes = withUnsafePointer(to: &execveErrno) {
      write(childToParentPipe.writeFD, UnsafePointer($0), errnoSize)
    }

    let writeErrno = errno
    if writtenBytes > 0 && writtenBytes < errnoSize {
      // We were able to write some of our error, but not all of it.
      // FIXME: Retry in this case.
      preconditionFailure("Unable to write entire error to child-to-parent " +
                          "pipe.")
    } else if writtenBytes == 0 {
      preconditionFailure("Unable to write error to child-to-parent pipe.")
    } else if writtenBytes < 0 {
      preconditionFailure("An error occurred when writing error to " +
                          "child-to-parent pipe; errno: \(writeErrno)")
    }

    // Close the pipe when we're done writing the error.
    close(childToParentPipe.writeFD)
  } else {
    close(childToParentPipe.writeFD)

    // Figure out if the child’s call to execve was successful or not.
    var readfds = _stdlib_fd_set()
    readfds.set(childToParentPipe.readFD)
    var writefds = _stdlib_fd_set()
    var errorfds = _stdlib_fd_set()
    errorfds.set(childToParentPipe.readFD)

    var ret: CInt
    repeat {
      ret = _stdlib_select(&readfds, &writefds, &errorfds, nil)
    } while ret == -1 && errno == EINTR
    if ret <= 0 {
      fatalError("select() returned an error: \(errno)")
    }

    if readfds.isset(childToParentPipe.readFD) || errorfds.isset(childToParentPipe.readFD) {
      var childErrno: CInt = 0
      let readResult: ssize_t = withUnsafeMutablePointer(to: &childErrno) {
        return read(childToParentPipe.readFD, $0, MemoryLayout.size(ofValue: $0.pointee))
      }
      if readResult == 0 {
        // We read an EOF indicating that the child's call to execve was successful.
      } else if readResult < 0 {
        fatalError("read() returned error: \(errno)")
      } else {
        // We read an error from the child.
        print(String(cString: strerror(childErrno)))
        preconditionFailure("execve() failed")
      }
    }

    close(childToParentPipe.readFD)
  }
#else
  var fileActions = _make_posix_spawn_file_actions_t()
  if _stdlib_posix_spawn_file_actions_init(&fileActions) != 0 {
    preconditionFailure("_stdlib_posix_spawn_file_actions_init() failed")
  }

  // Close the write end of the pipe on the child side.
  if _stdlib_posix_spawn_file_actions_addclose(
    &fileActions, childStdin.writeFD) != 0 {
    preconditionFailure("_stdlib_posix_spawn_file_actions_addclose() failed")
  }

  // Remap child's stdin.
  if _stdlib_posix_spawn_file_actions_adddup2(
    &fileActions, childStdin.readFD, STDIN_FILENO) != 0 {
    preconditionFailure("_stdlib_posix_spawn_file_actions_adddup2() failed")
  }

  // Close the read end of the pipe on the child side.
  if _stdlib_posix_spawn_file_actions_addclose(
    &fileActions, childStdout.readFD) != 0 {
    preconditionFailure("_stdlib_posix_spawn_file_actions_addclose() failed")
  }

  // Remap child's stdout.
  if _stdlib_posix_spawn_file_actions_adddup2(
    &fileActions, childStdout.writeFD, STDOUT_FILENO) != 0 {
    preconditionFailure("_stdlib_posix_spawn_file_actions_adddup2() failed")
  }

  // Close the read end of the pipe on the child side.
  if _stdlib_posix_spawn_file_actions_addclose(
    &fileActions, childStderr.readFD) != 0 {
    preconditionFailure("_stdlib_posix_spawn_file_actions_addclose() failed")
  }

  // Remap child's stderr.
  if _stdlib_posix_spawn_file_actions_adddup2(
    &fileActions, childStderr.writeFD, STDERR_FILENO) != 0 {
    preconditionFailure("_stdlib_posix_spawn_file_actions_adddup2() failed")
  }

  var pid: pid_t = -1
  var childArgs = args
  childArgs.insert(CommandLine.arguments[0], at: 0)
  let interpreter = getenv("SWIFT_INTERPRETER")
  if interpreter != nil {
    if let invocation = String(validatingUTF8: interpreter!) {
      childArgs.insert(invocation, at: 0)
    }
  }
  let spawnResult = withArrayOfCStrings(childArgs) {
    _stdlib_posix_spawn(
      &pid, childArgs[0], &fileActions, nil, $0, environ)
  }
  if spawnResult != 0 {
    print(String(cString: strerror(spawnResult)))
    preconditionFailure("_stdlib_posix_spawn() failed")
  }

  if _stdlib_posix_spawn_file_actions_destroy(&fileActions) != 0 {
    preconditionFailure("_stdlib_posix_spawn_file_actions_destroy() failed")
  }
#endif

  // Close the read end of the pipe on the parent side.
  if close(childStdin.readFD) != 0 {
    preconditionFailure("close() failed")
  }

  // Close the write end of the pipe on the parent side.
  if close(childStdout.writeFD) != 0 {
    preconditionFailure("close() failed")
  }

  // Close the write end of the pipe on the parent side.
  if close(childStderr.writeFD) != 0 {
    preconditionFailure("close() failed")
  }

  return (pid, childStdin.writeFD, childStdout.readFD, childStderr.readFD)
}

#if !os(Android) && !os(Haiku)
#if os(Linux)
internal func _make_posix_spawn_file_actions_t()
  -> _stdlib_posix_spawn_file_actions_t {
  return posix_spawn_file_actions_t()
}
#else
internal func _make_posix_spawn_file_actions_t()
  -> _stdlib_posix_spawn_file_actions_t {
  return nil
}
#endif
#endif

public func posixWaitpid(_ pid: pid_t) -> ProcessTerminationStatus {
  var status: CInt = 0
#if os(Cygwin)
  withUnsafeMutablePointer(to: &status) {
    statusPtr in
    let statusPtrWrapper = __wait_status_ptr_t(__int_ptr: statusPtr)
    while waitpid(pid, statusPtrWrapper, 0) < 0 {
      if errno != EINTR {
        preconditionFailure("waitpid() failed")
      }
    }
  }
#else
  while waitpid(pid, &status, 0) < 0 {
    if errno != EINTR {
      preconditionFailure("waitpid() failed")
    }
  }
#endif
  if WIFEXITED(status) {
    return .exit(Int(WEXITSTATUS(status)))
  }
  if WIFSIGNALED(status) {
    return .signal(Int(WTERMSIG(status)))
  }
  preconditionFailure("did not understand what happened to child process")
}

// !os(Windows)
#endif

