ctypes-unicode-proclaunch 0.1.0a0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Jifeng Wu
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.
@@ -0,0 +1,151 @@
1
+ Metadata-Version: 2.4
2
+ Name: ctypes-unicode-proclaunch
3
+ Version: 0.1.0a0
4
+ Summary: A minimal, robust, and Unicode-aware process launching library using ctypes.
5
+ Author-email: Jifeng Wu <jifengwu2k@gmail.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/jifengwu2k/ctypes-unicode-proclaunch
8
+ Project-URL: Bug Tracker, https://github.com/jifengwu2k/ctypes-unicode-proclaunch/issues
9
+ Classifier: Programming Language :: Python :: 2
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Operating System :: OS Independent
12
+ Requires-Python: >=2
13
+ Description-Content-Type: text/markdown
14
+ License-File: LICENSE
15
+ Requires-Dist: escape-nt-command-line-argument
16
+ Requires-Dist: find-unicode-executable
17
+ Requires-Dist: posix-or-nt
18
+ Requires-Dist: read-unicode-environment-variables-dictionary
19
+ Requires-Dist: send-recv-json
20
+ Requires-Dist: typing; python_version < "3.5"
21
+ Dynamic: license-file
22
+
23
+ # `ctypes-unicode-proclaunch`
24
+
25
+ A minimal, robust, and Unicode-aware process launching library using ctypes.
26
+
27
+ - No:
28
+ - Shell involvement: Arguments are not interpolated or globbed by any shell, ensuring "what you pass is what you
29
+ get".
30
+ - Python `subprocess` abstractions: No `Popen`/`communicate`/`universal_newlines`/encoding drama.
31
+ - Yes:
32
+ - Native system call usage for maximum control and correctness.
33
+ - `CreateProcessW` (NT)
34
+ - `fork` and `execve` (POSIX)
35
+ - Unicode on both Python 2 and 3.
36
+ - Fully typed.
37
+
38
+ ## Features
39
+
40
+ - Precise Process Spawning: Direct system call wrappers, not wrappers around wrappers.
41
+ - Escape-proof Argument Passing: Arguments are not interpreted or glob-expanded.
42
+ - Full Unicode Support: Give and receive Unicode paths/arguments everywhere.
43
+ - Explicit Environment: Can supply a Unicode environment dictionary. Does not mutate global environment variables.
44
+ - Handles Redirection: Pass open file descriptors for `stdin`, `stdout`, `stderr` just like with the C API.
45
+
46
+ ## Installation
47
+
48
+ ```bash
49
+ pip install ctypes-unicode-proclaunch
50
+ ```
51
+
52
+ ## Usage
53
+
54
+ ### Process Launched
55
+
56
+ A Python script `print_argv.py`:
57
+
58
+ ```python
59
+ # coding=utf-8
60
+ from __future__ import print_function
61
+ import sys
62
+
63
+ if __name__ == '__main__':
64
+ for i, arg in enumerate(sys.argv):
65
+ print('sys.argv[%d]=%s' % (i, arg))
66
+ ```
67
+
68
+ ### NT
69
+
70
+ ```python
71
+ # coding=utf-8
72
+ from ctypes_unicode_proclaunch import launch, wait
73
+
74
+ # No `cmd.exe`'s handling of special characters:
75
+ process_handle_1 = launch([u'python', u'print_argv.py', u'%USERNAME%'])
76
+ wait(process_handle_1)
77
+ # sys.argv[0]=print_argv.py
78
+ # sys.argv[1]=%USERNAME%
79
+
80
+ process_handle_2 = launch([u'python', u'print_argv.py', u'Hello', u'&', u'python', u'print_argv.py', u'injected'])
81
+ wait(process_handle_2)
82
+ # sys.argv[0]=print_argv.py
83
+ # sys.argv[1]=Hello
84
+ # sys.argv[2]=&
85
+ # sys.argv[3]=python
86
+ # sys.argv[4]=print_argv.py
87
+ # sys.argv[5]=injected
88
+
89
+ process_handle_3 = launch([u'python', u'print_argv.py', u'*.txt'])
90
+ wait(process_handle_3)
91
+ # sys.argv[0]=print_argv.py
92
+ # sys.argv[1]=*.txt
93
+ ```
94
+
95
+ ### POSIX
96
+
97
+ ```python
98
+ # coding=utf-8
99
+ from ctypes_unicode_proclaunch import launch, wait
100
+
101
+ pid_1 = launch([u'python', u'print_argv.py', u'$HOME'])
102
+ wait(pid_1)
103
+ # sys.argv[0]=print_argv.py
104
+ # sys.argv[1]=$HOME
105
+
106
+ pid_2 = launch([u'python', u'print_argv.py', u'"test"', u'>', u'$file'])
107
+ wait(pid_2)
108
+ # sys.argv[0]=print_argv.py
109
+ # sys.argv[1]="test"
110
+ # sys.argv[2]=>
111
+ # sys.argv[3]=$file
112
+
113
+ pid_3 = launch([u'python', u'print_argv.py', u'*.txt'])
114
+ wait(pid_3)
115
+ # sys.argv[0]=print_argv.py
116
+ # sys.argv[1]=*.txt
117
+ ```
118
+
119
+ ### Custom Environment and File Redirection
120
+
121
+ ```python
122
+ # coding=utf-8
123
+ from ctypes_unicode_proclaunch import launch, wait
124
+ from read_unicode_environment_variables_dictionary import read_unicode_environment_variables_dictionary
125
+
126
+ # Doesn't modify `os.environ`
127
+ unicode_environment_variables_dictionary = read_unicode_environment_variables_dictionary()
128
+ unicode_environment_variables_dictionary[u'COVERAGE_FILE'] = u'test_coverage.sqlite3'
129
+
130
+ # We pass the file descriptors of the opened files for redirection.
131
+ # Just as in the Unix C API.
132
+ # The launched processes DO NOT respect the encodings you specified if you opened the files in text mode.
133
+ # This means that you should always open files in binary mode (`'rb'`, `'wb'`, etc.) to make that explicit.
134
+ with open('test_stdin.txt', 'rb') as f_0, open('test_stdout.txt', 'wb') as f_1, open('test_stderr.txt', 'wb') as f_2:
135
+ pid_or_process_handle = launch(
136
+ [u'python', u'-m', u'coverage', u'run', u'test.py'],
137
+ environment=unicode_environment_variables_dictionary,
138
+ stdin_file_descriptor=f_0.fileno(),
139
+ stdout_file_descriptor=f_1.fileno(),
140
+ stderr_file_descriptor=f_2.fileno()
141
+ )
142
+ wait(pid_or_process_handle)
143
+ ```
144
+
145
+ ## Contributing
146
+
147
+ Contributions are welcome! Please submit pull requests or open issues on the GitHub repository.
148
+
149
+ ## License
150
+
151
+ This project is licensed under the [MIT License](LICENSE).
@@ -0,0 +1,129 @@
1
+ # `ctypes-unicode-proclaunch`
2
+
3
+ A minimal, robust, and Unicode-aware process launching library using ctypes.
4
+
5
+ - No:
6
+ - Shell involvement: Arguments are not interpolated or globbed by any shell, ensuring "what you pass is what you
7
+ get".
8
+ - Python `subprocess` abstractions: No `Popen`/`communicate`/`universal_newlines`/encoding drama.
9
+ - Yes:
10
+ - Native system call usage for maximum control and correctness.
11
+ - `CreateProcessW` (NT)
12
+ - `fork` and `execve` (POSIX)
13
+ - Unicode on both Python 2 and 3.
14
+ - Fully typed.
15
+
16
+ ## Features
17
+
18
+ - Precise Process Spawning: Direct system call wrappers, not wrappers around wrappers.
19
+ - Escape-proof Argument Passing: Arguments are not interpreted or glob-expanded.
20
+ - Full Unicode Support: Give and receive Unicode paths/arguments everywhere.
21
+ - Explicit Environment: Can supply a Unicode environment dictionary. Does not mutate global environment variables.
22
+ - Handles Redirection: Pass open file descriptors for `stdin`, `stdout`, `stderr` just like with the C API.
23
+
24
+ ## Installation
25
+
26
+ ```bash
27
+ pip install ctypes-unicode-proclaunch
28
+ ```
29
+
30
+ ## Usage
31
+
32
+ ### Process Launched
33
+
34
+ A Python script `print_argv.py`:
35
+
36
+ ```python
37
+ # coding=utf-8
38
+ from __future__ import print_function
39
+ import sys
40
+
41
+ if __name__ == '__main__':
42
+ for i, arg in enumerate(sys.argv):
43
+ print('sys.argv[%d]=%s' % (i, arg))
44
+ ```
45
+
46
+ ### NT
47
+
48
+ ```python
49
+ # coding=utf-8
50
+ from ctypes_unicode_proclaunch import launch, wait
51
+
52
+ # No `cmd.exe`'s handling of special characters:
53
+ process_handle_1 = launch([u'python', u'print_argv.py', u'%USERNAME%'])
54
+ wait(process_handle_1)
55
+ # sys.argv[0]=print_argv.py
56
+ # sys.argv[1]=%USERNAME%
57
+
58
+ process_handle_2 = launch([u'python', u'print_argv.py', u'Hello', u'&', u'python', u'print_argv.py', u'injected'])
59
+ wait(process_handle_2)
60
+ # sys.argv[0]=print_argv.py
61
+ # sys.argv[1]=Hello
62
+ # sys.argv[2]=&
63
+ # sys.argv[3]=python
64
+ # sys.argv[4]=print_argv.py
65
+ # sys.argv[5]=injected
66
+
67
+ process_handle_3 = launch([u'python', u'print_argv.py', u'*.txt'])
68
+ wait(process_handle_3)
69
+ # sys.argv[0]=print_argv.py
70
+ # sys.argv[1]=*.txt
71
+ ```
72
+
73
+ ### POSIX
74
+
75
+ ```python
76
+ # coding=utf-8
77
+ from ctypes_unicode_proclaunch import launch, wait
78
+
79
+ pid_1 = launch([u'python', u'print_argv.py', u'$HOME'])
80
+ wait(pid_1)
81
+ # sys.argv[0]=print_argv.py
82
+ # sys.argv[1]=$HOME
83
+
84
+ pid_2 = launch([u'python', u'print_argv.py', u'"test"', u'>', u'$file'])
85
+ wait(pid_2)
86
+ # sys.argv[0]=print_argv.py
87
+ # sys.argv[1]="test"
88
+ # sys.argv[2]=>
89
+ # sys.argv[3]=$file
90
+
91
+ pid_3 = launch([u'python', u'print_argv.py', u'*.txt'])
92
+ wait(pid_3)
93
+ # sys.argv[0]=print_argv.py
94
+ # sys.argv[1]=*.txt
95
+ ```
96
+
97
+ ### Custom Environment and File Redirection
98
+
99
+ ```python
100
+ # coding=utf-8
101
+ from ctypes_unicode_proclaunch import launch, wait
102
+ from read_unicode_environment_variables_dictionary import read_unicode_environment_variables_dictionary
103
+
104
+ # Doesn't modify `os.environ`
105
+ unicode_environment_variables_dictionary = read_unicode_environment_variables_dictionary()
106
+ unicode_environment_variables_dictionary[u'COVERAGE_FILE'] = u'test_coverage.sqlite3'
107
+
108
+ # We pass the file descriptors of the opened files for redirection.
109
+ # Just as in the Unix C API.
110
+ # The launched processes DO NOT respect the encodings you specified if you opened the files in text mode.
111
+ # This means that you should always open files in binary mode (`'rb'`, `'wb'`, etc.) to make that explicit.
112
+ with open('test_stdin.txt', 'rb') as f_0, open('test_stdout.txt', 'wb') as f_1, open('test_stderr.txt', 'wb') as f_2:
113
+ pid_or_process_handle = launch(
114
+ [u'python', u'-m', u'coverage', u'run', u'test.py'],
115
+ environment=unicode_environment_variables_dictionary,
116
+ stdin_file_descriptor=f_0.fileno(),
117
+ stdout_file_descriptor=f_1.fileno(),
118
+ stderr_file_descriptor=f_2.fileno()
119
+ )
120
+ wait(pid_or_process_handle)
121
+ ```
122
+
123
+ ## Contributing
124
+
125
+ Contributions are welcome! Please submit pull requests or open issues on the GitHub repository.
126
+
127
+ ## License
128
+
129
+ This project is licensed under the [MIT License](LICENSE).
@@ -0,0 +1,151 @@
1
+ Metadata-Version: 2.4
2
+ Name: ctypes-unicode-proclaunch
3
+ Version: 0.1.0a0
4
+ Summary: A minimal, robust, and Unicode-aware process launching library using ctypes.
5
+ Author-email: Jifeng Wu <jifengwu2k@gmail.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/jifengwu2k/ctypes-unicode-proclaunch
8
+ Project-URL: Bug Tracker, https://github.com/jifengwu2k/ctypes-unicode-proclaunch/issues
9
+ Classifier: Programming Language :: Python :: 2
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Operating System :: OS Independent
12
+ Requires-Python: >=2
13
+ Description-Content-Type: text/markdown
14
+ License-File: LICENSE
15
+ Requires-Dist: escape-nt-command-line-argument
16
+ Requires-Dist: find-unicode-executable
17
+ Requires-Dist: posix-or-nt
18
+ Requires-Dist: read-unicode-environment-variables-dictionary
19
+ Requires-Dist: send-recv-json
20
+ Requires-Dist: typing; python_version < "3.5"
21
+ Dynamic: license-file
22
+
23
+ # `ctypes-unicode-proclaunch`
24
+
25
+ A minimal, robust, and Unicode-aware process launching library using ctypes.
26
+
27
+ - No:
28
+ - Shell involvement: Arguments are not interpolated or globbed by any shell, ensuring "what you pass is what you
29
+ get".
30
+ - Python `subprocess` abstractions: No `Popen`/`communicate`/`universal_newlines`/encoding drama.
31
+ - Yes:
32
+ - Native system call usage for maximum control and correctness.
33
+ - `CreateProcessW` (NT)
34
+ - `fork` and `execve` (POSIX)
35
+ - Unicode on both Python 2 and 3.
36
+ - Fully typed.
37
+
38
+ ## Features
39
+
40
+ - Precise Process Spawning: Direct system call wrappers, not wrappers around wrappers.
41
+ - Escape-proof Argument Passing: Arguments are not interpreted or glob-expanded.
42
+ - Full Unicode Support: Give and receive Unicode paths/arguments everywhere.
43
+ - Explicit Environment: Can supply a Unicode environment dictionary. Does not mutate global environment variables.
44
+ - Handles Redirection: Pass open file descriptors for `stdin`, `stdout`, `stderr` just like with the C API.
45
+
46
+ ## Installation
47
+
48
+ ```bash
49
+ pip install ctypes-unicode-proclaunch
50
+ ```
51
+
52
+ ## Usage
53
+
54
+ ### Process Launched
55
+
56
+ A Python script `print_argv.py`:
57
+
58
+ ```python
59
+ # coding=utf-8
60
+ from __future__ import print_function
61
+ import sys
62
+
63
+ if __name__ == '__main__':
64
+ for i, arg in enumerate(sys.argv):
65
+ print('sys.argv[%d]=%s' % (i, arg))
66
+ ```
67
+
68
+ ### NT
69
+
70
+ ```python
71
+ # coding=utf-8
72
+ from ctypes_unicode_proclaunch import launch, wait
73
+
74
+ # No `cmd.exe`'s handling of special characters:
75
+ process_handle_1 = launch([u'python', u'print_argv.py', u'%USERNAME%'])
76
+ wait(process_handle_1)
77
+ # sys.argv[0]=print_argv.py
78
+ # sys.argv[1]=%USERNAME%
79
+
80
+ process_handle_2 = launch([u'python', u'print_argv.py', u'Hello', u'&', u'python', u'print_argv.py', u'injected'])
81
+ wait(process_handle_2)
82
+ # sys.argv[0]=print_argv.py
83
+ # sys.argv[1]=Hello
84
+ # sys.argv[2]=&
85
+ # sys.argv[3]=python
86
+ # sys.argv[4]=print_argv.py
87
+ # sys.argv[5]=injected
88
+
89
+ process_handle_3 = launch([u'python', u'print_argv.py', u'*.txt'])
90
+ wait(process_handle_3)
91
+ # sys.argv[0]=print_argv.py
92
+ # sys.argv[1]=*.txt
93
+ ```
94
+
95
+ ### POSIX
96
+
97
+ ```python
98
+ # coding=utf-8
99
+ from ctypes_unicode_proclaunch import launch, wait
100
+
101
+ pid_1 = launch([u'python', u'print_argv.py', u'$HOME'])
102
+ wait(pid_1)
103
+ # sys.argv[0]=print_argv.py
104
+ # sys.argv[1]=$HOME
105
+
106
+ pid_2 = launch([u'python', u'print_argv.py', u'"test"', u'>', u'$file'])
107
+ wait(pid_2)
108
+ # sys.argv[0]=print_argv.py
109
+ # sys.argv[1]="test"
110
+ # sys.argv[2]=>
111
+ # sys.argv[3]=$file
112
+
113
+ pid_3 = launch([u'python', u'print_argv.py', u'*.txt'])
114
+ wait(pid_3)
115
+ # sys.argv[0]=print_argv.py
116
+ # sys.argv[1]=*.txt
117
+ ```
118
+
119
+ ### Custom Environment and File Redirection
120
+
121
+ ```python
122
+ # coding=utf-8
123
+ from ctypes_unicode_proclaunch import launch, wait
124
+ from read_unicode_environment_variables_dictionary import read_unicode_environment_variables_dictionary
125
+
126
+ # Doesn't modify `os.environ`
127
+ unicode_environment_variables_dictionary = read_unicode_environment_variables_dictionary()
128
+ unicode_environment_variables_dictionary[u'COVERAGE_FILE'] = u'test_coverage.sqlite3'
129
+
130
+ # We pass the file descriptors of the opened files for redirection.
131
+ # Just as in the Unix C API.
132
+ # The launched processes DO NOT respect the encodings you specified if you opened the files in text mode.
133
+ # This means that you should always open files in binary mode (`'rb'`, `'wb'`, etc.) to make that explicit.
134
+ with open('test_stdin.txt', 'rb') as f_0, open('test_stdout.txt', 'wb') as f_1, open('test_stderr.txt', 'wb') as f_2:
135
+ pid_or_process_handle = launch(
136
+ [u'python', u'-m', u'coverage', u'run', u'test.py'],
137
+ environment=unicode_environment_variables_dictionary,
138
+ stdin_file_descriptor=f_0.fileno(),
139
+ stdout_file_descriptor=f_1.fileno(),
140
+ stderr_file_descriptor=f_2.fileno()
141
+ )
142
+ wait(pid_or_process_handle)
143
+ ```
144
+
145
+ ## Contributing
146
+
147
+ Contributions are welcome! Please submit pull requests or open issues on the GitHub repository.
148
+
149
+ ## License
150
+
151
+ This project is licensed under the [MIT License](LICENSE).
@@ -0,0 +1,10 @@
1
+ LICENSE
2
+ README.md
3
+ ctypes_unicode_proclaunch.py
4
+ pyproject.toml
5
+ setup.cfg
6
+ ctypes_unicode_proclaunch.egg-info/PKG-INFO
7
+ ctypes_unicode_proclaunch.egg-info/SOURCES.txt
8
+ ctypes_unicode_proclaunch.egg-info/dependency_links.txt
9
+ ctypes_unicode_proclaunch.egg-info/requires.txt
10
+ ctypes_unicode_proclaunch.egg-info/top_level.txt
@@ -0,0 +1,8 @@
1
+ escape-nt-command-line-argument
2
+ find-unicode-executable
3
+ posix-or-nt
4
+ read-unicode-environment-variables-dictionary
5
+ send-recv-json
6
+
7
+ [:python_version < "3.5"]
8
+ typing
@@ -0,0 +1,563 @@
1
+ # coding: utf-8
2
+ # Copyright (c) 2025 Jifeng Wu
3
+ # Licensed under the MIT License. See LICENSE file in the project root for full license information.
4
+ import ctypes
5
+ from itertools import chain
6
+
7
+ from escape_nt_command_line_argument import escape_nt_command_line_argument
8
+ from find_unicode_executable import find_unicode_executable
9
+ from posix_or_nt import posix_or_nt
10
+ from read_unicode_environment_variables_dictionary import read_unicode_environment_variables_dictionary
11
+ from send_recv_json import recv_json, send_json
12
+ from typing import Any, Iterable, Mapping, Optional, Sequence, Text, Tuple
13
+
14
+ # STDIN_FILENO, STDOUT_FILENO, STDERR_FILENO
15
+ STDIN_FILENO = 0
16
+ STDOUT_FILENO = 1
17
+ STDERR_FILENO = 2
18
+
19
+ if posix_or_nt() == 'nt':
20
+ import ctypes.wintypes
21
+ import msvcrt
22
+
23
+ # Load `user32`, `kernel32`
24
+ user32 = ctypes.WinDLL('user32', use_last_error=True)
25
+ kernel32 = ctypes.WinDLL('kernel32', use_last_error=True)
26
+
27
+
28
+ # `STARTUPINFOW` Definition
29
+ class STARTUPINFOW(ctypes.Structure):
30
+ _fields_ = [
31
+ ('cb', ctypes.wintypes.DWORD),
32
+ ('lpReserved', ctypes.wintypes.LPWSTR),
33
+ ('lpDesktop', ctypes.wintypes.LPWSTR),
34
+ ('lpTitle', ctypes.wintypes.LPWSTR),
35
+ ('dwX', ctypes.wintypes.DWORD),
36
+ ('dwY', ctypes.wintypes.DWORD),
37
+ ('dwXSize', ctypes.wintypes.DWORD),
38
+ ('dwYSize', ctypes.wintypes.DWORD),
39
+ ('dwXCountChars', ctypes.wintypes.DWORD),
40
+ ('dwYCountChars', ctypes.wintypes.DWORD),
41
+ ('dwFillAttribute', ctypes.wintypes.DWORD),
42
+ ('dwFlags', ctypes.wintypes.DWORD),
43
+ ('wShowWindow', ctypes.wintypes.WORD),
44
+ ('cbReserved2', ctypes.wintypes.WORD),
45
+ ('lpReserved2', ctypes.POINTER(ctypes.wintypes.BYTE)),
46
+ ('hStdInput', ctypes.wintypes.HANDLE),
47
+ ('hStdOutput', ctypes.wintypes.HANDLE),
48
+ ('hStdError', ctypes.wintypes.HANDLE),
49
+ ]
50
+
51
+
52
+ STARTF_USESTDHANDLES = 0x100
53
+
54
+
55
+ # `PROCESS_INFORMATION` Definition
56
+ class PROCESS_INFORMATION(ctypes.Structure):
57
+ _fields_ = [
58
+ ('hProcess', ctypes.wintypes.HANDLE),
59
+ ('hThread', ctypes.wintypes.HANDLE),
60
+ ('dwProcessId', ctypes.wintypes.DWORD),
61
+ ('dwThreadId', ctypes.wintypes.DWORD),
62
+ ]
63
+
64
+
65
+ # `GetCurrentProcess` Definition
66
+ GetCurrentProcess = kernel32.GetCurrentProcess
67
+ GetCurrentProcess.restype = ctypes.wintypes.HANDLE
68
+
69
+ # `DuplicateHandle` Definition
70
+ DuplicateHandle = kernel32.DuplicateHandle
71
+ DuplicateHandle.argtypes = [
72
+ ctypes.wintypes.HANDLE, # hSourceProcessHandle
73
+ ctypes.wintypes.HANDLE, # hSourceHandle
74
+ ctypes.wintypes.HANDLE, # hTargetProcessHandle
75
+ ctypes.POINTER(ctypes.wintypes.HANDLE), # lpTargetHandle
76
+ ctypes.wintypes.DWORD, # dwDesiredAccess
77
+ ctypes.wintypes.BOOL, # bInheritHandle
78
+ ctypes.wintypes.DWORD # dwOptions
79
+ ]
80
+ DuplicateHandle.restype = ctypes.wintypes.BOOL
81
+
82
+ DUPLICATE_SAME_ACCESS = 0x00000002
83
+
84
+ # `GetEnvironmentStringsW` Definition
85
+ # Declared to return a `void *`, which will be converted into an `int`
86
+ GetEnvironmentStringsW = kernel32.GetEnvironmentStringsW
87
+ GetEnvironmentStringsW.restype = ctypes.c_void_p
88
+
89
+ # `FreeEnvironmentStringsW` Definition
90
+ # Declared to return a `void *`, which will be converted into an `int`
91
+ FreeEnvironmentStringsW = kernel32.FreeEnvironmentStringsW
92
+ FreeEnvironmentStringsW.argtypes = [ctypes.c_void_p]
93
+ FreeEnvironmentStringsW.restype = ctypes.wintypes.BOOL
94
+
95
+ # `CreateProcessW` Definition
96
+ CreateProcessW = kernel32.CreateProcessW
97
+ CreateProcessW.argtypes = [
98
+ ctypes.wintypes.LPCWSTR, # lpApplicationName
99
+ ctypes.wintypes.LPWSTR, # lpCommandLine
100
+ ctypes.wintypes.LPVOID, # lpProcessAttributes
101
+ ctypes.wintypes.LPVOID, # lpThreadAttributes
102
+ ctypes.wintypes.BOOL, # bInheritHandles
103
+ ctypes.wintypes.DWORD, # dwCreationFlags
104
+ ctypes.wintypes.LPVOID, # lpEnvironment
105
+ ctypes.wintypes.LPCWSTR, # lpCurrentDirectory
106
+ ctypes.POINTER(STARTUPINFOW), # lpStartupInfo
107
+ ctypes.POINTER(PROCESS_INFORMATION) # lpProcessInformation
108
+ ]
109
+ CreateProcessW.restype = ctypes.wintypes.BOOL
110
+
111
+ # Must set the `CREATE_UNICODE_ENVIRONMENT` flag in `dwCreationFlags`
112
+ CREATE_UNICODE_ENVIRONMENT = 0x00000400
113
+
114
+ # `WaitForSingleObject` Definition
115
+ WaitForSingleObject = kernel32.WaitForSingleObject
116
+ WaitForSingleObject.argtypes = [ctypes.wintypes.HANDLE, ctypes.wintypes.DWORD]
117
+ WaitForSingleObject.restype = ctypes.wintypes.DWORD
118
+
119
+ INFINITE = -1 # Wait indefinitely
120
+ WAIT_OBJECT_0 = 0 # Wait successful
121
+
122
+ # `GetExitCodeProcess` Definition
123
+ GetExitCodeProcess = kernel32.GetExitCodeProcess
124
+ GetExitCodeProcess.argtypes = [
125
+ ctypes.wintypes.HANDLE, # hProcess
126
+ ctypes.POINTER(ctypes.wintypes.DWORD) # lpExitCode
127
+ ]
128
+ GetExitCodeProcess.restype = ctypes.wintypes.BOOL
129
+
130
+ STILL_ACTIVE = 259
131
+
132
+ # `CloseHandle` Definition
133
+ CloseHandle = kernel32.CloseHandle
134
+ CloseHandle.argtypes = [ctypes.wintypes.HANDLE]
135
+ CloseHandle.restype = ctypes.wintypes.BOOL
136
+
137
+ # `GenerateConsoleCtrlEvent` Definition
138
+ GenerateConsoleCtrlEvent = kernel32.GenerateConsoleCtrlEvent
139
+ GenerateConsoleCtrlEvent.argtypes = [
140
+ ctypes.wintypes.DWORD, # dwCtrlEvent
141
+ ctypes.wintypes.DWORD # dwProcessGroupId
142
+ ]
143
+ GenerateConsoleCtrlEvent.restype = ctypes.wintypes.BOOL
144
+
145
+ # Create the callback function type
146
+ WNDENUMPROC = ctypes.WINFUNCTYPE(ctypes.wintypes.BOOL, ctypes.wintypes.HWND, ctypes.wintypes.LPARAM)
147
+
148
+ # `EnumWindows` Definition
149
+ EnumWindows = user32.EnumWindows
150
+ EnumWindows.argtypes = [WNDENUMPROC, ctypes.wintypes.LPARAM]
151
+ EnumWindows.restype = ctypes.wintypes.BOOL
152
+
153
+ # `GetWindowThreadProcessId` Definition
154
+ GetWindowThreadProcessId = user32.GetWindowThreadProcessId
155
+ GetWindowThreadProcessId.argtypes = [ctypes.wintypes.HWND, ctypes.POINTER(ctypes.wintypes.DWORD)]
156
+ GetWindowThreadProcessId.restype = ctypes.wintypes.DWORD
157
+
158
+ # `GetParent` Definition
159
+ GetParent = user32.GetParent
160
+ GetParent.argtypes = [ctypes.wintypes.HWND]
161
+ GetParent.restype = ctypes.wintypes.HWND
162
+
163
+ # `PostMessageW` Definition
164
+ PostMessageW = user32.PostMessageW
165
+ PostMessageW.argtypes = (
166
+ ctypes.wintypes.HWND, # hWnd
167
+ ctypes.wintypes.UINT, # Msg
168
+ ctypes.wintypes.WPARAM, # wParam
169
+ ctypes.wintypes.LPARAM # lParam
170
+ )
171
+ PostMessageW.restype = ctypes.wintypes.BOOL
172
+
173
+ # `TerminateProcess` Definition
174
+ TerminateProcess = kernel32.TerminateProcess
175
+ TerminateProcess.argtypes = [ctypes.wintypes.HANDLE, ctypes.wintypes.UINT]
176
+ TerminateProcess.restype = ctypes.wintypes.BOOL
177
+
178
+
179
+ def join_arguments_to_nt_command_line(arguments):
180
+ # type: (Iterable[Text]) -> Text
181
+ return u' '.join(map(escape_nt_command_line_argument, arguments))
182
+
183
+
184
+ def make_environment_block(environment):
185
+ # type: (Mapping[Text, Text]) -> Text
186
+ return u'\0'.join(
187
+ chain(
188
+ (u'%s=%s' % (name, value) for name, value in environment.items()),
189
+ (u'\0',)
190
+ )
191
+ )
192
+
193
+
194
+ def duplicate_file_handle_of_file_descriptor(file_descriptor):
195
+ # type: (int) -> ctypes.wintypes.HANDLE
196
+ file_handle = msvcrt.get_osfhandle(file_descriptor)
197
+ inheritable = ctypes.wintypes.HANDLE()
198
+
199
+ if not DuplicateHandle(
200
+ GetCurrentProcess(),
201
+ file_handle,
202
+ GetCurrentProcess(),
203
+ ctypes.byref(inheritable),
204
+ 0,
205
+ True,
206
+ DUPLICATE_SAME_ACCESS
207
+ ):
208
+ raise ctypes.WinError(ctypes.get_last_error())
209
+
210
+ return inheritable
211
+
212
+
213
+ def launch(
214
+ arguments,
215
+ environment=None,
216
+ stdin_file_descriptor=None,
217
+ stdout_file_descriptor=None,
218
+ stderr_file_descriptor=None
219
+ ):
220
+ # type: (Sequence[Text], Optional[Mapping[Text, Text]], Optional[int], Optional[int], Optional[int]) -> ctypes.wintypes.HANDLE
221
+ if len(arguments) == 0:
222
+ raise ValueError('Empty command line')
223
+
224
+ command_line = join_arguments_to_nt_command_line(arguments)
225
+
226
+ use_redirection = stdin_file_descriptor is not None or stdout_file_descriptor is not None or stderr_file_descriptor is not None
227
+
228
+ lp_application_name = None
229
+ lp_command_line = ctypes.create_unicode_buffer(command_line)
230
+
231
+ if environment is not None:
232
+ lp_environment = ctypes.create_unicode_buffer(make_environment_block(environment))
233
+ else:
234
+ lp_environment = None
235
+
236
+ dw_creation_flags = CREATE_UNICODE_ENVIRONMENT
237
+
238
+ startup_info_w = STARTUPINFOW()
239
+ startup_info_w.cb = ctypes.sizeof(STARTUPINFOW)
240
+
241
+ if use_redirection:
242
+ h_std_input = duplicate_file_handle_of_file_descriptor(
243
+ stdin_file_descriptor if stdin_file_descriptor is not None else STDIN_FILENO)
244
+ h_std_output = duplicate_file_handle_of_file_descriptor(
245
+ stdout_file_descriptor if stdout_file_descriptor is not None else STDOUT_FILENO)
246
+ h_std_error = duplicate_file_handle_of_file_descriptor(
247
+ stderr_file_descriptor if stderr_file_descriptor is not None else STDERR_FILENO)
248
+
249
+ startup_info_w.dwFlags |= STARTF_USESTDHANDLES
250
+ startup_info_w.hStdInput = h_std_input
251
+ startup_info_w.hStdOutput = h_std_output
252
+ startup_info_w.hStdError = h_std_error
253
+
254
+ handles_of_file_descriptors_to_close = [h_std_input, h_std_output, h_std_error]
255
+ else:
256
+ handles_of_file_descriptors_to_close = []
257
+
258
+ process_information = PROCESS_INFORMATION()
259
+
260
+ if not CreateProcessW(
261
+ lp_application_name,
262
+ lp_command_line,
263
+ None,
264
+ None,
265
+ True,
266
+ dw_creation_flags,
267
+ lp_environment,
268
+ None,
269
+ ctypes.byref(startup_info_w),
270
+ ctypes.byref(process_information)
271
+ ):
272
+ # Clean up handles on failure
273
+ for handle in handles_of_file_descriptors_to_close:
274
+ CloseHandle(handle)
275
+
276
+ raise ctypes.WinError(ctypes.get_last_error())
277
+
278
+ for handle in handles_of_file_descriptors_to_close:
279
+ CloseHandle(handle)
280
+ CloseHandle(process_information.hThread)
281
+
282
+ return process_information.hProcess
283
+
284
+
285
+ def wait(process_handle):
286
+ # type: (ctypes.wintypes.HANDLE) -> int
287
+ if WaitForSingleObject(process_handle, INFINITE) != WAIT_OBJECT_0:
288
+ raise ctypes.WinError(ctypes.get_last_error())
289
+
290
+ exit_code = ctypes.wintypes.DWORD()
291
+ if not GetExitCodeProcess(process_handle, ctypes.byref(exit_code)):
292
+ raise ctypes.WinError(ctypes.get_last_error())
293
+
294
+ CloseHandle(process_handle)
295
+
296
+ return exit_code.value
297
+
298
+
299
+ def kill(process_handle):
300
+ # type: (ctypes.wintypes.HANDLE) -> None
301
+ exit_code = ctypes.wintypes.DWORD()
302
+ if not GetExitCodeProcess(process_handle, ctypes.byref(exit_code)):
303
+ raise ctypes.WinError(ctypes.get_last_error())
304
+
305
+ if exit_code.value == STILL_ACTIVE:
306
+ if not TerminateProcess(process_handle, 1):
307
+ raise ctypes.WinError(ctypes.get_last_error())
308
+
309
+ wait(process_handle)
310
+
311
+
312
+ else:
313
+ libc = ctypes.CDLL(None, use_errno=True)
314
+
315
+
316
+ # WIFEXITED, WEXITSTATUS, WIFSIGNALED, WTERMSIG
317
+ def WIFEXITED(status):
318
+ # type: (int) -> bool
319
+ return (status & 0x7F) == 0
320
+
321
+
322
+ def WEXITSTATUS(status):
323
+ # type: (int) -> int
324
+ return (status >> 8) & 0xFF
325
+
326
+
327
+ def WIFSIGNALED(status):
328
+ # type: (int) -> bool
329
+ return ((status & 0x7F) != 0) and ((status & 0x7F) != 0x7F)
330
+
331
+
332
+ def WTERMSIG(status):
333
+ # type: (int) -> int
334
+ return status & 0x7F
335
+
336
+
337
+ # pid_t
338
+ pid_t = ctypes.c_int
339
+
340
+ # close
341
+ close = libc.close
342
+ close.argtypes = [ctypes.c_int]
343
+ close.restype = ctypes.c_int
344
+
345
+ # dup2
346
+ dup2 = libc.dup2
347
+ dup2.argtypes = [ctypes.c_int, ctypes.c_int]
348
+ dup2.restype = ctypes.c_int
349
+
350
+ # _exit
351
+ _exit = libc._exit
352
+ _exit.argtypes = [ctypes.c_int]
353
+
354
+ # fork
355
+ fork = libc.fork
356
+ fork.restype = pid_t
357
+
358
+ # execve
359
+ execve = libc.execve
360
+ execve.argtypes = [ctypes.c_char_p, ctypes.POINTER(ctypes.c_char_p), ctypes.POINTER(ctypes.c_char_p)]
361
+ execve.restype = ctypes.c_int
362
+
363
+ # kill
364
+ kill_ = libc.kill
365
+ kill_.argtypes = [pid_t, ctypes.c_int]
366
+ kill_.restype = ctypes.c_int
367
+
368
+ # pipe
369
+ pipe = libc.pipe
370
+ pipe.argtypes = [ctypes.POINTER(ctypes.c_int)]
371
+ pipe.restype = ctypes.c_int
372
+
373
+ # read
374
+ read = libc.read
375
+ read.argtypes = [ctypes.c_int, ctypes.c_void_p, ctypes.c_size_t]
376
+ read.restype = ctypes.c_size_t
377
+
378
+ # strerror
379
+ strerror = libc.strerror
380
+ strerror.argtypes = [ctypes.c_int]
381
+ strerror.restype = ctypes.c_char_p
382
+
383
+ # waitpid
384
+ waitpid = libc.waitpid
385
+ waitpid.argtypes = [
386
+ pid_t,
387
+ ctypes.POINTER(ctypes.c_int),
388
+ ctypes.c_int
389
+ ]
390
+ waitpid.restype = pid_t
391
+
392
+ # write
393
+ write = libc.write
394
+ write.argtypes = [ctypes.c_int, ctypes.c_void_p, ctypes.c_size_t]
395
+ write.restype = ctypes.c_size_t
396
+
397
+
398
+ def create_pipe():
399
+ # type: () -> Tuple[int, int]
400
+ fds = (ctypes.c_int * 2)() # Array of 2 integers
401
+
402
+ # Create the pipe
403
+ result = pipe(fds)
404
+ if result < 0:
405
+ error_number = ctypes.get_errno()
406
+ raise OSError(error_number, 'pipe failed: %s' % strerror(error_number).decode('utf-8'))
407
+
408
+ return fds[0], fds[1]
409
+
410
+
411
+ def utf_8_c_char_p_array_from_unicode_strings(unicode_strings):
412
+ encoded_unicode_strings = []
413
+ encoded_unicode_strings.extend(unicode_string.encode('utf-8') for unicode_string in unicode_strings)
414
+ encoded_unicode_strings.append(None)
415
+
416
+ return (ctypes.c_char_p * len(encoded_unicode_strings))(*encoded_unicode_strings)
417
+
418
+
419
+ def serialize_to_os_error_json(errno, strerror):
420
+ # type: (int, str) -> Any
421
+ return {"errno": errno, "strerror": strerror}
422
+
423
+
424
+ def deserialize_from_os_error_json(os_error_json):
425
+ # type: (Any) -> OSError
426
+ return OSError(os_error_json['errno'], os_error_json['strerror'])
427
+
428
+
429
+ def send_to_pipe(write_fd, data):
430
+ # type: (int, bytes) -> int
431
+ return write(write_fd, data, len(data))
432
+
433
+
434
+ def recv_from_pipe(read_fd, size):
435
+ # type: (int, int) -> bytes
436
+ string_buffer = ctypes.create_string_buffer(size)
437
+ bytes_read = read(read_fd, string_buffer, size)
438
+ return string_buffer.raw[:bytes_read]
439
+
440
+
441
+ def launch(
442
+ arguments,
443
+ environment=None,
444
+ stdin_file_descriptor=None,
445
+ stdout_file_descriptor=None,
446
+ stderr_file_descriptor=None
447
+ ):
448
+ # type: (Sequence[Text], Optional[Mapping[Text, Text]], Optional[int], Optional[int], Optional[int]) -> int
449
+ if len(arguments) == 0:
450
+ raise ValueError('Empty command line')
451
+
452
+ executable = arguments[0]
453
+ path = next(find_unicode_executable(executable), None)
454
+ if path is None:
455
+ raise ValueError('Cannot find executable %s' % executable)
456
+
457
+ remaining_arguments = arguments[1:]
458
+
459
+ if environment is None:
460
+ environment = read_unicode_environment_variables_dictionary()
461
+
462
+ utf_8_c_char_p_path = ctypes.c_char_p(path.encode('utf-8'))
463
+ utf_8_c_char_p_array_argv = utf_8_c_char_p_array_from_unicode_strings(
464
+ chain([path], remaining_arguments)
465
+ )
466
+ utf_8_c_char_p_array_envp = utf_8_c_char_p_array_from_unicode_strings(
467
+ u'%s=%s' % (name, value)
468
+ for name, value in environment.items()
469
+ )
470
+
471
+ read_fd, write_fd = create_pipe()
472
+
473
+ pid = fork()
474
+
475
+ if pid < 0:
476
+ # fork failed, parent process
477
+ error_number = ctypes.get_errno()
478
+ raise OSError(error_number, 'fork failed: %s' % strerror(error_number).decode('utf-8'))
479
+
480
+ if pid == 0:
481
+ # fork succeeded, child process
482
+
483
+ # Close read_fd
484
+ close(read_fd)
485
+
486
+ # Handle redirection
487
+ for redirected_fd, original_fd in zip(
488
+ [stdin_file_descriptor, stdout_file_descriptor, stderr_file_descriptor],
489
+ [STDIN_FILENO, STDOUT_FILENO, STDERR_FILENO]):
490
+ if redirected_fd is not None and redirected_fd != original_fd:
491
+ if dup2(redirected_fd, original_fd) < 0:
492
+ error_number = ctypes.get_errno()
493
+ error_string = 'dup2 failed: %s' % strerror(error_number).decode('utf-8')
494
+ send_json(
495
+ lambda data: send_to_pipe(write_fd, data),
496
+ serialize_to_os_error_json(error_number, error_string)
497
+ )
498
+
499
+ _exit(1)
500
+ elif redirected_fd not in [STDIN_FILENO, STDOUT_FILENO, STDERR_FILENO]:
501
+ close(redirected_fd)
502
+
503
+ # Replace with new process
504
+ execve(utf_8_c_char_p_path, utf_8_c_char_p_array_argv, utf_8_c_char_p_array_envp)
505
+
506
+ # execvp fails
507
+ error_number = ctypes.get_errno()
508
+ error_string = 'execve failed: %s' % strerror(error_number).decode('utf-8')
509
+ send_json(
510
+ lambda data: send_to_pipe(write_fd, data),
511
+ serialize_to_os_error_json(error_number, error_string)
512
+ )
513
+
514
+ _exit(1)
515
+
516
+ # fork succeeded, parent process
517
+
518
+ # Close write_fd
519
+ close(write_fd)
520
+
521
+ # Attempt to recv an OSError JSON
522
+ try:
523
+ os_error_json = recv_json(lambda size: recv_from_pipe(read_fd, size))
524
+ raise deserialize_from_os_error_json(os_error_json)
525
+ except EOFError:
526
+ pass
527
+
528
+ return pid
529
+
530
+
531
+ def wait(pid):
532
+ # type: (int) -> int
533
+ # Call waitpid
534
+ status = ctypes.c_int()
535
+ if waitpid(pid, ctypes.byref(status), 0) < 0:
536
+ # Return an error and leave the state unchanged
537
+ error_number = ctypes.get_errno()
538
+ error_string = 'waitpid failed: %s' % strerror(error_number).decode('utf-8')
539
+ raise OSError(error_number, error_string)
540
+
541
+ # Decode exit status
542
+ if WIFEXITED(status.value):
543
+ return WEXITSTATUS(status.value) # Return exit code
544
+ elif WIFSIGNALED(status.value):
545
+ raise OSError('Child killed by signal: %d' % WTERMSIG(status.value))
546
+ else:
547
+ raise OSError('Child terminated abnormally')
548
+
549
+
550
+ def kill(pid):
551
+ # type: (int) -> None
552
+ # Send SIGKILL signal to terminate the process
553
+ if kill_(pid, 9) != 0:
554
+ error_number = ctypes.get_errno()
555
+ error_string = 'kill failed: %s' % strerror(error_number).decode('utf-8')
556
+ raise OSError(error_number, error_string)
557
+
558
+ # Call waitpid to prevent process from becoming zombie
559
+ status = ctypes.c_int()
560
+ if waitpid(pid, ctypes.byref(status), 0) == -1:
561
+ error_number = ctypes.get_errno()
562
+ error_string = 'waitpid failed: %s' % strerror(error_number).decode('utf-8')
563
+ raise OSError(error_number, error_string)
@@ -0,0 +1,31 @@
1
+ [build-system]
2
+ requires = ["setuptools"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "ctypes-unicode-proclaunch"
7
+ version = "0.1.0a0"
8
+ description = "A minimal, robust, and Unicode-aware process launching library using ctypes."
9
+ readme = "README.md"
10
+ requires-python = ">=2"
11
+ license = "MIT"
12
+ authors = [
13
+ { name="Jifeng Wu", email="jifengwu2k@gmail.com" }
14
+ ]
15
+ classifiers = [
16
+ "Programming Language :: Python :: 2",
17
+ "Programming Language :: Python :: 3",
18
+ "Operating System :: OS Independent",
19
+ ]
20
+ dependencies = [
21
+ "escape-nt-command-line-argument",
22
+ "find-unicode-executable",
23
+ "posix-or-nt",
24
+ "read-unicode-environment-variables-dictionary",
25
+ "send-recv-json",
26
+ "typing; python_version < '3.5'"
27
+ ]
28
+
29
+ [project.urls]
30
+ "Homepage" = "https://github.com/jifengwu2k/ctypes-unicode-proclaunch"
31
+ "Bug Tracker" = "https://github.com/jifengwu2k/ctypes-unicode-proclaunch/issues"
@@ -0,0 +1,7 @@
1
+ [bdist_wheel]
2
+ universal = 1
3
+
4
+ [egg_info]
5
+ tag_build =
6
+ tag_date = 0
7
+