├── .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 |
--------------------------------------------------------------------------------