/*
* Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved.
* This source file is part of the Cangjie project, licensed under Apache-2.0
* with Runtime Library Exception.
*
* See https://cangjie-lang.cn/pages/LICENSE for license information.
*/
package std.process
import std.fs.*
import std.collection.*
import std.io.*
import std.time.*
const MAX_TIMEOUT_NS: Int64 = 0x7FFF_FFFF_FFFF_FFFF
const PROCESS_STATUS_ALIVE: Int8 = 0
const PROCESS_STATUS_NOT_EXIST: Int8 = -1
const PROCESS_STATUS_PID_REUSED: Int8 = -2
const TIME_SECOND_TO_MILLISECOND: Int64 = 1000
const TIME_MILLISECOND_TO_NANOSECOND: Int64 = 1_000_000
let MAX_TIMEOUT_DURATION: Duration = Duration.nanosecond * MAX_TIMEOUT_NS
@When[os == "Windows"]
func closeProcessHandleIfNeeded(handle: IntNative): Unit {
if (handle != INVALID_HANDLE) {
unsafe { CJ_OS_CloseProcessHandle(handle) }
}
}
public open class Process {
var _pid: Int64 = -1
var _handle: IntNative = INVALID_HANDLE
var _name: ?String = ""
var _command: ?String = ""
var _arguments: ?Array<String> = Array<String>()
var _commandLine: ?Array<String> = Array<String>()
var _workingDirectory: ?Path = Path("")
var _environment: ?Map<String, String> = HashMap<String, String>()
// StartTime deal with the scene, old process instance be killed and a new process instance reused the same pid.
var _startTimeSinceUnixEpoch: Int64 = 0
var _startTime: Int64 = 0
/**
* Constructors
*
* @param pid - process instance id
* @param name - process name, always equals commandLine
* @param command - process command
* @param arguments - process command arguments
* @param commandLine - vm backend: commandLine = command + [programName] + arguments
* non-vm backend: commandLine = command + arguments
* @param workingDirectory - process working directory
* @param environment - process working environments
*
*/
init(pid: Int64, name: ?String, command: ?String, arguments: ?Array<String>, commandLine: ?Array<String>,
workingDirectory: ?Path, environment: ?Map<String, String>) {
this._pid = pid
this._name = name
this._command = command
this._arguments = arguments
this._commandLine = commandLine
this._workingDirectory = workingDirectory
this._environment = environment
this._startTimeSinceUnixEpoch = unsafe { CJ_OS_GetStartTimeFromUnixEpoch(Int32(pid)) }
this._startTime = getProcessStartTime(Int32(pid))
}
/**
* return a process instance for current process.The process instance can used to find processInfo and terminate
* current process. Also the process instance provide stdio, which can be used to read or write message.
*/
@Deprecated[message: "CurrentProcess has been marked as deprecated, use related global functions in the std.env instead."]
public static prop current: CurrentProcess {
get() {
return buildCurrentProcess()
}
}
public prop pid: Int64 {
get() {
this._pid
}
}
public prop name: String {
get() {
this._name ?? throw ProcessException("Can not get process name.")
}
}
public prop command: String {
get() {
this._command ?? throw ProcessException("Can not get process command.")
}
}
// Process start time
public prop startTime: DateTime {
get() {
return if (this._startTimeSinceUnixEpoch == -1) {
DateTime.UnixEpoch
} else {
let second = this._startTimeSinceUnixEpoch / TIME_SECOND_TO_MILLISECOND
let nanosecond = (this._startTimeSinceUnixEpoch - (second * TIME_SECOND_TO_MILLISECOND)) *
TIME_MILLISECOND_TO_NANOSECOND
DateTime.ofEpoch(second: second, nanosecond: nanosecond)
}
}
}
// Time spent on system calls executed directly or indirectly by the program
public prop systemTime: Duration {
get() {
return unsafe { CJ_OS_GetSystemTime(Int32(this._pid)) } * Duration.millisecond
}
}
// Time spent by the program itself and the subprocesses in the libraries it calls
public prop userTime: Duration {
get() {
return unsafe { CJ_OS_GetUserTime(Int32(this._pid)) } * Duration.millisecond
}
}
@Deprecated
public open prop arguments: Array<String> {
get() {
this._arguments ?? throw ProcessException("Can not get process arguments.")
}
}
@Deprecated[message: "Use global function `public func getCommandLine(): Array<String>` in std.env instead."]
public prop commandLine: Array<String> {
get() {
this._commandLine ?? throw ProcessException("Can not get process commandLine.")
}
}
@Deprecated[message: "Use global function `public func getHomeDirectory(): Path` in std.env instead."]
public prop workingDirectory: Path {
get() {
this._workingDirectory ?? throw ProcessException("Can not get process workingDirectory.")
}
}
@Deprecated[message: "Use global function `public func getVariables(): Array<(String, String)>` in std.env instead."]
public prop environment: Map<String, String> {
get() {
this.getEnvironments() ?? throw ProcessException("Can not get process environment.")
}
}
/**
* return a process instance for an existing process which pid is giving by user.
* The process instance can used to find processInfo and terminate the process.
* We can not get stdio for any pid process, so that do not provide the stream attribute to operate stdio.
*/
@Deprecated[message: "Use global function `public func findProcess(pid: Int64): Process` instead."]
public static func of(pid: Int64): Process {
if (pid > Int64(Int32.Max) || pid < 0) {
throw IllegalArgumentException("Invaild pid: \"${pid}\", can not get process.")
}
return buildProcess(Int32(pid))
}
/**
* start a new subProcess using command and arguments which provide by user.
*
* @param command - vaild system command, should not be empty string and can not include Null character.
* @param arguments - command arguments, each of the arguments should not be empty string and can not include Null character.
* @param workingDirectory - command working directory, if None, means inherit current process working directory.
* @param environment - command working environment, if None, means inherit current process environment.
* @param stdIn - stdIn redirect mode.
* @param stdOut - stdOut redirect mode.
* @param stdErr - stdErr redirect mode.
*
* @return SubProcess - a new subProcess instance.
*/
@Deprecated[message: "Use global function `public func launch(command: String, arguments: Array<String>, workingDirectory!: ?Path = None, environment!: ?Map<String, String> = None, stdIn!: ProcessRedirect = Inherit, stdOut!: ProcessRedirect = Inherit, stdErr!: ProcessRedirect = Inherit): SubProcess` instead."]
public static func start(
command: String,
arguments: Array<String>,
workingDirectory!: ?Path = None,
environment!: ?Map<String, String> = None,
stdIn!: ProcessRedirect = Inherit,
stdOut!: ProcessRedirect = Inherit,
stdErr!: ProcessRedirect = Inherit
): SubProcess {
launchImpl(command, arguments, workingDirectory, environment, stdIn, stdOut, stdErr)
}
/**
* func run is combine the start and wait.
* provide a convenient way for user, which want start a subprocess and wait the command exec return code.
*/
@Deprecated[message: "Use global function `public func execute(command: String, arguments: Array<String>, workingDirectory!: ?Path = None, environment!: ?Map<String, String> = None, stdIn!: ProcessRedirect = Inherit, stdOut!: ProcessRedirect = Inherit, stdErr!: ProcessRedirect = Inherit, timeout!: ?Duration = None): Int64` instead."]
public static func run(
command: String,
arguments: Array<String>,
workingDirectory!: ?Path = None,
environment!: ?Map<String, String> = None,
stdIn!: ProcessRedirect = Inherit,
stdOut!: ProcessRedirect = Inherit,
stdErr!: ProcessRedirect = Inherit,
timeout!: ?Duration = None
): Int64 {
let subProcess: SubProcess = start(command, arguments, workingDirectory: workingDirectory,
environment: environment, stdIn: stdIn, stdOut: stdOut, stdErr: stdErr)
return subProcess.wait(timeout: timeout)
}
/**
* func runOutput is combine the start and waitOutput.
* provide a convenient way for user, which want start a subprocess and wait the command exec output and err result.
*/
@Deprecated[message: "Use global function `public func executeWithOutput(command: String, arguments: Array<String>, workingDirectory!: ?Path = None, environment!: ?Map<String, String> = None, stdIn!: ProcessRedirect = Inherit, stdOut!: ProcessRedirect = Pipe, stdErr!: ProcessRedirect = Pipe): (Int64, Array<Byte>, Array<Byte>)` instead."]
public static func runOutput(
command: String,
arguments: Array<String>,
workingDirectory!: ?Path = None,
environment!: ?Map<String, String> = None,
stdIn!: ProcessRedirect = Inherit,
stdOut!: ProcessRedirect = Pipe,
stdErr!: ProcessRedirect = Pipe
): (Int64, Array<Byte>, Array<Byte>) {
let subProcess: SubProcess = start(command, arguments, workingDirectory: workingDirectory,
environment: environment, stdIn: stdIn, stdOut: stdOut, stdErr: stdErr)
return subProcess.waitOutput()
}
/**
* buildCurrentProcess
*
* @return CurrentProcess
*/
static func buildCurrentProcess(): CurrentProcess {
let current_pid: Int32 = unsafe { CJ_OS_GetCurrentPid() }
let processInfo_cp: CPointer<ProcessInfo> = unsafe { CJ_OS_GetProcessInfoByPid(current_pid) }
if (processInfo_cp.isNull()) {
throw ProcessException("Initialize current process failed.")
}
let processInfo: ProcessInfo = unsafe { processInfo_cp.read() }
try {
let (command, name, workDirPath, arguments, commandLine, environments) = getProcessInfo(processInfo)
let process = CurrentProcess(Int64(current_pid), name, command, arguments, commandLine, workDirPath,
environments)
process._handle = unsafe { CJ_OS_GetProcessHandle(current_pid) }
return process
} finally {
unsafe { CJ_OS_FreeProcessInfo(processInfo_cp) }
}
}
/**
* return a process instance for an existing process which pid is giving by user.
*
* @param pid - process instance id
*
* @return Process
*/
static func buildProcess(pid: Int32): Process {
let processInfo_cp: CPointer<ProcessInfo> = unsafe { CJ_OS_GetProcessInfoByPid(pid) }
if (processInfo_cp.isNull()) {
throw ProcessException("Process \"${pid}\" not alive, can not get process.")
}
let processInfo: ProcessInfo = unsafe { processInfo_cp.read() }
try {
let (command, name, workDirPath, arguments, commandLine, environments) = getProcessInfo(processInfo)
let process = PidProcess(Int64(pid), name, command, arguments, commandLine, workDirPath, environments)
process._handle = unsafe { CJ_OS_GetProcessHandle(pid) }
return process
} finally {
unsafe { CJ_OS_FreeProcessInfo(processInfo_cp) }
}
}
func getEnvironments(): ?Map<String, String> {
let processInfoC: CPointer<ProcessInfo> = unsafe { CJ_OS_GetProcessInfoByPid(Int32(this._pid)) }
if (processInfoC.isNull()) {
return None
}
try {
let processInfo: ProcessInfo = unsafe { processInfoC.read() }
return getEnvirments(processInfo.environment)
} finally {
unsafe { CJ_OS_FreeProcessInfo(processInfoC) }
}
}
public func isAlive(): Bool {
return unsafe { CJ_OS_GetProcessAliveStatus(Int32(this._pid), this._startTime) == PROCESS_STATUS_ALIVE }
}
/**
* terminate process
* when terminate the subprocess, the spawn which open to wait subprocess
* will modify status and change stdio stream to ByteBuffer let user can read
* stdOut & stdErr unread data after subprocess be killed.
*
* @param force - true terminated forcibly
* false terminated normally
*
* @throws ProcessException - if the pid invaild
* @return Unit
*/
public func terminate(force!: Bool = false): Unit {
if (this._pid > Int64(Int32.Max) || this._pid < 0) {
throw IllegalArgumentException("Invaild pid, \"${this._pid}\".")
}
terminateAliveProcess(Int32(this._pid), force)
}
/**
* terminate alive process
*
* @param pid - process id, need to terminate
* @param force - true terminated forcibly
* false terminated normally
*
* @throws ProcessException - if the pid invaild
* @return Unit
*/
protected open func terminateAliveProcess(pid: Int32, force: Bool): Unit {
let processStatus: Int8 = unsafe { CJ_OS_GetProcessAliveStatus(pid, this._startTime) }
// check the process still running, make sure not kill the wrong process.
if (processStatus == PROCESS_STATUS_ALIVE) {
unsafe { CJ_OS_Terminate(Int32(pid), force) }
return
}
if (processStatus == PROCESS_STATUS_PID_REUSED) {
throw ProcessException("Process pid \"${pid}\" has been reused, not allow kill the new process.")
}
throw ProcessException("Process pid \"${pid}\" not exist, not allow kill process.")
}
static func hasDiscardMode(std_redirects: Array<ProcessRedirect>): Bool {
for (stdio in std_redirects) {
if (let ProcessRedirect.Discard <- stdio) {
return true
}
}
return false
}
}
class PidProcess <: Process {
init(pid: Int64, name: ?String, command: ?String, arguments: ?Array<String>, commandLine: ?Array<String>,
workingDirectory: ?Path, environment: ?Map<String, String>) {
super(pid, name, command, arguments, commandLine, workingDirectory, environment)
}
@When[os == "Windows"]
~init() {
closeProcessHandleIfNeeded(this._handle)
}
}
/**
* Supported process redirect mode.
* Pipe: create a pipe between parent current and child process, which can provide a way to exchange info.
* Inherit: use the same stdio with the current process.
* FromFile: redirect stdio to a specified file which define by user.
* Discard: discard the stdio, redirect to system specific "null file".
*/
public enum ProcessRedirect {
| Inherit
| Pipe
| FromFile(File)
| Discard
}
enum StdIO {
| StdIn
| StdOut
| StdErr
}
@When[os == "Windows" || os == "macOS" || os == "iOS"]
func getProcessStartTime(pid: Int32): Int64 {
return unsafe { CJ_OS_GetStartTimeFromUnixEpoch(pid) }
}
@When[os != "Windows" && os != "macOS" && os != "iOS"]
func getProcessStartTime(pid: Int32): Int64 {
return unsafe { CJ_OS_GetStartTimeFromBoot(pid) }
}
@When[os != "iOS"]
func getProcessInfo(processInfo: ProcessInfo): (Option<String>, Option<String>, Option<Path>, Option<Array<String>>,
Option<Array<String>>, Option<Map<String, String>>) {
// Build subProcess instance use processInfo which return from System.
let command: ?String = cStringToOption(processInfo.command)
let name: ?String = getNameFromCommand(command)
let workDir: ?String = cStringToOption(processInfo.workingDirectory)
let workDirPath: ?Path = match (workDir) {
case Some(dir) => Path(dir)
case _ => None
}
let arguments: ?Array<String> = cpointerToArray(processInfo.arguments)
let commandLine: ?Array<String> = cpointerToArray(processInfo.commandLine)
let environments: ?Map<String, String> = getEnvirments(processInfo.environment)
return (command, name, workDirPath, arguments, commandLine, environments)
}
@When[os == "iOS"]
func getProcessInfo(processInfo: ProcessInfo): (Option<String>, Option<String>, Option<Path>, Option<Array<String>>,
Option<Array<String>>, Option<Map<String, String>>) {
let workDir: ?String = cStringToOption(processInfo.workingDirectory)
let name: ?String = processInfo.command.toString()
let command: ?String = cStringToOption(processInfo.command)
let commandLine: ?Array<String> = cpointerToArray(processInfo.commandLine)
let workDirPath: ?Path = match (workDir) {
case Some(dir) => Path(dir)
case _ => None
}
let environments: ?Map<String, String> = getEnvirments(processInfo.environment)
let arguments: ?Array<String> = cpointerToArray(processInfo.arguments)
return (command, name, workDirPath, arguments, commandLine, environments)
}
@When[os == "Windows"]
func getExecutablePath(cmdPath: String): ?Path {
return try {
let info: FileInfo = FileInfo(cmdPath)
if (info.canExecute()) {
info.path
} else {
None
}
} catch (_: FSException) { // Any exception except FSException will be throw
None
}
}
@When[os == "Windows"]
func searchExecutableInPaths(command: String, paths: String): ?Path {
for (path in paths.split(";")) {
if (let Some(cmd) <- getExecutablePath("${path}\\${command}")) {
return cmd
}
}
return None
}
@When[os == "Windows"]
func searchExecutableInSystemDirectory(command: String): ?Path {
try (dir = unsafe { CJ_OS_GetSystemDirectory().asResource() }) {
if (dir.value.isNull()) {
return None
} else {
return getExecutablePath("${dir.value}\\${command}")
}
}
return None
}
@When[os == "Windows"]
func searchExecutableInWindowsDirectory(command: String): ?Path {
try (dir = unsafe { CJ_OS_GetWindowsDirectory().asResource() }) {
return if (dir.value.isNull()) {
None
} else {
getExecutablePath("${dir.value}\\${command}")
}
}
return None
}
/**
* @brief Searches for an executable file in the following directories, according to the default behavior of the CreateProcessW function:
*
* 1. The directory from which the application is loaded.
* 2. The current directory of the parent process.
* 3. The 32-bit Windows system directory.
* 4. The 16-bit Windows system directory.
* 5. The Windows directory.
* 6. The directories listed in the PATH environment variable.
*
* NOTE: For safe reason, we don't search current directory and 16-bit windows system directory.
*/
@When[os == "Windows"]
func resolveExecutable(command: String, environment: ?Map<String, String>, workingDir: CString): ?Path {
if (command.size == 0) {
throw ProcessException("Created process failed, errMessage: \"The parameter is incorrect.\".")
}
let has_exe_extention = command.toAsciiLower().endsWith(".exe")
// If command is an path not filename, like C:\system32\xxx[.exe] or ./xxx[.exe]
// We will only search this path.
if (command.contains(":") || command.contains("\\") || command.contains("/")) {
return if (has_exe_extention) {
getExecutablePath(command)
} else {
getExecutablePath("${command}.exe")
}
}
// Command is a filename, search it by order.
var newCommand = command
if (!has_exe_extention) {
newCommand += ".exe"
}
if (let Some(envs) <- environment) {
if (envs.contains("Path")) {
if (let Some(path) <- searchExecutableInPaths(newCommand, envs["Path"])) {
return path
}
}
}
if (let Some(path) <- getExecutablePath("${workingDir}\\${newCommand}")) {
return path
}
if (let Some(path) <- searchExecutableInSystemDirectory(newCommand)) {
return path
}
if (let Some(path) <- searchExecutableInWindowsDirectory(newCommand)) {
return path
}
try (cStr = unsafe { LibC.mallocCString("Path").asResource() }) {
let envs: String = nativeGetEnv(cStr.value) ?? ""
if (let Some(path) <- searchExecutableInPaths(newCommand, envs)) {
return path
}
}
return None
}
@When[os == "Windows"]
func checkCommandLineLength(command: String, arguments: Array<String>) {
// MAX_PATH check: refer from chapter: maximum-file-path-limitation
var length: Int64 = command.size
let maxLength = Int64(Int16.Max - 1)
if (length >= maxLength) {
throw ProcessException("Created process failed, errMessage: \"The filename or extension is too long.\".")
}
for (arg in arguments) {
length += arg.size
if (length >= maxLength) {
throw ProcessException("Created process failed, errMessage: \"The filename or extension is too long.\".")
}
}
}
@When[os == "Windows"]
func buildSubProcess(command: String, arguments: Array<String>, workingDir_c: CString, environment: ?Map<String, String>,
std_redirects: Array<ProcessRedirect>): SubProcess {
checkCommandLineLength(command, arguments)
// If no valid path is found, preemptively throw an exception with the error message
// that would be returned by CreateProcessW when it fails to find the specified file.
let program = match (resolveExecutable(command, environment, workingDir_c)) {
case Some(programe) => programe.toString()
case _ => throw ProcessException(
"Created process failed, errMessage: \"The system cannot find the file specified.\".")
}
// convert attribute from String to CString
let subProcess: SubProcess
var processRtnData_cp: CPointer<ProcessRtnData> = CPointer<ProcessRtnData>()
var processInfo_cp: CPointer<ProcessInfo> = CPointer<ProcessInfo>()
var processStartInfo_cp: CPointer<ProcessStartInfo> = CPointer<ProcessStartInfo>()
// build commandLine and convert to CString
// build envBlock and convert to CString
var commandLine_c: CStringResource = CString(CPointer()).asResource()
var envBlock_c: CStringResource = CString(CPointer()).asResource()
var program_c: CStringResource = CString(CPointer()).asResource()
try {
program_c = unsafe { LibC.mallocCString(program).asResource() }
commandLine_c = buildCommandLine(command, arguments)
envBlock_c = createEnvironmentBlock(environment)
let stdIn_fd = matchRedirectFileHandle(std_redirects[0], StdIO.StdIn)
let stdOut_fd = matchRedirectFileHandle(std_redirects[1], StdIO.StdOut)
let stdErr_fd = matchRedirectFileHandle(std_redirects[2], StdIO.StdErr)
let processStartInfo: ProcessStartInfo = ProcessStartInfo(program_c.value, commandLine_c.value, workingDir_c,
envBlock_c.value, stdIn_fd, stdOut_fd, stdErr_fd)
processStartInfo_cp = unsafe { CJ_OS_CreateProcessStartInfo() }
unsafe { processStartInfo_cp.write(processStartInfo) }
processRtnData_cp = unsafe { CJ_OS_StartProcess(processStartInfo_cp) }
// processRtnData errcode not 0 means not success, and it's should not go on.
// should deal with the errcode give detail message.
if (processRtnData_cp.isNull()) {
throw ProcessException("Created process failed.")
}
let processRtnData = unsafe { processRtnData_cp.read() }
if (processRtnData.errCode != 0) {
throw ProcessException("Created process failed, errMessage: \"${processRtnData.errMessage}\".")
}
processInfo_cp = unsafe { CJ_OS_GetProcessInfoByPid(processRtnData.pid) }
if (processInfo_cp.isNull()) {
throw ProcessException("Created process failed.")
}
let processInfo = unsafe { processInfo_cp.read() }
let pid: Int32 = processRtnData.pid
let (commandInfo, name, workDirPath, argumentsInfo, commandLine, environments) = getProcessInfo(processInfo)
let (stdInStream, stdOutStream, stdErrStream) = initStdStream(processRtnData)
subProcess = SubProcess(Int64(pid), name, commandInfo, argumentsInfo, commandLine, workDirPath, environments,
stdInStream, stdOutStream, stdErrStream)
subProcess._handle = processRtnData.handle
} finally {
if (!program_c.isClosed()) {
program_c.close()
}
if (!commandLine_c.isClosed()) {
commandLine_c.close()
}
if (!envBlock_c.isClosed()) {
envBlock_c.close()
}
unsafe {
CJ_OS_FreeProcessInfo(processInfo_cp)
CJ_OS_FreeProcessRtnData(processRtnData_cp)
CJ_OS_FreeProcessStartInfo(processStartInfo_cp)
}
}
return subProcess
}
// check and concat command,arguments to commandLine
@When[os == "Windows"]
func buildCommandLine(command: String, arguments: Array<String>): CStringResource {
let cmdLineBuilder: StringBuilder = StringBuilder()
// concatenate command
let escapeCommand = escapeArgument(command)
cmdLineBuilder.append(escapeCommand)
// concatenate arguments
for (arg in arguments) {
cmdLineBuilder.append(r' ')
cmdLineBuilder.append(escapeArgument(arg))
}
return unsafe { LibC.mallocCString(cmdLineBuilder.toString()).asResource() }
}
@When[os == "Windows"]
func escapeArgument(arg: String): String {
if (arg.size == 0) {
return "\"\""
}
var needBackslash: Bool = false
var hasSpace: Bool = false
let argRuneArr: Array<Rune> = arg.toRuneArray()
for (argRune in argRuneArr) {
if (argRune == r'"' || argRune == r'\\') {
needBackslash = true
}
if (argRune == r' ' || argRune == r'\t') {
hasSpace = true
}
}
if (!needBackslash && !hasSpace) {
return arg
}
if (!needBackslash && hasSpace) {
let sb = StringBuilder(r'"')
sb.append(arg)
sb.append(r'"')
return sb.toString()
}
var escapeArgBuilder: StringBuilder = StringBuilder()
var slashCount = 0
for (argRune in argRuneArr) {
match (argRune) {
case r'\\' => slashCount++
case r'"' =>
while (slashCount > 0) {
escapeArgBuilder.append(r'\\')
slashCount--
}
escapeArgBuilder.append(r'\\')
case _ => slashCount = 0
}
escapeArgBuilder.append(argRune)
}
var escapeArg = escapeArgBuilder.toString()
if (hasSpace) {
while (slashCount > 0) {
escapeArgBuilder.append(r'\\')
slashCount--
}
let sb = StringBuilder(r'"')
sb.append(escapeArgBuilder)
sb.append(r'"')
escapeArg = sb.toString()
}
return escapeArg
}
@When[os == "Windows"]
func createEnvironmentBlock(environments: ?Map<String, String>): CStringResource {
let environmentMap: Map<String, String> = environments ?? return CString(CPointer()).asResource()
if (environmentMap.isEmpty()) {
return unsafe { LibC.mallocCString(String.fromUtf8Unchecked([0u8, 0u8])).asResource() }
}
let environment_arr: Array<String> = environmentMap |> map<(String, String), String> {
envEntry: (String, String) =>
let sb = StringBuilder(envEntry[0])
sb.append(r'=')
sb.append(envEntry[1])
sb.append(r'\0')
sb.toString()
} |> collectArray
let envBlockBuilder: StringBuilder = StringBuilder()
environment_arr |> forEach<String> {
envElement => envBlockBuilder.append(envElement)
}
envBlockBuilder.append(r'\0')
return unsafe { LibC.mallocCString(envBlockBuilder.toString()).asResource() }
}
func initStdStream(processRtnData: ProcessRtnData): (OutputStream, InputStream, InputStream) {
let stdInStream: OutputStream
if (processRtnData.stdInHandle == INVALID_HANDLE) {
stdInStream = NullProcessStream()
} else {
stdInStream = ProcessOutputStream(FileDescriptor(processRtnData.stdInHandle))
}
let stdOutStream: InputStream
if (processRtnData.stdOutHandle == INVALID_HANDLE) {
stdOutStream = NullProcessStream()
} else {
stdOutStream = ProcessInputStream(FileDescriptor(processRtnData.stdOutHandle))
}
let stdErrStream: InputStream
if (processRtnData.stdErrHandle == INVALID_HANDLE) {
stdErrStream = NullProcessStream()
} else {
stdErrStream = ProcessInputStream(FileDescriptor(processRtnData.stdErrHandle))
}
return (stdInStream, stdOutStream, stdErrStream)
}
@When[os != "Windows"]
func buildSubProcess(command: String, arguments: Array<String>, workingDir_c: CString, environment: ?Map<String, String>,
std_redirects: Array<ProcessRedirect>): SubProcess {
let cmdArgCombinList: ArrayList<String> = ArrayList<String>.of(command)
cmdArgCombinList.add(all: arguments)
var cmdArg_cArr: Array<CString> = cmdArgCombinList |> map {arg: String => unsafe { LibC.mallocCString(arg) }} |>
collectCStringArray
var environment_cArr: Array<CString> = Array<CString>()
try {
// if environment is None, it will be use current environment which from it's parent's process.
environment_cArr = match (environment) {
case Some(env) => env |> map<(String, String), CString> {
envEntry: (String, String) =>
let sb: StringBuilder = StringBuilder(envEntry[0])
sb.append(r'=')
sb.append(envEntry[1])
unsafe { LibC.mallocCString(sb.toString()) }
} |> collectCStringArray
case _ => Array<CString>()
}
} catch (e: IllegalMemoryException) {
for (arg_cStr in cmdArg_cArr) {
unsafe { LibC.free(arg_cStr) }
}
throw e
}
let cmdArg_cpHandle: CPointerHandle<CString> = unsafe { acquireArrayRawData<CString>(cmdArg_cArr) }
let environment_cpHandle: CPointerHandle<CString> = unsafe { acquireArrayRawData<CString>(environment_cArr) }
var environment_pt: CPointer<CString> = CPointer<CString>()
if (let Some(_) <- environment) {
environment_pt = environment_cpHandle.pointer
}
let subProcess: SubProcess
var processRtnData_cp: CPointer<ProcessRtnData> = CPointer<ProcessRtnData>()
var processInfo_cp: CPointer<ProcessInfo> = CPointer<ProcessInfo>()
var processStartInfo_cp: CPointer<ProcessStartInfo> = CPointer<ProcessStartInfo>()
// convert attribute from String to CString
var command_c: CString = CString(CPointer<UInt8>())
try {
let stdIn_fd = matchRedirectFileHandle(std_redirects[0], StdIO.StdIn)
let stdOut_fd = matchRedirectFileHandle(std_redirects[1], StdIO.StdOut)
let stdErr_fd = matchRedirectFileHandle(std_redirects[2], StdIO.StdErr)
command_c = unsafe { LibC.mallocCString(command) }
let processStartInfo: ProcessStartInfo = ProcessStartInfo(command_c, cmdArg_cpHandle.pointer,
UIntNative(cmdArg_cArr.size), workingDir_c, environment_pt, UIntNative(environment_cArr.size), stdIn_fd,
stdOut_fd, stdErr_fd)
processStartInfo_cp = unsafe { CJ_OS_CreateProcessStartInfo() }
unsafe { processStartInfo_cp.write(processStartInfo) }
processRtnData_cp = unsafe { CJ_OS_StartProcess(processStartInfo_cp) }
// processRtnData errcode not 0 means not success, and it's should not go on.
// should deal with the errcode give detail message.
if (processRtnData_cp.isNull()) {
throw ProcessException("Created process failed.")
}
let processRtnData: ProcessRtnData = unsafe { processRtnData_cp.read() }
if (processRtnData.errCode != 0) {
throw ProcessException("Created process failed, errMessage: \"${processRtnData.errMessage}\".")
}
processInfo_cp = unsafe { CJ_OS_GetProcessInfoByPid(processRtnData.pid) }
if (processInfo_cp.isNull()) {
throw ProcessException("Created process failed.")
}
let processInfo = unsafe { processInfo_cp.read() }
let pid: Int32 = processRtnData.pid
let (commandInfo, name, workDirPath, argumentsInfo, commandLine, environments) = getProcessInfo(processInfo)
let (stdInStream, stdOutStream, stdErrStream) = initStdStream(processRtnData)
subProcess = SubProcess(Int64(pid), name, commandInfo, argumentsInfo, commandLine, workDirPath, environments,
stdInStream, stdOutStream, stdErrStream)
} catch (e: Exception) {
if (!processRtnData_cp.isNull()) {
let processRtnData: ProcessRtnData = unsafe { processRtnData_cp.read() }
if (processRtnData.pid != 0) {
unsafe { CJ_OS_WaitSubProcessExit(processRtnData.pid) }
}
}
throw e
} finally {
unsafe {
if (!command_c.isNull()) {
LibC.free(command_c)
}
for (arg_cStr in cmdArg_cArr) {
LibC.free(arg_cStr)
}
releaseArrayRawData<CString>(cmdArg_cpHandle)
for (env_cStr in environment_cArr) {
LibC.free(env_cStr)
}
releaseArrayRawData<CString>(environment_cpHandle)
CJ_OS_FreeProcessInfo(processInfo_cp)
CJ_OS_FreeProcessRtnData(processRtnData_cp)
CJ_OS_FreeProcessStartInfo(processStartInfo_cp)
}
}
return subProcess
}
/**
* matchRedirectFileHandle
*
* @param redirect - process redirect mode, include pipe, inherit, discard and fromfile
* @param stdio - stdio type, include stdin, stdout and stderr
*
* @return IntNative
*/
func matchRedirectFileHandle(redirect: ProcessRedirect, stdio: StdIO): IntNative {
return match (redirect) {
case ProcessRedirect.Pipe => INVALID_HANDLE
case ProcessRedirect.Inherit => match (stdio) {
case StdIO.StdIn => getStdInHandle()
case StdIO.StdOut => getStdOutHandle()
case StdIO.StdErr => getStdErrHandle()
}
case ProcessRedirect.Discard =>
if (let Some(fileHandle) <- getNulFileHandle()) {
fileHandle
} else {
throw ProcessException("Failed to open null file, can not redirect stdio to discard mode.")
}
case ProcessRedirect.FromFile(file: File) =>
if (!exists(file.info.path) || file.isClosed()) {
throw ProcessException("Redirect file closed or not existed, not allow redirect.")
}
file.fileDescriptor.fileHandle
}
}
public func findProcess(pid: Int64): Process {
if (pid > Int64(Int32.Max) || pid < 0) {
throw IllegalArgumentException("Invaild pid: \"${pid}\", can not get process.")
}
return Process.buildProcess(Int32(pid))
}
private func launchImpl(
command: String,
arguments: Array<String>,
workingDirectory: ?Path,
environment: ?Map<String, String>,
stdIn: ProcessRedirect,
stdOut: ProcessRedirect,
stdErr: ProcessRedirect
): SubProcess {
var std_redirects: Array<ProcessRedirect> = [stdIn, stdOut, stdErr]
// check command, arguments and environment, can not be empty or Null.
// workingDirectory must be directory.
if (command.contains("\0")) {
throw IllegalArgumentException("Command \"${command}\" cannot contains null character.")
}
for (arg in arguments) {
if (arg.contains("\0")) {
throw IllegalArgumentException("Argument \"${arg}\" cannot contains null character.")
}
}
if (let Some(envMap) <- environment) {
for (envEntry in envMap) {
if (envEntry[0].contains("\0") || envEntry[0].contains("=") || envEntry[1].contains("\0")) {
throw IllegalArgumentException(
"Environment \"${envEntry[0]}\" entry cannot contains invalid character.")
}
}
}
if (let Some(dir) <- workingDirectory) {
try {
if (!FileInfo(dir).isDirectory()) {
throw IllegalArgumentException("WorkingDirectory \"${dir}\" must be directory.")
}
} catch (_: FSException) {
throw IllegalArgumentException("WorkingDirectory \"${dir}\" not exist in the file system.")
}
}
if (getNulFileHandle().isNone() && Process.hasDiscardMode(std_redirects)) {
throw IllegalArgumentException("Failed to open null file, can not redirect stdio to discard mode.")
}
// convert attribute from String to CString
// if workingDir is None, it means exec the command in current dir.
let workingDir_c: CString = match (workingDirectory) {
case Some(dir) => unsafe { LibC.mallocCString(dir.toString()) }
case _ => CString(CPointer())
}
try {
return buildSubProcess(command, arguments, workingDir_c, environment, std_redirects)
} finally {
// release malloc space
unsafe {
workingDir_c.asResource().close()
}
}
}
/**
* start a new subProcess using command and arguments which provide by user.
*
* @param command - vaild system command, should not be empty string and can not include Null character.
* @param arguments - command arguments, each of the arguments should not be empty string and can not include Null character.
* @param workingDirectory - command working directory, if None, means inherit current process working directory.
* @param environment - command working environment, if None, means inherit current process environment.
* @param stdIn - stdIn redirect mode.
* @param stdOut - stdOut redirect mode.
* @param stdErr - stdErr redirect mode.
*
* @return SubProcess - a new subProcess instance.
*/
@When[os != "iOS"]
public func launch(
command: String,
arguments: Array<String>,
workingDirectory!: ?Path = None,
environment!: ?Map<String, String> = None,
stdIn!: ProcessRedirect = Inherit,
stdOut!: ProcessRedirect = Inherit,
stdErr!: ProcessRedirect = Inherit
): SubProcess {
launchImpl(command, arguments, workingDirectory, environment, stdIn, stdOut, stdErr)
}
@When[os == "iOS"]
@Deprecated
public func launch(
command: String,
arguments: Array<String>,
workingDirectory!: ?Path = None,
environment!: ?Map<String, String> = None,
stdIn!: ProcessRedirect = Inherit,
stdOut!: ProcessRedirect = Inherit,
stdErr!: ProcessRedirect = Inherit
): SubProcess {
launchImpl(command, arguments, workingDirectory, environment, stdIn, stdOut, stdErr)
}
/**
* func execute is combine the launch and wait.
* provide a convenient way for user, which want start a subprocess and wait the command exec return code.
*/
@When[os != "iOS"]
public func execute(
command: String,
arguments: Array<String>,
workingDirectory!: ?Path = None,
environment!: ?Map<String, String> = None,
stdIn!: ProcessRedirect = Inherit,
stdOut!: ProcessRedirect = Inherit,
stdErr!: ProcessRedirect = Inherit,
timeout!: ?Duration = None
): Int64 {
let exitCode = executeImpl(command, arguments, workingDirectory, environment, stdIn, stdOut, stdErr, timeout)
exitCode
}
@When[os == "iOS"]
@Deprecated
public func execute(
command: String,
arguments: Array<String>,
workingDirectory!: ?Path = None,
environment!: ?Map<String, String> = None,
stdIn!: ProcessRedirect = Inherit,
stdOut!: ProcessRedirect = Inherit,
stdErr!: ProcessRedirect = Inherit,
timeout!: ?Duration = None
): Int64 {
executeImpl(command, arguments, workingDirectory, environment, stdIn, stdOut, stdErr, timeout)
}
private func executeImpl(
command: String,
arguments: Array<String>,
workingDirectory: ?Path,
environment: ?Map<String, String>,
stdIn: ProcessRedirect,
stdOut: ProcessRedirect,
stdErr: ProcessRedirect,
timeout: ?Duration
): Int64 {
let subProcess = launch(command, arguments, workingDirectory: workingDirectory, environment: environment,
stdIn: stdIn, stdOut: stdOut, stdErr: stdErr)
subProcess.wait(timeout: timeout)
}
/**
* func executeWithOutput is combine the start and waitOutput.
* provide a convenient way for user, which want start a subprocess and wait the command exec output and err result.
*/
@When[os != "iOS"]
public func executeWithOutput(
command: String,
arguments: Array<String>,
workingDirectory!: ?Path = None,
environment!: ?Map<String, String> = None,
stdIn!: ProcessRedirect = Inherit,
stdOut!: ProcessRedirect = Pipe,
stdErr!: ProcessRedirect = Pipe
): (Int64, Array<Byte>, Array<Byte>) {
let output = executeWithOutputImpl(command, arguments, workingDirectory, environment, stdIn, stdOut, stdErr)
output
}
@When[os == "iOS"]
@Deprecated
public func executeWithOutput(
command: String,
arguments: Array<String>,
workingDirectory!: ?Path = None,
environment!: ?Map<String, String> = None,
stdIn!: ProcessRedirect = Inherit,
stdOut!: ProcessRedirect = Pipe,
stdErr!: ProcessRedirect = Pipe
): (Int64, Array<Byte>, Array<Byte>) {
executeWithOutputImpl(command, arguments, workingDirectory, environment, stdIn, stdOut, stdErr)
}
private func executeWithOutputImpl(
command: String,
arguments: Array<String>,
workingDirectory: ?Path,
environment: ?Map<String, String>,
stdIn: ProcessRedirect,
stdOut: ProcessRedirect,
stdErr: ProcessRedirect
): (Int64, Array<Byte>, Array<Byte>) {
let subProcess = launch(command, arguments, workingDirectory: workingDirectory, environment: environment,
stdIn: stdIn, stdOut: stdOut, stdErr: stdErr)
subProcess.waitOutput()
}