├── .clang-format ├── .editorconfig ├── .gitattributes ├── .gitignore ├── BUILDING.md ├── ChildProcess.sln ├── ChildProcessExample.sln ├── Directory.Build.props ├── Directory.Build.targets ├── LICENSE ├── Protocol.md ├── README.ja.md ├── README.md ├── azure-pipelines.yml ├── build ├── BuildNativeLibUnix.ps1 ├── BuildNativeLibWin.ps1 ├── BuildPackage.ps1 ├── Common.ps1 ├── FetchNativeLib.ps1 ├── Invoke-CommandInVsDevCmd.cmd ├── SetAzurePipelineVariables.ps1 ├── SetVsDevCmdLocation.ps1 ├── StrongNameKeyPair.snk ├── SyncExample.ps1 ├── Version.txt ├── azure-pipelines │ ├── build-and-pack.yml │ ├── prepare-dotnet.yml │ ├── prepare-variables.yml │ └── test.yml ├── msbuild │ ├── ChildProcessNativeFiles.targets │ ├── CommonProps.csproj.props │ ├── CommonTargets.csproj.targets │ └── InjectChildProcessNativeFileDeps.targets └── psm │ └── Build.psm1 ├── global.json └── src ├── ChildProcess.Example ├── ChildProcess.Example.csproj ├── ChildProcessExamples.cs ├── ChildProcessExamplesUnix.cs ├── ChildProcessExamplesWindows.cs ├── GlobalSuppressions.cs └── Properties │ └── AssemblyInfo.cs ├── ChildProcess.ExamplePreview ├── ChildProcess.ExamplePreview.csproj ├── ChildProcessExamples.cs ├── ChildProcessExamplesUnix.cs ├── ChildProcessExamplesWindows.cs ├── GlobalSuppressions.cs └── Properties │ └── AssemblyInfo.cs ├── ChildProcess.ManualTest ├── ChildProcess.ManualTest.csproj ├── ChildProcessManualTestProgram.cs ├── ChildProcessManualTestProgramWindows.cs └── GlobalSuppressions.cs ├── ChildProcess.Native ├── .gitignore ├── AncillaryDataSocket.cpp ├── AsmichiChildProcess.symbols.txt ├── AsmichiChildProcess.version ├── Base.cpp ├── CMakeLists.txt ├── CMakeSettings.template.json ├── ChildProcessState.cpp ├── Exports.cpp ├── Globals.cpp ├── HelperExecutable.cpp ├── HelperMain.cpp ├── MiscHelpers.cpp ├── Request.cpp ├── Service.cpp ├── SignalHandler.cpp ├── SocketHelpers.cpp ├── Subbuild-unix.sh ├── Subbuild-win.ps1 ├── Subchannel.cpp ├── SubchannelCollection.cpp ├── WriteBuffer.cpp ├── cmake │ ├── toolchain-linux-arm.cmake │ ├── toolchain-linux-arm64.cmake │ ├── toolchain-linux-musl-arm64.cmake │ ├── toolchain-linux-musl-x64.cmake │ ├── toolchain-linux-x64.cmake │ ├── toolchain-osx-arm64.cmake │ ├── toolchain-osx-x64.cmake │ ├── toolchain-win-msvc.cmake │ ├── toolchain-win-x64.cmake │ └── toolchain-win-x86.cmake ├── config.h.in ├── include │ ├── AncillaryDataSocket.hpp │ ├── Base.hpp │ ├── BinaryReader.hpp │ ├── ChildProcessState.hpp │ ├── ErrorCodeExceptions.hpp │ ├── ExactBytesIO.hpp │ ├── Globals.hpp │ ├── MiscHelpers.hpp │ ├── Request.hpp │ ├── Service.hpp │ ├── SignalHandler.hpp │ ├── SocketHelpers.hpp │ ├── Subchannel.hpp │ ├── SubchannelCollection.hpp │ ├── UniqueResource.hpp │ └── WriteBuffer.hpp └── tests │ ├── DumpEnvironmentVariables.unix.cpp │ ├── DumpEnvironmentVariables.win.cpp │ ├── ReportSignal.unix.cpp │ ├── ReportSignal.win.cpp │ ├── Startup.unix.cpp │ ├── TestChildMain.cpp │ ├── TestSignalHandler.cpp │ └── TestSignalHandler.hpp ├── ChildProcess.Test ├── ChildProcess.Test.csproj ├── GlobalSuppressions.cs ├── ProcessManagement │ ├── ChildProcessCreationContextTest.cs │ ├── ChildProcessExecutionTestUtil.cs │ ├── ChildProcessStartInfoTest.cs │ ├── ChildProcessTest_Creation.cs │ ├── ChildProcessTest_EnvironmentVariables.cs │ ├── ChildProcessTest_Handle.cs │ ├── ChildProcessTest_Performance.cs │ ├── ChildProcessTest_ProcessTree.cs │ ├── ChildProcessTest_Redirection.cs │ ├── ChildProcessTest_Signals.cs │ ├── ChildProcessTest_Waiting.cs │ └── ChildProcessTest_Windows.cs ├── Properties │ └── AssemblyInfo.cs ├── Utilities │ ├── EnvironmentVariableListUtilTest.cs │ ├── SearchPathSearcherTest.cs │ ├── SortedEnvironmentVariableListBuilderTest.cs │ ├── TemporaryDirectory.cs │ ├── TestUtil.cs │ ├── WindowsCommandLineUtilTest.cs │ └── WindowsEnvironmentBlockUtilTest.cs └── xunit.runner.json ├── ChildProcess ├── ChildProcess.csproj ├── Interop │ ├── Linux │ │ └── LibChildProcess.cs │ └── Windows │ │ ├── InheritableHandleStore.cs │ │ ├── InputWriterOnlyPseudoConsole.cs │ │ ├── Kernel32.Console.cs │ │ ├── Kernel32.CreateProcess.cs │ │ ├── Kernel32.JobObject.cs │ │ ├── Kernel32.Pipe.cs │ │ ├── Kernel32.ProcThreadAttributeList.cs │ │ ├── Kernel32.cs │ │ ├── NtDll.cs │ │ ├── ProcThreadAttributeList.cs │ │ ├── SafeAnyHandle.cs │ │ ├── SafeJobObjectHandle.cs │ │ ├── SafePseudoConsoleHandle.cs │ │ ├── SafeThreadHandle.cs │ │ ├── SafeUnmanagedProcThreadAttributeList.cs │ │ └── WindowsVersion.cs ├── PlatformAbstraction │ ├── ConsolePal.cs │ ├── EnvironmentPal.cs │ ├── FilePal.cs │ ├── Pal.cs │ ├── Unix │ │ ├── UnixConsolePal.cs │ │ ├── UnixEnvironmentPal.cs │ │ └── UnixFilePal.cs │ ├── Utilities │ │ └── NamedPipeUtil.cs │ └── Windows │ │ ├── WindowsConsolePal.cs │ │ ├── WindowsEnvironmentPal.cs │ │ └── WindowsFilePal.cs ├── ProcessManagement │ ├── AsmichiChildProcessInternalLogicErrorException.cs │ ├── AsmichiChildProcessLibraryCrashedException.cs │ ├── ChildProcess.cs │ ├── ChildProcessCreationContext.cs │ ├── ChildProcessHelper.cs │ ├── ChildProcessImpl.cs │ ├── ChildProcessStartInfo.cs │ ├── ChildProcessStartInfoInternal.cs │ ├── ChildProcessStartingBlockedException.cs │ ├── EnvironmentVariableListCreation.cs │ ├── IChildProcess.cs │ ├── IChildProcessState.cs │ ├── IChildProcessStateHelper.cs │ ├── PipelineStdHandleCreator.cs │ ├── UnixChildProcessState.cs │ ├── UnixChildProcessStateHelper.cs │ ├── UnixHelperProcess.cs │ ├── UnixSubchannel.cs │ ├── WaitAsyncOperation.cs │ ├── WindowsChildProcessState.cs │ ├── WindowsChildProcessStateHelper.cs │ └── WindowsProcessWaitHandle.cs ├── Properties │ └── AssemblyInfo.cs └── Utilities │ ├── ArgumentValidationUtil.cs │ ├── CompletedBoolTask.cs │ ├── EnvironmentVariableListUtil.cs │ ├── EnvironmentVariablePairNameComparer.cs │ ├── EnvironmentVariableUtil.cs │ ├── MyBinaryWriter.cs │ ├── SearchPathSearcher.cs │ ├── SortedEnvironmentVariableListBuilder.cs │ ├── ThrowHelper.cs │ ├── WindowsCommandLineUtil.cs │ └── WindowsEnvironmentBlockUtil.cs ├── PrivateAssembly.ruleset ├── PublicAssembly.ruleset ├── TestChild ├── Kernel32.cs ├── Properties │ └── AssemblyInfo.cs ├── TestChild.csproj └── TestChildProgram.cs ├── docker ├── childprocess-buildtools-ubuntu-crosssysroot │ ├── Dockerfile │ ├── build-sysroot-alpine.sh │ └── extract-sysroot-ubuntu18.sh ├── childprocess-buildtools-ubuntu │ └── Dockerfile └── node-alpine-azuredevops │ └── Dockerfile └── stylecop.json /.clang-format: -------------------------------------------------------------------------------- 1 | --- 2 | Language: Cpp 3 | BasedOnStyle: LLVM 4 | AccessModifierOffset: -4 5 | AlignAfterOpenBracket: AlwaysBreak 6 | AlignConsecutiveAssignments: false 7 | AlignConsecutiveDeclarations: false 8 | AlignConsecutiveMacros: false 9 | AlignEscapedNewlines: false 10 | AlignOperands: false 11 | AlignTrailingComments: false 12 | AllowAllArgumentsOnNextLine: true 13 | AllowAllConstructorInitializersOnNextLine: true 14 | AllowAllParametersOfDeclarationOnNextLine: true 15 | AllowShortBlocksOnASingleLine: Empty 16 | AllowShortBlocksOnASingleLine: true 17 | AllowShortCaseLabelsOnASingleLine: true 18 | AllowShortFunctionsOnASingleLine: All 19 | AllowShortIfStatementsOnASingleLine: false 20 | AllowShortLambdasOnASingleLine: All 21 | AllowShortLoopsOnASingleLine: false 22 | AlwaysBreakAfterReturnType: None 23 | AlwaysBreakBeforeMultilineStrings: true 24 | AlwaysBreakTemplateDeclarations: true 25 | BinPackArguments: false 26 | BinPackParameters: false 27 | BreakBeforeBinaryOperators: None 28 | BreakBeforeBraces: Allman 29 | BreakBeforeBinaryOperators: NonAssignment 30 | BreakBeforeInheritanceComma: false 31 | BreakBeforeTernaryOperators: true 32 | BreakConstructorInitializersBeforeComma: false 33 | BreakConstructorInitializers: BeforeColon 34 | BreakInheritanceList: BeforeColon 35 | BreakStringLiterals: true 36 | ColumnLimit: 0 37 | CommentPragmas: '^ IWYU pragma:' 38 | CompactNamespaces: false 39 | ConstructorInitializerAllOnOneLineOrOnePerLine: true 40 | ConstructorInitializerIndentWidth: 4 41 | ContinuationIndentWidth: 4 42 | Cpp11BracedListStyle: true 43 | DerivePointerAlignment: false 44 | DisableFormat: false 45 | ExperimentalAutoDetectBinPacking: false 46 | FixNamespaceComments: true 47 | ForEachMacros: 48 | - foreach 49 | - Q_FOREACH 50 | - BOOST_FOREACH 51 | IncludeBlocks: Preserve 52 | IncludeCategories: 53 | - Regex: '^(<|"(gtest|gmock|isl|json)/)' 54 | Priority: 3 55 | - Regex: '<[[:alnum:].]+>' 56 | Priority: 4 57 | - Regex: '.*' 58 | Priority: 1 59 | IncludeIsMainRegex: '(Test)?$' 60 | IndentCaseLabels: false 61 | IndentPPDirectives: None 62 | IndentWidth: 4 63 | IndentWrappedFunctionNames: false 64 | KeepEmptyLinesAtTheStartOfBlocks: false 65 | MacroBlockBegin: '' 66 | MacroBlockEnd: '' 67 | MaxEmptyLinesToKeep: 1 68 | NamespaceIndentation: All 69 | NamespaceMacros: [] 70 | PenaltyBreakAssignment: 2 71 | PenaltyBreakBeforeFirstCallParameter: 19 72 | PenaltyBreakComment: 300 73 | PenaltyBreakFirstLessLess: 120 74 | PenaltyBreakString: 1000 75 | PenaltyExcessCharacter: 1000000 76 | PenaltyReturnTypeOnItsOwnLine: 60 77 | PointerAlignment: Left 78 | RawStringFormats: 79 | ReflowComments: true 80 | SortIncludes: true 81 | SortUsingDeclarations: true 82 | SpaceAfterCStyleCast: false 83 | SpaceAfterLogicalNot: false 84 | SpaceAfterTemplateKeyword: false 85 | SpaceBeforeAssignmentOperators: true 86 | SpaceBeforeCpp11BracedList: false 87 | SpaceBeforeCtorInitializerColon: true 88 | SpaceBeforeInheritanceColon: true 89 | SpaceBeforeParens: ControlStatements 90 | SpaceBeforeRangeBasedForLoopColon: true 91 | SpaceInEmptyParentheses: false 92 | SpacesBeforeTrailingComments: 1 93 | SpacesInAngles: false 94 | SpacesInCStyleCastParentheses: false 95 | SpacesInParentheses: false 96 | SpacesInSquareBrackets: false 97 | Standard: Cpp11 98 | StatementMacros: [] 99 | TabWidth: 4 100 | TypenameMacros: [] 101 | UseTab: Never 102 | ... 103 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * -text 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.binlog 2 | *.suo 3 | *.user 4 | *launchSettings.json 5 | .vs/ 6 | .vscode/ 7 | bin/ 8 | obj/ 9 | -------------------------------------------------------------------------------- /BUILDING.md: -------------------------------------------------------------------------------- 1 | # Building Asmichi.ChildProcess 2 | 3 | ## Environment 4 | 5 | - Windows 10 or 11 6 | - WSL2 required 7 | - PowerShell Core 7 8 | - Visual Studio 2019 (only if you are modifying the native implementation) 9 | - The Microsoft.VisualStudio.Workload.NativeDesktop workload required 10 | - Visual Studio 2022 11 | - Docker for Windows, Linux containers mode (only if you are modifying the native implementation) 12 | 13 | ## Writing and Testing code 14 | 15 | - `pwsh .\build\FetchNativeLib.ps1` 16 | - Open ChildProcess.sln with Visual Studio 2022. 17 | 18 | In order to edit the native implementation: 19 | 20 | - Set up an Ubuntu host (22.04 recommended) 21 | - Execute: 22 | ``` 23 | apt-get install clang lld make cmake 24 | ``` 25 | (See also [src\docker\childprocess-buildtools-ubuntu\Dockerfile](src\docker\childprocess-buildtools-ubuntu\Dockerfile)) 26 | - copy `src\ChildProcess.Native\CMakeSettings.template.json` to `src\ChildProcess.Native\CMakeSettings.json` 27 | - Launch Visual Studio 2019 and "Open CMake" at src\ChildProcess.Native\CMakeLists.txt. 28 | 29 | ## Building Native Implementation 30 | 31 | On Windows: 32 | ``` 33 | pwsh .\build\BuildNativeLibWin.ps1 34 | ``` 35 | 36 | On macOS: 37 | ``` 38 | pwsh .\build\BuildNativeLibUnix.ps1 39 | ``` 40 | 41 | 42 | ## Building Package 43 | 44 | On Windows: 45 | 46 | ```powershell 47 | pwsh .\build\BuildPackage.ps1 48 | ``` 49 | -------------------------------------------------------------------------------- /ChildProcess.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.1.32210.238 5 | MinimumVisualStudioVersion = 15.0.26124.0 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ChildProcess", "src\ChildProcess\ChildProcess.csproj", "{B0701ACF-C9E0-4835-8787-925D0A6EFB36}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ChildProcess.ExamplePreview", "src\ChildProcess.ExamplePreview\ChildProcess.ExamplePreview.csproj", "{0070EB5F-91AC-41AA-972A-8C2075D329CE}" 9 | EndProject 10 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ChildProcess.Test", "src\ChildProcess.Test\ChildProcess.Test.csproj", "{901BCC9B-101A-41C4-9944-53EC7AD71B4E}" 11 | EndProject 12 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TestChild", "src\TestChild\TestChild.csproj", "{F4613733-FC19-47E0-98CF-FAFE4FA7543C}" 13 | EndProject 14 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ChildProcess.ManualTest", "src\ChildProcess.ManualTest\ChildProcess.ManualTest.csproj", "{6BC75185-F986-486C-8E6A-4D7FEA19B8A3}" 15 | EndProject 16 | Global 17 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 18 | Debug|AnyCPU = Debug|AnyCPU 19 | Release|AnyCPU = Release|AnyCPU 20 | EndGlobalSection 21 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 22 | {B0701ACF-C9E0-4835-8787-925D0A6EFB36}.Debug|AnyCPU.ActiveCfg = Debug|Any CPU 23 | {B0701ACF-C9E0-4835-8787-925D0A6EFB36}.Debug|AnyCPU.Build.0 = Debug|Any CPU 24 | {B0701ACF-C9E0-4835-8787-925D0A6EFB36}.Release|AnyCPU.ActiveCfg = Release|Any CPU 25 | {B0701ACF-C9E0-4835-8787-925D0A6EFB36}.Release|AnyCPU.Build.0 = Release|Any CPU 26 | {0070EB5F-91AC-41AA-972A-8C2075D329CE}.Debug|AnyCPU.ActiveCfg = Debug|Any CPU 27 | {0070EB5F-91AC-41AA-972A-8C2075D329CE}.Debug|AnyCPU.Build.0 = Debug|Any CPU 28 | {0070EB5F-91AC-41AA-972A-8C2075D329CE}.Release|AnyCPU.ActiveCfg = Release|Any CPU 29 | {0070EB5F-91AC-41AA-972A-8C2075D329CE}.Release|AnyCPU.Build.0 = Release|Any CPU 30 | {901BCC9B-101A-41C4-9944-53EC7AD71B4E}.Debug|AnyCPU.ActiveCfg = Debug|Any CPU 31 | {901BCC9B-101A-41C4-9944-53EC7AD71B4E}.Debug|AnyCPU.Build.0 = Debug|Any CPU 32 | {901BCC9B-101A-41C4-9944-53EC7AD71B4E}.Release|AnyCPU.ActiveCfg = Release|Any CPU 33 | {901BCC9B-101A-41C4-9944-53EC7AD71B4E}.Release|AnyCPU.Build.0 = Release|Any CPU 34 | {F4613733-FC19-47E0-98CF-FAFE4FA7543C}.Debug|AnyCPU.ActiveCfg = Debug|Any CPU 35 | {F4613733-FC19-47E0-98CF-FAFE4FA7543C}.Debug|AnyCPU.Build.0 = Debug|Any CPU 36 | {F4613733-FC19-47E0-98CF-FAFE4FA7543C}.Release|AnyCPU.ActiveCfg = Release|Any CPU 37 | {F4613733-FC19-47E0-98CF-FAFE4FA7543C}.Release|AnyCPU.Build.0 = Release|Any CPU 38 | {6BC75185-F986-486C-8E6A-4D7FEA19B8A3}.Debug|AnyCPU.ActiveCfg = Debug|Any CPU 39 | {6BC75185-F986-486C-8E6A-4D7FEA19B8A3}.Debug|AnyCPU.Build.0 = Debug|Any CPU 40 | {6BC75185-F986-486C-8E6A-4D7FEA19B8A3}.Release|AnyCPU.ActiveCfg = Release|Any CPU 41 | {6BC75185-F986-486C-8E6A-4D7FEA19B8A3}.Release|AnyCPU.Build.0 = Release|Any CPU 42 | EndGlobalSection 43 | GlobalSection(SolutionProperties) = preSolution 44 | HideSolutionNode = FALSE 45 | EndGlobalSection 46 | GlobalSection(ExtensibilityGlobals) = postSolution 47 | SolutionGuid = {99E69FAD-659F-43A5-9F6F-AB5050052F4E} 48 | EndGlobalSection 49 | EndGlobal 50 | -------------------------------------------------------------------------------- /ChildProcessExample.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.1.32210.238 5 | MinimumVisualStudioVersion = 15.0.26124.0 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ChildProcess.Example", "src\ChildProcess.Example\ChildProcess.Example.csproj", "{53AC2561-9FDB-4E14-987A-C3FE51666FA2}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|AnyCPU = Debug|AnyCPU 11 | Release|AnyCPU = Release|AnyCPU 12 | EndGlobalSection 13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 14 | {53AC2561-9FDB-4E14-987A-C3FE51666FA2}.Debug|AnyCPU.ActiveCfg = Debug|Any CPU 15 | {53AC2561-9FDB-4E14-987A-C3FE51666FA2}.Debug|AnyCPU.Build.0 = Debug|Any CPU 16 | {53AC2561-9FDB-4E14-987A-C3FE51666FA2}.Release|AnyCPU.ActiveCfg = Release|Any CPU 17 | {53AC2561-9FDB-4E14-987A-C3FE51666FA2}.Release|AnyCPU.Build.0 = Release|Any CPU 18 | EndGlobalSection 19 | GlobalSection(SolutionProperties) = preSolution 20 | HideSolutionNode = FALSE 21 | EndGlobalSection 22 | GlobalSection(ExtensibilityGlobals) = postSolution 23 | SolutionGuid = {99E69FAD-659F-43A5-9F6F-AB5050052F4E} 24 | EndGlobalSection 25 | EndGlobal 26 | -------------------------------------------------------------------------------- /Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | $(MSBuildThisFileDirectory) 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Directory.Build.targets: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) @asmichi (https://github.com/asmichi) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Protocol.md: -------------------------------------------------------------------------------- 1 | 2 | # Protocol 3 | 4 | - All numeric values are encoded in native byte order. 5 | 6 | ## Channels 7 | 8 | - A) Main subchannel request channel, unidirectional, client → server 9 | - B) Main notification channel, unidirectional, server → client 10 | - C) Subchannel, bidirectional 11 | 12 | ### A) Main subchannel request channel 13 | 14 | Subchannel creation. 15 | 16 | The client shall send 1 dummy byte with a unix domain socket fd in the ancillary data. 17 | 18 | ### B) Main notification channel 19 | 20 | Notifications of exited chlid processes. 21 | 22 | For each child process that has exited, a ChildExitNotification struct shall be sent. 23 | 24 | ### C) Subchannel, full-duplex 25 | 26 | Every request shall be prefixed with two 32-bit integer. The first specifies a command number. 27 | The second specifies the length of the request body. 28 | 29 | The error code is defined as follows: 30 | 31 | - 0: Success 32 | - -1: Invalid request 33 | - Positive: errno 34 | 35 | #### Spawn Process (Command 0) 36 | 37 | `execve` semantics. 38 | 39 | Request body: 40 | 41 | - Process token (64) 42 | - flags (32) (NOTE: fds must be sent in this order). 43 | - Redirect stdin (1) 44 | - Redirect stdout (1) 45 | - Redirect stderr (1) 46 | - working directory (N) 47 | - file (N) 48 | - argv (N) 49 | - envp (N) 50 | 51 | Response: 52 | 53 | - Error code (32) 54 | - pid (32) 55 | 56 | #### Signal (Command 1) 57 | 58 | Request body: 59 | 60 | - Process token (64) 61 | - Signal (32) 62 | 63 | Response: 64 | 65 | - Error code (32) 66 | 67 | Signal: 68 | 69 | - 2: SIGINT 70 | - 9: SIGKILL 71 | - 15: SIGTERM 72 | 73 | -------------------------------------------------------------------------------- /build/BuildNativeLibUnix.ps1: -------------------------------------------------------------------------------- 1 | # Copyright (c) @asmichi (https://github.com/asmichi). Licensed under the MIT License. See LICENCE in the project worktreeRoot for details. 2 | 3 | #Requires -Version 7.0 4 | 5 | # TODO: Embed the commit hash. 6 | 7 | param( 8 | [switch] 9 | [bool] 10 | $Rebuild = $false, 11 | [parameter()] 12 | [string] 13 | $OS = "OSX" 14 | ) 15 | 16 | Set-StrictMode -Version latest 17 | 18 | Import-Module "$PSScriptRoot/psm/Build.psm1" 19 | 20 | $worktreeRoot = Resolve-Path (Join-Path $PSScriptRoot "..") 21 | 22 | if (-not (@("OSX") -contains $OS)) { 23 | Write-Error "Unknown OS: ${OS}" 24 | exit 1 25 | } 26 | 27 | # Prepare the output directory. 28 | $objDir = "${worktreeRoot}/obj/ChildProcess.Native/Subbuild-${OS}" 29 | $binDir = "${worktreeRoot}/bin/ChildProcess.Native" 30 | New-Item -ItemType Directory -Force $objDir | Out-Null 31 | New-Item -ItemType Directory -Force $binDir | Out-Null 32 | 33 | if ($Rebuild) { 34 | try { 35 | Remove-Item $objDir/* -Recurse 36 | } 37 | catch {} 38 | } 39 | 40 | $subbuildUnix = Join-Path $worktreeRoot "src/ChildProcess.Native/Subbuild-unix.sh" 41 | bash $subbuildUnix OSX ${objdir} $binDir 42 | 43 | if ($LASTEXITCODE -ne 0) { 44 | Write-Error "*** BuildNativeLib FAILED ***" 45 | exit 1 46 | } 47 | 48 | exit 0 49 | -------------------------------------------------------------------------------- /build/BuildNativeLibWin.ps1: -------------------------------------------------------------------------------- 1 | # Copyright (c) @asmichi (https://github.com/asmichi). Licensed under the MIT License. See LICENCE in the project worktreeRoot for details. 2 | 3 | #Requires -Version 7.0 4 | 5 | # TODO: Embed the commit hash. 6 | 7 | param( 8 | [switch] 9 | [bool] 10 | $Rebuild = $false, 11 | [parameter()] 12 | [string] 13 | $NamePrefix = "asmichi" 14 | ) 15 | 16 | Set-StrictMode -Version latest 17 | 18 | $ErrorActionPreference = "Stop" 19 | 20 | Import-Module "$PSScriptRoot\psm\Build.psm1" 21 | 22 | $worktreeRoot = Resolve-Path (Join-Path $PSScriptRoot "..") 23 | 24 | $linuxImageName = "asmichi/childprocess-buildtools-ubuntu-crosssysroot:22.04.20240702.1" 25 | $linuxContainerName = "${NamePrefix}-buildnativelib-linux" 26 | $buildVolumeName = "${NamePrefix}-buildnativelib-linux" 27 | 28 | $successful = $true 29 | 30 | # Prepare the output directory. 31 | $objDir = "${worktreeRoot}\obj\ChildProcess.Native" 32 | $binDir = "${worktreeRoot}\bin\ChildProcess.Native" 33 | New-Item -ItemType Directory -Force $objDir | Out-Null 34 | New-Item -ItemType Directory -Force $binDir | Out-Null 35 | 36 | # Build Windows binaries. 37 | if ($Rebuild) { 38 | try { 39 | Remove-Item $objDir/* -Recurse 40 | } 41 | catch {} 42 | } 43 | 44 | $pwsh = Join-Path $PSHOME "pwsh.exe" 45 | $subbuildWin = Join-Path $worktreeRoot "src/ChildProcess.Native/Subbuild-win.ps1" 46 | $subbuildWinArgs = @( 47 | $worktreeRoot 48 | ) 49 | 50 | $winJob = $null 51 | if (Test-Path Env:VCToolsInstallDir) { 52 | # Already within VsDevCmd 53 | $winJob = Start-ThreadJob -ScriptBlock { & $using:pwsh $using:subbuildWin @using:subbuildWinArgs } 54 | } 55 | else { 56 | $invokeCommandInVsDevCmd = Join-Path $worktreeRoot "build/Invoke-CommandInVsDevCmd.cmd" 57 | $vsDevCmd = Get-VsDevCmdLocation 58 | $winJob = Start-ThreadJob -ScriptBlock { & $using:invokeCommandInVsDevCmd $using:vsDevCmd $using:pwsh $using:subbuildWin @using:subbuildWinArgs } 59 | } 60 | 61 | # Build Linux binaries. 62 | if ($Rebuild) { 63 | try { 64 | docker volume rm $buildVolumeName 2>&1 | Out-Null 65 | } 66 | catch {} 67 | } 68 | 69 | try { 70 | docker rm $linuxContainerName 2>&1 | Out-Null 71 | } 72 | catch {} 73 | 74 | docker volume create $buildVolumeName | Out-Null 75 | docker run ` 76 | --name $linuxContainerName ` 77 | --mount "type=bind,readonly,source=${worktreeRoot}/src/ChildProcess.Native,target=/proj/src" ` 78 | --mount "type=volume,src=${buildVolumeName},dst=/proj/obj" ` 79 | $linuxImageName /bin/bash /proj/src/Subbuild-unix.sh Linux /proj/obj /proj/obj/out/ChildProcess.Native 80 | 81 | if ($LASTEXITCODE -ne 0) { 82 | $successful = $false 83 | } 84 | 85 | # If the container mounts and writes directly to a host directory, generated files will have 86 | # NTFS extended attributes (EAs) $LXUID/$LXGID/$LXMOD. There is no way to remove NTFS EAs via Win32 APIs. 87 | # Even worse, NTFS EAs will be copied by CopyFile. (We can of course effectively remove NTFS EAs by creating a new file 88 | # and copying only the file data to it). 89 | # 90 | # Avoid mouting a host directory and do docker cp. 91 | docker cp "${linuxContainerName}:/proj/obj/out/ChildProcess.Native/." "${worktreeRoot}/bin/ChildProcess.Native" 92 | docker rm $linuxContainerName | Out-Null 93 | 94 | if ($null -ne $winJob) { 95 | Receive-Job -Wait -AutoRemoveJob $winJob -ErrorAction Continue 96 | if (-not $?) { 97 | $successful = $false 98 | } 99 | } 100 | 101 | if (-not $successful) { 102 | Write-Error "*** BuildNativeLib FAILED ***" 103 | } 104 | -------------------------------------------------------------------------------- /build/BuildPackage.ps1: -------------------------------------------------------------------------------- 1 | 2 | # Copyright (c) @asmichi (https://github.com/asmichi). Licensed under the MIT License. See LICENCE in the project root for details. 3 | 4 | # Build the package on a local environment. 5 | 6 | #Requires -Version 7.0 7 | 8 | param( 9 | [parameter()] 10 | [switch] 11 | $IncrementalBuild = $false, 12 | [parameter()] 13 | [switch] 14 | $AllowRetailRelease = $false 15 | ) 16 | 17 | Set-StrictMode -Version latest 18 | $ErrorActionPreference = "Stop" 19 | 20 | Import-Module "$PSScriptRoot\psm\Build.psm1" 21 | 22 | $worktreeRoot = Resolve-Path "$PSScriptRoot\.." 23 | . $worktreeRoot\Build\Common.ps1 24 | $slnFile = "$worktreeRoot\ChildProcess.sln" 25 | 26 | $commitHash = $(git rev-parse HEAD) 27 | $branchName = $(git rev-parse --abbrev-ref HEAD) 28 | $versionInfo = Get-VersionInfo -CommitHash $commitHash -BranchName $branchName -AllowRetailRelease:$AllowRetailRelease 29 | 30 | $commonBuildOptions = Get-CommonBuildOptions -VersionInfo $versionInfo 31 | 32 | Exec { dotnet build @commonBuildOptions $slnFile } 33 | Exec { dotnet test @commonBuildOptions --no-build "$worktreeRoot\src\ChildProcess.Test\ChildProcess.Test.csproj" } 34 | Exec { dotnet pack @commonBuildOptions --no-build "$worktreeRoot\src\ChildProcess\ChildProcess.csproj" } 35 | -------------------------------------------------------------------------------- /build/Common.ps1: -------------------------------------------------------------------------------- 1 | 2 | # Copyright (c) @asmichi (https://github.com/asmichi). Licensed under the MIT License. See LICENCE in the project root for details. 3 | 4 | Set-StrictMode -Version latest 5 | $ErrorActionPreference = "Stop" 6 | 7 | function Exec { 8 | param( 9 | [parameter(Mandatory = $true)] 10 | [scriptblock] 11 | $cmd 12 | ) 13 | 14 | & $cmd 15 | 16 | if ($LASTEXITCODE -ne 0) { 17 | Write-Error "Command failed with exit code ${LASTEXITCODE}: $cmd" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /build/Invoke-CommandInVsDevCmd.cmd: -------------------------------------------------------------------------------- 1 | @echo off 2 | setlocal 3 | rem Invoke-CommandInVsDevCmd.cmd [path to VsDevCmd.bat] [path to command] [arg1] [arg2] ... [arg7] 4 | rem Invoke a command within the VsDevCmd environment 5 | call %1 -no_logo -arch=amd64 -host_arch=amd64 6 | %2 %3 %4 %5 %6 %7 %8 %9 7 | 8 | endlocal -------------------------------------------------------------------------------- /build/SetAzurePipelineVariables.ps1: -------------------------------------------------------------------------------- 1 | 2 | # Copyright (c) @asmichi (https://github.com/asmichi). Licensed under the MIT License. See LICENCE in the project root for details. 3 | 4 | #Requires -Version 7.0 5 | 6 | param 7 | ( 8 | [Parameter(Mandatory = $true)] 9 | [string] 10 | $CommitHash, 11 | [Parameter(Mandatory = $true)] 12 | [string] 13 | $BranchName, 14 | [parameter()] 15 | [switch] 16 | $AllowRetailRelease = $false 17 | ) 18 | 19 | Set-StrictMode -Version latest 20 | $ErrorActionPreference = "Stop" 21 | 22 | Import-Module "$PSScriptRoot\psm\Build.psm1" 23 | 24 | $versionInfo = Get-VersionInfo -CommitHash $CommitHash -BranchName $BranchName -AllowRetailRelease:$AllowRetailRelease 25 | $commonBuildOptions = Get-CommonBuildOptions -VersionInfo $versionInfo 26 | $commonBuildOptionsString = [string]$commonBuildOptions 27 | 28 | Write-Host "##vso[task.setvariable variable=PackageVersion]$($versionInfo.PackageVersion)" 29 | Write-Host "##vso[task.setvariable variable=CommonBuildOptions]$commonBuildOptionsString" 30 | Write-Host "##vso[task.setvariable variable=DOTNET_NOLOGO]1" 31 | Write-Host "##vso[task.setvariable variable=DOTNET_CLI_TELEMETRY_OPTOUT]1" 32 | -------------------------------------------------------------------------------- /build/SetVsDevCmdLocation.ps1: -------------------------------------------------------------------------------- 1 | 2 | # Copyright (c) @asmichi (https://github.com/asmichi). Licensed under the MIT License. See LICENCE in the project root for details. 3 | 4 | #Requires -Version 7.0 5 | 6 | param 7 | ( 8 | ) 9 | 10 | Set-StrictMode -Version latest 11 | $ErrorActionPreference = "Stop" 12 | 13 | Import-Module "$PSScriptRoot\psm\Build.psm1" 14 | 15 | $vsDevCmd = Get-VsDevCmdLocation 16 | 17 | Write-Host "##vso[task.setvariable variable=VSDEVCMD_LOCATION]$vsDevCmd" 18 | -------------------------------------------------------------------------------- /build/StrongNameKeyPair.snk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/asmichi/ChildProcess/823a7d5f1ba674013706b2f1d90a916a87be4a49/build/StrongNameKeyPair.snk -------------------------------------------------------------------------------- /build/SyncExample.ps1: -------------------------------------------------------------------------------- 1 | 2 | # Copyright (c) @asmichi (https://github.com/asmichi). Licensed under the MIT License. See LICENCE in the project root for details. 3 | 4 | # Update the example using the published version (which allows users to easily run the example). 5 | 6 | #Requires -Version 7.0 7 | 8 | Set-StrictMode -Version latest 9 | $ErrorActionPreference = "Stop" 10 | 11 | $worktreeRoot = Resolve-Path "$PSScriptRoot\.." 12 | $exampleDir = "$worktreeRoot\src\ChildProcess.Example" 13 | $examplePreviewDir = "$worktreeRoot\src\ChildProcess.ExamplePreview" 14 | $baseVersion = Get-Content "$worktreeRoot\build\Version.txt" 15 | 16 | Get-ChildItem $exampleDir -Include "*.cs" | Remove-Item 17 | Copy-Item "$examplePreviewDir\*.cs" $exampleDir 18 | $csprojContent = Get-Content "$exampleDir\ChildProcess.Example.csproj" 19 | $csprojContent = $csprojContent -creplace "Version=`".*`"","Version=`"$baseVersion`"" 20 | Set-Content "$exampleDir\ChildProcess.Example.csproj" -Value $csprojContent -Encoding utf8BOM 21 | -------------------------------------------------------------------------------- /build/Version.txt: -------------------------------------------------------------------------------- 1 | 0.19.0 2 | -------------------------------------------------------------------------------- /build/azure-pipelines/build-and-pack.yml: -------------------------------------------------------------------------------- 1 | parameters: [] 2 | 3 | steps: 4 | - template: 'prepare-variables.yml' 5 | - template: 'prepare-dotnet.yml' 6 | 7 | - task: DownloadPipelineArtifact@2 8 | inputs: 9 | source: current 10 | artifact: 'ChildProcess.Native-linux' 11 | path: '$(Build.SourcesDirectory)/bin/ChildProcess.Native' 12 | 13 | - task: DownloadPipelineArtifact@2 14 | inputs: 15 | source: current 16 | artifact: 'ChildProcess.Native-osx' 17 | path: '$(Build.SourcesDirectory)/bin/ChildProcess.Native' 18 | 19 | - task: DownloadPipelineArtifact@2 20 | inputs: 21 | source: current 22 | artifact: 'ChildProcess.Native-win' 23 | path: '$(Build.SourcesDirectory)/bin/ChildProcess.Native' 24 | 25 | - task: DotNetCoreCLI@2 26 | displayName: 'dotnet build' 27 | inputs: 28 | projects: '$(SolutionFile)' 29 | arguments: '$(CommonBuildOptions)' 30 | verbosityRestore: Quiet 31 | 32 | - task: NuGetToolInstaller@0 33 | displayName: 'Use NuGet 5.8.1' 34 | inputs: 35 | versionSpec: 5.8.1 36 | 37 | - task: DotNetCoreCLI@2 38 | displayName: 'dotnet pack' 39 | inputs: 40 | command: custom 41 | custom: pack 42 | arguments: '$(Build.SourcesDirectory)/src/ChildProcess/ChildProcess.csproj $(CommonBuildOptions) --output $(Build.ArtifactStagingDirectory)' 43 | 44 | - task: NuGetCommand@2 45 | displayName: 'NuGet push' 46 | inputs: 47 | command: push 48 | packagesToPush: '$(Build.ArtifactStagingDirectory)/**/*.nupkg;$(Build.ArtifactStagingDirectory)/**/*.snupkg' 49 | allowPackageConflicts: true 50 | configurationToPack: 'Release' 51 | nuGetFeedType: 'internal' 52 | publishVstsFeed: 'ChildProcess/CI' 53 | 54 | - task: PublishBuildArtifacts@1 55 | displayName: 'Publish Artifact: drop' 56 | inputs: 57 | PathtoPublish: '$(Build.ArtifactStagingDirectory)' 58 | 59 | - task: DotNetCoreCLI@2 60 | displayName: 'dotnet build' 61 | inputs: 62 | projects: '$(SolutionFile)' 63 | arguments: '$(CommonBuildOptions) -p:AddImportSearchPathAssemblyDirectory=true' # Workaround for https://github.com/dotnet/sdk/issues/1088 64 | verbosityRestore: Quiet 65 | 66 | - publish: 'bin/ChildProcess.Test' 67 | artifact: 'ChildProcess.Test' 68 | -------------------------------------------------------------------------------- /build/azure-pipelines/prepare-dotnet.yml: -------------------------------------------------------------------------------- 1 | parameters: [] 2 | 3 | steps: 4 | - task: UseDotNet@2 5 | displayName: 'Use .NET SDK 8.0.302' 6 | inputs: 7 | packageType: sdk 8 | version: 8.0.302 9 | -------------------------------------------------------------------------------- /build/azure-pipelines/prepare-variables.yml: -------------------------------------------------------------------------------- 1 | parameters: [] 2 | 3 | steps: 4 | - task: PowerShell@2 5 | displayName: 'Set Variables' 6 | inputs: 7 | pwsh: true 8 | targetType: 'filePath' 9 | failOnStderr: true 10 | filePath: '$(Build.SourcesDirectory)/build/SetAzurePipelineVariables.ps1' 11 | arguments: '-CommitHash $(Build.SourceVersion) -BranchName $(Build.SourceBranchName) -AllowRetailRelease' 12 | -------------------------------------------------------------------------------- /build/azure-pipelines/test.yml: -------------------------------------------------------------------------------- 1 | # NOTE: This does not invoke prepare-variables.yml. 2 | parameters: 3 | - name: platform 4 | type: string 5 | values: 6 | - osx 7 | - linux 8 | - win 9 | - name: rid 10 | type: string 11 | values: 12 | - osx-x64 13 | - linux-x64 14 | - linux-musl-x64 15 | - win-x64 16 | - name: configuration 17 | type: string 18 | values: 19 | - Debug 20 | - Release 21 | default: Release 22 | - name: testTimeoutMilliseconds 23 | type: number 24 | default: 180000 25 | 26 | steps: 27 | - template: 'prepare-dotnet.yml' 28 | 29 | - task: DownloadPipelineArtifact@2 30 | inputs: 31 | source: current 32 | artifact: 'ChildProcess.Native-${{ parameters.platform }}' 33 | path: '$(Build.SourcesDirectory)/bin/ChildProcess.Native' 34 | 35 | - task: DownloadPipelineArtifact@2 36 | inputs: 37 | source: current 38 | artifact: 'ChildProcess.Test' 39 | path: '$(Build.SourcesDirectory)/bin/ChildProcess.Test' 40 | 41 | # Azure DevOps's artifacts do not preserve permissions. Add +x here. 42 | - task: Bash@3 43 | condition: ${{ ne(parameters.platform, 'win') }} 44 | inputs: 45 | targetType: 'inline' 46 | script: 'chmod -R +x $(Build.SourcesDirectory)/bin/ChildProcess.Native $(Build.SourcesDirectory)/bin/ChildProcess.Test/AnyCPU/${{ parameters.configuration }}/net8.0/runtimes' 47 | 48 | # Our Alpine container cannot build, so copy TestChildNative here. 49 | - task: CopyFiles@2 50 | inputs: 51 | sourceFolder: '$(Build.SourcesDirectory)/bin/ChildProcess.Native/${{ parameters.rid }}/${{ parameters.configuration }}' 52 | contents: 'TestChildNative*' 53 | targetFolder: '$(Build.SourcesDirectory)/bin/ChildProcess.Test/AnyCPU/${{ parameters.configuration }}/net8.0' 54 | overwrite: true 55 | 56 | - task: DotNetCoreCLI@2 57 | displayName: 'dotnet test' 58 | inputs: 59 | command: test 60 | projects: '$(Build.SourcesDirectory)/bin/ChildProcess.Test/AnyCPU/${{ parameters.configuration }}/net8.0/Asmichi.ChildProcess.Test.dll' 61 | arguments: '-- RunConfiguration.TestSessionTimeout=${{ parameters.testTimeoutMilliseconds }}' 62 | testRunTitle: ${{ parameters.rid }} 63 | -------------------------------------------------------------------------------- /build/msbuild/ChildProcessNativeFiles.targets: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /build/msbuild/CommonProps.csproj.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Release 6 | AnyCPU 7 | 8 | 9 | 10 | 11 | false 12 | 13 | $(WorkTreeRoot)obj\ 14 | $(ObjDir)$(MSBuildProjectName)\ 15 | $(BaseIntermediateOutputPath)$(Platform)\$(Configuration)\ 16 | $(IntDir)$(TargetFramework)\ 17 | $(IntDir) 18 | 19 | 20 | $(WorkTreeRoot)bin\ 21 | $(BinDir)\$(MSBuildProjectName)\$(Platform)\$(Configuration)\ 22 | $(OutDir)$(TargetFramework)\ 23 | $(OutDir) 24 | 25 | $(BinDir)\nupkg\ 26 | 27 | 28 | 29 | 30 | true 31 | portable 32 | true 33 | 9.0 34 | enable 35 | 1591;1701;1702 36 | true 37 | latest 38 | AllEnabledByDefault 39 | 40 | 41 | 42 | 43 | 44 | en 45 | 46 | 47 | 48 | 49 | 0.19.0.0 50 | 0.19.0.0 51 | 0.19.0.0+unknown 52 | 0.19.0-localbuild+unknown 53 | 54 | 55 | 56 | 57 | asmichi 58 | asmichi 59 | Copyright (c) @asmichi (https://github.com/asmichi) 60 | A .NET library that provides functionality for creating child processes. 61 | A .NET library that provides functionality for creating child processes. Easier, less error-prone, more flexible than `System.Diagnostics.Process` at creating child processes. 62 | MIT 63 | https://github.com/asmichi/ChildProcess 64 | dotnetcore;process 65 | Asmichi.ChildProcess 66 | https://github.com/asmichi/ChildProcess 67 | 68 | 69 | 70 | 71 | false 72 | true 73 | snupkg 74 | 75 | 76 | 77 | 78 | $(WorkTreeRoot)build\StrongNameKeyPair.snk 79 | false 80 | true 81 | 82 | 83 | 84 | -------------------------------------------------------------------------------- /build/msbuild/CommonTargets.csproj.targets: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /build/msbuild/InjectChildProcessNativeFileDeps.targets: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | %(ChildProcessNativeFile.RelativeDir) 19 | %(ChildProcessNativeFile.RuntimeIdentifier) 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /build/psm/Build.psm1: -------------------------------------------------------------------------------- 1 | 2 | # Copyright (c) @asmichi (https://github.com/asmichi). Licensed under the MIT License. See LICENCE in the project root for details. 3 | 4 | Set-StrictMode -Version latest 5 | $ErrorActionPreference = "Stop" 6 | 7 | $worktreeRoot = Resolve-Path "$PSScriptRoot\..\.." 8 | 9 | function Get-VersionInfo { 10 | param 11 | ( 12 | [Parameter(Mandatory = $true)] 13 | [string] 14 | $CommitHash, 15 | [Parameter(Mandatory = $true)] 16 | [string] 17 | $BranchName, 18 | [parameter()] 19 | [switch] 20 | $AllowRetailRelease 21 | ) 22 | 23 | $isProtectedBranch = $BranchName -eq "master" -or $BranchName -clike "release/*" 24 | $shortCommitHash = $CommitHash.Substring(0, 10) 25 | $recentTag = $(git describe --abbrev=0) 26 | $commitCount = $(git rev-list --count "${recentTag}..HEAD") 27 | $baseVersion = Get-Content "$worktreeRoot\build\Version.txt" 28 | $assemblyVersion = "$baseVersion.0" 29 | $fileVersion = $assemblyVersion 30 | $informationalVersion = "$fileVersion+g$shortCommitHash" 31 | $retailRelease = $false 32 | if ($AllowRetailRelease) { 33 | [System.Object[]]$tags = $(git tag --points-at HEAD) 34 | $retailRelease = $null -ne $tags -and $tags.Length -gt 0 35 | } 36 | $packageVersion = if ($retailRelease) { 37 | $baseVersion 38 | } 39 | elseif ($isProtectedBranch) { 40 | "$baseVersion-pre.$commitCount+g$shortCommitHash" 41 | } 42 | else { 43 | # On non-protected branches (typically feature branches), the commit hash should be included in the prerelease version 44 | # (not in the build metadata) so that versions from different branches will be correctly distinguished. 45 | "$baseVersion-pre.$commitCount.g$shortCommitHash" 46 | } 47 | 48 | return @{ 49 | CommitHash = $CommitHash; 50 | ShortCommitHash = $shortCommitHash; 51 | BaseVersion = $baseVersion; 52 | AssemblyVersion = $assemblyVersion; 53 | FileVersion = $fileVersion; 54 | InformationalVersion = $informationalVersion; 55 | PackageVersion = $packageVersion; 56 | } 57 | } 58 | 59 | function Get-CommonBuildOptions { 60 | param 61 | ( 62 | [Parameter(Mandatory = $true)] 63 | [Hashtable] 64 | $VersionInfo 65 | ) 66 | 67 | return @( 68 | "-nologo", 69 | "--verbosity:quiet", 70 | "--configuration", 71 | "Release", 72 | "-p:Platform=AnyCPU", 73 | "-p:RepositoryCommit=$($VersionInfo.CommitHash)", 74 | "-p:AssemblyVersion=$($VersionInfo.AssemblyVersion)", 75 | "-p:FileVersion=$($VersionInfo.FileVersion)", 76 | "-p:InformationalVersion=$($VersionInfo.InformationalVersion)", 77 | "-p:PackageVersion=$($VersionInfo.PackageVersion)", 78 | "-p:Version=$($VersionInfo.AssemblyVersion)" 79 | ) 80 | } 81 | 82 | function Get-VsDevCmdLocation { 83 | $vswhere = Join-Path ${Env:ProgramFiles(x86)} "Microsoft Visual Studio/Installer/vswhere.exe" 84 | $vs2019 = & $vswhere -nologo -format json -latest -version "[16.0,17.0)" -requires Microsoft.VisualStudio.Workload.NativeDesktop | ConvertFrom-Json 85 | if ($null -eq $vs2019) { 86 | throw "VS2019 not found." 87 | } 88 | return Join-Path $vs2019.installationPath "Common7/Tools/VsDevCmd.bat" 89 | } 90 | 91 | Export-ModuleMember -Function Get-CommonBuildOptions 92 | Export-ModuleMember -Function Get-VersionInfo 93 | Export-ModuleMember -Function Get-VsDevCmdLocation 94 | -------------------------------------------------------------------------------- /global.json: -------------------------------------------------------------------------------- 1 | { 2 | "sdk": { 3 | "version": "8.0.302" 4 | } 5 | } -------------------------------------------------------------------------------- /src/ChildProcess.Example/ChildProcess.Example.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net8.0 5 | 6 | 7 | 8 | 9 | 10 | Asmichi.ChildProcess.Example 11 | ..\PrivateAssembly.ruleset 12 | Exe 13 | Asmichi 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /src/ChildProcess.Example/ChildProcessExamples.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) @asmichi (https://github.com/asmichi). Licensed under the MIT License. See LICENCE in the project root for details. 2 | 3 | using System.Runtime.InteropServices; 4 | using System.Threading.Tasks; 5 | 6 | namespace Asmichi 7 | { 8 | public static class ChildProcessExamples 9 | { 10 | public static async Task Main() 11 | { 12 | if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) 13 | { 14 | await ChildProcessExamplesWindows.Run(); 15 | } 16 | else 17 | { 18 | await ChildProcessExamplesUnix.Run(); 19 | } 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/ChildProcess.Example/GlobalSuppressions.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) @asmichi (https://github.com/asmichi). Licensed under the MIT License. See LICENCE in the project root for details. 2 | 3 | using System.Diagnostics.CodeAnalysis; 4 | 5 | [assembly: SuppressMessage("Reliability", "CA2007:Do not directly await a Task", Justification = "This is application-level code.")] 6 | -------------------------------------------------------------------------------- /src/ChildProcess.Example/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) @asmichi (https://github.com/asmichi). Licensed under the MIT License. See LICENCE in the project root for details. 2 | 3 | using System; 4 | 5 | [assembly: CLSCompliant(false)] 6 | -------------------------------------------------------------------------------- /src/ChildProcess.ExamplePreview/ChildProcess.ExamplePreview.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net8.0 5 | 6 | 7 | 8 | 9 | 10 | Asmichi.ChildProcess.ExamplePreview 11 | ..\PrivateAssembly.ruleset 12 | Exe 13 | Asmichi 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/ChildProcess.ExamplePreview/ChildProcessExamples.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) @asmichi (https://github.com/asmichi). Licensed under the MIT License. See LICENCE in the project root for details. 2 | 3 | using System.Runtime.InteropServices; 4 | using System.Threading.Tasks; 5 | 6 | namespace Asmichi 7 | { 8 | public static class ChildProcessExamples 9 | { 10 | public static async Task Main() 11 | { 12 | if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) 13 | { 14 | await ChildProcessExamplesWindows.Run(); 15 | } 16 | else 17 | { 18 | await ChildProcessExamplesUnix.Run(); 19 | } 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/ChildProcess.ExamplePreview/GlobalSuppressions.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) @asmichi (https://github.com/asmichi). Licensed under the MIT License. See LICENCE in the project root for details. 2 | 3 | using System.Diagnostics.CodeAnalysis; 4 | 5 | [assembly: SuppressMessage("Reliability", "CA2007:Do not directly await a Task", Justification = "This is application-level code.")] 6 | -------------------------------------------------------------------------------- /src/ChildProcess.ExamplePreview/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) @asmichi (https://github.com/asmichi). Licensed under the MIT License. See LICENCE in the project root for details. 2 | 3 | using System; 4 | 5 | [assembly: CLSCompliant(false)] 6 | -------------------------------------------------------------------------------- /src/ChildProcess.ManualTest/ChildProcess.ManualTest.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net8.0 5 | 6 | 7 | 8 | 9 | 10 | Asmichi.ChildProcess.ManualTest 11 | ..\PrivateAssembly.ruleset 12 | Exe 13 | Asmichi 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/ChildProcess.ManualTest/ChildProcessManualTestProgram.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) @asmichi (https://github.com/asmichi). Licensed under the MIT License. See LICENCE in the project root for details. 2 | 3 | using System.Runtime.InteropServices; 4 | 5 | namespace Asmichi 6 | { 7 | public static class ChildProcessManualTestProgram 8 | { 9 | public static void Main() 10 | { 11 | if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) 12 | { 13 | ChildProcessManualTestProgramWindows.Run(); 14 | } 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/ChildProcess.ManualTest/ChildProcessManualTestProgramWindows.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) @asmichi (https://github.com/asmichi). Licensed under the MIT License. See LICENCE in the project root for details. 2 | 3 | using Asmichi.ProcessManagement; 4 | 5 | #pragma warning disable CA1849 // Call async methods when in an async method 6 | 7 | namespace Asmichi 8 | { 9 | public static class ChildProcessManualTestProgramWindows 10 | { 11 | public static void Run() 12 | { 13 | var si = new ChildProcessStartInfo("waitfor", "/T", "3", nameof(ChildProcessManualTestProgramWindows)) 14 | { 15 | // Without the "kill on dispose" workaround (uncomment this), the "Application Error 0xc0000142" dialog will pop up. 16 | // Flags = ChildProcessFlags.DisableKillOnDispose, 17 | }; 18 | 19 | // Demonstrate https://github.com/asmichi/ChildProcess/issues/2: intermittent "Application Error 0xc0000142" dialog 20 | // when the parent is killed (the pseudo console is closed) before a child finishes initialization. 21 | // 22 | // This will almost certanly cause that. 23 | for (int i = 0; i < 5; i++) 24 | { 25 | ChildProcess.Start(si).Dispose(); 26 | } 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/ChildProcess.ManualTest/GlobalSuppressions.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) @asmichi (https://github.com/asmichi). Licensed under the MIT License. See LICENCE in the project root for details. 2 | 3 | using System.Diagnostics.CodeAnalysis; 4 | 5 | [assembly: SuppressMessage("Reliability", "CA2007:Do not directly await a Task", Justification = "This is application-level code.")] 6 | -------------------------------------------------------------------------------- /src/ChildProcess.Native/.gitignore: -------------------------------------------------------------------------------- 1 | /CMakeSettings.json 2 | /out/ 3 | -------------------------------------------------------------------------------- /src/ChildProcess.Native/AsmichiChildProcess.symbols.txt: -------------------------------------------------------------------------------- 1 | _ConnectToUnixSocket 2 | _CreatePipe 3 | _CreateUnixStreamSocketPair 4 | _DuplicateStdFileForChild 5 | _GetDllPath 6 | _GetENOENT 7 | _GetMaxSocketPathLength 8 | _GetPid 9 | _OpenNullDevice 10 | _HelperMain 11 | _SubchannelCreate 12 | _SubchannelDestroy 13 | _SubchannelRecvExactBytes 14 | _SubchannelSendExactBytes 15 | _SubchannelSendExactBytesAndFds 16 | -------------------------------------------------------------------------------- /src/ChildProcess.Native/AsmichiChildProcess.version: -------------------------------------------------------------------------------- 1 | { 2 | global: 3 | ConnectToUnixSocket; 4 | CreatePipe; 5 | CreateUnixStreamSocketPair; 6 | DuplicateStdFileForChild; 7 | GetDllPath; 8 | GetENOENT; 9 | GetMaxSocketPathLength; 10 | GetPid; 11 | OpenNullDevice; 12 | HelperMain; 13 | SubchannelCreate; 14 | SubchannelDestroy; 15 | SubchannelRecvExactBytes; 16 | SubchannelSendExactBytes; 17 | SubchannelSendExactBytesAndFds; 18 | local: 19 | *; 20 | }; -------------------------------------------------------------------------------- /src/ChildProcess.Native/Base.cpp: -------------------------------------------------------------------------------- 1 | // Copyright (c) @asmichi (https://github.com/asmichi). Licensed under the MIT License. See LICENCE in the project root for details. 2 | 3 | #include "Base.hpp" 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | void PutFatalError(const char* str) noexcept 11 | { 12 | std::fprintf(stderr, "[ChildProcess] fatal error: %s\n", str); 13 | } 14 | 15 | void PutFatalError(int err, const char* str) noexcept 16 | { 17 | errno = err; 18 | std::fputs("[ChildProcess] fatal error: ", stderr); 19 | perror(str); 20 | } 21 | 22 | void FatalErrorAbort(const char* str) noexcept 23 | { 24 | PutFatalError(str); 25 | std::abort(); 26 | } 27 | 28 | void FatalErrorAbort(int err, const char* str) noexcept 29 | { 30 | PutFatalError(err, str); 31 | std::abort(); 32 | } 33 | 34 | void FatalErrorExit(int err, const char* str) noexcept 35 | { 36 | PutFatalError(err, str); 37 | std::exit(1); 38 | } 39 | -------------------------------------------------------------------------------- /src/ChildProcess.Native/CMakeSettings.template.json: -------------------------------------------------------------------------------- 1 | { 2 | "configurations": [ 3 | { 4 | "name": "x64-Debug", 5 | "generator": "Ninja", 6 | "configurationType": "Debug", 7 | "buildRoot": "${projectDir}\\out\\build\\${name}", 8 | "installRoot": "${projectDir}\\out\\install\\${name}", 9 | "cmakeCommandArgs": "", 10 | "buildCommandArgs": "", 11 | "ctestCommandArgs": "", 12 | "inheritEnvironments": [ "msvc_x64_x64" ] 13 | }, 14 | { 15 | "name": "x64-Release", 16 | "generator": "Ninja", 17 | "configurationType": "RelWithDebInfo", 18 | "buildRoot": "${projectDir}\\out\\build\\${name}", 19 | "installRoot": "${projectDir}\\out\\install\\${name}", 20 | "cmakeCommandArgs": "", 21 | "buildCommandArgs": "", 22 | "ctestCommandArgs": "", 23 | "inheritEnvironments": [ "msvc_x64_x64" ] 24 | }, 25 | { 26 | "name": "x86-Debug", 27 | "generator": "Ninja", 28 | "configurationType": "Debug", 29 | "buildRoot": "${projectDir}\\out\\build\\${name}", 30 | "installRoot": "${projectDir}\\out\\install\\${name}", 31 | "cmakeCommandArgs": "", 32 | "buildCommandArgs": "", 33 | "ctestCommandArgs": "", 34 | "inheritEnvironments": [ "msvc_x86" ] 35 | }, 36 | { 37 | "name": "x86-Release", 38 | "generator": "Ninja", 39 | "configurationType": "RelWithDebInfo", 40 | "buildRoot": "${projectDir}\\out\\build\\${name}", 41 | "installRoot": "${projectDir}\\out\\install\\${name}", 42 | "cmakeCommandArgs": "", 43 | "buildCommandArgs": "", 44 | "ctestCommandArgs": "", 45 | "inheritEnvironments": [ "msvc_x86" ] 46 | }, 47 | { 48 | "name": "WSL-x64-Debug", 49 | "generator": "Unix Makefiles", 50 | "configurationType": "Debug", 51 | "buildRoot": "${projectDir}\\out\\build\\${name}", 52 | "installRoot": "${projectDir}\\out\\install\\${name}", 53 | "cmakeCommandArgs": "", 54 | "buildCommandArgs": "", 55 | "ctestCommandArgs": "", 56 | "inheritEnvironments": [ "linux_clang_x64" ], 57 | "addressSanitizerRuntimeFlags": "detect_leaks=0", 58 | "wslPath": "${defaultWSLPath}", 59 | "cmakeExecutable": "cmake", 60 | "variables": [] 61 | }, 62 | { 63 | "name": "WSL-x64-Release", 64 | "generator": "Unix Makefiles", 65 | "configurationType": "RelWithDebInfo", 66 | "buildRoot": "${projectDir}\\out\\build\\${name}", 67 | "installRoot": "${projectDir}\\out\\install\\${name}", 68 | "cmakeCommandArgs": "", 69 | "buildCommandArgs": "", 70 | "ctestCommandArgs": "", 71 | "inheritEnvironments": [ "linux_clang_x64" ], 72 | "addressSanitizerRuntimeFlags": "detect_leaks=0", 73 | "wslPath": "${defaultWSLPath}", 74 | "cmakeExecutable": "cmake", 75 | "variables": [] 76 | } 77 | ] 78 | } -------------------------------------------------------------------------------- /src/ChildProcess.Native/Globals.cpp: -------------------------------------------------------------------------------- 1 | // Copyright (c) @asmichi (https://github.com/asmichi). Licensed under the MIT License. See LICENCE in the project root for details. 2 | 3 | #include "Globals.hpp" 4 | #include "ChildProcessState.hpp" 5 | #include "Service.hpp" 6 | 7 | ChildProcessStateMap g_ChildProcessStateMap; 8 | Service g_Service; 9 | -------------------------------------------------------------------------------- /src/ChildProcess.Native/HelperExecutable.cpp: -------------------------------------------------------------------------------- 1 | // Copyright (c) @asmichi (https://github.com/asmichi). Licensed under the MIT License. See LICENCE in the project root for details. 2 | 3 | // This file is the only file not included in the library. 4 | // The sole purpose is to produce an executable that invokes HelperMain of the library. 5 | 6 | extern "C" int HelperMain(int argc, const char** argv); 7 | 8 | int main(int argc, const char** argv) 9 | { 10 | return HelperMain(argc, argv); 11 | } 12 | -------------------------------------------------------------------------------- /src/ChildProcess.Native/HelperMain.cpp: -------------------------------------------------------------------------------- 1 | // Copyright (c) @asmichi (https://github.com/asmichi). Licensed under the MIT License. See LICENCE in the project root for details. 2 | 3 | #include "Base.hpp" 4 | #include "ExactBytesIO.hpp" 5 | #include "Globals.hpp" 6 | #include "MiscHelpers.hpp" 7 | #include "Service.hpp" 8 | #include "SocketHelpers.hpp" 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | 15 | const int HelperHelloBytes = 4; 16 | const unsigned char HelperHello[] = {0x41, 0x53, 0x4d, 0x43}; 17 | static_assert(sizeof(HelperHello) == HelperHelloBytes); 18 | 19 | // The parent process will use System.Diagnostics.Process to create this helper process 20 | // in order to avoid creating unmanged process in a .NET process. 21 | // Otherwise the signal handler of CoreFX would 'steal' such an unmanaged process. 22 | // 23 | // Connect to the parent process from this child process because System.Diagnostics.Process does not let 24 | // this process inherit fds from the parent process. 25 | extern "C" int HelperMain(int argc, const char** argv) 26 | { 27 | // Usage: AsmichiChildProcessHelper socket_path 28 | if (argc != 2) 29 | { 30 | PutFatalError("Invalid argc."); 31 | return 1; 32 | } 33 | 34 | const auto* path = argv[1]; 35 | 36 | struct sockaddr_un addr; 37 | if (strlen(path) > sizeof(addr.sun_path) - 1) 38 | { 39 | PutFatalError("Socket path too long."); 40 | return 1; 41 | } 42 | 43 | // Connect to the parent. 44 | std::memset(&addr, 0, sizeof(addr)); 45 | addr.sun_family = AF_UNIX; 46 | strcpy(addr.sun_path, path); 47 | 48 | auto maybeSock = CreateUnixStreamSocket(); 49 | if (!maybeSock) 50 | { 51 | PutFatalError(errno, "socket"); 52 | return 1; 53 | } 54 | 55 | int ret = connect(maybeSock->Get(), reinterpret_cast(&addr), sizeof(struct sockaddr_un)); 56 | if (ret == -1) 57 | { 58 | PutFatalError(errno, "connect"); 59 | return 1; 60 | } 61 | 62 | if (!SendExactBytes(maybeSock->Get(), HelperHello, HelperHelloBytes)) 63 | { 64 | PutFatalError(errno, "send"); 65 | return 1; 66 | } 67 | 68 | close(STDIN_FILENO); 69 | 70 | g_Service.Initialize(std::move(*maybeSock)); 71 | const int exitCode = g_Service.Run(); 72 | TRACE_INFO("Helper exiting: %d\n", exitCode); 73 | return exitCode; 74 | } 75 | -------------------------------------------------------------------------------- /src/ChildProcess.Native/Request.cpp: -------------------------------------------------------------------------------- 1 | // Copyright (c) @asmichi (https://github.com/asmichi). Licensed under the MIT License. See LICENCE in the project root for details. 2 | 3 | #include "Request.hpp" 4 | #include "BinaryReader.hpp" 5 | #include "ErrorCodeExceptions.hpp" 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | namespace 13 | { 14 | void GetStringArrayAndAdvance(BinaryReader& br, std::vector* buf) 15 | { 16 | const auto count = br.Read(); 17 | if (count > MaxStringArrayCount) 18 | { 19 | TRACE_ERROR("count > MaxStringArrayCount: %u\n", static_cast(count)); 20 | throw BadRequestError(E2BIG); 21 | } 22 | 23 | for (std::uint32_t i = 0; i < count; i++) 24 | { 25 | buf->push_back(br.GetStringAndAdvance()); 26 | } 27 | } 28 | } // namespace 29 | 30 | void DeserializeSpawnProcessRequest(SpawnProcessRequest* r, std::unique_ptr data, std::size_t length) 31 | { 32 | try 33 | { 34 | BinaryReader br{data.get(), length}; 35 | r->Data = std::move(data); 36 | r->Token = br.Read(); 37 | r->Flags = br.Read(); 38 | r->WorkingDirectory = br.GetStringAndAdvance(); 39 | r->ExecutablePath = br.GetStringAndAdvance(); 40 | GetStringArrayAndAdvance(br, &r->Argv); 41 | GetStringArrayAndAdvance(br, &r->Envp); 42 | 43 | r->Argv.push_back(nullptr); 44 | r->Envp.push_back(nullptr); 45 | 46 | if (r->ExecutablePath == nullptr) 47 | { 48 | TRACE_ERROR("ExecutablePath was nullptr.\n"); 49 | throw BadRequestError(ErrorCode::InvalidRequest); 50 | } 51 | } 52 | catch ([[maybe_unused]] const BadBinaryError& exn) 53 | { 54 | TRACE_ERROR("BadBinaryError: %s\n", exn.what()); 55 | throw BadRequestError(ErrorCode::InvalidRequest); 56 | } 57 | } 58 | 59 | void DeserializeSendSignalRequest(SendSignalRequest* r, std::unique_ptr data, std::size_t length) 60 | { 61 | try 62 | { 63 | BinaryReader br{data.get(), length}; 64 | r->Token = br.Read(); 65 | r->Signal = static_cast(br.Read()); 66 | } 67 | catch ([[maybe_unused]] const BadBinaryError& exn) 68 | { 69 | TRACE_ERROR("BadBinaryError: %s\n", exn.what()); 70 | throw BadRequestError(ErrorCode::InvalidRequest); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/ChildProcess.Native/SignalHandler.cpp: -------------------------------------------------------------------------------- 1 | // Copyright (c) @asmichi (https://github.com/asmichi). Licensed under the MIT License. See LICENCE in the project root for details. 2 | 3 | #include "SignalHandler.hpp" 4 | #include "Base.hpp" 5 | #include "Globals.hpp" 6 | #include "MiscHelpers.hpp" 7 | #include "Service.hpp" 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | 14 | bool IsSignalIgnored(int signum); 15 | void SetSignalAction(int signum, int extraFlags); 16 | void SignalHandler(int signum, siginfo_t* siginfo, void* context); 17 | 18 | void SetupSignalHandlers() 19 | { 20 | // Preserve the ignored state as far as possible so that our children will inherit the state. 21 | if (!IsSignalIgnored(SIGINT)) 22 | { 23 | SetSignalAction(SIGINT, 0); 24 | } 25 | if (!IsSignalIgnored(SIGTERM)) 26 | { 27 | SetSignalAction(SIGTERM, 0); 28 | } 29 | if (!IsSignalIgnored(SIGQUIT)) 30 | { 31 | SetSignalAction(SIGQUIT, 0); 32 | } 33 | if (!IsSignalIgnored(SIGPIPE)) 34 | { 35 | SetSignalAction(SIGPIPE, 0); 36 | } 37 | 38 | SetSignalAction(SIGCHLD, SA_NOCLDSTOP); 39 | } 40 | 41 | void RaiseQuitOnSelf() 42 | { 43 | struct sigaction act = {}; 44 | act.sa_flags = 0; 45 | sigemptyset(&act.sa_mask); 46 | act.sa_handler = SIG_DFL; 47 | 48 | if (sigaction(SIGQUIT, &act, nullptr) != 0) 49 | { 50 | FatalErrorAbort("sigaction"); 51 | } 52 | 53 | if (kill(getpid(), SIGQUIT) == -1) 54 | { 55 | FatalErrorAbort("kill"); 56 | } 57 | 58 | std::abort(); 59 | } 60 | 61 | bool IsSignalIgnored(int signum) 62 | { 63 | struct sigaction oldact; 64 | [[maybe_unused]] int isError = sigaction(signum, nullptr, &oldact); 65 | assert(isError == 0); 66 | return oldact.sa_handler == SIG_IGN; 67 | } 68 | 69 | void SetSignalAction(int signum, int extraFlags) 70 | { 71 | struct sigaction act = {}; 72 | act.sa_flags = SA_RESTART | SA_SIGINFO | extraFlags; 73 | sigemptyset(&act.sa_mask); 74 | act.sa_sigaction = SignalHandler; 75 | 76 | [[maybe_unused]] int isError = sigaction(signum, &act, nullptr); 77 | assert(isError == 0); 78 | } 79 | 80 | void SignalHandler(int signum, siginfo_t*, void*) 81 | { 82 | // Avoid doing the real work in the signal handler. 83 | // Dispatch the real work to the service thread. 84 | switch (signum) 85 | { 86 | case SIGQUIT: 87 | case SIGCHLD: 88 | { 89 | const int err = errno; 90 | g_Service.NotifySignal(signum); 91 | errno = err; 92 | break; 93 | } 94 | 95 | // NOTE: It's up to the client whether the service should exit on SIGINT/SIGTERM. (The service will exit when the connection is closed.) 96 | // NOTE: It's up to the client whether child processs in other process groups should be sent SIGINT/SIGTERM. 97 | case SIGINT: 98 | case SIGTERM: 99 | case SIGPIPE: 100 | default: 101 | // Ignored 102 | break; 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/ChildProcess.Native/SocketHelpers.cpp: -------------------------------------------------------------------------------- 1 | // Copyright (c) @asmichi (https://github.com/asmichi). Licensed under the MIT License. See LICENCE in the project root for details. 2 | 3 | #include "SocketHelpers.hpp" 4 | #include "Base.hpp" 5 | #include "ExactBytesIO.hpp" 6 | #include "MiscHelpers.hpp" 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | 20 | bool SendExactBytes(int fd, const void* buf, std::size_t len) noexcept 21 | { 22 | auto f = [fd](const void* p, std::size_t partialLen) { return send_restarting(fd, p, partialLen, MakeSockFlags(BlockingFlag::Blocking)); }; 23 | if (!WriteExactBytes(f, buf, len)) 24 | { 25 | return HandleSendError(BlockingFlag::Blocking, "send", errno); 26 | } 27 | 28 | return true; 29 | } 30 | 31 | bool RecvExactBytes(int fd, void* buf, std::size_t len) noexcept 32 | { 33 | auto f = [fd](void* p, std::size_t partialLen) { return recv_restarting(fd, p, partialLen, MakeSockFlags(BlockingFlag::Blocking)); }; 34 | return ReadExactBytes(f, buf, len); 35 | } 36 | 37 | bool SendExactBytesWithFd(int fd, const void* buf, std::size_t len, const int* fds, std::size_t fdCount) noexcept 38 | { 39 | if (fds == nullptr || fdCount == 0) 40 | { 41 | return SendExactBytes(fd, buf, len); 42 | } 43 | 44 | // Make sure to send fds only once. 45 | ssize_t bytesSent = SendWithFd(fd, buf, len, fds, fdCount, BlockingFlag::Blocking); 46 | if (!HandleSendResult(BlockingFlag::Blocking, "sendmsg", bytesSent, errno)) 47 | { 48 | return false; 49 | } 50 | 51 | // Send out remaining bytes. 52 | std::size_t positiveBytesSent = static_cast(bytesSent); 53 | if (positiveBytesSent >= len) 54 | { 55 | assert(positiveBytesSent == len); 56 | return true; 57 | } 58 | else 59 | { 60 | return SendExactBytes(fd, static_cast(buf) + positiveBytesSent, len - positiveBytesSent); 61 | } 62 | } 63 | 64 | ssize_t SendWithFd(int fd, const void* buf, std::size_t len, const int* fds, std::size_t fdCount, BlockingFlag blocking) noexcept 65 | { 66 | if (fds == nullptr || fdCount == 0) 67 | { 68 | return send_restarting(fd, buf, len, MakeSockFlags(blocking)); 69 | } 70 | 71 | if (fdCount > SocketMaxFdsPerCall) 72 | { 73 | errno = EINVAL; 74 | return -1; 75 | } 76 | 77 | iovec iov; 78 | msghdr msg; 79 | CmsgFds cmsgFds; 80 | 81 | iov.iov_base = const_cast(buf); 82 | iov.iov_len = len; 83 | msg.msg_name = nullptr; 84 | msg.msg_namelen = 0; 85 | msg.msg_iov = &iov; 86 | msg.msg_iovlen = 1; 87 | msg.msg_control = cmsgFds.Buffer; 88 | msg.msg_controllen = CMSG_SPACE(sizeof(int) * fdCount); 89 | msg.msg_flags = 0; 90 | 91 | struct cmsghdr* pcmsghdr = CMSG_FIRSTHDR(&msg); 92 | pcmsghdr->cmsg_len = CMSG_LEN(sizeof(int) * fdCount); 93 | pcmsghdr->cmsg_level = SOL_SOCKET; 94 | pcmsghdr->cmsg_type = SCM_RIGHTS; 95 | std::memcpy(CMSG_DATA(pcmsghdr), fds, sizeof(int) * fdCount); 96 | 97 | return sendmsg_restarting(fd, &msg, MakeSockFlags(blocking)); 98 | } 99 | -------------------------------------------------------------------------------- /src/ChildProcess.Native/Subbuild-unix.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Copyright (c) @asmichi (https://github.com/asmichi). Licensed under the MIT License. See LICENCE in the project root for details. 3 | 4 | set -eu 5 | 6 | SrcRoot=$(dirname $0) 7 | OS=$1 8 | ObjDir=$2 9 | BinDir=$3 10 | ToolchainFile_LinuxX64=${SrcRoot}/cmake/toolchain-linux-x64.cmake 11 | ToolchainFile_LinuxArm=${SrcRoot}/cmake/toolchain-linux-arm.cmake 12 | ToolchainFile_LinuxArm64=${SrcRoot}/cmake/toolchain-linux-arm64.cmake 13 | ToolchainFile_LinuxMuslX64=${SrcRoot}/cmake/toolchain-linux-musl-x64.cmake 14 | ToolchainFile_LinuxMuslArm64=${SrcRoot}/cmake/toolchain-linux-musl-arm64.cmake 15 | ToolchainFile_OSXX64=${SrcRoot}/cmake/toolchain-osx-x64.cmake 16 | ToolchainFile_OSXArm64=${SrcRoot}/cmake/toolchain-osx-arm64.cmake 17 | Jobs=$(($(getconf _NPROCESSORS_ONLN) / 2)) 18 | pids=() 19 | 20 | function build_impl() 21 | { 22 | local rid=$1 23 | local configuration=$2 24 | local toolchainFile=$3 25 | local extraArgs=${@:4} 26 | local buildDir=${ObjDir}/${rid}/${configuration} 27 | local outDir=${BinDir}/${rid}/${configuration} 28 | 29 | mkdir -p ${buildDir} 30 | (cd ${buildDir}; cmake ${SrcRoot} -G "Unix Makefiles" -DCMAKE_BUILD_TYPE:STRING=${configuration} -DCMAKE_TOOLCHAIN_FILE:FILEPATH=${toolchainFile} $extraArgs) || return 31 | 32 | make -C ${buildDir} -j ${Jobs} || return 33 | 34 | mkdir -p ${outDir} 35 | cp ${buildDir}/bin/* ${outDir} 36 | cp ${buildDir}/lib/* ${outDir} 37 | } 38 | 39 | function build() 40 | { 41 | build_impl "$@" & 42 | pids+=($!) 43 | } 44 | 45 | case ${OS} in 46 | "Linux") 47 | build linux-x64 Debug ${ToolchainFile_LinuxX64} 48 | build linux-x64 Release ${ToolchainFile_LinuxX64} 49 | build linux-arm Debug ${ToolchainFile_LinuxArm} 50 | build linux-arm Release ${ToolchainFile_LinuxArm} 51 | build linux-arm64 Debug ${ToolchainFile_LinuxArm64} 52 | build linux-arm64 Release ${ToolchainFile_LinuxArm64} 53 | build linux-musl-x64 Debug ${ToolchainFile_LinuxMuslX64} 54 | build linux-musl-x64 Release ${ToolchainFile_LinuxMuslX64} 55 | build linux-musl-arm64 Debug ${ToolchainFile_LinuxMuslArm64} 56 | build linux-musl-arm64 Release ${ToolchainFile_LinuxMuslArm64} 57 | ;; 58 | "OSX") 59 | build osx-x64 Debug ${ToolchainFile_OSXX64} -DCMAKE_OSX_ARCHITECTURES=x86_64 60 | build osx-x64 Release ${ToolchainFile_OSXX64} -DCMAKE_OSX_ARCHITECTURES=x86_64 61 | build osx-arm64 Debug ${ToolchainFile_OSXArm64} -DCMAKE_OSX_ARCHITECTURES=arm64 62 | build osx-arm64 Release ${ToolchainFile_OSXArm64} -DCMAKE_OSX_ARCHITECTURES=arm64 63 | ;; 64 | *) 65 | echo "Unknown OS: ${OS}" 1>&2 66 | exit 1 67 | ;; 68 | esac 69 | 70 | status=0 71 | for pid in ${pids[*]}; do 72 | wait $pid 73 | if [ $? -ne 0 ]; then 74 | status=1 75 | fi 76 | done 77 | 78 | exit $status 79 | -------------------------------------------------------------------------------- /src/ChildProcess.Native/Subbuild-win.ps1: -------------------------------------------------------------------------------- 1 | # Copyright (c) @asmichi (https://github.com/asmichi). Licensed under the MIT License. See LICENCE in the project root for details. 2 | 3 | param( 4 | [parameter()] 5 | [string] 6 | $WorktreeRoot 7 | ) 8 | 9 | Set-StrictMode -Version latest 10 | $ErrorActionPreference = "Stop" 11 | 12 | $SrcRoot = $PSScriptRoot 13 | 14 | function Start-Build() { 15 | param( 16 | [parameter()] 17 | [string] 18 | $Arch, 19 | [parameter()] 20 | [string] 21 | $Configuration 22 | ) 23 | 24 | Start-ThreadJob -ScriptBlock { 25 | $Arch = $using:Arch 26 | $Configuration = $using:Configuration 27 | $WorktreeRoot = $using:WorktreeRoot 28 | $SrcRoot = $using:SrcRoot 29 | 30 | $rid = "win-${Arch}" 31 | $buildDir = Join-Path $WorktreeRoot "obj/ChildProcess.Native/$rid/${Configuration}" 32 | $outDir = Join-Path $WorktreeRoot "bin/ChildProcess.Native/$rid/${Configuration}" 33 | 34 | New-Item -ItemType Directory -Force $buildDir | Out-Null 35 | Push-Location -LiteralPath $buildDir 36 | # --no-warn-unused-cli: https://gitlab.kitware.com/cmake/cmake/-/issues/17261 37 | cmake $SrcRoot -G Ninja --no-warn-unused-cli "-DCMAKE_BUILD_TYPE=${Configuration}" "-DCMAKE_TOOLCHAIN_FILE=${SrcRoot}/cmake/toolchain-win-${Arch}.cmake" 38 | Pop-Location 39 | 40 | ninja -C $buildDir 41 | 42 | if ($LASTEXITCODE -ne 0) { 43 | Write-Error "build failed ($Arch $Configuration)" 44 | } 45 | else { 46 | New-Item -ItemType Directory -Force $outDir | Out-Null 47 | Copy-Item "$buildDir/bin/*" -Destination $outDir 48 | } 49 | } 50 | } 51 | 52 | @( 53 | Start-Build -Arch x86 -Configuration Debug 54 | Start-Build -Arch x86 -Configuration Release 55 | Start-Build -Arch x64 -Configuration Debug 56 | Start-Build -Arch x64 -Configuration Release 57 | ) | Receive-Job -Wait -AutoRemoveJob 58 | -------------------------------------------------------------------------------- /src/ChildProcess.Native/SubchannelCollection.cpp: -------------------------------------------------------------------------------- 1 | // Copyright (c) @asmichi (https://github.com/asmichi). Licensed under the MIT License. See LICENCE in the project root for details. 2 | 3 | #include "SubchannelCollection.hpp" 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | SubchannelCollection::~SubchannelCollection() 10 | { 11 | const std::lock_guard guard(mapMutex_); 12 | 13 | if (map_.size() > 0) 14 | { 15 | // ~SubchannelCollection and Delete are racing. 16 | // We need to wait for all subchannels to finish before exiting. 17 | TRACE_ERROR("Exiting while subchannel(s) are still running. (%zu remaining)\n", map_.size()); 18 | 19 | // Try to at least prevent double-free (although we are already in a broken state). 20 | map_.clear(); 21 | } 22 | } 23 | 24 | Subchannel* SubchannelCollection::Add(std::unique_ptr subchannel) 25 | { 26 | const std::lock_guard guard(mapMutex_); 27 | 28 | const auto [it, inserted] = map_.insert(std::pair{subchannel.get(), std::move(subchannel)}); 29 | if (!inserted) 30 | { 31 | FatalErrorAbort("Attempted to register a Subchannel twice."); 32 | } 33 | 34 | return (*it).first; 35 | } 36 | 37 | void SubchannelCollection::Delete(Subchannel* key) 38 | { 39 | const std::lock_guard guard(mapMutex_); 40 | 41 | const auto it = map_.find(key); 42 | assert(it != map_.end()); 43 | map_.erase(it); 44 | } 45 | 46 | size_t SubchannelCollection::Size() const 47 | { 48 | const std::lock_guard guard(mapMutex_); 49 | 50 | return map_.size(); 51 | } 52 | -------------------------------------------------------------------------------- /src/ChildProcess.Native/WriteBuffer.cpp: -------------------------------------------------------------------------------- 1 | // Copyright (c) @asmichi (https://github.com/asmichi). Licensed under the MIT License. See LICENCE in the project root for details. 2 | 3 | #include "WriteBuffer.hpp" 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | 15 | namespace 16 | { 17 | const constexpr std::size_t BlockLength = 32 * 1024; 18 | } // namespace 19 | 20 | void WriteBuffer::Enqueue(const void* buf, std::size_t len) 21 | { 22 | auto* byteBuf = static_cast(buf); 23 | auto* const pEnd = byteBuf + len; 24 | if (!blocks_.empty()) 25 | { 26 | auto& back = blocks_.back(); 27 | byteBuf += StoreToBlock(&back, byteBuf, len); 28 | assert(byteBuf <= pEnd); 29 | } 30 | 31 | while (byteBuf < pEnd) 32 | { 33 | auto b = CreateBlock(); 34 | byteBuf += StoreToBlock(&b, byteBuf, pEnd - byteBuf); 35 | blocks_.push_back(std::move(b)); 36 | } 37 | 38 | assert(byteBuf == pEnd); 39 | } 40 | 41 | void WriteBuffer::Dequeue(std::size_t len) noexcept 42 | { 43 | while (len != 0) 44 | { 45 | auto& front = blocks_.front(); 46 | const auto remainingBytes = front.DataBytes - front.CurrentOffset; 47 | if (remainingBytes <= len) 48 | { 49 | blocks_.erase(blocks_.begin()); 50 | len -= remainingBytes; 51 | } 52 | else 53 | { 54 | front.CurrentOffset += len; 55 | len = 0; 56 | } 57 | } 58 | 59 | assert(false); 60 | } 61 | 62 | std::tuple WriteBuffer::GetPendingData() noexcept 63 | { 64 | if (blocks_.empty()) 65 | { 66 | std::abort(); 67 | } 68 | 69 | const auto& first = blocks_.front(); 70 | return std::make_tuple(first.Data.get() + first.CurrentOffset, first.DataBytes - first.CurrentOffset); 71 | } 72 | 73 | WriteBuffer::Block WriteBuffer::CreateBlock() 74 | { 75 | Block b; 76 | b.Data = std::make_unique(BlockLength); 77 | b.DataBytes = 0; 78 | b.CurrentOffset = 0; 79 | return std::move(b); 80 | } 81 | 82 | std::size_t WriteBuffer::StoreToBlock(Block* pBlock, const std::byte* buf, std::size_t len) noexcept 83 | { 84 | const auto freeBytes = BlockLength - pBlock->DataBytes; 85 | const auto bytesToStore = std::min(freeBytes, len); 86 | 87 | std::memcpy(pBlock->Data.get() + pBlock->DataBytes, buf, bytesToStore); 88 | pBlock->DataBytes += bytesToStore; 89 | assert(pBlock->DataBytes <= BlockLength); 90 | 91 | return bytesToStore; 92 | } 93 | -------------------------------------------------------------------------------- /src/ChildProcess.Native/cmake/toolchain-linux-arm.cmake: -------------------------------------------------------------------------------- 1 | # Assume x64 Ubuntu 22.04 & Clang & LLD & Ubuntu 18 sysroot 2 | set(CMAKE_SYSTEM_NAME Linux) 3 | set(CMAKE_SYSTEM_PROCESSOR arm) 4 | 5 | set(CMAKE_CXX_COMPILER /usr/bin/clang++) 6 | set(CMAKE_CXX_COMPILER_TARGET arm-linux-gnueabihf) 7 | set(CMAKE_SYSROOT $ENV{SYSROOTS_DIR}/sysroot-ubuntu18) 8 | include_directories(SYSTEM 9 | ${CMAKE_SYSROOT}/usr/arm-linux-gnueabihf/include/c++/7.5.0 10 | ${CMAKE_SYSROOT}/usr/arm-linux-gnueabihf/include/c++/7.5.0/arm-linux-gnueabihf 11 | ${CMAKE_SYSROOT}/usr/arm-linux-gnueabihf/include/c++/7.5.0/backward 12 | ${CMAKE_SYSROOT}/usr/arm-linux-gnueabihf/include 13 | /usr/lib/llvm-14/lib/clang/14.0.0/include) 14 | 15 | # Workaround for https://gitlab.kitware.com/cmake/cmake/-/issues/17966 16 | unset(CMAKE_CXX_IMPLICIT_INCLUDE_DIRECTORIES) 17 | 18 | add_compile_options(-nostdinc) 19 | add_link_options(-fuse-ld=lld) 20 | 21 | set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) 22 | set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) 23 | set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) 24 | set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY) 25 | -------------------------------------------------------------------------------- /src/ChildProcess.Native/cmake/toolchain-linux-arm64.cmake: -------------------------------------------------------------------------------- 1 | # Assume x64 Ubuntu 22.04 & Clang & LLD & Ubuntu 18 sysroot 2 | set(CMAKE_SYSTEM_NAME Linux) 3 | set(CMAKE_SYSTEM_PROCESSOR aarch64) 4 | 5 | set(CMAKE_CXX_COMPILER /usr/bin/clang++) 6 | set(CMAKE_CXX_COMPILER_TARGET aarch64-linux-gnu) 7 | set(CMAKE_SYSROOT $ENV{SYSROOTS_DIR}/sysroot-ubuntu18) 8 | include_directories(SYSTEM 9 | ${CMAKE_SYSROOT}/usr/aarch64-linux-gnu/include/c++/7.5.0 10 | ${CMAKE_SYSROOT}/usr/aarch64-linux-gnu/include/c++/7.5.0/aarch64-linux-gnu 11 | ${CMAKE_SYSROOT}/usr/aarch64-linux-gnu/include/c++/7.5.0/backward 12 | ${CMAKE_SYSROOT}/usr/aarch64-linux-gnu/include 13 | /usr/lib/llvm-14/lib/clang/14.0.0/include) 14 | 15 | # Workaround for https://gitlab.kitware.com/cmake/cmake/-/issues/17966 16 | unset(CMAKE_CXX_IMPLICIT_INCLUDE_DIRECTORIES) 17 | 18 | add_compile_options(-nostdinc) 19 | add_link_options(-fuse-ld=lld) 20 | 21 | set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) 22 | set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) 23 | set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) 24 | set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY) 25 | -------------------------------------------------------------------------------- /src/ChildProcess.Native/cmake/toolchain-linux-musl-arm64.cmake: -------------------------------------------------------------------------------- 1 | # Assume x64 Ubuntu 22.04 & Clang & LLD 2 | set(CMAKE_SYSTEM_NAME Linux) 3 | set(CMAKE_SYSTEM_PROCESSOR aarch64) 4 | 5 | set(CMAKE_CXX_COMPILER /usr/bin/clang++) 6 | set(CMAKE_CXX_COMPILER_TARGET aarch64-alpine-linux-musl) 7 | set(CMAKE_SYSROOT $ENV{SYSROOTS_DIR}/sysroot-alpine-aarch64-alpine-linux-musl) 8 | 9 | set(LIBC_MUSL 1) 10 | add_link_options(-fuse-ld=lld) 11 | 12 | set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) 13 | set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) 14 | set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) 15 | set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY) 16 | -------------------------------------------------------------------------------- /src/ChildProcess.Native/cmake/toolchain-linux-musl-x64.cmake: -------------------------------------------------------------------------------- 1 | # Assume x64 Ubuntu 22.04 & Clang 14 & LLD 2 | set(CMAKE_SYSTEM_NAME Linux) 3 | set(CMAKE_SYSTEM_PROCESSOR x86_64) 4 | 5 | set(CMAKE_CXX_COMPILER /usr/bin/clang++) 6 | set(CMAKE_CXX_COMPILER_TARGET x86_64-alpine-linux-musl) 7 | set(CMAKE_SYSROOT $ENV{SYSROOTS_DIR}/sysroot-alpine-x86_64-alpine-linux-musl) 8 | 9 | set(LIBC_MUSL 1) 10 | add_link_options(-fuse-ld=lld) 11 | 12 | set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) 13 | set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) 14 | set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) 15 | set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY) 16 | -------------------------------------------------------------------------------- /src/ChildProcess.Native/cmake/toolchain-linux-x64.cmake: -------------------------------------------------------------------------------- 1 | # Assume x64 Ubuntu 22.04 & Clang 14 & LLD & Ubuntu 18 sysroot 2 | set(CMAKE_SYSTEM_NAME Linux) 3 | set(CMAKE_SYSTEM_PROCESSOR x86_64) 4 | 5 | set(CMAKE_CXX_COMPILER /usr/bin/clang++) 6 | set(CMAKE_CXX_COMPILER_TARGET x86_64-linux-gnu) 7 | set(CMAKE_SYSROOT $ENV{SYSROOTS_DIR}/sysroot-ubuntu18) 8 | include_directories(SYSTEM 9 | ${CMAKE_SYSROOT}/usr/x86_64-linux-gnu/include/c++/7.5.0 10 | ${CMAKE_SYSROOT}/usr/x86_64-linux-gnu/include/c++/7.5.0/x86_64-linux-gnu 11 | ${CMAKE_SYSROOT}/usr/x86_64-linux-gnu/include/c++/7.5.0/backward 12 | ${CMAKE_SYSROOT}/usr/x86_64-linux-gnu/include 13 | /usr/lib/llvm-14/lib/clang/14.0.0/include) 14 | 15 | # Workaround for https://gitlab.kitware.com/cmake/cmake/-/issues/17966 16 | unset(CMAKE_CXX_IMPLICIT_INCLUDE_DIRECTORIES) 17 | 18 | add_compile_options(-nostdinc) 19 | add_link_options(-fuse-ld=lld) 20 | 21 | set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) 22 | set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) 23 | set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) 24 | set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY) 25 | -------------------------------------------------------------------------------- /src/ChildProcess.Native/cmake/toolchain-osx-arm64.cmake: -------------------------------------------------------------------------------- 1 | set(CMAKE_OSX_ARCHITECTURES arm64 CACHE STRING "CMAKE_OSX_ARCHITECTURES") 2 | set(CMAKE_OSX_DEPLOYMENT_TARGET 11.0 CACHE STRING "CMAKE_OSX_DEPLOYMENT_TARGET") 3 | -------------------------------------------------------------------------------- /src/ChildProcess.Native/cmake/toolchain-osx-x64.cmake: -------------------------------------------------------------------------------- 1 | set(CMAKE_OSX_ARCHITECTURES x86_64 CACHE STRING "CMAKE_OSX_ARCHITECTURES") 2 | set(CMAKE_OSX_DEPLOYMENT_TARGET 10.15 CACHE STRING "CMAKE_OSX_DEPLOYMENT_TARGET") 3 | -------------------------------------------------------------------------------- /src/ChildProcess.Native/cmake/toolchain-win-msvc.cmake: -------------------------------------------------------------------------------- 1 | file(TO_CMAKE_PATH $ENV{VCTOOLSINSTALLDIR} VCTOOLSINSTALLDIR) 2 | file(TO_CMAKE_PATH $ENV{WINDOWSSDKDIR} WINDOWSSDKDIR) 3 | file(TO_CMAKE_PATH $ENV{WINDOWSSDKLIBVERSION} WINDOWSSDKLIBVERSION) 4 | set(msvcbin ${VCTOOLSINSTALLDIR}/bin/HostX64/${MSVC_ARCHITECTURE}) 5 | set(CMAKE_C_COMPILER "${msvcbin}/cl.exe") 6 | set(CMAKE_CXX_COMPILER "${msvcbin}/cl.exe") 7 | 8 | set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) 9 | set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) 10 | set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) 11 | set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY) 12 | 13 | set(MSVC_LIBRARY_PATH 14 | "${VCTOOLSINSTALLDIR}/lib/${MSVC_ARCHITECTURE}" 15 | "${WINDOWSSDKDIR}/lib/${WINDOWSSDKLIBVERSION}/ucrt/${MSVC_ARCHITECTURE}" 16 | "${WINDOWSSDKDIR}/lib/${WINDOWSSDKLIBVERSION}/um/${MSVC_ARCHITECTURE}") 17 | 18 | foreach(x ${MSVC_LIBRARY_PATH}) 19 | set(MSVC_LIBRARY_PATH_FLAGS "${MSVC_LIBRARY_PATH_FLAGS} /LIBPATH:\"${x}\"") 20 | endforeach() 21 | 22 | set(MSVC_LINKER_FLAGS "/DEBUG /DYNAMICBASE /NXCOMPAT /TLBID:1 ${MSVC_LIBRARY_PATH_FLAGS}") 23 | 24 | set(CMAKE_EXE_LINKER_FLAGS ${MSVC_LINKER_FLAGS}) 25 | set(CMAKE_SHARED_LINKER_FLAGS "/DLL ${MSVC_LINKER_FLAGS}") 26 | -------------------------------------------------------------------------------- /src/ChildProcess.Native/cmake/toolchain-win-x64.cmake: -------------------------------------------------------------------------------- 1 | set(CMAKE_SYSTEM_PROCESSOR AMD64) 2 | set(MSVC_ARCHITECTURE x64) 3 | 4 | include(${CMAKE_CURRENT_LIST_DIR}/toolchain-win-msvc.cmake) 5 | -------------------------------------------------------------------------------- /src/ChildProcess.Native/cmake/toolchain-win-x86.cmake: -------------------------------------------------------------------------------- 1 | set(CMAKE_SYSTEM_PROCESSOR x86) 2 | set(MSVC_ARCHITECTURE x86) 3 | 4 | include(${CMAKE_CURRENT_LIST_DIR}/toolchain-win-msvc.cmake) 5 | -------------------------------------------------------------------------------- /src/ChildProcess.Native/config.h.in: -------------------------------------------------------------------------------- 1 | // Copyright (c) @asmichi (https://github.com/asmichi). Licensed under the MIT License. See LICENCE in the project root for details. 2 | 3 | #cmakedefine01 HAVE_MSG_CMSG_CLOEXEC 4 | #cmakedefine01 HAVE_PIPE2 5 | #cmakedefine01 HAVE_SOCK_CLOEXEC 6 | -------------------------------------------------------------------------------- /src/ChildProcess.Native/include/AncillaryDataSocket.hpp: -------------------------------------------------------------------------------- 1 | // Copyright (c) @asmichi (https://github.com/asmichi). Licensed under the MIT License. See LICENCE in the project root for details. 2 | 3 | #pragma once 4 | 5 | #include "Base.hpp" 6 | #include "UniqueResource.hpp" 7 | #include "WriteBuffer.hpp" 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | // Receives fds via ancillary data on a unix domain socket. Employs a send buffer if nonblocking. 14 | class AncillaryDataSocket final 15 | { 16 | public: 17 | static const constexpr int MaxFdsPerCall = 3; 18 | 19 | AncillaryDataSocket(UniqueFd&& sockFd, int cancellationPipeReadEnd) noexcept; 20 | 21 | [[nodiscard]] ssize_t Send(const void* buf, std::size_t len, BlockingFlag blocking) noexcept; 22 | [[nodiscard]] bool SendExactBytes(const void* buf, std::size_t len) noexcept; 23 | [[nodiscard]] bool SendBuffered(const void* buf, std::size_t len, BlockingFlag blocking) noexcept; 24 | [[nodiscard]] bool Flush(BlockingFlag blocking) noexcept; 25 | [[nodiscard]] bool HasPendingData() noexcept { return sendBuffer_.HasPendingData(); } 26 | 27 | [[nodiscard]] ssize_t Recv(void* buf, std::size_t len, BlockingFlag blocking) noexcept; 28 | [[nodiscard]] bool RecvExactBytes(void* buf, std::size_t len) noexcept; 29 | 30 | void Shutdown() noexcept; 31 | 32 | [[nodiscard]] int GetFd() const noexcept { return fd_.Get(); } 33 | 34 | [[nodiscard]] std::size_t ReceivedFdCount() const noexcept { return receivedFds_.size(); } 35 | 36 | [[nodiscard]] std::optional PopReceivedFd() noexcept 37 | { 38 | if (receivedFds_.size() == 0) 39 | { 40 | return std::nullopt; 41 | } 42 | 43 | UniqueFd fd{std::move(receivedFds_.front())}; 44 | receivedFds_.pop(); 45 | return fd; 46 | } 47 | 48 | private: 49 | // true: won't block; false: cancellation requested. 50 | bool PollForInput(); 51 | // true: won't block; false: cancellation requested. 52 | bool PollForOutput(); 53 | bool PollFor(short event); 54 | 55 | UniqueFd fd_; 56 | // Close the counterpart to cancel current and future blocking operations. 57 | int cancellationPipeReadEnd_; 58 | std::queue receivedFds_; 59 | WriteBuffer sendBuffer_; 60 | }; 61 | -------------------------------------------------------------------------------- /src/ChildProcess.Native/include/Base.hpp: -------------------------------------------------------------------------------- 1 | // Copyright (c) @asmichi (https://github.com/asmichi). Licensed under the MIT License. See LICENCE in the project root for details. 2 | 3 | #pragma once 4 | 5 | // Common implementations. 6 | 7 | #include 8 | #include 9 | #include 10 | 11 | class MyException : public std::exception 12 | { 13 | public: 14 | MyException(const char* description) : description_(description) {} 15 | virtual const char* what() const noexcept override { return description_; } 16 | 17 | private: 18 | const char* const description_; 19 | }; 20 | 21 | struct ErrnoRestorer final 22 | { 23 | public: 24 | ErrnoRestorer() noexcept : saved_(errno) {} 25 | ~ErrnoRestorer() noexcept { errno = saved_; } 26 | 27 | private: 28 | int saved_; 29 | }; 30 | 31 | enum class BlockingFlag : bool 32 | { 33 | Blocking = false, 34 | NonBlocking = true, 35 | }; 36 | 37 | void PutFatalError(const char* str) noexcept; 38 | void PutFatalError(int err, const char* str) noexcept; 39 | [[noreturn]] void FatalErrorAbort(const char* str) noexcept; 40 | [[noreturn]] void FatalErrorAbort(int err, const char* str) noexcept; 41 | [[noreturn]] void FatalErrorExit(int err, const char* str) noexcept; 42 | 43 | [[nodiscard]] inline bool IsWouldBlockError(int err) { return err == EAGAIN || err == EWOULDBLOCK; } 44 | [[nodiscard]] inline bool IsConnectionClosedError(int err) noexcept { return err == ECONNRESET || err == EPIPE; } 45 | 46 | #if defined(ENABLE_TRACE_DEBUG) 47 | #define TRACE_DEBUG(format, ...) static_cast(std::fprintf(stderr, "[ChildProcess] debug: " format, ##__VA_ARGS__)) 48 | #else 49 | #define TRACE_DEBUG(format, ...) static_cast(0) 50 | #endif 51 | 52 | #if defined(ENABLE_TRACE_INFO) 53 | #define TRACE_INFO(format, ...) static_cast(std::fprintf(stderr, "[ChildProcess] info: " format, ##__VA_ARGS__)) 54 | #else 55 | #define TRACE_INFO(format, ...) static_cast(0) 56 | #endif 57 | 58 | #if defined(ENABLE_TRACE_ERROR) 59 | #define TRACE_ERROR(format, ...) static_cast(std::fprintf(stderr, "[ChildProcess] error: " format, ##__VA_ARGS__)) 60 | #else 61 | #define TRACE_ERROR(format, ...) static_cast(0) 62 | #endif 63 | 64 | #define TRACE_FATAL(format, ...) static_cast(std::fprintf(stderr, "[ChildProcess] fatal error: " format, ##__VA_ARGS__)) 65 | -------------------------------------------------------------------------------- /src/ChildProcess.Native/include/BinaryReader.hpp: -------------------------------------------------------------------------------- 1 | // Copyright (c) @asmichi (https://github.com/asmichi). Licensed under the MIT License. See LICENCE in the project root for details. 2 | 3 | #pragma once 4 | 5 | #include "Base.hpp" 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | class BadBinaryError : public MyException 13 | { 14 | public: 15 | BadBinaryError(const char* description) : MyException(description) {} 16 | }; 17 | 18 | // Throws std::out_of_range when it would read beyond the end. 19 | class BinaryReader final 20 | { 21 | public: 22 | BinaryReader(const void* data, std::size_t len) noexcept 23 | : cur_(static_cast(data)), end_(static_cast(data) + len) {} 24 | 25 | template 26 | T Read() 27 | { 28 | T value; 29 | std::memcpy(&value, GetCurrentAndAdvance(sizeof(T)), sizeof(T)); 30 | return value; 31 | } 32 | 33 | // NOTE: Pointers returned by GetString will become invalid When data becomes invalid. 34 | const char* GetStringAndAdvance() 35 | { 36 | const std::uint32_t bytes = Read(); 37 | if (bytes == 0) 38 | { 39 | return nullptr; 40 | } 41 | 42 | auto p = reinterpret_cast(GetCurrentAndAdvance(bytes)); 43 | if (p[bytes - 1] != '\0') 44 | { 45 | throw BadBinaryError("String not null-terminated."); 46 | } 47 | return p; 48 | } 49 | 50 | private: 51 | const std::byte* GetCurrentAndAdvance(std::size_t bytesRead) 52 | { 53 | const auto curPos = cur_; 54 | const auto newPos = cur_ + bytesRead; 55 | if (newPos > end_) 56 | { 57 | throw BadBinaryError("Attempted to read beyond the end."); 58 | } 59 | 60 | cur_ = newPos; 61 | return curPos; 62 | } 63 | 64 | const std::byte* cur_; 65 | const std::byte* const end_; 66 | }; 67 | -------------------------------------------------------------------------------- /src/ChildProcess.Native/include/ChildProcessState.hpp: -------------------------------------------------------------------------------- 1 | // Copyright (c) @asmichi (https://github.com/asmichi). Licensed under the MIT License. See LICENCE in the project root for details. 2 | 3 | #pragma once 4 | 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | 17 | // ChildProcessState should not access g_ChildProcessStateMap to avoid dead locks. 18 | class ChildProcessState final 19 | { 20 | public: 21 | ChildProcessState(int pid, std::uint64_t token, bool isNewProcessGroup, bool shouldAutoTerminate) 22 | : token_(token), pid_(pid), isNewProcessGroup_(isNewProcessGroup), shouldAutoTerminate_(shouldAutoTerminate) {} 23 | 24 | std::uint64_t GetToken() const { return token_; } 25 | int GetPid() const { return pid_; } 26 | bool ShouldAutoTerminate() const { return shouldAutoTerminate_; } 27 | 28 | // Should only be called from the service (main) thread. 29 | void Reap(); 30 | 31 | // If alsoSendSigCont, also send SIGCONT to ensure termination. 32 | [[nodiscard]] bool SendSignal(int sig, bool alsoSendSigCont = false) const; 33 | 34 | private: 35 | // Serializes all accesses to the process (signal, reap, etc.). 36 | // NOTE: We must not access a process after we reap it. Otherwise we are vulnerable to PID recycling. 37 | mutable std::mutex mutex_; 38 | const std::uint64_t token_; 39 | const int pid_; 40 | const bool isNewProcessGroup_; 41 | const bool shouldAutoTerminate_; 42 | bool isReaped_ = false; 43 | }; 44 | 45 | // Maintains ChildProcessState elements for all our children. 46 | // An element must be allocated and deleted before we reap the corresponding child. 47 | // NOTE: Once we fork, an element must *always* be allocated for the child. No exception. 48 | // The element must be deleted just before we reap the child. 49 | // Otherwise we are vulnerable to PID recycling. 50 | class ChildProcessStateMap final 51 | { 52 | public: 53 | void Allocate(int pid, std::uint64_t token, bool isNewProcessGroup, bool shouldAutoTerminate); 54 | [[nodiscard]] std::shared_ptr GetByPid(int pid) const; // Used by the reaping process only. 55 | [[nodiscard]] std::shared_ptr GetByToken(std::uint64_t token) const; 56 | void Delete(ChildProcessState* pState); 57 | 58 | // Send SIGTERM then SIGCONT to all children whose shouldAutoTerminate_ is set. 59 | // Should only be called from the service (main) thread. 60 | void AutoTerminateAll(); 61 | 62 | private: 63 | // Serializes lookup, insertion and removal. 64 | mutable std::mutex mapMutex_; 65 | std::unordered_map> byPid_; 66 | std::unordered_map> byToken_; 67 | }; 68 | -------------------------------------------------------------------------------- /src/ChildProcess.Native/include/ErrorCodeExceptions.hpp: -------------------------------------------------------------------------------- 1 | // Copyright (c) @asmichi (https://github.com/asmichi). Licensed under the MIT License. See LICENCE in the project root for details. 2 | 3 | #pragma once 4 | 5 | // NOTE: Make sure to sync with the client. 6 | enum ErrorCode : int 7 | { 8 | InvalidRequest = -1, 9 | }; 10 | 11 | // 0: Success 12 | // Positive: errno 13 | // Negative: ErrorCode 14 | class ErrorCodeException 15 | { 16 | public: 17 | ErrorCodeException(int err) noexcept : err_(err) {} 18 | ErrorCodeException(ErrorCode err) noexcept : err_(static_cast(err)) {} 19 | int GetError() const noexcept { return err_; } 20 | 21 | private: 22 | const int err_; 23 | }; 24 | 25 | class CommunicationError : public ErrorCodeException 26 | { 27 | public: 28 | CommunicationError(int err) noexcept : ErrorCodeException(err) {} 29 | CommunicationError(ErrorCode err) noexcept : ErrorCodeException(err) {} 30 | }; 31 | 32 | class BadRequestError : public ErrorCodeException 33 | { 34 | public: 35 | BadRequestError(int err) noexcept : ErrorCodeException(err) {} 36 | BadRequestError(ErrorCode err) noexcept : ErrorCodeException(err) {} 37 | }; 38 | -------------------------------------------------------------------------------- /src/ChildProcess.Native/include/ExactBytesIO.hpp: -------------------------------------------------------------------------------- 1 | // Copyright (c) @asmichi (https://github.com/asmichi). Licensed under the MIT License. See LICENCE in the project root for details. 2 | 3 | #pragma once 4 | 5 | #include "Base.hpp" 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | // NOTE: Returns false and errno = 0 on a normal shutdown. 12 | template 13 | [[nodiscard]] bool ReadExactBytes(Func f, void* buf, std::size_t len) noexcept 14 | { 15 | std::byte* byteBuf = static_cast(buf); 16 | 17 | std::size_t offset = 0; 18 | while (offset < len) 19 | { 20 | ssize_t bytesRead = f(byteBuf + offset, len - offset); 21 | if (bytesRead == 0) 22 | { 23 | errno = 0; 24 | return false; 25 | } 26 | else if (bytesRead <= -1) 27 | { 28 | return false; 29 | } 30 | 31 | offset += bytesRead; 32 | } 33 | 34 | return true; 35 | } 36 | 37 | template 38 | [[nodiscard]] bool WriteExactBytes(Func f, const void* buf, std::size_t len) noexcept 39 | { 40 | const std::byte* byteBuf = static_cast(buf); 41 | 42 | std::size_t offset = 0; 43 | while (offset < len) 44 | { 45 | ssize_t bytesWritten = f(byteBuf + offset, len - offset); 46 | if (bytesWritten == 0) 47 | { 48 | // POSIX-conformant write & send will not return 0. 49 | FatalErrorAbort(errno, "write/send returned 0!"); 50 | } 51 | else if (bytesWritten <= -1) 52 | { 53 | return false; 54 | } 55 | 56 | offset += bytesWritten; 57 | } 58 | 59 | return true; 60 | } 61 | -------------------------------------------------------------------------------- /src/ChildProcess.Native/include/Globals.hpp: -------------------------------------------------------------------------------- 1 | // Copyright (c) @asmichi (https://github.com/asmichi). Licensed under the MIT License. See LICENCE in the project root for details. 2 | 3 | #pragma once 4 | 5 | class ChildProcessStateMap; 6 | extern ChildProcessStateMap g_ChildProcessStateMap; 7 | 8 | class Service; 9 | extern Service g_Service; 10 | -------------------------------------------------------------------------------- /src/ChildProcess.Native/include/MiscHelpers.hpp: -------------------------------------------------------------------------------- 1 | // Copyright (c) @asmichi (https://github.com/asmichi). Licensed under the MIT License. See LICENCE in the project root for details. 2 | 3 | #pragma once 4 | 5 | // Miscellaneous helper functions. 6 | 7 | #include "UniqueResource.hpp" 8 | #include 9 | #include 10 | #include 11 | 12 | struct pollfd; 13 | 14 | // Wrappers that restarts the operation on EINTR. 15 | [[nodiscard]] ssize_t recv_restarting(int fd, void* buf, size_t len, int flags) noexcept; 16 | [[nodiscard]] ssize_t recvmsg_restarting(int fd, struct msghdr* msg, int flags) noexcept; 17 | [[nodiscard]] ssize_t send_restarting(int fd, const void* buf, size_t len, int flags) noexcept; 18 | [[nodiscard]] ssize_t sendmsg_restarting(int fd, const struct msghdr* msg, int flags) noexcept; 19 | [[nodiscard]] ssize_t read_restarting(int fd, void* buf, size_t len) noexcept; 20 | [[nodiscard]] ssize_t write_restarting(int fd, const void* buf, size_t len) noexcept; 21 | [[nodiscard]] bool ReadExactBytes(int fd, void* buf, std::size_t len) noexcept; 22 | [[nodiscard]] bool WriteExactBytes(int fd, const void* buf, std::size_t len) noexcept; 23 | [[nodiscard]] int poll_restarting(struct pollfd* fds, unsigned int nfds, int timeout) noexcept; 24 | [[nodiscard]] int chdir_restarting(const char* path) noexcept; 25 | 26 | // RAII wrappers. 27 | struct PipeEnds 28 | { 29 | UniqueFd ReadEnd; 30 | UniqueFd WriteEnd; 31 | }; 32 | 33 | [[nodiscard]] std::optional CreatePipe() noexcept; 34 | [[nodiscard]] std::optional CreateUnixStreamSocket() noexcept; 35 | [[nodiscard]] std::optional> CreateUnixStreamSocketPair() noexcept; 36 | [[nodiscard]] std::optional DuplicateFd(int fd) noexcept; 37 | 38 | // Wrappers with my default values. 39 | enum CreateThreadFlags : int 40 | { 41 | CreateThreadFlagsDetached = 1, 42 | }; 43 | 44 | [[nodiscard]] std::optional CreateThreadWithMyDefault(void* (*startRoutine)(void*), void* arg, int flags) noexcept; 45 | -------------------------------------------------------------------------------- /src/ChildProcess.Native/include/Request.hpp: -------------------------------------------------------------------------------- 1 | // Copyright (c) @asmichi (https://github.com/asmichi). Licensed under the MIT License. See LICENCE in the project root for details. 2 | 3 | #pragma once 4 | 5 | #include "UniqueResource.hpp" 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | // Limitations to prevent OOM errors. 12 | const std::uint32_t MaxMessageLength = 2 * 1024 * 1024; 13 | const std::uint32_t MaxStringArrayCount = 64 * 1024; 14 | 15 | // NOTE: Make sure to sync with the client. 16 | enum class RequestCommand : std::uint32_t 17 | { 18 | SpawnProcess = 0, 19 | SendSignal = 1, 20 | }; 21 | 22 | enum class AbstractSignal : std::uint32_t 23 | { 24 | Interrupt = 2, 25 | Kill = 9, 26 | Termination = 15, 27 | }; 28 | 29 | enum SpawnProcessRequestFlags 30 | { 31 | RequestFlagsRedirectStdin = 1 << 0, 32 | RequestFlagsRedirectStdout = 1 << 1, 33 | RequestFlagsRedirectStderr = 1 << 2, 34 | RequestFlagsCreateNewProcessGroup = 1 << 3, 35 | RequestFlagsEnableAutoTermination = 1 << 4, 36 | }; 37 | 38 | struct SpawnProcessRequest final 39 | { 40 | std::unique_ptr Data; 41 | std::uint64_t Token; 42 | std::uint32_t Flags; 43 | const char* WorkingDirectory; 44 | const char* ExecutablePath; 45 | std::vector Argv; 46 | std::vector Envp; 47 | UniqueFd StdinFd; 48 | UniqueFd StdoutFd; 49 | UniqueFd StderrFd; 50 | }; 51 | 52 | struct SendSignalRequest final 53 | { 54 | std::uint64_t Token; 55 | AbstractSignal Signal; 56 | }; 57 | 58 | // NOTE: DeserializeSpawnProcessRequest does not set fds. 59 | void DeserializeSpawnProcessRequest(SpawnProcessRequest* r, std::unique_ptr data, std::size_t length); 60 | void DeserializeSendSignalRequest(SendSignalRequest* r, std::unique_ptr data, std::size_t length); 61 | -------------------------------------------------------------------------------- /src/ChildProcess.Native/include/Service.hpp: -------------------------------------------------------------------------------- 1 | // Copyright (c) @asmichi (https://github.com/asmichi). Licensed under the MIT License. See LICENCE in the project root for details. 2 | 3 | #pragma once 4 | 5 | #include "AncillaryDataSocket.hpp" 6 | #include "ChildProcessState.hpp" 7 | #include "SubchannelCollection.hpp" 8 | #include "UniqueResource.hpp" 9 | #include 10 | #include 11 | #include 12 | #include 13 | 14 | enum class NotificationToService : std::uint8_t 15 | { 16 | // SIGQUIT 17 | Quit, 18 | // Request the service to reap children (SIGCHLD or "child process registered to g_ChildProcessStateMap") 19 | ReapRequest, 20 | // A subchannel is closed, indicating that we may be able to exit. 21 | SubchannelClosed, 22 | }; 23 | 24 | class Service final 25 | { 26 | public: 27 | // Interface for main. 28 | // Delayed initialization. 29 | void Initialize(UniqueFd mainChannelFd); 30 | [[nodiscard]] int Run(); 31 | 32 | // Interface for subchannels. 33 | void NotifyChildRegistration(); 34 | void NotifySubchannelClosed(Subchannel* pSubchannel); 35 | 36 | // Interface for the signal handler. 37 | void NotifySignal(int signum); 38 | 39 | private: 40 | [[nodiscard]] bool WriteNotification(NotificationToService notification); 41 | void InitiateShutdown(); 42 | bool ShouldExit(); 43 | void HandleNotificationPipeInput(); 44 | void ReapAllExitedChildren(); 45 | void HandleMainChannelInput(); 46 | void HandleMainChannelOutput(); 47 | void NotifyClientOfExitedChild(ChildProcessState* pState, siginfo_t siginfo); 48 | 49 | bool shuttingDown_ = false; 50 | 51 | // Write to wake up the service thread. 52 | int notificationPipeReadEnd_ = 0; 53 | int notificationPipeWriteEnd_ = 0; 54 | 55 | // Close the write end to cancel all current and future blocking send/recv. 56 | int cancellationPipeWriteEnd_ = 0; 57 | int cancellationPipeReadEnd_ = 0; 58 | 59 | SubchannelCollection subchannelCollection_; 60 | std::unique_ptr mainChannel_; 61 | }; 62 | -------------------------------------------------------------------------------- /src/ChildProcess.Native/include/SignalHandler.hpp: -------------------------------------------------------------------------------- 1 | // Copyright (c) @asmichi (https://github.com/asmichi). Licensed under the MIT License. See LICENCE in the project root for details. 2 | 3 | #pragma once 4 | 5 | #include "UniqueResource.hpp" 6 | #include 7 | 8 | void SetupSignalHandlers(); 9 | [[noreturn]] void RaiseQuitOnSelf(); 10 | -------------------------------------------------------------------------------- /src/ChildProcess.Native/include/SocketHelpers.hpp: -------------------------------------------------------------------------------- 1 | // Copyright (c) @asmichi (https://github.com/asmichi). Licensed under the MIT License. See LICENCE in the project root for details. 2 | 3 | #pragma once 4 | 5 | // Socket helper functions. 6 | 7 | #include "Base.hpp" 8 | #include 9 | #include 10 | #include 11 | 12 | constexpr const int SocketMaxFdsPerCall = 3; 13 | 14 | struct CmsgFds 15 | { 16 | static const constexpr std::size_t BufferSize = CMSG_SPACE(sizeof(int) * SocketMaxFdsPerCall); 17 | alignas(cmsghdr) char Buffer[BufferSize]; 18 | }; 19 | 20 | [[nodiscard]] bool SendExactBytes(int fd, const void* buf, std::size_t len) noexcept; 21 | [[nodiscard]] bool SendExactBytesWithFd(int fd, const void* buf, std::size_t len, const int* fds, std::size_t fdCount) noexcept; 22 | [[nodiscard]] ssize_t SendWithFd(int fd, const void* buf, std::size_t len, const int* fds, std::size_t fdCount, BlockingFlag blocking) noexcept; 23 | [[nodiscard]] bool RecvExactBytes(int fd, void* buf, std::size_t len) noexcept; 24 | 25 | [[nodiscard]] constexpr int MakeSockFlags(BlockingFlag blocking) noexcept 26 | { 27 | return (blocking == BlockingFlag::NonBlocking ? MSG_DONTWAIT : 0) | MSG_NOSIGNAL; 28 | } 29 | 30 | // true: successful (including EWOULDBLOCK) 31 | // false: connection closed 32 | [[nodiscard]] inline bool HandleSendError(BlockingFlag blocking, const char* str, int err) noexcept 33 | { 34 | if (blocking == BlockingFlag::NonBlocking && IsWouldBlockError(err)) 35 | { 36 | return true; 37 | } 38 | else if (IsConnectionClosedError(err)) 39 | { 40 | return false; 41 | } 42 | else 43 | { 44 | FatalErrorAbort(err, str); 45 | } 46 | } 47 | 48 | inline void AbortIfFatalSendError(BlockingFlag blocking, const char* str, int err) noexcept 49 | { 50 | (void)HandleSendError(blocking, str, err); 51 | } 52 | 53 | // true: successful (including EWOULDBLOCK) 54 | // false: connection closed 55 | [[nodiscard]] inline bool HandleSendResult(BlockingFlag blocking, const char* str, ssize_t bytesSent, int err) noexcept 56 | { 57 | if (bytesSent > 0) 58 | { 59 | return true; 60 | } 61 | else if (bytesSent == 0) 62 | { 63 | // POSIX-conformant send will not return 0. 64 | FatalErrorAbort(errno, "send returned 0!"); 65 | } 66 | else 67 | { 68 | return HandleSendError(blocking, str, err); 69 | } 70 | } 71 | 72 | // true: successful (including EWOULDBLOCK) 73 | // false: connection closed 74 | [[nodiscard]] inline bool HandleRecvError(BlockingFlag blocking, const char* str, int err) noexcept 75 | { 76 | if (blocking == BlockingFlag::NonBlocking && IsWouldBlockError(err)) 77 | { 78 | return true; 79 | } 80 | else if (IsConnectionClosedError(err)) 81 | { 82 | return false; 83 | } 84 | else 85 | { 86 | FatalErrorAbort(err, str); 87 | } 88 | } 89 | 90 | inline void AbortIfFatalRecvError(BlockingFlag blocking, const char* str, int err) noexcept 91 | { 92 | (void)HandleRecvError(blocking, str, err); 93 | } 94 | 95 | // true: successful (including EWOULDBLOCK) 96 | // false: connection closed 97 | [[nodiscard]] inline bool HandleRecvResult(BlockingFlag blocking, const char* str, ssize_t bytesReceived, int err) noexcept 98 | { 99 | if (bytesReceived > 0) 100 | { 101 | return true; 102 | } 103 | else if (bytesReceived == 0) 104 | { 105 | return false; 106 | } 107 | else 108 | { 109 | return HandleRecvError(blocking, str, err); 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/ChildProcess.Native/include/Subchannel.hpp: -------------------------------------------------------------------------------- 1 | // Copyright (c) @asmichi (https://github.com/asmichi). Licensed under the MIT License. See LICENCE in the project root for details. 2 | 3 | #pragma once 4 | 5 | #include "AncillaryDataSocket.hpp" 6 | #include "Request.hpp" 7 | #include "UniqueResource.hpp" 8 | #include 9 | #include 10 | #include 11 | 12 | // Random big value to prevent exhausting memory (by storing a request in memory). 13 | const std::uint32_t MaxRequestLength = 2 * 1024 * 1024; 14 | 15 | struct RawRequest final 16 | { 17 | RequestCommand Command; 18 | uint32_t BodyLength; 19 | std::unique_ptr Body; 20 | }; 21 | 22 | class Subchannel final 23 | { 24 | public: 25 | explicit Subchannel(UniqueFd sockFd, int cancellationPipeReadEnd) noexcept : sock_(std::move(sockFd), cancellationPipeReadEnd) {} 26 | 27 | [[nodiscard]] bool StartCommunicationThread(); 28 | 29 | private: 30 | static void* CommunicationThreadFunc(void* arg); 31 | void CommunicationLoop(); 32 | 33 | void HandleProcessCreationCommand(std::unique_ptr body, std::uint32_t bodyLength); 34 | void ToProcessCreationRequest(SpawnProcessRequest* r, std::unique_ptr body, std::uint32_t bodyLength); 35 | // return: {err, pid} 36 | std::pair CreateProcess(const SpawnProcessRequest& r); 37 | 38 | void HandleSendSignalCommand(std::unique_ptr body, std::uint32_t bodyLength); 39 | std::optional ToNativeSignal(AbstractSignal abstractSignal) noexcept; 40 | 41 | void RecvRawRequest(RawRequest* r); 42 | void SendSuccess(std::int32_t data); 43 | void SendError(int err); 44 | void SendResponse(int err, std::int32_t data); 45 | 46 | AncillaryDataSocket sock_; 47 | }; 48 | -------------------------------------------------------------------------------- /src/ChildProcess.Native/include/SubchannelCollection.hpp: -------------------------------------------------------------------------------- 1 | // Copyright (c) @asmichi (https://github.com/asmichi). Licensed under the MIT License. See LICENCE in the project root for details. 2 | 3 | #pragma once 4 | 5 | #include "Subchannel.hpp" 6 | #include 7 | #include 8 | #include 9 | 10 | class SubchannelCollection final 11 | { 12 | public: 13 | ~SubchannelCollection(); 14 | Subchannel* Add(std::unique_ptr subchannel); 15 | void Delete(Subchannel* key); 16 | size_t Size() const; 17 | 18 | private: 19 | // Serializes lookup, insertion and removal. 20 | mutable std::mutex mapMutex_; 21 | std::unordered_map> map_; 22 | }; 23 | -------------------------------------------------------------------------------- /src/ChildProcess.Native/include/UniqueResource.hpp: -------------------------------------------------------------------------------- 1 | // Copyright (c) @asmichi (https://github.com/asmichi). Licensed under the MIT License. See LICENCE in the project root for details. 2 | 3 | #pragma once 4 | 5 | #include 6 | 7 | // Represents unique ownership of a kind of resource defined by UniqueResourcePolicy. 8 | // 9 | // Unlike unique_ptr, because it is not a pointer, it does not provide operator->. 10 | template 11 | class UniqueResourceImpl final 12 | { 13 | public: 14 | using ThisType = UniqueResourceImpl; 15 | using ValueType = typename UniqueResourcePolicy::ValueType; 16 | 17 | explicit UniqueResourceImpl(ValueType value) noexcept : _value(value) {} 18 | UniqueResourceImpl(ThisType&& other) noexcept : UniqueResourceImpl(other.Release()) {} 19 | UniqueResourceImpl() noexcept : _value(UniqueResourcePolicy::NullValue) {} 20 | UniqueResourceImpl(const ThisType&) = delete; 21 | ~UniqueResourceImpl() noexcept { Reset(); } 22 | 23 | ThisType& operator=(const ThisType&) = delete; 24 | ThisType& operator=(ThisType&& other) noexcept 25 | { 26 | Reset(other.Release()); 27 | return *this; 28 | } 29 | 30 | ValueType Get() const noexcept { return _value; } 31 | bool IsValid() const noexcept { return UniqueResourcePolicy::IsValid(_value); } 32 | 33 | void Reset(ValueType newValue = UniqueResourcePolicy::NullValue) noexcept 34 | { 35 | if (IsValid()) 36 | { 37 | UniqueResourcePolicy::Delete(_value); 38 | } 39 | 40 | _value = newValue; 41 | } 42 | 43 | [[nodiscard]] ValueType Release() noexcept 44 | { 45 | auto tmp = _value; 46 | _value = UniqueResourcePolicy::NullValue; 47 | return tmp; 48 | } 49 | 50 | private: 51 | ValueType _value; 52 | }; 53 | 54 | struct FileDescriptorUniqueResourcePolicy final 55 | { 56 | using ValueType = int; 57 | 58 | static constexpr ValueType NullValue = -1; 59 | 60 | static bool IsValid(const ValueType& value) noexcept 61 | { 62 | return value >= 0; 63 | } 64 | 65 | static void Delete(const ValueType& value) noexcept 66 | { 67 | if (IsValid(value)) 68 | { 69 | ::close(value); 70 | } 71 | } 72 | }; 73 | 74 | // UniqueResource with the file descriptor semantics. 75 | using UniqueFd = UniqueResourceImpl; 76 | -------------------------------------------------------------------------------- /src/ChildProcess.Native/include/WriteBuffer.hpp: -------------------------------------------------------------------------------- 1 | // Copyright (c) @asmichi (https://github.com/asmichi). Licensed under the MIT License. See LICENCE in the project root for details. 2 | 3 | #pragma once 4 | 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | class WriteBuffer final 12 | { 13 | public: 14 | void Enqueue(const void* buf, std::size_t len); 15 | void Dequeue(std::size_t len) noexcept; 16 | bool HasPendingData() noexcept { return !blocks_.empty(); } 17 | std::tuple GetPendingData() noexcept; 18 | 19 | private: 20 | struct Block 21 | { 22 | std::unique_ptr Data; 23 | std::size_t DataBytes; 24 | std::size_t CurrentOffset; 25 | }; 26 | 27 | Block CreateBlock(); 28 | std::size_t StoreToBlock(Block* pBlock, const std::byte* pSrc, std::size_t len) noexcept; 29 | 30 | std::vector blocks_; 31 | }; 32 | -------------------------------------------------------------------------------- /src/ChildProcess.Native/tests/DumpEnvironmentVariables.unix.cpp: -------------------------------------------------------------------------------- 1 | // Copyright (c) @asmichi (https://github.com/asmichi). Licensed under the MIT License. See LICENCE in the project root for details. 2 | 3 | #include "TestSignalHandler.hpp" 4 | #include 5 | #include 6 | #include 7 | 8 | extern char** environ; 9 | 10 | int TestCommandDumpEnvironmentVariables(int, const char* const*) 11 | { 12 | for (char** p = environ; *p != nullptr; p++) 13 | { 14 | std::size_t len = std::strlen(*p); 15 | if (std::fwrite(*p, sizeof(char), len + 1, stdout) != len + 1) 16 | { 17 | perror("fwrite"); 18 | return 1; 19 | } 20 | } 21 | 22 | std::fflush(stdout); 23 | 24 | return 0; 25 | } 26 | -------------------------------------------------------------------------------- /src/ChildProcess.Native/tests/DumpEnvironmentVariables.win.cpp: -------------------------------------------------------------------------------- 1 | // Copyright (c) @asmichi (https://github.com/asmichi). Licensed under the MIT License. See LICENCE in the project root for details. 2 | 3 | #define WIN32_LEAN_AND_MEAN 4 | #define NOMINMAX 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | 14 | int TestCommandDumpEnvironmentVariables(int argc, const char* const* argv) 15 | { 16 | if (_setmode(_fileno(stdout), O_BINARY) == -1) 17 | { 18 | perror("_setmode"); 19 | return 1; 20 | } 21 | 22 | wchar_t* pFirstEnv = GetEnvironmentStringsW(); 23 | if (pFirstEnv == nullptr) 24 | { 25 | return 1; 26 | } 27 | 28 | wchar_t* pEnv = pFirstEnv; 29 | while (*pEnv != '\0') 30 | { 31 | const std::size_t len = wcslen(pEnv); 32 | if (len > UNICODE_STRING_MAX_CHARS) 33 | { 34 | std::fprintf(stderr, "Broken environment block.\n"); 35 | return 1; 36 | } 37 | 38 | if (*pEnv == '=') 39 | { 40 | // ignore hidden environment variables 41 | pEnv += len + 1; 42 | continue; 43 | } 44 | 45 | const int requiredBytes = WideCharToMultiByte(CP_UTF8, WC_ERR_INVALID_CHARS, pEnv, static_cast(len) + 1, nullptr, 0, nullptr, nullptr); 46 | auto buf = std::make_unique(requiredBytes); 47 | 48 | const int actualBytes = WideCharToMultiByte(CP_UTF8, WC_ERR_INVALID_CHARS, pEnv, static_cast(len) + 1, buf.get(), requiredBytes, nullptr, nullptr); 49 | if (actualBytes == 0) 50 | { 51 | std::fprintf(stderr, "WideCharToMultiByte failed with %d.\n", GetLastError()); 52 | return 1; 53 | } 54 | 55 | if (std::fwrite(buf.get(), sizeof(char), actualBytes, stdout) != actualBytes) 56 | { 57 | perror("fwrite"); 58 | return 1; 59 | } 60 | 61 | pEnv += len + 1; 62 | } 63 | 64 | std::fflush(stdout); 65 | FreeEnvironmentStringsW(pFirstEnv); 66 | return 0; 67 | } 68 | -------------------------------------------------------------------------------- /src/ChildProcess.Native/tests/ReportSignal.unix.cpp: -------------------------------------------------------------------------------- 1 | // Copyright (c) @asmichi (https://github.com/asmichi). Licensed under the MIT License. See LICENCE in the project root for details. 2 | 3 | #include "TestSignalHandler.hpp" 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | namespace 11 | { 12 | void SignalHandler(int signum) 13 | { 14 | ssize_t bytes; 15 | 16 | switch (signum) 17 | { 18 | case SIGINT: 19 | bytes = write(STDOUT_FILENO, "I", 1); 20 | break; 21 | 22 | case SIGTERM: 23 | bytes = write(STDOUT_FILENO, "T", 1); 24 | break; 25 | 26 | default: 27 | break; 28 | } 29 | } 30 | } // namespace 31 | 32 | int TestCommandReportSignal(int, const char* const*) 33 | { 34 | SetSignalHandler(SIGINT, SA_RESTART, SignalHandler); 35 | SetSignalHandler(SIGTERM, SA_RESTART, SignalHandler); 36 | 37 | // Tell the parent we are ready. 38 | std::fprintf(stdout, "R"); 39 | std::fflush(stdout); 40 | 41 | // Wait for stdin to be closed. 42 | while (getc(stdin) != EOF) 43 | { 44 | } 45 | 46 | return 0; 47 | } 48 | -------------------------------------------------------------------------------- /src/ChildProcess.Native/tests/ReportSignal.win.cpp: -------------------------------------------------------------------------------- 1 | // Copyright (c) @asmichi (https://github.com/asmichi). Licensed under the MIT License. See LICENCE in the project root for details. 2 | 3 | #define WIN32_LEAN_AND_MEAN 4 | #define NOMINMAX 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | namespace 12 | { 13 | BOOL WINAPI CtrlHandler(DWORD dwCtrlType) 14 | { 15 | switch (dwCtrlType) 16 | { 17 | case CTRL_C_EVENT: 18 | std::fprintf(stdout, "I"); 19 | std::fflush(stdout); 20 | return TRUE; 21 | 22 | case CTRL_CLOSE_EVENT: 23 | case CTRL_BREAK_EVENT: 24 | std::fprintf(stdout, "T"); 25 | std::fflush(stdout); 26 | return TRUE; 27 | 28 | default: 29 | return FALSE; 30 | } 31 | } 32 | } // namespace 33 | 34 | int TestCommandReportSignal(int argc, const char* const* argv) 35 | { 36 | if (!SetConsoleCtrlHandler(CtrlHandler, TRUE)) 37 | { 38 | std::abort(); 39 | } 40 | 41 | // Tell the parent we are ready. 42 | std::fprintf(stdout, "R"); 43 | std::fflush(stdout); 44 | 45 | // Wait for stdin to be closed. 46 | // NOTE: If stdin is not redirected, this getc call weirdly returns EOF on CTRL+C (Win10 20H2 19042.804). 47 | while (getc(stdin) != EOF) 48 | { 49 | } 50 | 51 | return 0; 52 | } 53 | -------------------------------------------------------------------------------- /src/ChildProcess.Native/tests/Startup.unix.cpp: -------------------------------------------------------------------------------- 1 | // Copyright (c) @asmichi (https://github.com/asmichi). Licensed under the MIT License. See LICENCE in the project root for details. 2 | 3 | #include "Base.hpp" 4 | #include "TestSignalHandler.hpp" 5 | #include 6 | #include 7 | 8 | void SetupDefaultTestSignalHandlers() 9 | { 10 | SetSignalHandler(SIGINT, 0, SIG_DFL); 11 | SetSignalHandler(SIGTERM, 0, SIG_DFL); 12 | SetSignalHandler(SIGQUIT, 0, SIG_DFL); 13 | SetSignalHandler(SIGPIPE, 0, SIG_IGN); 14 | SetSignalHandler(SIGCHLD, 0, SIG_IGN); 15 | } 16 | -------------------------------------------------------------------------------- /src/ChildProcess.Native/tests/TestChildMain.cpp: -------------------------------------------------------------------------------- 1 | // Copyright (c) @asmichi (https://github.com/asmichi). Licensed under the MIT License. See LICENCE in the project root for details. 2 | 3 | #include 4 | #include 5 | 6 | // *nix-specific startup code 7 | #if !defined(_WIN32) 8 | extern void SetupDefaultTestSignalHandlers(); 9 | #endif 10 | 11 | // Handlers 12 | extern int TestCommandReportSignal(int argc, const char* const* argv); 13 | extern int TestCommandDumpEnvironmentVariables(int argc, const char* const* argv); 14 | #if defined(_WIN32) 15 | #else 16 | #endif 17 | 18 | namespace 19 | { 20 | struct TestCommandDefinition 21 | { 22 | const char* const SubcommandName; 23 | int (*const Handler)(int argc, const char* const* argv); 24 | }; 25 | 26 | TestCommandDefinition TestCommandDefinitions[] = { 27 | {"ReportSignal", TestCommandReportSignal}, 28 | {"DumpEnvironmentVariables", TestCommandDumpEnvironmentVariables}, 29 | #if defined(_WIN32) 30 | #else 31 | #endif 32 | }; 33 | } // namespace 34 | 35 | int main(int argc, const char** argv) 36 | { 37 | if (argc < 2) 38 | { 39 | std::fprintf(stderr, "Usage: TestChildNative name [args]\n"); 40 | return 1; 41 | } 42 | 43 | #if !defined(_WIN32) 44 | SetupDefaultTestSignalHandlers(); 45 | #endif 46 | 47 | const char* const subcommand = argv[1]; 48 | for (const auto& def : TestCommandDefinitions) 49 | { 50 | if (strcmp(subcommand, def.SubcommandName) == 0) 51 | { 52 | return def.Handler(argc, argv); 53 | } 54 | } 55 | 56 | std::fprintf(stderr, "error: Unknown subcommand '%s'\n", subcommand); 57 | return 1; 58 | } 59 | -------------------------------------------------------------------------------- /src/ChildProcess.Native/tests/TestSignalHandler.cpp: -------------------------------------------------------------------------------- 1 | // Copyright (c) @asmichi (https://github.com/asmichi). Licensed under the MIT License. See LICENCE in the project root for details. 2 | 3 | #include "TestSignalHandler.hpp" 4 | #include "Base.hpp" 5 | #include 6 | #include 7 | 8 | void SetSignalHandler(int signum, int flags, void (*handler)(int)) 9 | { 10 | struct sigaction act = {}; 11 | act.sa_flags = flags; 12 | sigemptyset(&act.sa_mask); 13 | act.sa_handler = handler; 14 | 15 | if (sigaction(signum, &act, nullptr) != 0) 16 | { 17 | FatalErrorAbort("sigaction"); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/ChildProcess.Native/tests/TestSignalHandler.hpp: -------------------------------------------------------------------------------- 1 | // Copyright (c) @asmichi (https://github.com/asmichi). Licensed under the MIT License. See LICENCE in the project root for details. 2 | 3 | void SetSignalHandler(int signum, int flags, void (*handler)(int)); 4 | -------------------------------------------------------------------------------- /src/ChildProcess.Test/GlobalSuppressions.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) @asmichi (https://github.com/asmichi). Licensed under the MIT License. See LICENCE in the project root for details. 2 | 3 | using System.Diagnostics.CodeAnalysis; 4 | 5 | [assembly: SuppressMessage("Design", "CA1062:Validate arguments of public methods", Justification = "This is not production code.")] 6 | [assembly: SuppressMessage("Naming", "CA1707: Identifiers should not contain underscores", Justification = "This is test code.")] 7 | [assembly: SuppressMessage("Reliability", "CA2007:Do not directly await a Task", Justification = "This is application-level code.")] 8 | -------------------------------------------------------------------------------- /src/ChildProcess.Test/ProcessManagement/ChildProcessExecutionTestUtil.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) @asmichi (https://github.com/asmichi). Licensed under the MIT License. See LICENCE in the project root for details. 2 | 3 | using System.IO; 4 | using System.Text; 5 | 6 | namespace Asmichi.ProcessManagement 7 | { 8 | public static class ChildProcessExecutionTestUtil 9 | { 10 | public static string ExecuteForStandardOutput(ChildProcessStartInfo si, Encoding? encoding = null) 11 | { 12 | using var p = ChildProcess.Start(si); 13 | 14 | if (p.HasStandardInput) 15 | { 16 | p.StandardInput.Close(); 17 | } 18 | 19 | if (p.HasStandardError) 20 | { 21 | p.StandardError.Close(); 22 | } 23 | 24 | using var sr = new StreamReader(p.StandardOutput, encoding ?? Encoding.UTF8); 25 | var standardOutput = sr.ReadToEnd(); 26 | p.WaitForExit(); 27 | 28 | if (p.ExitCode != 0) 29 | { 30 | throw new ChildProcessFailedException($"Child process failed with exit code {p.ExitCode} (0x{p.ExitCode:X8})."); 31 | } 32 | 33 | return standardOutput; 34 | } 35 | } 36 | 37 | public class ChildProcessFailedException : System.Exception 38 | { 39 | public ChildProcessFailedException() 40 | { 41 | } 42 | 43 | public ChildProcessFailedException(string message) 44 | : base(message) 45 | { 46 | } 47 | 48 | public ChildProcessFailedException(string message, System.Exception inner) 49 | : base(message, inner) 50 | { 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/ChildProcess.Test/ProcessManagement/ChildProcessStartInfoTest.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) @asmichi (https://github.com/asmichi). Licensed under the MIT License. See LICENCE in the project root for details. 2 | 3 | using System; 4 | using System.Collections.Generic; 5 | using Xunit; 6 | 7 | namespace Asmichi.ProcessManagement 8 | { 9 | public class ChildProcessStartInfoTest 10 | { 11 | [Fact] 12 | public void DefaultValueTest() 13 | { 14 | var sut = new ChildProcessStartInfo(); 15 | 16 | Assert.Equal(InputRedirection.NullDevice, sut.StdInputRedirection); 17 | Assert.Equal(OutputRedirection.ParentOutput, sut.StdOutputRedirection); 18 | Assert.Equal(OutputRedirection.ParentError, sut.StdErrorRedirection); 19 | Assert.Null(sut.StdInputFile); 20 | Assert.Null(sut.StdInputHandle); 21 | Assert.Null(sut.StdOutputFile); 22 | Assert.Null(sut.StdOutputHandle); 23 | Assert.Null(sut.StdErrorFile); 24 | Assert.Null(sut.StdErrorHandle); 25 | Assert.Null(sut.FileName); 26 | Assert.Equal(Array.Empty(), sut.Arguments); 27 | Assert.Null(sut.WorkingDirectory); 28 | Assert.Equal(Array.Empty>(), sut.ExtraEnvironmentVariables); 29 | Assert.Equal(ChildProcessFlags.None, sut.Flags); 30 | Assert.Equal(65001, sut.CodePage); 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/ChildProcess.Test/ProcessManagement/ChildProcessTest_Handle.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) @asmichi (https://github.com/asmichi). Licensed under the MIT License. See LICENCE in the project root for details. 2 | 3 | using System; 4 | using System.Runtime.InteropServices; 5 | using Asmichi.Interop.Windows; 6 | using Asmichi.Utilities; 7 | using Xunit; 8 | 9 | namespace Asmichi.ProcessManagement 10 | { 11 | public sealed class ChildProcessTest_Handle 12 | { 13 | // Currently supported only on Windows. 14 | private static bool IsSupported => RuntimeInformation.IsOSPlatform(OSPlatform.Windows); 15 | 16 | [Fact] 17 | public void CannotObtainHandleByDefault() 18 | { 19 | if (!IsSupported) 20 | { 21 | return; 22 | } 23 | 24 | var si = new ChildProcessStartInfo(TestUtil.DotnetCommandName, TestUtil.TestChildPath); 25 | using var sut = ChildProcess.Start(si); 26 | 27 | // Should not dispose PseudoConsole (on NotSupportedException below) too early while the child process is initializing 28 | sut.WaitForExit(); 29 | 30 | Assert.False(sut.HasHandle); 31 | Assert.Throws(() => sut.Handle); 32 | } 33 | 34 | [Fact] 35 | public void CanObtainHandle() 36 | { 37 | if (!IsSupported) 38 | { 39 | return; 40 | } 41 | 42 | var si = new ChildProcessStartInfo(TestUtil.DotnetCommandName, TestUtil.TestChildPath, "EchoBack") 43 | { 44 | Flags = ChildProcessFlags.EnableHandle, 45 | StdInputRedirection = InputRedirection.InputPipe, 46 | StdOutputRedirection = OutputRedirection.NullDevice, 47 | }; 48 | 49 | using var sut = ChildProcess.Start(si); 50 | 51 | Assert.True(sut.HasHandle); 52 | _ = sut.Handle; 53 | _ = sut.PrimaryThreadHandle; 54 | 55 | Assert.False(sut.WaitForExit(0)); 56 | Kernel32.TerminateProcess(sut.Handle, -1); 57 | sut.WaitForExit(); 58 | 59 | Assert.Equal(-1, sut.ExitCode); 60 | } 61 | 62 | [Fact] 63 | public void RejectsEnableHandleOnUnsupportedPlatforms() 64 | { 65 | if (IsSupported) 66 | { 67 | return; 68 | } 69 | 70 | var si = new ChildProcessStartInfo(TestUtil.DotnetCommandName, TestUtil.TestChildPath) 71 | { 72 | Flags = ChildProcessFlags.EnableHandle, 73 | }; 74 | 75 | Assert.Throws(() => { ChildProcess.Start(si).Dispose(); }); 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/ChildProcess.Test/ProcessManagement/ChildProcessTest_Performance.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) @asmichi (https://github.com/asmichi). Licensed under the MIT License. See LICENCE in the project root for details. 2 | 3 | using System; 4 | using System.Diagnostics; 5 | using System.Linq; 6 | using System.Threading.Tasks; 7 | using Asmichi.Utilities; 8 | using Xunit; 9 | 10 | namespace Asmichi.ProcessManagement 11 | { 12 | public class ChildProcessTest_Performance 13 | { 14 | // TODO: This test is flaky. This cannot be done as part of unit tests and should be an independent benchmark done in a separate process. 15 | [Fact(Skip = "Super flaky on Azure macOS image")] 16 | public void ChildProcessWaitForAsyncIsTrulyAsynchronous() 17 | { 18 | var si = new ChildProcessStartInfo(TestUtil.DotnetCommandName, TestUtil.TestChildPath, "Sleep", "1000") 19 | { 20 | StdOutputRedirection = OutputRedirection.NullDevice, 21 | StdErrorRedirection = OutputRedirection.NullDevice, 22 | }; 23 | 24 | using var sut = ChildProcess.Start(si); 25 | WaitForAsyncIsTrulyAsynchronous(sut); 26 | sut.WaitForExit(); 27 | Assert.Equal(0, sut.ExitCode); 28 | } 29 | 30 | private static void WaitForAsyncIsTrulyAsynchronous(IChildProcess sut) 31 | { 32 | var sw = Stopwatch.StartNew(); 33 | // Because WaitForExitAsync is truly asynchronous and does not block a thread-pool thread, 34 | // we can create WaitForExitAsync tasks without consuming thread-pool threads. 35 | // In other words, if WaitForExitAsync would consume a thread-pool thread, the works queued by Task.Run would be blocked. 36 | var waitTasks = 37 | Enumerable.Range(0, Environment.ProcessorCount * 8) 38 | .Select(_ => sut.WaitForExitAsync(1000)) 39 | .ToArray(); 40 | Assert.True(waitTasks.All(x => !x.IsCompleted)); 41 | 42 | var emptyTasks = 43 | Enumerable.Range(0, Environment.ProcessorCount * 8) 44 | .Select(_ => Task.Run(() => { })) 45 | .ToArray(); 46 | Task.WaitAll(emptyTasks); 47 | 48 | Assert.True(sw.ElapsedMilliseconds < 100); 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/ChildProcess.Test/ProcessManagement/ChildProcessTest_Signals.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) @asmichi (https://github.com/asmichi). Licensed under the MIT License. See LICENCE in the project root for details. 2 | 3 | using System; 4 | using System.Runtime.InteropServices; 5 | using Asmichi.Interop.Windows; 6 | using Asmichi.Utilities; 7 | using Xunit; 8 | 9 | namespace Asmichi.ProcessManagement 10 | { 11 | public sealed class ChildProcessTest_Signals 12 | { 13 | [Fact] 14 | public void CanSendSignal() 15 | { 16 | var si = new ChildProcessStartInfo(TestUtil.TestChildNativePath, "ReportSignal") 17 | { 18 | StdInputRedirection = InputRedirection.InputPipe, 19 | StdOutputRedirection = OutputRedirection.OutputPipe, 20 | }; 21 | 22 | using var sut = ChildProcess.Start(si); 23 | 24 | Assert.True(sut.CanSignal); 25 | 26 | Assert.Equal('R', sut.StandardOutput.ReadByte()); 27 | 28 | sut.SignalInterrupt(); 29 | Assert.Equal('I', sut.StandardOutput.ReadByte()); 30 | 31 | sut.SignalInterrupt(); 32 | Assert.Equal('I', sut.StandardOutput.ReadByte()); 33 | 34 | if (!HasWorkaroundForWindows1809) 35 | { 36 | // NOTE: On Windows, a console app cannot cancel CTRL_CLOSE_EVENT (generated when the attached pseudo console is closed). 37 | // It will be killed after the 5s-timeout elapses. Once we call SignalTermination, we must treat the app as already terminated. 38 | // https://docs.microsoft.com/en-us/windows/console/handlerroutine#timeouts 39 | sut.SignalTermination(); 40 | Assert.Equal('T', sut.StandardOutput.ReadByte()); 41 | } 42 | 43 | sut.Kill(); 44 | sut.WaitForExit(); 45 | 46 | Assert.NotEqual(0, sut.ExitCode); 47 | } 48 | 49 | [Fact] 50 | public void CannotSendSignalIfAttachedToCurrentConsole() 51 | { 52 | var si = new ChildProcessStartInfo(TestUtil.TestChildNativePath, "ReportSignal") 53 | { 54 | StdInputRedirection = InputRedirection.InputPipe, 55 | StdOutputRedirection = OutputRedirection.OutputPipe, 56 | Flags = ChildProcessFlags.AttachToCurrentConsole, 57 | }; 58 | 59 | using var sut = ChildProcess.Start(si); 60 | 61 | Assert.False(sut.CanSignal); 62 | Assert.Throws(() => sut.SignalInterrupt()); 63 | Assert.Throws(() => sut.SignalTermination()); 64 | Assert.Equal('R', sut.StandardOutput.ReadByte()); 65 | 66 | sut.Kill(); 67 | sut.WaitForExit(); 68 | 69 | Assert.NotEqual(0, sut.ExitCode); 70 | } 71 | 72 | private static bool HasWorkaroundForWindows1809 => 73 | RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && WindowsVersion.NeedsWorkaroundForWindows1809; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/ChildProcess.Test/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) @asmichi (https://github.com/asmichi). Licensed under the MIT License. See LICENCE in the project root for details. 2 | 3 | using System; 4 | 5 | [assembly: CLSCompliant(false)] 6 | -------------------------------------------------------------------------------- /src/ChildProcess.Test/Utilities/TemporaryDirectory.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) @asmichi (https://github.com/asmichi). Licensed under the MIT License. See LICENCE in the project root for details. 2 | 3 | using System; 4 | using System.IO; 5 | 6 | namespace Asmichi.Utilities 7 | { 8 | internal sealed class TemporaryDirectory : IDisposable 9 | { 10 | public TemporaryDirectory() 11 | { 12 | Location = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); 13 | Directory.CreateDirectory(Location); 14 | } 15 | 16 | public void Dispose() 17 | { 18 | Directory.Delete(Location, true); 19 | } 20 | 21 | public string Location { get; } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/ChildProcess.Test/Utilities/TestUtil.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) @asmichi (https://github.com/asmichi). Licensed under the MIT License. See LICENCE in the project root for details. 2 | 3 | using System; 4 | using System.IO; 5 | 6 | namespace Asmichi.Utilities 7 | { 8 | internal static class TestUtil 9 | { 10 | public static string DotnetCommandName => "dotnet"; 11 | public static string TestChildPath => Path.Combine(Environment.CurrentDirectory, "TestChild.dll"); 12 | public static string TestChildNativePath => Path.Combine(Environment.CurrentDirectory, "TestChildNative"); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/ChildProcess.Test/Utilities/WindowsEnvironmentBlockUtilTest.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) @asmichi (https://github.com/asmichi). Licensed under the MIT License. See LICENCE in the project root for details. 2 | 3 | using Xunit; 4 | using KV = System.Collections.Generic.KeyValuePair; 5 | 6 | namespace Asmichi.Utilities 7 | { 8 | public class WindowsEnvironmentBlockUtilTest 9 | { 10 | public static readonly object[][] TestMakeEnvironmentBlockWin32TestCases = new object[][] 11 | { 12 | new object[2] { "A=a\0\0", new[] { new KV("A", "a") } }, 13 | new object[2] { "A=a\0BB=bb\0\0", new[] { new KV("A", "a"), new KV("BB", "bb") } }, 14 | }; 15 | 16 | [Theory] 17 | [MemberData(nameof(TestMakeEnvironmentBlockWin32TestCases))] 18 | public void TestMakeEnvironmentBlockWin32(string expected, KV[] input) 19 | { 20 | Assert.Equal( 21 | expected.ToCharArray(), 22 | WindowsEnvironmentBlockUtil.MakeEnvironmentBlock(input)); 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/ChildProcess.Test/xunit.runner.json: -------------------------------------------------------------------------------- 1 | { 2 | "methodDisplay": "method" 3 | } -------------------------------------------------------------------------------- /src/ChildProcess/ChildProcess.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netcoreapp3.1 5 | 6 | 7 | 8 | 9 | 10 | true 11 | Asmichi.ChildProcess 12 | ..\PublicAssembly.ruleset 13 | $(OutDir)$(AssemblyName).xml 14 | Asmichi 15 | $(DefineConstants);ADD_IMPORT_SEARCH_PATH_ASSEMBLY_DIRECTORY 16 | 17 | 18 | 19 | true 20 | Asmichi.ChildProcess 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 33 | 34 | 35 | true 36 | PreserveNewest 37 | %(ChildProcessNativeFile.Identity) 38 | %(ChildProcessNativeFile.RelativeDir) 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /src/ChildProcess/Interop/Windows/InputWriterOnlyPseudoConsole.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) @asmichi (https://github.com/asmichi). Licensed under the MIT License. See LICENCE in the project root for details. 2 | 3 | using System; 4 | using System.IO; 5 | using Asmichi.PlatformAbstraction; 6 | using Microsoft.Win32.SafeHandles; 7 | 8 | namespace Asmichi.Interop.Windows 9 | { 10 | internal sealed class InputWriterOnlyPseudoConsole : IDisposable 11 | { 12 | private readonly SafePseudoConsoleHandle _handle; 13 | private readonly SafeFileHandle _consoleInputWriter; 14 | 15 | private InputWriterOnlyPseudoConsole(SafePseudoConsoleHandle handle, SafeFileHandle consoleInputWriter) 16 | { 17 | _handle = handle; 18 | _consoleInputWriter = consoleInputWriter; 19 | } 20 | 21 | public void Dispose() 22 | { 23 | _handle.Dispose(); 24 | _consoleInputWriter.Dispose(); 25 | } 26 | 27 | public SafePseudoConsoleHandle Handle => _handle; 28 | public SafeFileHandle ConsoleInputWriter => _consoleInputWriter; 29 | 30 | public static InputWriterOnlyPseudoConsole Create() 31 | { 32 | var (inputReader, inputWriter) = FilePal.CreatePipePair(); 33 | try 34 | { 35 | using var outputWriter = FilePal.OpenNullDevice(FileAccess.Write); 36 | var hPC = SafePseudoConsoleHandle.Create(inputReader, outputWriter); 37 | return new InputWriterOnlyPseudoConsole(hPC, inputWriter); 38 | } 39 | catch 40 | { 41 | inputWriter.Dispose(); 42 | throw; 43 | } 44 | finally 45 | { 46 | inputReader.Dispose(); 47 | } 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/ChildProcess/Interop/Windows/Kernel32.Console.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) @asmichi (https://github.com/asmichi). Licensed under the MIT License. See LICENCE in the project root for details. 2 | 3 | using System; 4 | using System.Runtime.InteropServices; 5 | 6 | namespace Asmichi.Interop.Windows 7 | { 8 | internal static partial class Kernel32 9 | { 10 | [DllImport(DllName, SetLastError = true)] 11 | public static extern int GetConsoleCP(); 12 | 13 | [DllImport(DllName, SetLastError = true)] 14 | public static extern bool GetConsoleMode([In] SafeHandle hConsoleHandle, [Out] out int lpMode); 15 | 16 | // Not using SafeHandle; we do not own the returned handle. 17 | [DllImport(DllName, SetLastError = true)] 18 | public static extern IntPtr GetStdHandle([In] int nStdHandle); 19 | 20 | // return: HRESULT 21 | [DllImport(DllName)] 22 | internal static extern int CreatePseudoConsole( 23 | [In] COORD size, 24 | [In] SafeHandle hInput, 25 | [In] SafeHandle hOutput, 26 | [In] uint dwFlags, 27 | [Out] out IntPtr phPC); 28 | 29 | // return: HRESULT 30 | [DllImport(DllName)] 31 | internal static extern int ResizePseudoConsole([In] IntPtr hPC, [In] COORD size); 32 | 33 | [DllImport(DllName)] 34 | internal static extern void ClosePseudoConsole([In] IntPtr hPC); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/ChildProcess/Interop/Windows/Kernel32.CreateProcess.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) @asmichi (https://github.com/asmichi). Licensed under the MIT License. See LICENCE in the project root for details. 2 | 3 | using System; 4 | using System.Runtime.InteropServices; 5 | using System.Text; 6 | 7 | #pragma warning disable SA1307 // Accessible fields must begin with upper-case letter 8 | #pragma warning disable SA1310 // Field names must not contain underscore 9 | 10 | namespace Asmichi.Interop.Windows 11 | { 12 | internal static partial class Kernel32 13 | { 14 | // Process Creation Flags 15 | public const int CREATE_SUSPENDED = 0x00000004; 16 | public const int CREATE_UNICODE_ENVIRONMENT = 0x00000400; 17 | public const int EXTENDED_STARTUPINFO_PRESENT = 0x00080000; 18 | public const int CREATE_NO_WINDOW = 0x08000000; 19 | 20 | // STARTUPINFOEX.dwFlags 21 | public const int STARTF_USESTDHANDLES = 0x00000100; 22 | 23 | [DllImport(DllName, EntryPoint = "CreateProcessW", SetLastError = true, CharSet = CharSet.Unicode)] 24 | public static extern unsafe bool CreateProcess( 25 | [In] string? lpApplicationName, 26 | [In] StringBuilder lpCommandLine, 27 | [In] IntPtr procSecAttrs, 28 | [In] IntPtr threadSecAttrs, 29 | [In] bool bInheritHandles, 30 | [In] int dwCreationFlags, 31 | [In] char* lpEnvironment, 32 | [In] string? lpCurrentDirectory, 33 | [In][Out] ref STARTUPINFOEX lpStartupInfo, 34 | [Out] out PROCESS_INFORMATION lpProcessInformation); 35 | 36 | [StructLayout(LayoutKind.Sequential)] 37 | public struct PROCESS_INFORMATION 38 | { 39 | public IntPtr hProcess; 40 | public IntPtr hThread; 41 | public int dwProcessId; 42 | public int dwThreadId; 43 | } 44 | 45 | [StructLayout(LayoutKind.Sequential)] 46 | public struct STARTUPINFOEX 47 | { 48 | public int cb; 49 | public IntPtr lpReserved; 50 | public IntPtr lpDesktop; 51 | public IntPtr lpTitle; 52 | public int dwX; 53 | public int dwY; 54 | public int dwXSize; 55 | public int dwYSize; 56 | public int dwXCountChars; 57 | public int dwYCountChars; 58 | public int dwFillAttribute; 59 | public int dwFlags; 60 | public short wShowWindow; 61 | public short cbReserved2; 62 | public IntPtr lpReserved2; 63 | public IntPtr hStdInput; 64 | public IntPtr hStdOutput; 65 | public IntPtr hStdError; 66 | public IntPtr lpAttributeList; 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/ChildProcess/Interop/Windows/Kernel32.JobObject.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) @asmichi (https://github.com/asmichi). Licensed under the MIT License. See LICENCE in the project root for details. 2 | 3 | using System; 4 | using System.Runtime.InteropServices; 5 | using Microsoft.Win32.SafeHandles; 6 | 7 | #pragma warning disable SA1310 // Field names must not contain underscore 8 | 9 | namespace Asmichi.Interop.Windows 10 | { 11 | internal static partial class Kernel32 12 | { 13 | // https://docs.microsoft.com/en-us/windows/win32/procthread/job-objects 14 | // JOBOBJECT_BASIC_LIMIT_INFORMATION.LimitFlags 15 | public const int JOB_OBJECT_LIMIT_BREAKAWAY_OK = 0x00000800; 16 | public const int JOB_OBJECT_LIMIT_DIE_ON_UNHANDLED_EXCEPTION = 0x00000400; 17 | public const int JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE = 0x00002000; 18 | 19 | [DllImport(DllName, EntryPoint = "CreateJobObjectW", SetLastError = true, CharSet = CharSet.Unicode)] 20 | public static extern SafeJobObjectHandle CreateJobObject( 21 | [In] IntPtr lpJobAttributes, 22 | [In] char[]? lpName); 23 | 24 | [DllImport(DllName, SetLastError = true)] 25 | public static extern unsafe bool SetInformationJobObject( 26 | [In] SafeJobObjectHandle hJob, 27 | [In] JOBOBJECTINFOCLASS jobObjectInformationClass, 28 | [In] void* lpJobObjectInformation, 29 | [In] int cbJobObjectInformationLength); 30 | 31 | [DllImport(DllName, SetLastError = true)] 32 | public static extern bool TerminateJobObject( 33 | [In] SafeJobObjectHandle hJob, 34 | [In] int uExitCode); 35 | 36 | public enum JOBOBJECTINFOCLASS : int 37 | { 38 | JobObjectBasicLimitInformation = 2, 39 | JobObjectExtendedLimitInformation = 9, 40 | } 41 | 42 | [StructLayout(LayoutKind.Sequential)] 43 | public struct JOBOBJECT_BASIC_LIMIT_INFORMATION 44 | { 45 | public long PerProcessUserTimeLimit; 46 | public long PerJobUserTimeLimit; 47 | public uint LimitFlags; 48 | public nuint MinimumWorkingSetSize; 49 | public nuint MaximumWorkingSetSize; 50 | public ulong ActiveProcessLimit; 51 | public nuint Affinity; 52 | public uint PriorityClass; 53 | public uint SchedulingClass; 54 | } 55 | 56 | [StructLayout(LayoutKind.Sequential)] 57 | public struct JOBOBJECT_EXTENDED_LIMIT_INFORMATION 58 | { 59 | public JOBOBJECT_BASIC_LIMIT_INFORMATION BasicLimitInformation; 60 | public IO_COUNTERS IoInfo; 61 | public nuint ProcessMemoryLimit; 62 | public nuint JobMemoryLimit; 63 | public nuint PeakProcessMemoryUsed; 64 | public nuint PeakJobMemoryUsed; 65 | } 66 | 67 | [StructLayout(LayoutKind.Sequential)] 68 | public struct IO_COUNTERS 69 | { 70 | public ulong ReadOperationCount; 71 | public ulong WriteOperationCount; 72 | public ulong OtherOperationCount; 73 | public ulong ReadTransferCount; 74 | public ulong WriteTransferCount; 75 | public ulong OtherTransferCount; 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/ChildProcess/Interop/Windows/Kernel32.Pipe.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) @asmichi (https://github.com/asmichi). Licensed under the MIT License. See LICENCE in the project root for details. 2 | 3 | using System; 4 | using System.Runtime.InteropServices; 5 | using Microsoft.Win32.SafeHandles; 6 | 7 | #pragma warning disable SA1310 // Field names must not contain underscore 8 | 9 | namespace Asmichi.Interop.Windows 10 | { 11 | internal static partial class Kernel32 12 | { 13 | public const uint PIPE_ACCESS_INBOUND = 1; 14 | public const uint PIPE_ACCESS_OUTBOUND = 2; 15 | public const uint PIPE_ACCESS_DUPLEX = 3; 16 | public const uint FILE_FLAG_FIRST_PIPE_INSTANCE = 0x00080000; 17 | public const uint FILE_FLAG_OVERLAPPED = 0x40000000; 18 | public const uint PIPE_TYPE_BYTE = 0; 19 | public const uint PIPE_READMODE_BYTE = 0; 20 | public const uint PIPE_WAIT = 0; 21 | public const uint PIPE_REJECT_REMOTE_CLIENTS = 8; 22 | 23 | [DllImport(DllName, SetLastError = true)] 24 | public static extern bool CreatePipe( 25 | [Out] out SafeFileHandle hReadPipe, 26 | [Out] out SafeFileHandle hWritePipe, 27 | [In] IntPtr lpPipeAttributes, 28 | [In] int nSize); 29 | 30 | [DllImport(DllName, EntryPoint = "CreateNamedPipeW", SetLastError = true, CharSet = CharSet.Unicode)] 31 | public static extern SafeFileHandle CreateNamedPipe( 32 | [In] string lpName, 33 | [In] uint dwOpenMode, 34 | [In] uint dwPipeMode, 35 | [In] uint nMaxInstances, 36 | [In] uint nOutBufferSize, 37 | [In] uint nInBufferSize, 38 | [In] uint nDefaultTimeOut, 39 | [In] IntPtr lpSecurityAttributes); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/ChildProcess/Interop/Windows/Kernel32.ProcThreadAttributeList.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) @asmichi (https://github.com/asmichi). Licensed under the MIT License. See LICENCE in the project root for details. 2 | 3 | using System; 4 | using System.Runtime.InteropServices; 5 | 6 | #pragma warning disable SA1310 // Field names must not contain underscore 7 | #pragma warning disable SA1313 // Parameter names must begin with lower-case letter 8 | 9 | namespace Asmichi.Interop.Windows 10 | { 11 | internal static partial class Kernel32 12 | { 13 | public static readonly IntPtr PROC_THREAD_ATTRIBUTE_HANDLE_LIST = new IntPtr(0x20002); 14 | public static readonly IntPtr PROC_THREAD_ATTRIBUTE_JOB_LIST = new IntPtr(0x2000d); 15 | public static readonly IntPtr PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE = new IntPtr(0x20016); 16 | 17 | [DllImport(DllName, SetLastError = true)] 18 | public static extern bool InitializeProcThreadAttributeList( 19 | [In] IntPtr lpAttributeList, 20 | [In] int dwAttributeCount, 21 | [In] int dwFlags, 22 | [In][Out] ref nint lpSize); 23 | 24 | [DllImport(DllName, SetLastError = true)] 25 | public static extern void DeleteProcThreadAttributeList( 26 | [In] IntPtr lpAttributeList); 27 | 28 | [DllImport(DllName, SetLastError = true)] 29 | public static extern unsafe bool UpdateProcThreadAttribute( 30 | [In] SafeUnmanagedProcThreadAttributeList lpAttributeList, 31 | [In] int dwFlags, 32 | [In] IntPtr Attribute, 33 | [In] void* lpValue, 34 | [In] nint cbSize, 35 | [In] IntPtr lpPreviousValue, 36 | [In] IntPtr lpReturnSize); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/ChildProcess/Interop/Windows/NtDll.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) @asmichi (https://github.com/asmichi). Licensed under the MIT License. See LICENCE in the project root for details. 2 | 3 | using System.Runtime.InteropServices; 4 | 5 | #pragma warning disable SA1307 // Accessible fields should begin with upper-case letter 6 | 7 | namespace Asmichi.Interop.Windows 8 | { 9 | internal static partial class NtDll 10 | { 11 | private const string DllName = "ntdll.dll"; 12 | 13 | [DllImport(DllName, EntryPoint = "RtlGetVersion", SetLastError = false, CharSet = CharSet.Unicode)] 14 | public static extern int RtlGetVersion([In, Out] ref RTL_OSVERSIONINFOW lpVersionInformation); 15 | 16 | [StructLayout(LayoutKind.Sequential)] 17 | public unsafe struct RTL_OSVERSIONINFOW 18 | { 19 | public uint dwOSVersionInfoSize; 20 | public uint dwMajorVersion; 21 | public uint dwMinorVersion; 22 | public uint dwBuildNumber; 23 | public uint dwPlatformId; 24 | public fixed char szCSDVersion[128]; 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/ChildProcess/Interop/Windows/ProcThreadAttributeList.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) @asmichi (https://github.com/asmichi). Licensed under the MIT License. See LICENCE in the project root for details. 2 | 3 | using System; 4 | using System.ComponentModel; 5 | 6 | namespace Asmichi.Interop.Windows 7 | { 8 | internal sealed class ProcThreadAttributeList : IDisposable 9 | { 10 | private readonly SafeUnmanagedProcThreadAttributeList _unmanaged; 11 | private bool _isDisposed; 12 | 13 | public ProcThreadAttributeList(int attributeCount) 14 | { 15 | _unmanaged = SafeUnmanagedProcThreadAttributeList.Create(attributeCount); 16 | } 17 | 18 | public void Dispose() 19 | { 20 | if (!_isDisposed) 21 | { 22 | _unmanaged.Dispose(); 23 | _isDisposed = true; 24 | } 25 | } 26 | 27 | public unsafe void UpdateHandleList(IntPtr* handles, int count) 28 | { 29 | if (!Kernel32.UpdateProcThreadAttribute( 30 | _unmanaged, 31 | 0, 32 | Kernel32.PROC_THREAD_ATTRIBUTE_HANDLE_LIST, 33 | handles, 34 | sizeof(IntPtr) * count, 35 | IntPtr.Zero, 36 | IntPtr.Zero)) 37 | { 38 | throw new Win32Exception(); 39 | } 40 | } 41 | 42 | public unsafe void UpdateJobList(IntPtr* handles, int count) 43 | { 44 | if (!Kernel32.UpdateProcThreadAttribute( 45 | _unmanaged, 46 | 0, 47 | Kernel32.PROC_THREAD_ATTRIBUTE_JOB_LIST, 48 | handles, 49 | sizeof(IntPtr) * count, 50 | IntPtr.Zero, 51 | IntPtr.Zero)) 52 | { 53 | throw new Win32Exception(); 54 | } 55 | } 56 | 57 | public unsafe void UpdatePseudoConsole(IntPtr hPC) 58 | { 59 | if (!Kernel32.UpdateProcThreadAttribute( 60 | _unmanaged, 61 | 0, 62 | Kernel32.PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE, 63 | hPC.ToPointer(), 64 | sizeof(IntPtr), 65 | IntPtr.Zero, 66 | IntPtr.Zero)) 67 | { 68 | throw new Win32Exception(); 69 | } 70 | } 71 | 72 | public IntPtr DangerousGetHandle() => _unmanaged.DangerousGetHandle(); 73 | public void DangerousAddRef(ref bool success) => _unmanaged.DangerousAddRef(ref success); 74 | public void DangerousRelease() => _unmanaged.DangerousRelease(); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/ChildProcess/Interop/Windows/SafeAnyHandle.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) @asmichi (https://github.com/asmichi). Licensed under the MIT License. See LICENCE in the project root for details. 2 | 3 | using System; 4 | using Microsoft.Win32.SafeHandles; 5 | 6 | namespace Asmichi.Interop.Windows 7 | { 8 | internal sealed class SafeAnyHandle : SafeHandleZeroOrMinusOneIsInvalid 9 | { 10 | public SafeAnyHandle() 11 | : this(IntPtr.Zero, true) 12 | { 13 | } 14 | 15 | public SafeAnyHandle(IntPtr handle) 16 | : this(handle, true) 17 | { 18 | } 19 | 20 | public SafeAnyHandle(IntPtr existingHandle, bool ownsHandle) 21 | : base(ownsHandle) 22 | { 23 | SetHandle(existingHandle); 24 | } 25 | 26 | protected override bool ReleaseHandle() 27 | { 28 | Kernel32.CloseHandle(handle); 29 | return true; 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/ChildProcess/Interop/Windows/SafeJobObjectHandle.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) @asmichi (https://github.com/asmichi). Licensed under the MIT License. See LICENCE in the project root for details. 2 | 3 | using System; 4 | using Microsoft.Win32.SafeHandles; 5 | 6 | namespace Asmichi.Interop.Windows 7 | { 8 | internal sealed class SafeJobObjectHandle : SafeHandleZeroOrMinusOneIsInvalid 9 | { 10 | public SafeJobObjectHandle() 11 | : this(IntPtr.Zero, true) 12 | { 13 | } 14 | 15 | public SafeJobObjectHandle(IntPtr handle) 16 | : this(handle, true) 17 | { 18 | } 19 | 20 | public SafeJobObjectHandle(IntPtr existingHandle, bool ownsHandle) 21 | : base(ownsHandle) 22 | { 23 | SetHandle(existingHandle); 24 | } 25 | 26 | protected override bool ReleaseHandle() 27 | { 28 | Kernel32.CloseHandle(handle); 29 | return true; 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/ChildProcess/Interop/Windows/SafePseudoConsoleHandle.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) @asmichi (https://github.com/asmichi). Licensed under the MIT License. See LICENCE in the project root for details. 2 | 3 | using System; 4 | using System.Runtime.InteropServices; 5 | using Microsoft.Win32.SafeHandles; 6 | 7 | namespace Asmichi.Interop.Windows 8 | { 9 | internal sealed class SafePseudoConsoleHandle : SafeHandleZeroOrMinusOneIsInvalid 10 | { 11 | public SafePseudoConsoleHandle() 12 | : this(IntPtr.Zero, true) 13 | { 14 | } 15 | 16 | public SafePseudoConsoleHandle(IntPtr handle) 17 | : this(handle, true) 18 | { 19 | } 20 | 21 | public SafePseudoConsoleHandle(IntPtr existingHandle, bool ownsHandle) 22 | : base(ownsHandle) 23 | { 24 | SetHandle(existingHandle); 25 | } 26 | 27 | protected override bool ReleaseHandle() 28 | { 29 | Kernel32.ClosePseudoConsole(handle); 30 | return true; 31 | } 32 | 33 | // inputReader and outputWriter can be closed after this method returns. 34 | // outputWriter can be a handle to the null device. 35 | public static SafePseudoConsoleHandle Create(SafeHandle inputReader, SafeHandle outputWriter) 36 | { 37 | // The console size does not matter since we redirect... maybe? 38 | var consoleSize = new Kernel32.COORD { X = 80, Y = 25 }; 39 | int hr = Kernel32.CreatePseudoConsole(consoleSize, inputReader, outputWriter, 0, out var hPC); 40 | Marshal.ThrowExceptionForHR(hr); 41 | return new SafePseudoConsoleHandle(hPC); 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/ChildProcess/Interop/Windows/SafeThreadHandle.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) @asmichi (https://github.com/asmichi). Licensed under the MIT License. See LICENCE in the project root for details. 2 | 3 | using System; 4 | using Microsoft.Win32.SafeHandles; 5 | 6 | namespace Asmichi.Interop.Windows 7 | { 8 | internal sealed class SafeThreadHandle : SafeHandleZeroOrMinusOneIsInvalid 9 | { 10 | public SafeThreadHandle() 11 | : this(IntPtr.Zero, true) 12 | { 13 | } 14 | 15 | public SafeThreadHandle(IntPtr handle) 16 | : this(handle, true) 17 | { 18 | } 19 | 20 | public SafeThreadHandle(IntPtr existingHandle, bool ownsHandle) 21 | : base(ownsHandle) 22 | { 23 | SetHandle(existingHandle); 24 | } 25 | 26 | protected override bool ReleaseHandle() 27 | { 28 | Kernel32.CloseHandle(handle); 29 | return true; 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/ChildProcess/Interop/Windows/SafeUnmanagedProcThreadAttributeList.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) @asmichi (https://github.com/asmichi). Licensed under the MIT License. See LICENCE in the project root for details. 2 | 3 | using System; 4 | using System.ComponentModel; 5 | using System.Runtime.InteropServices; 6 | using Microsoft.Win32.SafeHandles; 7 | 8 | namespace Asmichi.Interop.Windows 9 | { 10 | internal sealed class SafeUnmanagedProcThreadAttributeList : SafeHandleZeroOrMinusOneIsInvalid 11 | { 12 | internal SafeUnmanagedProcThreadAttributeList() 13 | : this(IntPtr.Zero) 14 | { 15 | } 16 | 17 | public SafeUnmanagedProcThreadAttributeList(IntPtr memory) 18 | : base(true) 19 | { 20 | SetHandle(memory); 21 | } 22 | 23 | protected override bool ReleaseHandle() 24 | { 25 | // Must be DeleteProcThreadAttributeList'ed before freed. 26 | Kernel32.DeleteProcThreadAttributeList(handle); 27 | Marshal.FreeHGlobal(handle); 28 | return true; 29 | } 30 | 31 | public static SafeUnmanagedProcThreadAttributeList Create(int attributeCount) 32 | { 33 | nint size = 0; 34 | 35 | Kernel32.InitializeProcThreadAttributeList(IntPtr.Zero, attributeCount, 0, ref size); 36 | 37 | var buffer = Marshal.AllocHGlobal(checked((int)size)); 38 | 39 | if (!Kernel32.InitializeProcThreadAttributeList(buffer, attributeCount, 0, ref size)) 40 | { 41 | Marshal.FreeHGlobal(buffer); 42 | throw new Win32Exception(); 43 | } 44 | 45 | return new SafeUnmanagedProcThreadAttributeList(buffer); 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/ChildProcess/Interop/Windows/WindowsVersion.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) @asmichi (https://github.com/asmichi). Licensed under the MIT License. See LICENCE in the project root for details. 2 | 3 | using Asmichi.ProcessManagement; 4 | using static System.FormattableString; 5 | 6 | namespace Asmichi.Interop.Windows 7 | { 8 | internal static class WindowsVersion 9 | { 10 | /// 11 | /// On Windows 10 1809 (including Windows Server 2019), 12 | /// will just perform . 13 | /// This is due to a Windows pseudoconsole bug where ClosePseudoConsole does not terminate 14 | /// applications attached to the pseudoconsole. 15 | /// 16 | public static bool NeedsWorkaroundForWindows1809 { get; } = GetIsWindows1809(); 17 | 18 | private static unsafe bool GetIsWindows1809() 19 | { 20 | // Resort to ntdll. OsGetVersionEx and hence Environment.OSVersion.Version (till .NET Core 3.1) 21 | // will always return Windows 8.1 if the app is not manifested to support newer versions. 22 | // https://docs.microsoft.com/en-us/windows/win32/api/sysinfoapi/nf-sysinfoapi-getversionexw 23 | // https://github.com/dotnet/runtime/pull/33651 24 | var osvi = default(NtDll.RTL_OSVERSIONINFOW); 25 | osvi.dwOSVersionInfoSize = (uint)sizeof(NtDll.RTL_OSVERSIONINFOW); 26 | int ntstatus = NtDll.RtlGetVersion(ref osvi); 27 | if (ntstatus < 0) 28 | { 29 | throw new AsmichiChildProcessInternalLogicErrorException(Invariant($"RtlGetVersion failed (0x{ntstatus:X}).")); 30 | } 31 | 32 | return osvi.dwPlatformId == 2 33 | && osvi.dwMajorVersion == 10 34 | && osvi.dwMinorVersion == 0 35 | && osvi.dwBuildNumber == 17763; 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/ChildProcess/PlatformAbstraction/ConsolePal.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) @asmichi (https://github.com/asmichi). Licensed under the MIT License. See LICENCE in the project root for details. 2 | 3 | using System; 4 | using Microsoft.Win32.SafeHandles; 5 | 6 | namespace Asmichi.PlatformAbstraction 7 | { 8 | internal interface IConsolePal 9 | { 10 | SafeFileHandle? CreateStdInputHandleForChild(bool createNewConsole); 11 | SafeFileHandle? CreateStdOutputHandleForChild(bool createNewConsole); 12 | SafeFileHandle? CreateStdErrorHandleForChild(bool createNewConsole); 13 | bool HasConsoleWindow(); 14 | } 15 | 16 | internal static class ConsolePal 17 | { 18 | private static readonly IConsolePal Impl = CreatePlatformSpecificImpl(); 19 | 20 | private static IConsolePal CreatePlatformSpecificImpl() 21 | { 22 | return Pal.PlatformKind switch 23 | { 24 | PlatformKind.Win32 => new Windows.WindowsConsolePal(), 25 | PlatformKind.Unix => new Unix.UnixConsolePal(), 26 | PlatformKind.Unknown => throw new PlatformNotSupportedException(), 27 | _ => throw new PlatformNotSupportedException(), 28 | }; 29 | } 30 | 31 | /// 32 | /// Creates a duplicate handle to the stdin of the current process that can be inherited by a child process. 33 | /// 34 | /// 35 | /// The created handle. if such an inheritable handle cannot be created. 36 | /// 37 | public static SafeFileHandle? CreateStdInputHandleForChild(bool createNewConsole) => Impl.CreateStdInputHandleForChild(createNewConsole); 38 | public static SafeFileHandle? CreateStdOutputHandleForChild(bool createNewConsole) => Impl.CreateStdOutputHandleForChild(createNewConsole); 39 | public static SafeFileHandle? CreateStdErrorHandleForChild(bool createNewConsole) => Impl.CreateStdErrorHandleForChild(createNewConsole); 40 | public static bool HasConsoleWindow() => Impl.HasConsoleWindow(); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/ChildProcess/PlatformAbstraction/EnvironmentPal.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) @asmichi (https://github.com/asmichi). Licensed under the MIT License. See LICENCE in the project root for details. 2 | 3 | using System; 4 | 5 | namespace Asmichi.PlatformAbstraction 6 | { 7 | internal interface IEnvironmentPal 8 | { 9 | bool IsFileNotFoundError(int error); 10 | bool IsEnvironmentVariableNameCaseSensitive { get; } 11 | } 12 | 13 | internal static class EnvironmentPal 14 | { 15 | private static readonly IEnvironmentPal Impl = CreatePlatformSpecificImpl(); 16 | 17 | private static IEnvironmentPal CreatePlatformSpecificImpl() 18 | { 19 | return Pal.PlatformKind switch 20 | { 21 | PlatformKind.Win32 => new Windows.WindowsEnvironmentPal(), 22 | PlatformKind.Unix => new Unix.UnixEnvironmentPal(), 23 | PlatformKind.Unknown => throw new PlatformNotSupportedException(), 24 | _ => throw new PlatformNotSupportedException(), 25 | }; 26 | } 27 | 28 | public static bool IsFileNotFoundError(int error) => Impl.IsFileNotFoundError(error); 29 | 30 | public static bool IsEnvironmentVariableNameCaseSensitive { get; } = Impl.IsEnvironmentVariableNameCaseSensitive; 31 | 32 | public static StringComparison EnvironmentVariableNameComparison { get; } = 33 | IsEnvironmentVariableNameCaseSensitive ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/ChildProcess/PlatformAbstraction/FilePal.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) @asmichi (https://github.com/asmichi). Licensed under the MIT License. See LICENCE in the project root for details. 2 | 3 | using System; 4 | using System.IO; 5 | using System.IO.Pipes; 6 | using Microsoft.Win32.SafeHandles; 7 | 8 | namespace Asmichi.PlatformAbstraction 9 | { 10 | internal interface IFilePal 11 | { 12 | (SafeFileHandle readPipe, SafeFileHandle writePipe) CreatePipePair(); 13 | (Stream serverStream, SafeFileHandle clientPipe) CreatePipePairWithAsyncServerSide(PipeDirection pipeDirection); 14 | SafeFileHandle OpenNullDevice(FileAccess fileAccess); 15 | } 16 | 17 | internal static class FilePal 18 | { 19 | private static readonly IFilePal Impl = CreatePlatformSpecificImpl(); 20 | 21 | private static IFilePal CreatePlatformSpecificImpl() 22 | { 23 | return Pal.PlatformKind switch 24 | { 25 | PlatformKind.Win32 => new Windows.WindowsFilePal(), 26 | PlatformKind.Unix => new Unix.UnixFilePal(), 27 | PlatformKind.Unknown => throw new PlatformNotSupportedException(), 28 | _ => throw new PlatformNotSupportedException(), 29 | }; 30 | } 31 | 32 | public static SafeFileHandle OpenNullDevice(FileAccess fileAccess) => Impl.OpenNullDevice(fileAccess); 33 | 34 | public static (SafeFileHandle readPipe, SafeFileHandle writePipe) CreatePipePair() => Impl.CreatePipePair(); 35 | 36 | /// 37 | /// Creates a pipe pair. Asynchronous IO is enabled for the server side. 38 | /// If is , clientPipe is created with asynchronous IO enabled. 39 | /// If , serverStream is created with asynchronous moIOde enabled. 40 | /// 41 | /// Specifies which side is the server side. 42 | /// A pipe pair. 43 | public static (Stream serverStream, SafeFileHandle clientPipe) CreatePipePairWithAsyncServerSide(PipeDirection pipeDirection) => 44 | Impl.CreatePipePairWithAsyncServerSide(pipeDirection); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/ChildProcess/PlatformAbstraction/Pal.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) @asmichi (https://github.com/asmichi). Licensed under the MIT License. See LICENCE in the project root for details. 2 | 3 | using System.Runtime.InteropServices; 4 | 5 | // PERF: Not using virtual calls via interfaces so that those calls will be easy to inline. 6 | namespace Asmichi.PlatformAbstraction 7 | { 8 | internal enum PlatformKind 9 | { 10 | Unknown, 11 | Win32, 12 | Unix, 13 | } 14 | 15 | internal static class Pal 16 | { 17 | public static readonly PlatformKind PlatformKind = GetPlatformKind(); 18 | 19 | private static PlatformKind GetPlatformKind() 20 | { 21 | if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) 22 | { 23 | return PlatformKind.Win32; 24 | } 25 | else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) 26 | { 27 | return PlatformKind.Unix; 28 | } 29 | else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) 30 | { 31 | return PlatformKind.Unix; 32 | } 33 | else 34 | { 35 | return PlatformKind.Unknown; 36 | } 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/ChildProcess/PlatformAbstraction/Unix/UnixConsolePal.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) @asmichi (https://github.com/asmichi). Licensed under the MIT License. See LICENCE in the project root for details. 2 | 3 | using Asmichi.Interop.Linux; 4 | using Microsoft.Win32.SafeHandles; 5 | 6 | namespace Asmichi.PlatformAbstraction.Unix 7 | { 8 | internal class UnixConsolePal : IConsolePal 9 | { 10 | private const int StdInFileNo = 0; 11 | private const int StdOutFileNo = 1; 12 | private const int StdErrFileNo = 2; 13 | 14 | public SafeFileHandle? CreateStdInputHandleForChild(bool createNewConsole) => 15 | DuplicateStdFileForChild(StdInFileNo, createNewConsole); 16 | public SafeFileHandle? CreateStdOutputHandleForChild(bool createNewConsole) => 17 | DuplicateStdFileForChild(StdOutFileNo, createNewConsole); 18 | public SafeFileHandle? CreateStdErrorHandleForChild(bool createNewConsole) => 19 | DuplicateStdFileForChild(StdErrFileNo, createNewConsole); 20 | 21 | private static SafeFileHandle? DuplicateStdFileForChild(int stdFd, bool createNewConsole) 22 | { 23 | if (!LibChildProcess.DuplicateStdFileForChild(stdFd, createNewConsole, out var newFd)) 24 | { 25 | // Probably EBADF. 26 | return null; 27 | } 28 | 29 | return newFd.IsInvalid ? null : newFd; 30 | } 31 | 32 | public bool HasConsoleWindow() => true; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/ChildProcess/PlatformAbstraction/Unix/UnixEnvironmentPal.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) @asmichi (https://github.com/asmichi). Licensed under the MIT License. See LICENCE in the project root for details. 2 | 3 | using Asmichi.Interop.Linux; 4 | 5 | namespace Asmichi.PlatformAbstraction.Unix 6 | { 7 | internal sealed class UnixEnvironmentPal : IEnvironmentPal 8 | { 9 | private static readonly int ENOENT = LibChildProcess.GetENOENT(); 10 | 11 | public char SearchPathSeparator { get; } = ':'; 12 | 13 | public bool IsEnvironmentVariableNameCaseSensitive => false; 14 | 15 | public bool IsFileNotFoundError(int error) => error == ENOENT; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/ChildProcess/PlatformAbstraction/Utilities/NamedPipeUtil.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) @asmichi (https://github.com/asmichi). Licensed under the MIT License. See LICENCE in the project root for details. 2 | 3 | using System.Globalization; 4 | using System.IO; 5 | 6 | namespace Asmichi.PlatformAbstraction.Utilities 7 | { 8 | internal static class NamedPipeUtil 9 | { 10 | // NOTE: There may be multiple instances of this assembly (AppDomain or AssemblyLoadContext). 11 | // Therefore embedding the process id only does not provide uniqueness. 12 | public static string MakePipePathPrefix(string pathPrefix, uint processId) => 13 | Path.Combine( 14 | pathPrefix, 15 | string.Format(CultureInfo.InvariantCulture, "Asmichi.ChildProcess.{0:D5}.{1}.", processId, Path.GetRandomFileName())); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/ChildProcess/PlatformAbstraction/Windows/WindowsConsolePal.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) @asmichi (https://github.com/asmichi). Licensed under the MIT License. See LICENCE in the project root for details. 2 | 3 | using System; 4 | using Asmichi.Interop.Windows; 5 | using Microsoft.Win32.SafeHandles; 6 | 7 | namespace Asmichi.PlatformAbstraction.Windows 8 | { 9 | internal sealed class WindowsConsolePal : IConsolePal 10 | { 11 | public SafeFileHandle? CreateStdInputHandleForChild(bool createNewConsole) => 12 | GetStdHandleForChild(Kernel32.STD_INPUT_HANDLE, createNewConsole); 13 | public SafeFileHandle? CreateStdOutputHandleForChild(bool createNewConsole) => 14 | GetStdHandleForChild(Kernel32.STD_OUTPUT_HANDLE, createNewConsole); 15 | public SafeFileHandle? CreateStdErrorHandleForChild(bool createNewConsole) => 16 | GetStdHandleForChild(Kernel32.STD_ERROR_HANDLE, createNewConsole); 17 | 18 | /// 19 | /// Returns the std* handle of the current process that can be inherited by a child process. 20 | /// 21 | /// 22 | /// The std* handle of the current process. 23 | /// if the current process does not have any or if the handle cannot be inherited by a child process. 24 | /// 25 | private static SafeFileHandle? GetStdHandleForChild(int kind, bool createNewConsole) 26 | { 27 | // GetStdHandle may return INVALID_HANDLE_VALUE on success because one can perform SetStdHandle(..., INVALID_HANDLE_VALUE). 28 | var handleValue = Kernel32.GetStdHandle(kind); 29 | if (handleValue == IntPtr.Zero || handleValue == Kernel32.InvalidHandleValue) 30 | { 31 | return null; 32 | } 33 | 34 | var handle = new SafeFileHandle(handleValue, false); 35 | 36 | // Console handles can be inherited only by a child process within the same console. 37 | // If the child process will be attached to a new pseudo console, 38 | // ignore console handles and redirect from/to NUL instead. 39 | if (createNewConsole && IsConsoleHandle(handle)) 40 | { 41 | handle.Dispose(); 42 | return null; 43 | } 44 | 45 | return handle; 46 | } 47 | 48 | private static bool IsConsoleHandle(SafeFileHandle handle) 49 | { 50 | // Make sure the handle is a character device. 51 | // Maybe calling GetConsoleMode is enough, though. 52 | int fileType = Kernel32.GetFileType(handle); 53 | if (fileType == Kernel32.FILE_TYPE_UNKNOWN || (fileType & Kernel32.FILE_TYPE_CHAR) == 0) 54 | { 55 | return false; 56 | } 57 | 58 | // If GetConsoleMode succeeds, handle is a console handle. 59 | return Kernel32.GetConsoleMode(handle, out var _); 60 | } 61 | 62 | public bool HasConsoleWindow() => Kernel32.GetConsoleCP() != 0; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/ChildProcess/PlatformAbstraction/Windows/WindowsEnvironmentPal.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) @asmichi (https://github.com/asmichi). Licensed under the MIT License. See LICENCE in the project root for details. 2 | 3 | using Asmichi.Interop.Windows; 4 | 5 | namespace Asmichi.PlatformAbstraction.Windows 6 | { 7 | internal sealed class WindowsEnvironmentPal : IEnvironmentPal 8 | { 9 | public char SearchPathSeparator { get; } = ';'; 10 | 11 | public bool IsEnvironmentVariableNameCaseSensitive => false; 12 | 13 | public bool IsFileNotFoundError(int error) => error == Kernel32.ERROR_FILE_NOT_FOUND; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/ChildProcess/ProcessManagement/AsmichiChildProcessInternalLogicErrorException.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) @asmichi (https://github.com/asmichi). Licensed under the MIT License. See LICENCE in the project root for details. 2 | 3 | using System; 4 | using System.Diagnostics; 5 | using System.Runtime.Serialization; 6 | 7 | namespace Asmichi.ProcessManagement 8 | { 9 | /// 10 | /// Thrown when an internal logic error was detected within the Asmichi.ChildProcess library. 11 | /// 12 | [Serializable] 13 | public class AsmichiChildProcessInternalLogicErrorException : InvalidOperationException 14 | { 15 | /// 16 | /// Initializes a new instance of the class. 17 | /// 18 | public AsmichiChildProcessInternalLogicErrorException() 19 | : base("Internal logic error.") 20 | { 21 | Debug.Fail(Message); 22 | } 23 | 24 | /// 25 | /// Initializes a new instance of the class 26 | /// with a message. 27 | /// 28 | /// Error message. 29 | public AsmichiChildProcessInternalLogicErrorException(string message) 30 | : base(message) 31 | { 32 | Debug.Fail(Message); 33 | } 34 | 35 | /// 36 | /// Initializes a new instance of the class 37 | /// with a message and an inner exception. 38 | /// 39 | /// Error message. 40 | /// Inner exception. 41 | public AsmichiChildProcessInternalLogicErrorException(string message, Exception innerException) 42 | : base(message, innerException) 43 | { 44 | Debug.Fail(Message); 45 | } 46 | 47 | /// 48 | /// Initializes a new instance of the class with serialized data. 49 | /// 50 | /// . 51 | /// . 52 | protected AsmichiChildProcessInternalLogicErrorException(SerializationInfo info, StreamingContext context) 53 | : base(info, context) 54 | { 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/ChildProcess/ProcessManagement/AsmichiChildProcessLibraryCrashedException.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) @asmichi (https://github.com/asmichi). Licensed under the MIT License. See LICENCE in the project root for details. 2 | 3 | using System; 4 | using System.Runtime.Serialization; 5 | 6 | namespace Asmichi.ProcessManagement 7 | { 8 | /// 9 | /// Thrown when the library (typically the helper process) crashed due to critical disturbance 10 | /// (the helper was killed, etc.) or a bug. 11 | /// 12 | [Serializable] 13 | public class AsmichiChildProcessLibraryCrashedException : Exception 14 | { 15 | /// 16 | /// Initializes a new instance of the class. 17 | /// 18 | public AsmichiChildProcessLibraryCrashedException() 19 | { 20 | } 21 | 22 | /// 23 | /// Initializes a new instance of the class 24 | /// with a message. 25 | /// 26 | /// Error message. 27 | public AsmichiChildProcessLibraryCrashedException(string message) 28 | : base(message) 29 | { 30 | } 31 | 32 | /// 33 | /// Initializes a new instance of the class 34 | /// with a message and an inner exception. 35 | /// 36 | /// Error message. 37 | /// Inner exception. 38 | public AsmichiChildProcessLibraryCrashedException(string message, Exception innerException) 39 | : base(message, innerException) 40 | { 41 | } 42 | 43 | /// 44 | /// Initializes a new instance of the class with serialized data. 45 | /// 46 | /// . 47 | /// . 48 | protected AsmichiChildProcessLibraryCrashedException(SerializationInfo info, StreamingContext context) 49 | : base(info, context) 50 | { 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/ChildProcess/ProcessManagement/ChildProcessHelper.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) @asmichi (https://github.com/asmichi). Licensed under the MIT License. See LICENCE in the project root for details. 2 | 3 | using System; 4 | using Asmichi.PlatformAbstraction; 5 | 6 | namespace Asmichi.ProcessManagement 7 | { 8 | internal static class ChildProcessHelper 9 | { 10 | public static IChildProcessStateHelper Shared { get; } = CreateSharedHelper(); 11 | 12 | private static IChildProcessStateHelper CreateSharedHelper() => 13 | Pal.PlatformKind switch 14 | { 15 | PlatformKind.Win32 => new WindowsChildProcessStateHelper(), 16 | PlatformKind.Unix => new UnixChildProcessStateHelper(), 17 | PlatformKind.Unknown => throw new PlatformNotSupportedException(), 18 | _ => throw new AsmichiChildProcessInternalLogicErrorException(), 19 | }; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/ChildProcess/ProcessManagement/ChildProcessStartingBlockedException.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) @asmichi (https://github.com/asmichi). Licensed under the MIT License. See LICENCE in the project root for details. 2 | 3 | using System; 4 | using System.Runtime.Serialization; 5 | 6 | namespace Asmichi.ProcessManagement 7 | { 8 | /// 9 | /// Thrown when starting a child process is blocked because it may execute an unexpected program. 10 | /// 11 | /// 12 | /// 13 | /// (Windows-specific) Thrown when is "cmd.exe" (without the directory part), 14 | /// but is not set.
15 | /// If arguments originate from an untrusted input, it may be a good idea to avoid executing "cmd.exe". 16 | /// Incorrectly escaped arguments can lead to execution of an arbitrary executable because "cmd.exe /c" takes 17 | /// an arbitrary shell command line. Search "BatBadBut vulnerability" for the background.
18 | /// If arguments are trusted, set and escape arguments on your own. 19 | /// Note you need to escape arguments in a way specific to the command being invoked. There is no standard quoting method possible for "cmd.exe". 20 | ///
21 | /// 22 | /// (Windows-specific) Thrown when has the ".bat" or ".cmd" extension. 23 | /// Supply the batch file to "cmd.exe /c" instead. See also the above paragraph. 24 | /// 25 | ///
26 | [Serializable] 27 | public class ChildProcessStartingBlockedException : Exception 28 | { 29 | /// 30 | /// Initializes a new instance of the class. 31 | /// 32 | public ChildProcessStartingBlockedException() 33 | { 34 | } 35 | 36 | /// 37 | /// Initializes a new instance of the class 38 | /// with a message. 39 | /// 40 | /// Error message. 41 | public ChildProcessStartingBlockedException(string message) 42 | : base(message) 43 | { 44 | } 45 | 46 | /// 47 | /// Initializes a new instance of the class 48 | /// with a message and an inner exception. 49 | /// 50 | /// Error message. 51 | /// Inner exception. 52 | public ChildProcessStartingBlockedException(string message, Exception innerException) 53 | : base(message, innerException) 54 | { 55 | } 56 | 57 | /// 58 | /// Initializes a new instance of the class with serialized data. 59 | /// 60 | /// . 61 | /// . 62 | protected ChildProcessStartingBlockedException(SerializationInfo info, StreamingContext context) 63 | : base(info, context) 64 | { 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/ChildProcess/ProcessManagement/EnvironmentVariableListCreation.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) @asmichi (https://github.com/asmichi). Licensed under the MIT License. See LICENCE in the project root for details. 2 | 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Diagnostics; 6 | using Asmichi.Utilities; 7 | 8 | namespace Asmichi.ProcessManagement 9 | { 10 | /// 11 | /// Creates an environment variable list passed to a child processes. 12 | /// 13 | internal static class EnvironmentVariableListCreation 14 | { 15 | internal static ReadOnlyMemory> SortExtraEnvVars( 16 | IReadOnlyCollection> extraEnvVars) 17 | { 18 | if (extraEnvVars.Count == 0) 19 | { 20 | return Array.Empty>(); 21 | } 22 | 23 | var builder = SortedEnvironmentVariableListBuilder.Create(extraEnvVars.Count); 24 | builder.InsertOrRemoveRange(extraEnvVars); 25 | return builder.Build(); 26 | } 27 | 28 | public static ReadOnlyMemory> MergeExtraEnvVarsWithContext( 29 | ReadOnlyMemory> contextEnvVars, 30 | IReadOnlyCollection> extraEnvVars) 31 | { 32 | if (extraEnvVars.Count == 0) 33 | { 34 | // PERF: Use the values from the context as is. 35 | return contextEnvVars; 36 | } 37 | 38 | var builder = SortedEnvironmentVariableListBuilder.CreateFromContext(contextEnvVars.Length + extraEnvVars.Count, contextEnvVars); 39 | builder.InsertOrRemoveRange(extraEnvVars); 40 | return builder.Build(); 41 | } 42 | 43 | public static ReadOnlyMemory> MergeExtraEnvVarsWithProcess( 44 | IReadOnlyCollection> extraEnvVars) 45 | { 46 | Debug.Assert(extraEnvVars.Count != 0, "This case should fall into 'UseCustomEnvironmentVariables = false'."); 47 | 48 | var processEnvVars = Environment.GetEnvironmentVariables(); 49 | var builder = SortedEnvironmentVariableListBuilder.CreateFromProcess(processEnvVars.Count + extraEnvVars.Count, processEnvVars); 50 | builder.InsertOrRemoveRange(extraEnvVars); 51 | return builder.Build(); 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/ChildProcess/ProcessManagement/IChildProcessState.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) @asmichi (https://github.com/asmichi). Licensed under the MIT License. See LICENCE in the project root for details. 2 | 3 | using System; 4 | using System.Threading; 5 | using Asmichi.Interop.Windows; 6 | using Microsoft.Win32.SafeHandles; 7 | 8 | namespace Asmichi.ProcessManagement 9 | { 10 | /// 11 | /// Reference-counts an instance. 12 | /// 13 | internal interface IChildProcessStateHolder : IDisposable 14 | { 15 | IChildProcessState State { get; } 16 | } 17 | 18 | /// 19 | /// Represents the state associated to one process. 20 | /// 21 | /// Modifies the state of a child process. 22 | /// Detects changes in the states of child processes. 23 | /// 24 | /// 25 | // NOTE: A pipe to a process itself is not a part of the state of a child process (but of ours). 26 | internal interface IChildProcessState 27 | { 28 | int ProcessId { get; } 29 | int ExitCode { get; } 30 | WaitHandle ExitedWaitHandle { get; } 31 | bool HasExitCode { get; } 32 | 33 | // Pre: The process has exited 34 | void DangerousRetrieveExitCode(); 35 | 36 | bool HasHandle { get; } 37 | SafeProcessHandle ProcessHandle { get; } 38 | SafeThreadHandle PrimaryThreadHandle { get; } 39 | 40 | bool CanSignal { get; } 41 | void SignalInterrupt(); 42 | void SignalTermination(); 43 | void Kill(); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/ChildProcess/ProcessManagement/IChildProcessStateHelper.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) @asmichi (https://github.com/asmichi). Licensed under the MIT License. See LICENCE in the project root for details. 2 | 3 | using System; 4 | using System.Runtime.InteropServices; 5 | 6 | namespace Asmichi.ProcessManagement 7 | { 8 | /// 9 | /// 10 | /// Spawns child processes. 11 | /// Modifies the state of a child process. 12 | /// Detects changes in the states of child processes. 13 | /// 14 | /// 15 | internal interface IChildProcessStateHelper : IDisposable 16 | { 17 | void ValidatePlatformSpecificStartInfo( 18 | in ChildProcessStartInfoInternal startInfo); 19 | 20 | IChildProcessStateHolder SpawnProcess( 21 | ref ChildProcessStartInfoInternal startInfo, 22 | string resolvedPath, 23 | SafeHandle stdIn, 24 | SafeHandle stdOut, 25 | SafeHandle stdErr); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/ChildProcess/ProcessManagement/WindowsProcessWaitHandle.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) @asmichi (https://github.com/asmichi). Licensed under the MIT License. See LICENCE in the project root for details. 2 | 3 | using System.ComponentModel; 4 | using System.Threading; 5 | using Asmichi.Interop.Windows; 6 | using Microsoft.Win32.SafeHandles; 7 | 8 | namespace Asmichi.ProcessManagement 9 | { 10 | internal sealed class WindowsProcessWaitHandle : WaitHandle 11 | { 12 | public WindowsProcessWaitHandle(SafeProcessHandle processHandle) 13 | { 14 | WaitHandleExtensions.SetSafeWaitHandle(this, ToSafeWaitHandle(processHandle)); 15 | } 16 | 17 | private static SafeWaitHandle ToSafeWaitHandle(SafeProcessHandle handle) 18 | { 19 | if (!Kernel32.DuplicateHandle( 20 | Kernel32.GetCurrentProcess(), 21 | handle, 22 | Kernel32.GetCurrentProcess(), 23 | out SafeWaitHandle waitHandle, 24 | 0, 25 | false, 26 | Kernel32.DUPLICATE_SAME_ACCESS)) 27 | { 28 | throw new Win32Exception(); 29 | } 30 | 31 | return waitHandle; 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/ChildProcess/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) @asmichi (https://github.com/asmichi). Licensed under the MIT License. See LICENCE in the project root for details. 2 | 3 | using System; 4 | using System.Runtime.CompilerServices; 5 | using System.Runtime.InteropServices; 6 | 7 | [assembly: CLSCompliant(false)] 8 | [assembly: InternalsVisibleTo("Asmichi.ChildProcess.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f9668e0c5bb910623df15dc298b71a5e8a57bcc946964c15217646a95b8c7ff18f904e94a96b14fe03317c72dcd3f12c761092ae7268e23b6c7dbbb4f4b555a31fcfb4363b780d251ce00acaaa17ca59d2031d2d9ea10c4236d5ea7e7931631f2da07b337cc86b50c755e64adf5e42b629837509b437780b29798c5835991abd")] 9 | [assembly: DefaultDllImportSearchPaths(DllImportSearchPath.System32)] 10 | -------------------------------------------------------------------------------- /src/ChildProcess/Utilities/ArgumentValidationUtil.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) @asmichi (https://github.com/asmichi). Licensed under the MIT License. See LICENCE in the project root for details. 2 | 3 | using System; 4 | using System.Threading; 5 | 6 | namespace Asmichi.Utilities 7 | { 8 | internal static class ArgumentValidationUtil 9 | { 10 | public static int ValidateTimeoutRange(int millisecondsTimeout) 11 | { 12 | if (millisecondsTimeout < Timeout.Infinite) 13 | { 14 | throw new ArgumentOutOfRangeException( 15 | nameof(millisecondsTimeout), "Timeout must be Timeout.Infinite or a non-negative integer."); 16 | } 17 | 18 | return millisecondsTimeout; 19 | } 20 | 21 | public static TimeSpan ValidateTimeoutRange(TimeSpan timeout) 22 | { 23 | if (!(timeout.Ticks >= 0 || timeout == Timeout.InfiniteTimeSpan)) 24 | { 25 | throw new ArgumentOutOfRangeException( 26 | nameof(timeout), "Timeout must be Timeout.InfiniteTimeSpan or a non-negative TimeSpan."); 27 | } 28 | 29 | return timeout; 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/ChildProcess/Utilities/CompletedBoolTask.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) @asmichi (https://github.com/asmichi). Licensed under the MIT License. See LICENCE in the project root for details. 2 | 3 | using System.Threading.Tasks; 4 | 5 | namespace Asmichi.Utilities 6 | { 7 | // Cached completed Task 8 | internal static class CompletedBoolTask 9 | { 10 | public static readonly Task True = Task.FromResult(true); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/ChildProcess/Utilities/EnvironmentVariableListUtil.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) @asmichi (https://github.com/asmichi). Licensed under the MIT License. See LICENCE in the project root for details. 2 | 3 | using System; 4 | using System.Collections; 5 | using System.Collections.Generic; 6 | using System.Diagnostics; 7 | using Asmichi.PlatformAbstraction; 8 | 9 | namespace Asmichi.Utilities 10 | { 11 | /// 12 | /// Utiliities for manipulating a list of environment variables. 13 | /// 14 | internal static class EnvironmentVariableListUtil 15 | { 16 | public static ArraySegment> ToSortedDistinctKeyValuePairs(IDictionary envVarDictionary) 17 | { 18 | var nameComparison = EnvironmentPal.EnvironmentVariableNameComparison; 19 | var envVars = ToSortedKeyValuePairs(envVarDictionary); 20 | 21 | // Remove duplicates in place. 22 | int lastStoredIndex = 0; 23 | for (int i = 1; i < envVars.Length; i++) 24 | { 25 | if (!string.Equals(envVars[lastStoredIndex].Key, envVars[i].Key, nameComparison)) 26 | { 27 | lastStoredIndex++; 28 | envVars[lastStoredIndex] = envVars[i]; 29 | } 30 | } 31 | 32 | return new ArraySegment>(envVars, 0, lastStoredIndex + 1); 33 | } 34 | 35 | public static KeyValuePair[] ToSortedKeyValuePairs(IDictionary envVarDictionary) 36 | { 37 | var array = new KeyValuePair[envVarDictionary.Count]; 38 | ToSortedKeyValuePairs(envVarDictionary, array); 39 | return array; 40 | } 41 | 42 | public static void ToSortedKeyValuePairs(IDictionary envVarDictionary, KeyValuePair[] buffer) 43 | { 44 | int count = envVarDictionary.Count; 45 | 46 | if (buffer.Length < count) 47 | { 48 | throw new ArgumentException("Buffer too small.", nameof(buffer)); 49 | } 50 | 51 | int i = 0; 52 | #pragma warning disable CS8605 // Unboxing a possibly null value. 53 | foreach (DictionaryEntry de in envVarDictionary) 54 | #pragma warning restore CS8605 // Unboxing a possibly null value. 55 | { 56 | var name = (string)de.Key; 57 | var value = (string)de.Value!; 58 | buffer[i++] = new KeyValuePair(name, value); 59 | } 60 | 61 | Debug.Assert(i == count); 62 | 63 | Array.Sort(buffer, 0, count, EnvironmentVariablePairNameComparer.DefaultThenOrdinal); 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/ChildProcess/Utilities/EnvironmentVariablePairNameComparer.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) @asmichi (https://github.com/asmichi). Licensed under the MIT License. See LICENCE in the project root for details. 2 | 3 | using System; 4 | using System.Collections.Generic; 5 | using Asmichi.PlatformAbstraction; 6 | 7 | namespace Asmichi.Utilities 8 | { 9 | /// 10 | /// Compare only the name parts of environment variable values. 11 | /// 12 | internal static class EnvironmentVariablePairNameComparer 13 | { 14 | private static IComparer> Ordinal { get; } = 15 | new ImplByStringComparison(StringComparison.Ordinal); 16 | private static IComparer> OrdinalIgnoreCase { get; } = 17 | new ImplByStringComparison(StringComparison.OrdinalIgnoreCase); 18 | public static IComparer> Default { get; } = 19 | EnvironmentPal.IsEnvironmentVariableNameCaseSensitive ? Ordinal : OrdinalIgnoreCase; 20 | public static IComparer> DefaultThenOrdinal { get; } = 21 | EnvironmentPal.IsEnvironmentVariableNameCaseSensitive ? Ordinal : new ImplOrdinalIgnoreCaseThenOrdinal(); 22 | 23 | private sealed class ImplByStringComparison : IComparer> 24 | { 25 | private readonly StringComparison _nameComparison; 26 | 27 | public ImplByStringComparison(StringComparison nameComparison) => _nameComparison = nameComparison; 28 | 29 | public int Compare(KeyValuePair x, KeyValuePair y) => 30 | string.Compare(x.Key, y.Key, _nameComparison); 31 | } 32 | 33 | private sealed class ImplOrdinalIgnoreCaseThenOrdinal : IComparer> 34 | { 35 | public int Compare(KeyValuePair x, KeyValuePair y) 36 | { 37 | var ignoreCaseResult = string.Compare(x.Key, y.Key, StringComparison.OrdinalIgnoreCase); 38 | if (ignoreCaseResult != 0) 39 | { 40 | return ignoreCaseResult; 41 | } 42 | 43 | return string.Compare(x.Key, y.Key, StringComparison.Ordinal); 44 | } 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/ChildProcess/Utilities/EnvironmentVariableUtil.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) @asmichi (https://github.com/asmichi). Licensed under the MIT License. See LICENCE in the project root for details. 2 | 3 | using System; 4 | 5 | namespace Asmichi.Utilities 6 | { 7 | /// 8 | /// Inspects environment variables. 9 | /// 10 | internal static class EnvironmentVariableUtil 11 | { 12 | public static void ValidateNameAndValue(string name, string value) 13 | { 14 | if (name == null) 15 | { 16 | throw new ArgumentNullException(nameof(name)); 17 | } 18 | if (name.Length == 0) 19 | { 20 | throw new ArgumentException("Environment variable name must not be empty.", nameof(name)); 21 | } 22 | if (name.Contains('\0', StringComparison.Ordinal)) 23 | { 24 | throw new ArgumentException("Environment variable name must not contain '\0'.", nameof(name)); 25 | } 26 | if (name.Contains('=', StringComparison.Ordinal)) 27 | { 28 | throw new ArgumentException("Environment variable name must not contain '='.", nameof(name)); 29 | } 30 | 31 | if (!string.IsNullOrEmpty(value)) 32 | { 33 | if (value.Contains('\0', StringComparison.Ordinal)) 34 | { 35 | throw new ArgumentException("Environment variable value must not contain '\0'.", nameof(value)); 36 | } 37 | } 38 | else 39 | { 40 | // null or empty indicates that this variable should be removed. 41 | } 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/ChildProcess/Utilities/ThrowHelper.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) @asmichi (https://github.com/asmichi). Licensed under the MIT License. See LICENCE in the project root for details. 2 | 3 | using System; 4 | using System.Diagnostics.CodeAnalysis; 5 | using System.Globalization; 6 | using System.IO; 7 | using Asmichi.ProcessManagement; 8 | 9 | namespace Asmichi.Utilities 10 | { 11 | internal static class ThrowHelper 12 | { 13 | [DoesNotReturn] 14 | public static void ThrowExecutableNotFoundException(string fileName, ChildProcessFlags flags, Exception? innerException = null) 15 | { 16 | bool ignoreSearchPath = flags.HasIgnoreSearchPath(); 17 | var format = ignoreSearchPath ? "Executable not found: {0}" : "Executable not found on the search path: {0}"; 18 | throw new FileNotFoundException(string.Format(CultureInfo.InvariantCulture, format, fileName), fileName, innerException); 19 | } 20 | 21 | [DoesNotReturn] 22 | public static void ThrowChcpFailedException(int codePage, int exitCode, string paramName) 23 | { 24 | throw new ArgumentException( 25 | string.Format(CultureInfo.InvariantCulture, "chcp.com {0} failed with exit code {1} most likely because code page {0} is invalid", codePage, exitCode), 26 | paramName); 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/ChildProcess/Utilities/WindowsEnvironmentBlockUtil.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) @asmichi (https://github.com/asmichi). Licensed under the MIT License. See LICENCE in the project root for details. 2 | 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Diagnostics; 6 | 7 | namespace Asmichi.Utilities 8 | { 9 | internal static class WindowsEnvironmentBlockUtil 10 | { 11 | /// 12 | /// Constructs an environment block for CreateProcess. 13 | /// 14 | /// Collection of environment variables. 15 | /// A string that contains the environment block. 16 | public static char[] MakeEnvironmentBlock(ReadOnlySpan> envVars) 17 | { 18 | var buf = new char[CalculateLength(envVars)]; 19 | 20 | int cur = 0; 21 | foreach (var (name, value) in envVars) 22 | { 23 | // name=value\0 24 | name.CopyTo(0, buf, cur, name.Length); 25 | cur += name.Length; 26 | 27 | buf[cur++] = '='; 28 | 29 | value.CopyTo(0, buf, cur, value.Length); 30 | cur += value.Length; 31 | 32 | buf[cur++] = '\0'; 33 | } 34 | 35 | // Terminating \0 36 | buf[cur++] = '\0'; 37 | 38 | Debug.Assert(cur == buf.Length); 39 | 40 | return buf; 41 | } 42 | 43 | private static int CalculateLength(ReadOnlySpan> envVars) 44 | { 45 | int length = 0; 46 | foreach (var (name, value) in envVars) 47 | { 48 | // name=value\0 49 | length += name.Length + value.Length + 2; 50 | } 51 | 52 | // Terminating \0 53 | length++; 54 | 55 | return length; 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/PrivateAssembly.ruleset: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/PublicAssembly.ruleset: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/TestChild/Kernel32.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) @asmichi (https://github.com/asmichi). Licensed under the MIT License. See LICENCE in the project root for details. 2 | 3 | using System.Runtime.InteropServices; 4 | 5 | namespace Asmichi 6 | { 7 | internal static class Kernel32 8 | { 9 | private const string DllName = "kernel32.dll"; 10 | 11 | [DllImport(DllName, SetLastError = true)] 12 | public static extern int GetConsoleOutputCP(); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/TestChild/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) @asmichi (https://github.com/asmichi). Licensed under the MIT License. See LICENCE in the project root for details. 2 | 3 | using System; 4 | using System.Runtime.InteropServices; 5 | 6 | [assembly: CLSCompliant(false)] 7 | [assembly: DefaultDllImportSearchPaths(DllImportSearchPath.System32)] 8 | -------------------------------------------------------------------------------- /src/TestChild/TestChild.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net8.0 5 | 6 | 7 | 8 | 9 | 10 | TestChild 11 | ..\PrivateAssembly.ruleset 12 | Exe 13 | Asmichi 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/docker/childprocess-buildtools-ubuntu-crosssysroot/Dockerfile: -------------------------------------------------------------------------------- 1 | # Build: 2 | # docker build -t asmichi/childprocess-buildtools-ubuntu-crosssysroot:22.04.${version} . 3 | FROM alpine:3.13 as alpine-sysroot 4 | ADD build-sysroot-alpine.sh /tmp/ 5 | RUN /tmp/build-sysroot-alpine.sh 6 | 7 | FROM ubuntu:18.04 as ubuntu18-sysroot 8 | RUN sed -i -r 's!(deb|deb-src) \S+!\1 mirror+http://mirrors.ubuntu.com/mirrors.txt!' /etc/apt/sources.list \ 9 | && apt-get update \ 10 | && apt-get install -y --no-install-recommends ca-certificates \ 11 | && apt-get install -y --no-install-recommends \ 12 | libc6-amd64-cross \ 13 | libc6-dev-amd64-cross \ 14 | libgcc1-amd64-cross \ 15 | libstdc++-7-dev-amd64-cross \ 16 | libstdc++6-amd64-cross \ 17 | linux-libc-dev-amd64-cross \ 18 | libc6-arm64-cross \ 19 | libc6-dev-arm64-cross \ 20 | libgcc1-arm64-cross \ 21 | libstdc++-7-dev-arm64-cross \ 22 | libstdc++6-arm64-cross \ 23 | linux-libc-dev-arm64-cross \ 24 | libc6-armhf-cross \ 25 | libc6-dev-armhf-cross \ 26 | libgcc1-armhf-cross \ 27 | libstdc++-7-dev-armhf-cross \ 28 | libstdc++6-armhf-cross \ 29 | linux-libc-dev-armhf-cross \ 30 | && rm -rf /var/lib/apt/lists/* 31 | ADD extract-sysroot-ubuntu18.sh /tmp/ 32 | RUN /tmp/extract-sysroot-ubuntu18.sh 33 | 34 | FROM asmichi/childprocess-buildtools-ubuntu:22.04.20240702.1 35 | COPY --from=alpine-sysroot /sysroots/ /sysroots/ 36 | COPY --from=ubuntu18-sysroot /sysroots/ /sysroots/ 37 | ENV SYSROOTS_DIR="/sysroots" 38 | -------------------------------------------------------------------------------- /src/docker/childprocess-buildtools-ubuntu-crosssysroot/build-sysroot-alpine.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # Copyright (c) @asmichi (https://github.com/asmichi). Licensed under the MIT License. See LICENCE in the project root for details. 3 | 4 | set -eu 5 | 6 | function build_sysroot() 7 | { 8 | local arch=$1 9 | local sysroot_name=sysroot-alpine-${arch}-alpine-linux-musl 10 | local tmp_root=/tmp/${sysroot_name} 11 | local root=/sysroots/${sysroot_name} 12 | 13 | if [ -e ${tmp_root} ]; then 14 | rm -rf ${tmp_root} 15 | fi 16 | if [ -e ${root} ]; then 17 | rm -rf ${root} 18 | fi 19 | 20 | apk --root ${tmp_root} --keys-dir /usr/share/apk/keys --repositories-file /etc/apk/repositories --arch ${arch} --initdb add \ 21 | linux-headers libgcc libc-dev libstdc++ gcc g++ 22 | 23 | rm -rf ${tmp_root}/lib/apk 24 | # Remove unnecessary huge static libraries to save space 25 | rm ${tmp_root}/usr/lib/libstdc++.a ${tmp_root}/usr/lib/libc.a 26 | 27 | mkdir -p ${root} 28 | mkdir -p ${root}/usr 29 | mkdir -p ${root}/usr 30 | 31 | cp -R ${tmp_root}/lib ${root}/ 32 | cp -R ${tmp_root}/usr/include ${root}/usr 33 | cp -R ${tmp_root}/usr/lib ${root}/usr 34 | 35 | rm -rf ${tmp_root} 36 | } 37 | 38 | build_sysroot x86_64 39 | build_sysroot aarch64 40 | -------------------------------------------------------------------------------- /src/docker/childprocess-buildtools-ubuntu-crosssysroot/extract-sysroot-ubuntu18.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Copyright (c) @asmichi (https://github.com/asmichi). Licensed under the MIT License. See LICENCE in the project root for details. 3 | 4 | set -eu 5 | 6 | root=/sysroots/sysroot-ubuntu18 7 | triples="x86_64-linux-gnu arm-linux-gnueabihf aarch64-linux-gnu" 8 | 9 | function copy_dirs() 10 | { 11 | local src_parent=/${1} 12 | local dst_parent=${root}/${1} 13 | local dirs=${@:2} 14 | 15 | mkdir -p ${dst_parent} 16 | 17 | for x in $dirs; do 18 | cp -R ${src_parent}/${x} ${dst_parent} 19 | done 20 | } 21 | 22 | function copy_files() 23 | { 24 | local src_parent=/${1} 25 | local dst_parent=${root}/${1} 26 | local files=${@:2} 27 | 28 | mkdir -p ${dst_parent} 29 | 30 | for x in $files; do 31 | cp -P ${src_parent}/${x} ${dst_parent}/${x} 32 | done 33 | } 34 | 35 | if [ -e ${root} ]; then 36 | rm -rf ${root} 37 | fi 38 | 39 | # headers 40 | for triple in ${triples}; do 41 | copy_dirs usr/${triple} include 42 | done 43 | 44 | # libs 45 | for triple in ${triples}; do 46 | copy_dirs usr/lib/gcc-cross/${triple} 7 47 | # Remove unnecessary huge static libraries to save space 48 | rm ${root}/usr/lib/gcc-cross/${triple}/7/*.a 49 | copy_files usr/lib/gcc-cross/${triple}/7 libgcc.a 50 | 51 | copy_dirs usr/${triple} lib 52 | # Remove unnecessary huge static libraries to save space 53 | rm ${root}/usr/${triple}/lib/*.a 54 | copy_files usr/${triple}/lib libc_nonshared.a libpthread_nonshared.a 55 | done 56 | 57 | copy_files usr/x86_64-linux-gnu/lib libmvec_nonshared.a 58 | -------------------------------------------------------------------------------- /src/docker/childprocess-buildtools-ubuntu/Dockerfile: -------------------------------------------------------------------------------- 1 | # Build: 2 | # docker build -t asmichi/childprocess-buildtools-ubuntu:22.04.${version} . 3 | FROM ubuntu:22.04 4 | RUN sed -i -r 's!(deb|deb-src) \S+!\1 mirror+http://mirrors.ubuntu.com/mirrors.txt!' /etc/apt/sources.list \ 5 | && apt-get update \ 6 | && apt-get install -y --no-install-recommends ca-certificates \ 7 | && apt-get install -y --no-install-recommends \ 8 | clang \ 9 | lld \ 10 | make \ 11 | cmake \ 12 | && rm -rf /var/lib/apt/lists/* 13 | -------------------------------------------------------------------------------- /src/docker/node-alpine-azuredevops/Dockerfile: -------------------------------------------------------------------------------- 1 | # Build: 2 | # docker build -t asmichi/node-azuredevops:20-alpine3.20 . 3 | # 4 | # https://docs.microsoft.com/en-us/azure/devops/pipelines/process/container-phases?view=azure-devops#non-glibc-based-containers 5 | # https://learn.microsoft.com/en-us/dotnet/core/install/linux-alpine 6 | FROM node:20-alpine3.20 7 | 8 | RUN apk add --no-cache --virtual .pipeline-deps readline linux-pam \ 9 | && apk add --no-cache \ 10 | # Azure Pipeline depndencies 11 | bash \ 12 | sudo \ 13 | shadow \ 14 | # .NET Core dependencies 15 | ca-certificates-bundle \ 16 | libgcc \ 17 | libssl3 \ 18 | libstdc++ \ 19 | zlib \ 20 | && apk del .pipeline-deps 21 | 22 | ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT="1" 23 | 24 | LABEL "com.azure.dev.pipelines.agent.handler.node.path"="/usr/local/bin/node" 25 | 26 | CMD [ "node" ] 27 | -------------------------------------------------------------------------------- /src/stylecop.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/DotNetAnalyzers/StyleCopAnalyzers/master/StyleCop.Analyzers/StyleCop.Analyzers/Settings/stylecop.schema.json", 3 | "settings": { 4 | "indentation": { 5 | "indentationSize": 4, 6 | "useTabs": false 7 | }, 8 | "readabilityRules": { 9 | "allowBuiltInTypeAliases": false 10 | }, 11 | "orderingRules": { 12 | "elementOrder": [ "constant", "readonly" ], 13 | "systemUsingDirectivesFirst": true, 14 | "usingDirectivesPlacement": "outsideNamespace", 15 | "blankLinesBetweenUsingGroups": "omit" 16 | }, 17 | "namingRules": { 18 | "allowCommonHungarianPrefixes": false, 19 | "tupleElementNameCasing": "camelCase" 20 | }, 21 | "maintainabilityRules": { 22 | "topLevelTypes": [] 23 | }, 24 | "layoutRules": { 25 | "newlineAtEndOfFile": "require", 26 | "allowConsecutiveUsings": true 27 | }, 28 | "documentationRules": { 29 | "copyrightText": "Copyright (c) @asmichi (https://github.com/asmichi). Licensed under the MIT License. See LICENCE in the project root for details.", 30 | "xmlHeader": false, 31 | "documentExposedElements": true, 32 | "documentInterfaces": false, 33 | "documentInternalElements": false, 34 | "documentPrivateElements": false, 35 | "documentPrivateFields": false 36 | } 37 | } 38 | } 39 | --------------------------------------------------------------------------------