/*
* 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.
*/
// The Cangjie API is in Beta. For details on its capabilities and limitations, please refer to the README file.
package std.env
import std.collection.*
import std.fs.*
import std.sync.*
const STDIN_FD: IntNative = 0
const STDOUT_FD: IntNative = 1
const STDERR_FD: IntNative = 2
const RTN_CODE_MIN_VAL: Int64 = -0x0000_0000_8000_0000 //-2147483648
const RTN_CODE_MAX_VAL: Int64 = 0x0000_0000_7fff_ffff //2147483647
const NULL_BYTE = "\0"
let mtx: Mutex = Mutex()
let outConsole: ConsoleWriter = ConsoleWriter(Int32(STDOUT_FD))
let errConsole: ConsoleWriter = ConsoleWriter(Int32(STDERR_FD))
let inConsole: ConsoleReader = ConsoleReader()
public func getProcessId(): Int64 {
let currentPid: Int32 = unsafe { CJ_OS_GetCurrentPid() }
return Int64(currentPid)
}
public func getCommand(): String {
match (getCurrentProcessInfo()[0]) {
case Some(command) => command
case _ => throw EnvException("Can not get process command.")
}
}
@When[os == "iOS"]
@Deprecated
public func getCommandLine(): Array<String> {
match (getCurrentProcessInfo()[2]) {
case Some(commandLine) => commandLine
case _ => throw EnvException("Can not get process commandLine.")
}
}
@When[os != "iOS"]
public func getCommandLine(): Array<String> {
match (getCurrentProcessInfo()[2]) {
case Some(commandLine) => commandLine
case _ => throw EnvException("Can not get process commandLine.")
}
}
public func getWorkingDirectory(): Path {
match (getCurrentProcessInfo()[1]) {
case Some(workDirPath) => workDirPath
case _ => throw EnvException("Can not get process workingDirectory.")
}
}
public func getHomeDirectory(): Path {
homeDirectory()
}
public func getTempDirectory(): Path {
tempDirectory()
}
public func getVariable(key: String): ?String {
if (key.contains(NULL_BYTE)) {
throw IllegalArgumentException("The path cannot contain null character!")
}
unsafe {
let ck: CString = LibC.mallocCString(key)
try {
return synchronized(mtx) {
nativeGetEnv(ck)
}
} finally {
LibC.free(ck)
}
}
}
@When[os == "iOS"]
@Deprecated
public func getVariables(): Array<(String, String)> {
match (getCurrentProcessInfo()[3]) {
case Some(environments) => environments
case _ => throw EnvException("Can not get process environment.")
}
}
@When[os != "iOS"]
public func getVariables(): Array<(String, String)> {
match (getCurrentProcessInfo()[3]) {
case Some(environments) => environments
case _ => throw EnvException("Can not get process environment.")
}
}
public func setVariable(key: String, value: String): Unit {
if (key.contains(NULL_BYTE) || value.contains(NULL_BYTE)) {
throw IllegalArgumentException("The path cannot contain null character!")
}
unsafe {
try (ck = LibC.mallocCString(key).asResource(), cv = LibC.mallocCString(value).asResource()) {
let errno = synchronized(mtx) {
nativeSetEnv(ck.value, cv.value, Int32(1))
}
if (errno != 0) {
throw EnvException("Failed to set variable ${key}.")
}
}
}
}
public func removeVariable(key: String): Unit {
if (key.contains(NULL_BYTE)) {
throw IllegalArgumentException("The path cannot contain null character!")
}
unsafe {
try (ck = LibC.mallocCString(key).asResource()) {
let errno = synchronized(mtx) {
nativeRemoveEnv(ck.value)
}
if (errno != 0) {
throw EnvException("Failed to remove variable ${key}.")
}
}
}
}
public func getStdErr(): ConsoleWriter {
errConsole
}
public func getStdIn(): ConsoleReader {
inConsole
}
public func getStdOut(): ConsoleWriter {
outConsole
}
public func atExit(callback: () -> Unit): Unit {
CJ_CORE_AddAtexitCallback(callback)
}
public func exit(code: Int64): Nothing {
let c: Int64 = if (code < RTN_CODE_MIN_VAL) {
RTN_CODE_MIN_VAL
} else if (code > RTN_CODE_MAX_VAL) {
RTN_CODE_MAX_VAL
} else {
code
}
CJ_CORE_ExecAtexitCallbacks()
processExit(Int32(c))
}
func getCurrentProcessInfo(): (Option<String>, Option<Path>, Option<Array<String>>, Option<Array<(String, String)>>) {
let currentPid: Int32 = unsafe { CJ_OS_GetCurrentPid() }
var processInfoC: CPointer<ProcessInfo> = synchronized(mtx) {
unsafe { CJ_OS_GetProcessInfoByPid(currentPid) }
}
if (processInfoC.isNull()) {
throw EnvException("Initialize current process failed.")
}
try {
let processInfo: ProcessInfo = unsafe { processInfoC.read() }
return getProcessInfo(processInfo)
} finally {
unsafe { CJ_OS_FreeProcessInfo(processInfoC) }
}
}
@When[os != "iOS"]
func getProcessInfo(processInfo: ProcessInfo): (Option<String>, Option<Path>, Option<Array<String>>, Option<Array<(String,
String)>>) {
let command: ?String = cStringToOption(processInfo.command)
let workDir: ?String = cStringToOption(processInfo.workingDirectory)
let workDirPath: ?Path = workDir.map {dir => Path(dir)}
let commandLine: ?Array<String> = cpointerToArray(processInfo.commandLine)
let environments: ?Array<(String, String)> = getEnvirments(processInfo.environment)
return (command, workDirPath, commandLine, environments)
}
@When[os == "iOS"]
func getProcessInfo(processInfo: ProcessInfo): (Option<String>, Option<Path>, Option<Array<String>>, Option<Array<(String, String)>>) {
let workDir: ?String = cStringToOption(processInfo.workingDirectory)
let command: ?String = cStringToOption(processInfo.command)
let commandLine: ?Array<String> = None
let workDirPath: ?Path = workDir.map {dir => Path(dir)}
let environments: ?Array<(String, String)> = getEnvirments(processInfo.environment)
return (command, workDirPath, commandLine, environments)
}
func cStringToOption(cstring: CString): ?String {
if (cstring.isNull()) {
return None
}
return cstring.toString()
}
func cpointerToArray(cpointer: CPointer<CString>): ?Array<String> {
if (cpointer.isNull()) {
return None
}
let arrList: ArrayList<String> = ArrayList<String>()
var index: Int64 = 0
var val: CString = unsafe { cpointer.read(index) }
while (!val.isNull()) {
arrList.add(val.toString())
index++
val = unsafe { cpointer.read(index) }
}
return arrList.toArray()
}
func getEnvirments(env: CPointer<CString>): ?Array<(String, String)> {
if (env.isNull()) {
return None
}
var envsList: ArrayList<(String, String)> = ArrayList<(String, String)>()
var index: Int64 = 0
var envStr: CString = unsafe { env.read(index) }
while (!envStr.isNull()) {
let envItem: String = envStr.toString()
let spiltIndex: Int64 = envItem.indexOf("=") ?? -1
if (spiltIndex > -1) {
let envKey = envItem[..spiltIndex]
let envVal = envItem[spiltIndex + 1..]
envsList.add((envKey, envVal))
}
index++
envStr = unsafe { env.read(index) }
}
return envsList.toArray()
}
func homeDirectory(): Path {
let homeDir: CString = unsafe { CJ_OS_HomeDir() } // Note: no need to free homeDir.
Path(homeDir.toString())
}
func tempDirectory(): Path {
let tmpDirs: Array<?String> = [getVariable("TMPDIR"), getVariable("TMP"), getVariable("TEMP"),
getVariable("TEMPDIR")]
for (each in tmpDirs where each.isSome()) {
return Path(each.getOrThrow())
}
Path("/tmp")
}
@When[os == "Windows"]
func nativeGetEnv(name: CString): Option<String> {
let res: CString = unsafe { CJ_OS_GetEnvVal(name) } // No need to free name
if (res.isNull()) {
return None
}
try {
return res.toString()
} finally {
unsafe { LibC.free(res) }
}
}
@When[os != "Windows"]
func nativeGetEnv(name: CString): Option<String> {
let res: CString = unsafe { getenv(name) } // No need to free name
return if (res.isNull()) {
None
} else {
res.toString()
}
}
@When[os != "Windows"]
func nativeSetEnv(name: CString, value: CString, overwrite: Int32): Int32 {
unsafe { setenv(name, value, overwrite) }
}
@When[os == "Windows"]
func nativeSetEnv(name: CString, value: CString, _: Int32): Int32 {
let envstr: CString = unsafe { LibC.mallocCString(name.toString() + "=" + value.toString()) }
let ret = unsafe { CJ_OS_SetEnvEntry(envstr) }
unsafe { LibC.free(envstr) }
ret
}
@When[os == "Windows"]
func getStdErrHandle(): IntNative {
getStdHandle(STDERR_FD)
}
@When[os != "Windows"]
func getStdErrHandle(): IntNative {
STDERR_FD
}
@When[os != "Windows"]
func nativeRemoveEnv(name: CString): Int32 {
unsafe { unsetenv(name) }
}
@When[os == "Windows"]
func nativeRemoveEnv(name: CString): Int32 {
let envstr: CString = unsafe { LibC.mallocCString(name.toString() + "=") }
let ret = unsafe { CJ_OS_SetEnvEntry(envstr) }
unsafe { LibC.free(envstr) }
ret
}
@When[os == "Windows"]
func getStdOutHandle(): IntNative {
getStdHandle(STDOUT_FD)
}
@When[os != "Windows"]
func getStdOutHandle(): IntNative {
STDOUT_FD
}
@When[os == "Windows"]
func getStdInHandle(): IntNative {
getStdHandle(STDIN_FD)
}
@When[os != "Windows"]
func getStdInHandle(): IntNative {
STDIN_FD
}
@When[os == "Windows"]
func getStdHandle(fd: IntNative): IntNative {
unsafe { CJ_OS_GetStdHandle(fd) }
}
@When[backend == "cjnative"]
func processExit(exitCode: Int32): Nothing {
unsafe {
FiniCJRuntime()
exit(exitCode)
}
throw EnvException("Process is already closed.")
}