From 675263de44de1c5a9581d175f3672a95fed1eada Mon Sep 17 00:00:00 2001 From: Adeel <3840695+am11@users.noreply.github.com> Date: Tue, 28 Jul 2026 19:33:13 +0300 Subject: [PATCH] Implement GetCommandLineArgs fallback on Unix --- .../System.Native/Interop.GetCommandLine.cs | 16 + .../System.Private.CoreLib.Shared.projitems | 3 + .../src/System/Environment.Browser.cs | 6 - .../src/System/Environment.Unix.cs | 8 - .../src/System/Environment.UnixOrBrowser.cs | 25 + .../src/System/Environment.cs | 2 + .../System/Environment.GetCommandLineArgs.cs | 13 +- src/native/libs/System.Native/entrypoints.c | 2 + src/native/libs/System.Native/pal_process.c | 11 + src/native/libs/System.Native/pal_process.h | 11 + .../libs/System.Native/pal_process_wasi.c | 11 + src/native/minipal/getcmdline.h | 435 ++++++++++++++++++ 12 files changed, 519 insertions(+), 24 deletions(-) create mode 100644 src/libraries/Common/src/Interop/Unix/System.Native/Interop.GetCommandLine.cs create mode 100644 src/native/minipal/getcmdline.h diff --git a/src/libraries/Common/src/Interop/Unix/System.Native/Interop.GetCommandLine.cs b/src/libraries/Common/src/Interop/Unix/System.Native/Interop.GetCommandLine.cs new file mode 100644 index 00000000000000..7eccb665dec75c --- /dev/null +++ b/src/libraries/Common/src/Interop/Unix/System.Native/Interop.GetCommandLine.cs @@ -0,0 +1,16 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Runtime.InteropServices; + +internal static partial class Interop +{ + internal static partial class Sys + { + [LibraryImport(Libraries.SystemNative, EntryPoint = "SystemNative_GetCommandLine")] + internal static unsafe partial byte** GetCommandLine(out int count); + + [LibraryImport(Libraries.SystemNative, EntryPoint = "SystemNative_FreeCommandLine")] + internal static unsafe partial void FreeCommandLine(byte** buffer); + } +} diff --git a/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems b/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems index d297affcabc559..0935a27185c6cb 100644 --- a/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems +++ b/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems @@ -2523,6 +2523,9 @@ Common\Interop\Unix\System.Native\Interop.GetProcessPath.cs + + Common\Interop\Unix\System.Native\Interop.GetCommandLine.cs + Common\Interop\Unix\System.Native\Interop.GetRandomBytes.cs diff --git a/src/libraries/System.Private.CoreLib/src/System/Environment.Browser.cs b/src/libraries/System.Private.CoreLib/src/System/Environment.Browser.cs index 312f065e325c2e..6d3aabb565fba6 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Environment.Browser.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Environment.Browser.cs @@ -34,11 +34,5 @@ private static OperatingSystem GetOSVersion() /// /// Path of the executable that started the currently executing process private static string? GetProcessPath() => null; - -#if !MONO - // This is only used for delegate created from native host - private static string[] GetCommandLineArgsNative() => []; -#endif - } } diff --git a/src/libraries/System.Private.CoreLib/src/System/Environment.Unix.cs b/src/libraries/System.Private.CoreLib/src/System/Environment.Unix.cs index 7dd6f903cf9dfa..6f62f672217c8c 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Environment.Unix.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Environment.Unix.cs @@ -33,13 +33,5 @@ public static string MachineName [MethodImplAttribute(MethodImplOptions.NoInlining)] // Avoid inlining PInvoke frame into the hot path private static string? GetProcessPath() => Interop.Sys.GetProcessPath(); - - private static string[] GetCommandLineArgsNative() - { - // This is only used for delegate created from native host - - // Consider to use /proc/self/cmdline to get command line - return []; - } } } diff --git a/src/libraries/System.Private.CoreLib/src/System/Environment.UnixOrBrowser.cs b/src/libraries/System.Private.CoreLib/src/System/Environment.UnixOrBrowser.cs index 693c36b15d6d63..1d954fcefb714c 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Environment.UnixOrBrowser.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Environment.UnixOrBrowser.cs @@ -5,6 +5,7 @@ using System.IO; using System.Reflection; using System.Runtime.InteropServices; +using System.Runtime.InteropServices.Marshalling; using System.Runtime.Versioning; using System.Text; using System.Threading; @@ -97,5 +98,29 @@ public static ProcessCpuUsage CpuUsage /// Gets the number of milliseconds elapsed since the system started. /// A 64-bit signed integer containing the amount of time in milliseconds that has passed since the last time the computer was started. public static long TickCount64 => Interop.Sys.GetLowResolutionTimestamp(); + + private static unsafe string[] GetCommandLineArgsNative() + { + byte** nativeArgv = Interop.Sys.GetCommandLine(out int argc); + if (nativeArgv == null || argc <= 0) + { + return []; + } + + try + { + string[] args = new string[argc]; + for (int i = 0; i < argc; i++) + { + args[i] = Utf8StringMarshaller.ConvertToManaged(nativeArgv[i]) ?? string.Empty; + } + + return args; + } + finally + { + Interop.Sys.FreeCommandLine(nativeArgv); + } + } } } diff --git a/src/libraries/System.Private.CoreLib/src/System/Environment.cs b/src/libraries/System.Private.CoreLib/src/System/Environment.cs index dabc44cab4c4bf..0425ae678e48be 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Environment.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Environment.cs @@ -2,9 +2,11 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Collections; +using System.Collections.Generic; using System.Diagnostics; using System.Runtime.CompilerServices; using System.Threading; +using System.Text; namespace System { diff --git a/src/libraries/System.Runtime/tests/System.Runtime.Extensions.Tests/System/Environment.GetCommandLineArgs.cs b/src/libraries/System.Runtime/tests/System.Runtime.Extensions.Tests/System/Environment.GetCommandLineArgs.cs index 2d19226a2787d8..20852e71c714f2 100644 --- a/src/libraries/System.Runtime/tests/System.Runtime.Extensions.Tests/System/Environment.GetCommandLineArgs.cs +++ b/src/libraries/System.Runtime/tests/System.Runtime.Extensions.Tests/System/Environment.GetCommandLineArgs.cs @@ -74,11 +74,9 @@ public static int CheckCommandLineArgs(string[] args) [ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] public void GetCommandLineArgs_Fallback_Returns() { - if (PlatformDetection.IsNotMonoRuntime - && PlatformDetection.IsNotNativeAot - && PlatformDetection.IsWindows) + if (PlatformDetection.IsNotMonoRuntime) { - // Currently fallback command line is only implemented on Windows coreclr + // Currently fallback command line is not implemented by mono RemoteExecutor.Invoke(CheckCommandLineArgsFallback).Dispose(); } } @@ -104,12 +102,7 @@ public static int CheckCommandLineArgsFallback() return RemoteExecutor.SuccessExitCode; } - public static bool IsWindowsCoreCLRJit - => PlatformDetection.IsWindows - && PlatformDetection.IsNotMonoRuntime - && PlatformDetection.IsNotNativeAot; - - [ConditionalTheory(typeof(GetCommandLineArgs), nameof(IsWindowsCoreCLRJit))] + [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsWindows), nameof(PlatformDetection.IsNotMonoRuntime))] [InlineData(@"cmd ""abc"" d e", new[] { "cmd", "abc", "d", "e" })] [InlineData(@"cmd a\\b d""e f""g h", new[] { "cmd", @"a\\b", "de fg", "h" })] [InlineData(@"cmd a\\\""b c d", new[] { "cmd", @"a\""b", "c", "d" })] diff --git a/src/native/libs/System.Native/entrypoints.c b/src/native/libs/System.Native/entrypoints.c index 78e08565015511..0c81fc156f6959 100644 --- a/src/native/libs/System.Native/entrypoints.c +++ b/src/native/libs/System.Native/entrypoints.c @@ -230,6 +230,8 @@ static const Entry s_sysNative[] = DllImportEntry(SystemNative_SchedSetAffinity) DllImportEntry(SystemNative_SchedGetAffinity) DllImportEntry(SystemNative_GetProcessPath) + DllImportEntry(SystemNative_GetCommandLine) + DllImportEntry(SystemNative_FreeCommandLine) DllImportEntry(SystemNative_GetNonCryptographicallySecureRandomBytes) DllImportEntry(SystemNative_GetCryptographicallySecureRandomBytes) DllImportEntry(SystemNative_GetUnixRelease) diff --git a/src/native/libs/System.Native/pal_process.c b/src/native/libs/System.Native/pal_process.c index c5af90e397287e..902b61e5615250 100644 --- a/src/native/libs/System.Native/pal_process.c +++ b/src/native/libs/System.Native/pal_process.c @@ -64,6 +64,7 @@ #endif #include +#include // Validate that our SysLogPriority values are correct for the platform c_static_assert(PAL_LOG_EMERG == LOG_EMERG); @@ -1388,3 +1389,13 @@ char* SystemNative_GetProcessPath(void) { return minipal_getexepath(); } + +char** SystemNative_GetCommandLine(int* argc) +{ + return minipal_getcmdline(argc); +} + +void SystemNative_FreeCommandLine(char** buffer) +{ + free(buffer); +} diff --git a/src/native/libs/System.Native/pal_process.h b/src/native/libs/System.Native/pal_process.h index 515cfa5ea8e090..566322bde7057e 100644 --- a/src/native/libs/System.Native/pal_process.h +++ b/src/native/libs/System.Native/pal_process.h @@ -243,3 +243,14 @@ PALEXPORT int32_t SystemNative_SchedGetAffinity(int32_t pid, intptr_t* mask); * resolving symbolic links. The caller is responsible for releasing the buffer. */ PALEXPORT char* SystemNative_GetProcessPath(void); + +/** + * Returns the command line for the current process as an array of arguments. + * The caller is responsible for releasing the buffer. + */ +PALEXPORT char** SystemNative_GetCommandLine(int* argc); + +/** + * Releases the command line buffer returned by SystemNative_GetCommandLine. + */ +PALEXPORT void SystemNative_FreeCommandLine(char** buffer); diff --git a/src/native/libs/System.Native/pal_process_wasi.c b/src/native/libs/System.Native/pal_process_wasi.c index 8bfa5ef3438bc7..bc2b8e23271995 100644 --- a/src/native/libs/System.Native/pal_process_wasi.c +++ b/src/native/libs/System.Native/pal_process_wasi.c @@ -16,6 +16,7 @@ #include #include +#include int32_t SystemNative_ForkAndExecProcess(const char* filename, char* const argv[], @@ -127,3 +128,13 @@ char* SystemNative_GetProcessPath(void) { return minipal_getexepath(); } + +char** SystemNative_GetCommandLine(int* argc) +{ + return minipal_getcmdline(argc); +} + +void SystemNative_FreeCommandLine(char** buffer) +{ + free(buffer); +} diff --git a/src/native/minipal/getcmdline.h b/src/native/minipal/getcmdline.h new file mode 100644 index 00000000000000..cde9178fad3071 --- /dev/null +++ b/src/native/minipal/getcmdline.h @@ -0,0 +1,435 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#ifndef HAVE_MINIPAL_GETCMDLINE_H +#define HAVE_MINIPAL_GETCMDLINE_H + +#include +#include +#include + +#if defined(__APPLE__) +#include +#include +#elif defined(__FreeBSD__) +#include +#include +#include +#include +#elif defined(__OpenBSD__) +#include +#include +#include +#include +#elif defined(_WIN32) +#include +#elif defined(__HAIKU__) +#include +#include +#elif defined(TARGET_WASI) +#include +#elif HAVE_GETAUXVAL +#include +#endif + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * Get the raw command line for the current process as an array of strings. + * The returned array is NULL-terminated (the last element is NULL). + * The caller is responsible for releasing the memory by calling a single free() on the returned pointer. + * + * @param count A pointer to an integer that receives the number of arguments returned. + * @return A NULL-terminated array of null-terminated strings, or NULL if an error occurs. + */ +static inline char** minipal_getcmdline(int* count) +{ + if (count == NULL) + { + return NULL; + } + *count = 0; + +#if defined(TARGET_BROWSER) + size_t allocSize = (2 * sizeof(char*)) + 2; + char** resultArgv = (char**)malloc(allocSize); + if (resultArgv == NULL) + { + return NULL; + } + char* stringBuffer = (char*)(resultArgv + 2); + stringBuffer[0] = '/'; + stringBuffer[1] = '\0'; + + resultArgv[0] = stringBuffer; + resultArgv[1] = NULL; + + *count = 1; + return resultArgv; + +#elif defined(__linux__) || defined(__sun) + int fd = open("/proc/self/cmdline", O_RDONLY); + if (fd < 0) + { + return NULL; + } + + size_t bufSize = 1024; + char* buf = (char*)malloc(bufSize); + size_t totalBytes = 0; + ssize_t bytesRead; + + while ((bytesRead = read(fd, buf + totalBytes, bufSize - totalBytes - 1)) > 0) + { + totalBytes += (size_t)bytesRead; + if (totalBytes >= bufSize - 1) + { + bufSize *= 2; + char* newBuf = (char*)realloc(buf, bufSize); + if (newBuf == NULL) + { + free(buf); + close(fd); + return NULL; + } + buf = newBuf; + } + } + close(fd); + + if (totalBytes == 0) + { + free(buf); + return NULL; + } + buf[totalBytes] = '\0'; + + int argc = 0; + for (size_t i = 0; i < totalBytes; i++) + { + if (buf[i] == '\0') + { + argc++; + } + } + if (totalBytes > 0 && buf[totalBytes - 1] != '\0') + { + argc++; + } + + size_t allocSize = ((size_t)(argc + 1) * sizeof(char*)) + totalBytes + 1; + char** resultArgv = (char**)malloc(allocSize); + if (resultArgv == NULL) + { + free(buf); + return NULL; + } + + char* stringBuffer = (char*)(resultArgv + argc + 1); + memcpy(stringBuffer, buf, totalBytes + 1); + free(buf); + + int idx = 0; + char* ptr = stringBuffer; + while (idx < argc) + { + resultArgv[idx++] = ptr; + ptr += strlen(ptr) + 1; + } + resultArgv[argc] = NULL; + + *count = argc; + return resultArgv; + +#elif defined(__FreeBSD__) + int name[] = { CTL_KERN, KERN_PROC, KERN_PROC_ARGS, getpid() }; + size_t len = 0; + if (sysctl(name, 4, NULL, &len, NULL, 0) != 0 || len == 0) + { + return NULL; + } + + char* sysctlBuf = (char*)malloc(len); + if (sysctlBuf == NULL) + { + return NULL; + } + + if (sysctl(name, 4, sysctlBuf, &len, NULL, 0) != 0) + { + free(sysctlBuf); + return NULL; + } + + int argc = 0; + for (size_t i = 0; i < len; i++) + { + if (sysctlBuf[i] == '\0') + { + argc++; + } + } + + size_t allocSize = ((size_t)(argc + 1) * sizeof(char*)) + len; + char** resultArgv = (char**)malloc(allocSize); + if (resultArgv == NULL) + { + free(sysctlBuf); + return NULL; + } + + char* stringBuffer = (char*)(resultArgv + argc + 1); + memcpy(stringBuffer, sysctlBuf, len); + free(sysctlBuf); + + int idx = 0; + char* ptr = stringBuffer; + while (idx < argc) + { + resultArgv[idx++] = ptr; + ptr += strlen(ptr) + 1; + } + resultArgv[argc] = NULL; + + *count = argc; + return resultArgv; + +#elif defined(__OpenBSD__) + int name[] = { CTL_KERN, KERN_PROC_ARGS, getpid(), KERN_PROC_ARGV }; + size_t len = 0; + if (sysctl(name, 4, NULL, &len, NULL, 0) != 0 || len == 0) + { + return NULL; + } + + char* sysctlBuf = (char*)malloc(len); + if (sysctlBuf == NULL) + { + return NULL; + } + + if (sysctl(name, 4, sysctlBuf, &len, NULL, 0) != 0) + { + free(sysctlBuf); + return NULL; + } + + char** openbsdArgv = (char**)sysctlBuf; + int argc = 0; + size_t totalStringBytes = 0; + while (openbsdArgv[argc] != NULL) + { + totalStringBytes += strlen(openbsdArgv[argc]) + 1; + argc++; + } + + size_t allocSize = ((size_t)(argc + 1) * sizeof(char*)) + totalStringBytes; + char** resultArgv = (char**)malloc(allocSize); + if (resultArgv == NULL) + { + free(sysctlBuf); + return NULL; + } + + char* stringBuffer = (char*)(resultArgv + argc + 1); + for (int i = 0; i < argc; i++) + { + resultArgv[i] = stringBuffer; + size_t argLen = strlen(openbsdArgv[i]) + 1; + memcpy(stringBuffer, openbsdArgv[i], argLen); + stringBuffer += argLen; + } + resultArgv[argc] = NULL; + + *count = argc; + free(sysctlBuf); + return resultArgv; +#elif defined(__APPLE__) + int name[] = { CTL_KERN, KERN_PROCARGS2, getpid() }; + size_t len = 0; + if (sysctl(name, 3, NULL, &len, NULL, 0) != 0) + { + return NULL; + } + + char* sysctlBuf = (char*)malloc(len); + if (sysctlBuf == NULL) + { + return NULL; + } + + if (sysctl(name, 3, sysctlBuf, &len, NULL, 0) != 0) + { + free(sysctlBuf); + return NULL; + } + + int argc = 0; + memcpy(&argc, sysctlBuf, sizeof(argc)); + + char* ptr = sysctlBuf + sizeof(argc); + ptr += strlen(ptr) + 1; + + while (ptr < sysctlBuf + len && *ptr == '\0') + { + ptr++; + } + + char* startPtr = ptr; + size_t totalStringBytes = 0; + int actualCount = 0; + for (int i = 0; i < argc; i++) + { + if (ptr >= sysctlBuf + len) + { + break; + } + size_t argLen = strlen(ptr) + 1; + totalStringBytes += argLen; + ptr += argLen; + actualCount++; + } + + size_t allocSize = (((size_t)actualCount + 1) * sizeof(char*)) + totalStringBytes; + char** resultArgv = (char**)malloc(allocSize); + if (resultArgv == NULL) + { + free(sysctlBuf); + return NULL; + } + + char* stringBuffer = (char*)(resultArgv + actualCount + 1); + if (totalStringBytes > 0) + { + memcpy(stringBuffer, startPtr, totalStringBytes); + } + free(sysctlBuf); + + char* fillPtr = stringBuffer; + for (int i = 0; i < actualCount; i++) + { + resultArgv[i] = fillPtr; + fillPtr += strlen(fillPtr) + 1; + } + resultArgv[actualCount] = NULL; + + *count = actualCount; + return resultArgv; + +#elif defined(__HAIKU__) + image_info info; + int32 cookie = 0; + while (get_next_image_info(B_CURRENT_TEAM, &cookie, &info) == B_OK) + { + if (info.type == B_APP_IMAGE) + { + size_t len = strlen(info.args); + int argc = 0; + + if (len > 0) argc = 1; + for (size_t i = 0; i < len; i++) + { + if (info.args[i] == ' ') argc++; + } + + size_t allocSize = ((size_t)(argc + 1) * sizeof(char*)) + len + 1; + char** resultArgv = (char**)malloc(allocSize); + if (resultArgv == NULL) + { + return NULL; + } + + char* stringBuffer = (char*)(resultArgv + argc + 1); + memcpy(stringBuffer, info.args, len + 1); + + int idx = 0; + char* scanPtr = stringBuffer; + if (len > 0) + { + resultArgv[idx++] = scanPtr; + } + for (size_t i = 0; i < len; i++) + { + if (stringBuffer[i] == ' ') + { + stringBuffer[i] = '\0'; + resultArgv[idx++] = &stringBuffer[i + 1]; + } + } + resultArgv[argc] = NULL; + + *count = argc; + return resultArgv; + } + } + return NULL; + +#elif defined(TARGET_WASI) + unsigned short __wasi_args_sizes_get(size_t *retptr0, size_t *retptr1) __attribute__((__import_module__("wasi_snapshot_preview1"), __import_name__("args_sizes_get"))); + unsigned short __wasi_args_get(uint8_t **argv, uint8_t *argv_buf) __attribute__((__import_module__("wasi_snapshot_preview1"), __import_name__("args_get"))); + + size_t argc = 0; + size_t argvBufSize = 0; + if (__wasi_args_sizes_get(&argc, &argvBufSize) != 0) + { + return NULL; + } + + char** wasiArgv = (char**)malloc((argc + 1) * sizeof(char*)); + char* wasiBuf = (char*)malloc(argvBufSize); + if (wasiArgv == NULL || wasiBuf == NULL) + { + free(wasiArgv); + free(wasiBuf); + return NULL; + } + + if (__wasi_args_get((uint8_t**)wasiArgv, (uint8_t*)wasiBuf) != 0) + { + free(wasiArgv); + free(wasiBuf); + return NULL; + } + + size_t allocSize = ((argc + 1) * sizeof(char*)) + argvBufSize; + char** resultArgv = (char**)malloc(allocSize); + if (resultArgv == NULL) + { + free(wasiArgv); + free(wasiBuf); + return NULL; + } + + char* stringBuffer = (char*)((char**)resultArgv + argc + 1); + memcpy(stringBuffer, wasiBuf, argvBufSize); + + size_t wasiOffset = 0; + size_t k = 0; +wasi_rebase: + if (k < argc) + { + resultArgv[k] = stringBuffer + wasiOffset; + wasiOffset += strlen(stringBuffer + wasiOffset) + 1; + k++; + goto wasi_rebase; + } + resultArgv[argc] = NULL; + + *count = (int)argc; + + free(wasiArgv); + free(wasiBuf); + return resultArgv; + +#else + return NULL; +#endif +} + +#ifdef __cplusplus +} +#endif // extern "C" + +#endif // HAVE_MINIPAL_GETCMDLINE_H