procsock 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.
- procsock-0.1.0a0/LICENSE +21 -0
- procsock-0.1.0a0/PKG-INFO +204 -0
- procsock-0.1.0a0/README.md +184 -0
- procsock-0.1.0a0/procsock.egg-info/PKG-INFO +204 -0
- procsock-0.1.0a0/procsock.egg-info/SOURCES.txt +11 -0
- procsock-0.1.0a0/procsock.egg-info/dependency_links.txt +1 -0
- procsock-0.1.0a0/procsock.egg-info/entry_points.txt +2 -0
- procsock-0.1.0a0/procsock.egg-info/requires.txt +6 -0
- procsock-0.1.0a0/procsock.egg-info/top_level.txt +1 -0
- procsock-0.1.0a0/procsock.py +537 -0
- procsock-0.1.0a0/pyproject.toml +32 -0
- procsock-0.1.0a0/setup.cfg +7 -0
procsock-0.1.0a0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 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,204 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: procsock
|
|
3
|
+
Version: 0.1.0a0
|
|
4
|
+
Summary: A small tool for running and tracking background processes over a local TCP port.
|
|
5
|
+
Author-email: Jifeng Wu <jifengwu2k@gmail.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/jifengwu2k/procsock
|
|
8
|
+
Project-URL: Bug Tracker, https://github.com/jifengwu2k/procsock/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: ctypes-unicode-proclaunch
|
|
16
|
+
Requires-Dist: read-unicode-environment-variables-dictionary
|
|
17
|
+
Requires-Dist: send-recv-json
|
|
18
|
+
Requires-Dist: typing; python_version < "3.5"
|
|
19
|
+
Dynamic: license-file
|
|
20
|
+
|
|
21
|
+
# procsock
|
|
22
|
+
|
|
23
|
+
A small tool for running and tracking background processes over a local TCP port.
|
|
24
|
+
|
|
25
|
+
It is **not** a terminal multiplexer and does **not** use PTYs. It launches plain child processes, redirects their stdin/stdout/stderr to files, and lets clients query whether a process is still running or has finished.
|
|
26
|
+
|
|
27
|
+
## Overview
|
|
28
|
+
|
|
29
|
+
`procsock` consists of two pieces:
|
|
30
|
+
|
|
31
|
+
- a **server**
|
|
32
|
+
- a **client** that connects over a local TCP port when the server is available
|
|
33
|
+
|
|
34
|
+
The server accepts JSON-RPC requests to start and inspect processes. Each process is started with:
|
|
35
|
+
|
|
36
|
+
- argv
|
|
37
|
+
- client's current working directory
|
|
38
|
+
- client's current environment
|
|
39
|
+
- optional stdin file
|
|
40
|
+
- optional stdout file
|
|
41
|
+
- optional stderr file
|
|
42
|
+
|
|
43
|
+
When not specified, stdin/stdout/stderr default to `os.devnull`.
|
|
44
|
+
|
|
45
|
+
The launcher returns a process identifier:
|
|
46
|
+
|
|
47
|
+
- on Unix, it is the process ID
|
|
48
|
+
- on Windows/NT, it is the process handle
|
|
49
|
+
|
|
50
|
+
The server keeps the process state in memory only. If the server exits or restarts, all state is lost.
|
|
51
|
+
|
|
52
|
+
## Example session
|
|
53
|
+
|
|
54
|
+
Start server:
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
procsock server --port 9000
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Launch process:
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
procsock launch \
|
|
64
|
+
--port 9000 \
|
|
65
|
+
--stdin /tmp/in.txt \
|
|
66
|
+
--stdout /tmp/out.txt \
|
|
67
|
+
--stderr /tmp/err.txt \
|
|
68
|
+
-- /usr/bin/python3 -c 'print("hello")'
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
List:
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
procsock list --port 9000
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Terminate:
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
procsock terminate --port 9000 12345
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## How it works
|
|
84
|
+
|
|
85
|
+
1. Start the server in the foreground.
|
|
86
|
+
2. The server binds a local TCP port.
|
|
87
|
+
3. A client connects and sends JSON-RPC commands.
|
|
88
|
+
4. The server launches child processes with `ctypes-unicode-proclaunch`.
|
|
89
|
+
5. Each launched process is started from a dedicated launcher thread.
|
|
90
|
+
6. That launcher thread uses the client's current working directory as the process working directory.
|
|
91
|
+
7. This is implemented with a small synchronized `TempCwd` helper class exposing `__enter__` and `__exit__`.
|
|
92
|
+
8. A waiter thread waits for process completion and updates the in-memory process status on exit.
|
|
93
|
+
9. The client can list the status of all processes or terminate a process.
|
|
94
|
+
10. If the server exits, managed children are terminated, and the in-memory process state is lost.
|
|
95
|
+
|
|
96
|
+
## Commands
|
|
97
|
+
|
|
98
|
+
### `launch`
|
|
99
|
+
|
|
100
|
+
Launch a new child process via `ctypes-unicode-proclaunch`.
|
|
101
|
+
|
|
102
|
+
For the CLI, the launched process inherits the current environment of the `procsock launch` client process.
|
|
103
|
+
|
|
104
|
+
For each launched process:
|
|
105
|
+
|
|
106
|
+
- stdin is opened from the requested file path, typically read-only
|
|
107
|
+
- stdout is opened to the requested file path
|
|
108
|
+
- stderr is opened to the requested file path
|
|
109
|
+
- any unspecified stdin/stdout/stderr path defaults to `os.devnull`
|
|
110
|
+
|
|
111
|
+
Behavior should match normal process redirection semantics:
|
|
112
|
+
|
|
113
|
+
- stdin: if a path is provided, it must already exist
|
|
114
|
+
- stdout: if a path is provided, open with create/truncate behavior
|
|
115
|
+
- stderr: if a path is provided, open with create/truncate behavior
|
|
116
|
+
- parent directories are not created automatically
|
|
117
|
+
|
|
118
|
+
Request:
|
|
119
|
+
|
|
120
|
+
```json
|
|
121
|
+
{
|
|
122
|
+
"jsonrpc": "2.0",
|
|
123
|
+
"id": 1,
|
|
124
|
+
"method": "launch",
|
|
125
|
+
"params": {
|
|
126
|
+
"argv": ["/bin/sh", "-c", "echo hello; sleep 2"],
|
|
127
|
+
"cwd": "/tmp",
|
|
128
|
+
"stdin_path": "/tmp/in.txt",
|
|
129
|
+
"stdout_path": "/tmp/out.txt",
|
|
130
|
+
"stderr_path": "/tmp/err.txt"
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Response:
|
|
136
|
+
|
|
137
|
+
```json
|
|
138
|
+
{
|
|
139
|
+
"jsonrpc": "2.0",
|
|
140
|
+
"id": 1,
|
|
141
|
+
"result": {
|
|
142
|
+
"pid": 12345,
|
|
143
|
+
"argv": ["/bin/sh", "-c", "echo hello; sleep 2"],
|
|
144
|
+
"cwd": "/tmp",
|
|
145
|
+
"stdin_path": "/tmp/in.txt",
|
|
146
|
+
"stdout_path": "/tmp/out.txt",
|
|
147
|
+
"stderr_path": "/tmp/err.txt",
|
|
148
|
+
"finished": false,
|
|
149
|
+
"started_at": 1760000000.0,
|
|
150
|
+
"finished_at": null,
|
|
151
|
+
"exit_code": null
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
### `list`
|
|
157
|
+
|
|
158
|
+
Return the status of all known in-memory processes.
|
|
159
|
+
|
|
160
|
+
Request:
|
|
161
|
+
|
|
162
|
+
```json
|
|
163
|
+
{
|
|
164
|
+
"jsonrpc": "2.0",
|
|
165
|
+
"id": 2,
|
|
166
|
+
"method": "list",
|
|
167
|
+
"params": {}
|
|
168
|
+
}
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
Response:
|
|
172
|
+
|
|
173
|
+
```json
|
|
174
|
+
{
|
|
175
|
+
"jsonrpc": "2.0",
|
|
176
|
+
"id": 2,
|
|
177
|
+
"result": [
|
|
178
|
+
{
|
|
179
|
+
"pid": 12345,
|
|
180
|
+
"argv": ["/bin/sh", "-c", "echo hello; sleep 2"],
|
|
181
|
+
"cwd": "/tmp",
|
|
182
|
+
"stdin_path": "/tmp/in.txt",
|
|
183
|
+
"stdout_path": "/tmp/out.txt",
|
|
184
|
+
"stderr_path": "/tmp/err.txt",
|
|
185
|
+
"finished": false,
|
|
186
|
+
"started_at": 1760000000.0,
|
|
187
|
+
"finished_at": null,
|
|
188
|
+
"exit_code": null
|
|
189
|
+
}
|
|
190
|
+
]
|
|
191
|
+
}
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
### `terminate`
|
|
195
|
+
|
|
196
|
+
Terminate a process with `SIGTERM`.
|
|
197
|
+
|
|
198
|
+
## Contributing
|
|
199
|
+
|
|
200
|
+
Contributions are welcome! Please submit pull requests or open issues on the GitHub repository.
|
|
201
|
+
|
|
202
|
+
## License
|
|
203
|
+
|
|
204
|
+
This project is licensed under the [MIT License](LICENSE).
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
# procsock
|
|
2
|
+
|
|
3
|
+
A small tool for running and tracking background processes over a local TCP port.
|
|
4
|
+
|
|
5
|
+
It is **not** a terminal multiplexer and does **not** use PTYs. It launches plain child processes, redirects their stdin/stdout/stderr to files, and lets clients query whether a process is still running or has finished.
|
|
6
|
+
|
|
7
|
+
## Overview
|
|
8
|
+
|
|
9
|
+
`procsock` consists of two pieces:
|
|
10
|
+
|
|
11
|
+
- a **server**
|
|
12
|
+
- a **client** that connects over a local TCP port when the server is available
|
|
13
|
+
|
|
14
|
+
The server accepts JSON-RPC requests to start and inspect processes. Each process is started with:
|
|
15
|
+
|
|
16
|
+
- argv
|
|
17
|
+
- client's current working directory
|
|
18
|
+
- client's current environment
|
|
19
|
+
- optional stdin file
|
|
20
|
+
- optional stdout file
|
|
21
|
+
- optional stderr file
|
|
22
|
+
|
|
23
|
+
When not specified, stdin/stdout/stderr default to `os.devnull`.
|
|
24
|
+
|
|
25
|
+
The launcher returns a process identifier:
|
|
26
|
+
|
|
27
|
+
- on Unix, it is the process ID
|
|
28
|
+
- on Windows/NT, it is the process handle
|
|
29
|
+
|
|
30
|
+
The server keeps the process state in memory only. If the server exits or restarts, all state is lost.
|
|
31
|
+
|
|
32
|
+
## Example session
|
|
33
|
+
|
|
34
|
+
Start server:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
procsock server --port 9000
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Launch process:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
procsock launch \
|
|
44
|
+
--port 9000 \
|
|
45
|
+
--stdin /tmp/in.txt \
|
|
46
|
+
--stdout /tmp/out.txt \
|
|
47
|
+
--stderr /tmp/err.txt \
|
|
48
|
+
-- /usr/bin/python3 -c 'print("hello")'
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
List:
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
procsock list --port 9000
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Terminate:
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
procsock terminate --port 9000 12345
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## How it works
|
|
64
|
+
|
|
65
|
+
1. Start the server in the foreground.
|
|
66
|
+
2. The server binds a local TCP port.
|
|
67
|
+
3. A client connects and sends JSON-RPC commands.
|
|
68
|
+
4. The server launches child processes with `ctypes-unicode-proclaunch`.
|
|
69
|
+
5. Each launched process is started from a dedicated launcher thread.
|
|
70
|
+
6. That launcher thread uses the client's current working directory as the process working directory.
|
|
71
|
+
7. This is implemented with a small synchronized `TempCwd` helper class exposing `__enter__` and `__exit__`.
|
|
72
|
+
8. A waiter thread waits for process completion and updates the in-memory process status on exit.
|
|
73
|
+
9. The client can list the status of all processes or terminate a process.
|
|
74
|
+
10. If the server exits, managed children are terminated, and the in-memory process state is lost.
|
|
75
|
+
|
|
76
|
+
## Commands
|
|
77
|
+
|
|
78
|
+
### `launch`
|
|
79
|
+
|
|
80
|
+
Launch a new child process via `ctypes-unicode-proclaunch`.
|
|
81
|
+
|
|
82
|
+
For the CLI, the launched process inherits the current environment of the `procsock launch` client process.
|
|
83
|
+
|
|
84
|
+
For each launched process:
|
|
85
|
+
|
|
86
|
+
- stdin is opened from the requested file path, typically read-only
|
|
87
|
+
- stdout is opened to the requested file path
|
|
88
|
+
- stderr is opened to the requested file path
|
|
89
|
+
- any unspecified stdin/stdout/stderr path defaults to `os.devnull`
|
|
90
|
+
|
|
91
|
+
Behavior should match normal process redirection semantics:
|
|
92
|
+
|
|
93
|
+
- stdin: if a path is provided, it must already exist
|
|
94
|
+
- stdout: if a path is provided, open with create/truncate behavior
|
|
95
|
+
- stderr: if a path is provided, open with create/truncate behavior
|
|
96
|
+
- parent directories are not created automatically
|
|
97
|
+
|
|
98
|
+
Request:
|
|
99
|
+
|
|
100
|
+
```json
|
|
101
|
+
{
|
|
102
|
+
"jsonrpc": "2.0",
|
|
103
|
+
"id": 1,
|
|
104
|
+
"method": "launch",
|
|
105
|
+
"params": {
|
|
106
|
+
"argv": ["/bin/sh", "-c", "echo hello; sleep 2"],
|
|
107
|
+
"cwd": "/tmp",
|
|
108
|
+
"stdin_path": "/tmp/in.txt",
|
|
109
|
+
"stdout_path": "/tmp/out.txt",
|
|
110
|
+
"stderr_path": "/tmp/err.txt"
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Response:
|
|
116
|
+
|
|
117
|
+
```json
|
|
118
|
+
{
|
|
119
|
+
"jsonrpc": "2.0",
|
|
120
|
+
"id": 1,
|
|
121
|
+
"result": {
|
|
122
|
+
"pid": 12345,
|
|
123
|
+
"argv": ["/bin/sh", "-c", "echo hello; sleep 2"],
|
|
124
|
+
"cwd": "/tmp",
|
|
125
|
+
"stdin_path": "/tmp/in.txt",
|
|
126
|
+
"stdout_path": "/tmp/out.txt",
|
|
127
|
+
"stderr_path": "/tmp/err.txt",
|
|
128
|
+
"finished": false,
|
|
129
|
+
"started_at": 1760000000.0,
|
|
130
|
+
"finished_at": null,
|
|
131
|
+
"exit_code": null
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
### `list`
|
|
137
|
+
|
|
138
|
+
Return the status of all known in-memory processes.
|
|
139
|
+
|
|
140
|
+
Request:
|
|
141
|
+
|
|
142
|
+
```json
|
|
143
|
+
{
|
|
144
|
+
"jsonrpc": "2.0",
|
|
145
|
+
"id": 2,
|
|
146
|
+
"method": "list",
|
|
147
|
+
"params": {}
|
|
148
|
+
}
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
Response:
|
|
152
|
+
|
|
153
|
+
```json
|
|
154
|
+
{
|
|
155
|
+
"jsonrpc": "2.0",
|
|
156
|
+
"id": 2,
|
|
157
|
+
"result": [
|
|
158
|
+
{
|
|
159
|
+
"pid": 12345,
|
|
160
|
+
"argv": ["/bin/sh", "-c", "echo hello; sleep 2"],
|
|
161
|
+
"cwd": "/tmp",
|
|
162
|
+
"stdin_path": "/tmp/in.txt",
|
|
163
|
+
"stdout_path": "/tmp/out.txt",
|
|
164
|
+
"stderr_path": "/tmp/err.txt",
|
|
165
|
+
"finished": false,
|
|
166
|
+
"started_at": 1760000000.0,
|
|
167
|
+
"finished_at": null,
|
|
168
|
+
"exit_code": null
|
|
169
|
+
}
|
|
170
|
+
]
|
|
171
|
+
}
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
### `terminate`
|
|
175
|
+
|
|
176
|
+
Terminate a process with `SIGTERM`.
|
|
177
|
+
|
|
178
|
+
## Contributing
|
|
179
|
+
|
|
180
|
+
Contributions are welcome! Please submit pull requests or open issues on the GitHub repository.
|
|
181
|
+
|
|
182
|
+
## License
|
|
183
|
+
|
|
184
|
+
This project is licensed under the [MIT License](LICENSE).
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: procsock
|
|
3
|
+
Version: 0.1.0a0
|
|
4
|
+
Summary: A small tool for running and tracking background processes over a local TCP port.
|
|
5
|
+
Author-email: Jifeng Wu <jifengwu2k@gmail.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/jifengwu2k/procsock
|
|
8
|
+
Project-URL: Bug Tracker, https://github.com/jifengwu2k/procsock/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: ctypes-unicode-proclaunch
|
|
16
|
+
Requires-Dist: read-unicode-environment-variables-dictionary
|
|
17
|
+
Requires-Dist: send-recv-json
|
|
18
|
+
Requires-Dist: typing; python_version < "3.5"
|
|
19
|
+
Dynamic: license-file
|
|
20
|
+
|
|
21
|
+
# procsock
|
|
22
|
+
|
|
23
|
+
A small tool for running and tracking background processes over a local TCP port.
|
|
24
|
+
|
|
25
|
+
It is **not** a terminal multiplexer and does **not** use PTYs. It launches plain child processes, redirects their stdin/stdout/stderr to files, and lets clients query whether a process is still running or has finished.
|
|
26
|
+
|
|
27
|
+
## Overview
|
|
28
|
+
|
|
29
|
+
`procsock` consists of two pieces:
|
|
30
|
+
|
|
31
|
+
- a **server**
|
|
32
|
+
- a **client** that connects over a local TCP port when the server is available
|
|
33
|
+
|
|
34
|
+
The server accepts JSON-RPC requests to start and inspect processes. Each process is started with:
|
|
35
|
+
|
|
36
|
+
- argv
|
|
37
|
+
- client's current working directory
|
|
38
|
+
- client's current environment
|
|
39
|
+
- optional stdin file
|
|
40
|
+
- optional stdout file
|
|
41
|
+
- optional stderr file
|
|
42
|
+
|
|
43
|
+
When not specified, stdin/stdout/stderr default to `os.devnull`.
|
|
44
|
+
|
|
45
|
+
The launcher returns a process identifier:
|
|
46
|
+
|
|
47
|
+
- on Unix, it is the process ID
|
|
48
|
+
- on Windows/NT, it is the process handle
|
|
49
|
+
|
|
50
|
+
The server keeps the process state in memory only. If the server exits or restarts, all state is lost.
|
|
51
|
+
|
|
52
|
+
## Example session
|
|
53
|
+
|
|
54
|
+
Start server:
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
procsock server --port 9000
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Launch process:
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
procsock launch \
|
|
64
|
+
--port 9000 \
|
|
65
|
+
--stdin /tmp/in.txt \
|
|
66
|
+
--stdout /tmp/out.txt \
|
|
67
|
+
--stderr /tmp/err.txt \
|
|
68
|
+
-- /usr/bin/python3 -c 'print("hello")'
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
List:
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
procsock list --port 9000
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Terminate:
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
procsock terminate --port 9000 12345
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## How it works
|
|
84
|
+
|
|
85
|
+
1. Start the server in the foreground.
|
|
86
|
+
2. The server binds a local TCP port.
|
|
87
|
+
3. A client connects and sends JSON-RPC commands.
|
|
88
|
+
4. The server launches child processes with `ctypes-unicode-proclaunch`.
|
|
89
|
+
5. Each launched process is started from a dedicated launcher thread.
|
|
90
|
+
6. That launcher thread uses the client's current working directory as the process working directory.
|
|
91
|
+
7. This is implemented with a small synchronized `TempCwd` helper class exposing `__enter__` and `__exit__`.
|
|
92
|
+
8. A waiter thread waits for process completion and updates the in-memory process status on exit.
|
|
93
|
+
9. The client can list the status of all processes or terminate a process.
|
|
94
|
+
10. If the server exits, managed children are terminated, and the in-memory process state is lost.
|
|
95
|
+
|
|
96
|
+
## Commands
|
|
97
|
+
|
|
98
|
+
### `launch`
|
|
99
|
+
|
|
100
|
+
Launch a new child process via `ctypes-unicode-proclaunch`.
|
|
101
|
+
|
|
102
|
+
For the CLI, the launched process inherits the current environment of the `procsock launch` client process.
|
|
103
|
+
|
|
104
|
+
For each launched process:
|
|
105
|
+
|
|
106
|
+
- stdin is opened from the requested file path, typically read-only
|
|
107
|
+
- stdout is opened to the requested file path
|
|
108
|
+
- stderr is opened to the requested file path
|
|
109
|
+
- any unspecified stdin/stdout/stderr path defaults to `os.devnull`
|
|
110
|
+
|
|
111
|
+
Behavior should match normal process redirection semantics:
|
|
112
|
+
|
|
113
|
+
- stdin: if a path is provided, it must already exist
|
|
114
|
+
- stdout: if a path is provided, open with create/truncate behavior
|
|
115
|
+
- stderr: if a path is provided, open with create/truncate behavior
|
|
116
|
+
- parent directories are not created automatically
|
|
117
|
+
|
|
118
|
+
Request:
|
|
119
|
+
|
|
120
|
+
```json
|
|
121
|
+
{
|
|
122
|
+
"jsonrpc": "2.0",
|
|
123
|
+
"id": 1,
|
|
124
|
+
"method": "launch",
|
|
125
|
+
"params": {
|
|
126
|
+
"argv": ["/bin/sh", "-c", "echo hello; sleep 2"],
|
|
127
|
+
"cwd": "/tmp",
|
|
128
|
+
"stdin_path": "/tmp/in.txt",
|
|
129
|
+
"stdout_path": "/tmp/out.txt",
|
|
130
|
+
"stderr_path": "/tmp/err.txt"
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Response:
|
|
136
|
+
|
|
137
|
+
```json
|
|
138
|
+
{
|
|
139
|
+
"jsonrpc": "2.0",
|
|
140
|
+
"id": 1,
|
|
141
|
+
"result": {
|
|
142
|
+
"pid": 12345,
|
|
143
|
+
"argv": ["/bin/sh", "-c", "echo hello; sleep 2"],
|
|
144
|
+
"cwd": "/tmp",
|
|
145
|
+
"stdin_path": "/tmp/in.txt",
|
|
146
|
+
"stdout_path": "/tmp/out.txt",
|
|
147
|
+
"stderr_path": "/tmp/err.txt",
|
|
148
|
+
"finished": false,
|
|
149
|
+
"started_at": 1760000000.0,
|
|
150
|
+
"finished_at": null,
|
|
151
|
+
"exit_code": null
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
### `list`
|
|
157
|
+
|
|
158
|
+
Return the status of all known in-memory processes.
|
|
159
|
+
|
|
160
|
+
Request:
|
|
161
|
+
|
|
162
|
+
```json
|
|
163
|
+
{
|
|
164
|
+
"jsonrpc": "2.0",
|
|
165
|
+
"id": 2,
|
|
166
|
+
"method": "list",
|
|
167
|
+
"params": {}
|
|
168
|
+
}
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
Response:
|
|
172
|
+
|
|
173
|
+
```json
|
|
174
|
+
{
|
|
175
|
+
"jsonrpc": "2.0",
|
|
176
|
+
"id": 2,
|
|
177
|
+
"result": [
|
|
178
|
+
{
|
|
179
|
+
"pid": 12345,
|
|
180
|
+
"argv": ["/bin/sh", "-c", "echo hello; sleep 2"],
|
|
181
|
+
"cwd": "/tmp",
|
|
182
|
+
"stdin_path": "/tmp/in.txt",
|
|
183
|
+
"stdout_path": "/tmp/out.txt",
|
|
184
|
+
"stderr_path": "/tmp/err.txt",
|
|
185
|
+
"finished": false,
|
|
186
|
+
"started_at": 1760000000.0,
|
|
187
|
+
"finished_at": null,
|
|
188
|
+
"exit_code": null
|
|
189
|
+
}
|
|
190
|
+
]
|
|
191
|
+
}
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
### `terminate`
|
|
195
|
+
|
|
196
|
+
Terminate a process with `SIGTERM`.
|
|
197
|
+
|
|
198
|
+
## Contributing
|
|
199
|
+
|
|
200
|
+
Contributions are welcome! Please submit pull requests or open issues on the GitHub repository.
|
|
201
|
+
|
|
202
|
+
## License
|
|
203
|
+
|
|
204
|
+
This project is licensed under the [MIT License](LICENSE).
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
procsock.py
|
|
4
|
+
pyproject.toml
|
|
5
|
+
setup.cfg
|
|
6
|
+
procsock.egg-info/PKG-INFO
|
|
7
|
+
procsock.egg-info/SOURCES.txt
|
|
8
|
+
procsock.egg-info/dependency_links.txt
|
|
9
|
+
procsock.egg-info/entry_points.txt
|
|
10
|
+
procsock.egg-info/requires.txt
|
|
11
|
+
procsock.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
procsock
|
|
@@ -0,0 +1,537 @@
|
|
|
1
|
+
# Copyright (c) 2025 Jifeng Wu
|
|
2
|
+
# Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
|
3
|
+
#!/usr/bin/env python
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import json
|
|
7
|
+
import logging
|
|
8
|
+
import os
|
|
9
|
+
import signal
|
|
10
|
+
import socket
|
|
11
|
+
import sys
|
|
12
|
+
import threading
|
|
13
|
+
import time
|
|
14
|
+
from itertools import count
|
|
15
|
+
from typing import Any, Dict, List, Optional, Set, Tuple
|
|
16
|
+
|
|
17
|
+
import ctypes_unicode_proclaunch
|
|
18
|
+
from read_unicode_environment_variables_dictionary import (
|
|
19
|
+
read_unicode_environment_variables_dictionary,
|
|
20
|
+
)
|
|
21
|
+
from send_recv_json import recv_json, send_json
|
|
22
|
+
|
|
23
|
+
JSONRPC_VERSION = "2.0"
|
|
24
|
+
DEFAULT_HOST = "localhost"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class UnknownPidError(Exception):
|
|
28
|
+
pass
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class TempCwd(object):
|
|
32
|
+
__slots__ = ("cwd", "previous_cwd")
|
|
33
|
+
|
|
34
|
+
_lock = threading.RLock()
|
|
35
|
+
|
|
36
|
+
def __init__(self, cwd):
|
|
37
|
+
# type: (str) -> None
|
|
38
|
+
self.cwd = cwd # type: str
|
|
39
|
+
self.previous_cwd = None # type: Optional[str]
|
|
40
|
+
|
|
41
|
+
def __enter__(self):
|
|
42
|
+
# type: () -> "TempCwd"
|
|
43
|
+
self._lock.acquire()
|
|
44
|
+
try:
|
|
45
|
+
self.previous_cwd = os.getcwd()
|
|
46
|
+
os.chdir(self.cwd)
|
|
47
|
+
return self
|
|
48
|
+
except Exception:
|
|
49
|
+
self._lock.release()
|
|
50
|
+
raise
|
|
51
|
+
|
|
52
|
+
def __exit__(self, exc_type, exc, tb):
|
|
53
|
+
# type: (Any, Any, Any) -> None
|
|
54
|
+
try:
|
|
55
|
+
if self.previous_cwd is not None:
|
|
56
|
+
os.chdir(self.previous_cwd)
|
|
57
|
+
finally:
|
|
58
|
+
self._lock.release()
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class Process(object):
|
|
62
|
+
__slots__ = (
|
|
63
|
+
"pid",
|
|
64
|
+
"argv",
|
|
65
|
+
"cwd",
|
|
66
|
+
"stdin_path",
|
|
67
|
+
"stdout_path",
|
|
68
|
+
"stderr_path",
|
|
69
|
+
"finished",
|
|
70
|
+
"started_at",
|
|
71
|
+
"finished_at",
|
|
72
|
+
"exit_code",
|
|
73
|
+
"wait_thread",
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
def __init__(
|
|
77
|
+
self,
|
|
78
|
+
pid,
|
|
79
|
+
argv,
|
|
80
|
+
cwd,
|
|
81
|
+
stdin_path,
|
|
82
|
+
stdout_path,
|
|
83
|
+
stderr_path,
|
|
84
|
+
finished,
|
|
85
|
+
started_at,
|
|
86
|
+
finished_at,
|
|
87
|
+
exit_code,
|
|
88
|
+
wait_thread=None,
|
|
89
|
+
):
|
|
90
|
+
# type: (int, List[str], str, str, str, str, bool, float, Optional[float], Optional[int], Optional[threading.Thread]) -> None
|
|
91
|
+
self.pid = pid # type: int
|
|
92
|
+
self.argv = argv # type: List[str]
|
|
93
|
+
self.cwd = cwd # type: str
|
|
94
|
+
self.stdin_path = stdin_path # type: str
|
|
95
|
+
self.stdout_path = stdout_path # type: str
|
|
96
|
+
self.stderr_path = stderr_path # type: str
|
|
97
|
+
self.finished = finished # type: bool
|
|
98
|
+
self.started_at = started_at # type: float
|
|
99
|
+
self.finished_at = finished_at # type: Optional[float]
|
|
100
|
+
self.exit_code = exit_code # type: Optional[int]
|
|
101
|
+
self.wait_thread = wait_thread # type: Optional[threading.Thread]
|
|
102
|
+
|
|
103
|
+
def to_dict(self):
|
|
104
|
+
# type: () -> Dict[str, Any]
|
|
105
|
+
return {
|
|
106
|
+
"pid": self.pid,
|
|
107
|
+
"argv": self.argv,
|
|
108
|
+
"cwd": self.cwd,
|
|
109
|
+
"stdin_path": self.stdin_path,
|
|
110
|
+
"stdout_path": self.stdout_path,
|
|
111
|
+
"stderr_path": self.stderr_path,
|
|
112
|
+
"finished": self.finished,
|
|
113
|
+
"started_at": self.started_at,
|
|
114
|
+
"finished_at": self.finished_at,
|
|
115
|
+
"exit_code": self.exit_code,
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
class ProcessTable(object):
|
|
120
|
+
__slots__ = ("_lock", "_processes")
|
|
121
|
+
|
|
122
|
+
def __init__(self):
|
|
123
|
+
# type: () -> None
|
|
124
|
+
self._lock = threading.RLock()
|
|
125
|
+
self._processes = {} # type: Dict[int, Process]
|
|
126
|
+
|
|
127
|
+
def add(self, process):
|
|
128
|
+
# type: (Process) -> None
|
|
129
|
+
with self._lock:
|
|
130
|
+
if process.pid in self._processes:
|
|
131
|
+
raise RuntimeError("pid already tracked: {}".format(process.pid))
|
|
132
|
+
self._processes[process.pid] = process
|
|
133
|
+
|
|
134
|
+
def get(self, pid):
|
|
135
|
+
# type: (int) -> Process
|
|
136
|
+
with self._lock:
|
|
137
|
+
try:
|
|
138
|
+
return self._processes[pid]
|
|
139
|
+
except KeyError as exc:
|
|
140
|
+
raise UnknownPidError("unknown pid: {}".format(pid))
|
|
141
|
+
|
|
142
|
+
def list(self):
|
|
143
|
+
# type: () -> List[Process]
|
|
144
|
+
with self._lock:
|
|
145
|
+
return sorted(
|
|
146
|
+
self._processes.values(),
|
|
147
|
+
key=lambda process: (process.started_at, process.pid),
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
def mark_finished(self, pid, exit_code):
|
|
151
|
+
# type: (int, Optional[int]) -> None
|
|
152
|
+
with self._lock:
|
|
153
|
+
process = self._processes.get(pid)
|
|
154
|
+
if process is None:
|
|
155
|
+
return
|
|
156
|
+
process.finished = True
|
|
157
|
+
process.finished_at = time.time()
|
|
158
|
+
process.exit_code = exit_code
|
|
159
|
+
|
|
160
|
+
def unfinished(self):
|
|
161
|
+
# type: () -> List[Process]
|
|
162
|
+
with self._lock:
|
|
163
|
+
return [
|
|
164
|
+
process for process in self._processes.values() if not process.finished
|
|
165
|
+
]
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
class ProcSockServer(object):
|
|
169
|
+
__slots__ = (
|
|
170
|
+
"host",
|
|
171
|
+
"port",
|
|
172
|
+
"processes",
|
|
173
|
+
"stop_event",
|
|
174
|
+
"server_socket",
|
|
175
|
+
"connection_threads",
|
|
176
|
+
"connection_threads_lock",
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
def __init__(self, host, port):
|
|
180
|
+
# type: (str, int) -> None
|
|
181
|
+
self.host = host # type: str
|
|
182
|
+
self.port = port # type: int
|
|
183
|
+
self.processes = ProcessTable()
|
|
184
|
+
self.stop_event = threading.Event()
|
|
185
|
+
self.server_socket = None # type: Optional[socket.socket]
|
|
186
|
+
self.connection_threads = set() # type: Set[threading.Thread]
|
|
187
|
+
self.connection_threads_lock = threading.Lock()
|
|
188
|
+
|
|
189
|
+
def serve_forever(self):
|
|
190
|
+
# type: () -> None
|
|
191
|
+
self.server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
192
|
+
self.server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
193
|
+
self.server_socket.bind((self.host, self.port))
|
|
194
|
+
self.server_socket.listen()
|
|
195
|
+
logging.info("listening on %s:%s", self.host, self.port)
|
|
196
|
+
|
|
197
|
+
self._install_signal_handlers()
|
|
198
|
+
|
|
199
|
+
try:
|
|
200
|
+
while not self.stop_event.is_set():
|
|
201
|
+
try:
|
|
202
|
+
conn, addr = self.server_socket.accept()
|
|
203
|
+
except OSError:
|
|
204
|
+
if self.stop_event.is_set():
|
|
205
|
+
break
|
|
206
|
+
raise
|
|
207
|
+
thread = threading.Thread(
|
|
208
|
+
target=self._handle_connection, args=(conn, addr), daemon=True
|
|
209
|
+
)
|
|
210
|
+
with self.connection_threads_lock:
|
|
211
|
+
self.connection_threads.add(thread)
|
|
212
|
+
thread.start()
|
|
213
|
+
finally:
|
|
214
|
+
self.shutdown()
|
|
215
|
+
|
|
216
|
+
def _install_signal_handlers(self):
|
|
217
|
+
# type: () -> None
|
|
218
|
+
def handler(signum, frame):
|
|
219
|
+
# type: (int, Any) -> None
|
|
220
|
+
logging.info("received signal %s, shutting down", signum)
|
|
221
|
+
self.stop_event.set()
|
|
222
|
+
if self.server_socket is not None:
|
|
223
|
+
try:
|
|
224
|
+
self.server_socket.close()
|
|
225
|
+
except OSError:
|
|
226
|
+
pass
|
|
227
|
+
|
|
228
|
+
for signum in (signal.SIGINT, signal.SIGTERM):
|
|
229
|
+
signal.signal(signum, handler)
|
|
230
|
+
|
|
231
|
+
def shutdown(self):
|
|
232
|
+
# type: () -> None
|
|
233
|
+
if self.stop_event.is_set():
|
|
234
|
+
pass
|
|
235
|
+
else:
|
|
236
|
+
self.stop_event.set()
|
|
237
|
+
if self.server_socket is not None:
|
|
238
|
+
try:
|
|
239
|
+
self.server_socket.close()
|
|
240
|
+
except OSError:
|
|
241
|
+
pass
|
|
242
|
+
self.server_socket = None
|
|
243
|
+
|
|
244
|
+
unfinished = self.processes.unfinished()
|
|
245
|
+
for process in unfinished:
|
|
246
|
+
try:
|
|
247
|
+
ctypes_unicode_proclaunch.terminate(process.pid)
|
|
248
|
+
except Exception as exc: # noqa: BLE001
|
|
249
|
+
logging.warning("failed to terminate pid=%s: %s", process.pid, exc)
|
|
250
|
+
|
|
251
|
+
for process in unfinished:
|
|
252
|
+
thread = process.wait_thread
|
|
253
|
+
if thread is None:
|
|
254
|
+
continue
|
|
255
|
+
thread.join()
|
|
256
|
+
|
|
257
|
+
def _handle_connection(self, conn, addr):
|
|
258
|
+
# type: (socket.socket, Tuple[str, int]) -> None
|
|
259
|
+
request = None # type: Any
|
|
260
|
+
try:
|
|
261
|
+
with conn:
|
|
262
|
+
try:
|
|
263
|
+
request = recv_json(conn.recv)
|
|
264
|
+
response = self._dispatch_request(request)
|
|
265
|
+
except Exception as exc: # noqa: BLE001
|
|
266
|
+
logging.warning(
|
|
267
|
+
"request handling failed from %s: %s: %s",
|
|
268
|
+
addr,
|
|
269
|
+
type(exc).__name__,
|
|
270
|
+
exc,
|
|
271
|
+
)
|
|
272
|
+
response = make_error_response(
|
|
273
|
+
request_id=(
|
|
274
|
+
request.get("id") if isinstance(request, dict) else None
|
|
275
|
+
),
|
|
276
|
+
exc=exc,
|
|
277
|
+
)
|
|
278
|
+
send_json(conn.send, response)
|
|
279
|
+
finally:
|
|
280
|
+
with self.connection_threads_lock:
|
|
281
|
+
thread = threading.current_thread()
|
|
282
|
+
self.connection_threads.discard(thread)
|
|
283
|
+
|
|
284
|
+
def _dispatch_request(self, request):
|
|
285
|
+
# type: (Any) -> Dict[str, Any]
|
|
286
|
+
request_id = request["id"]
|
|
287
|
+
method = request["method"]
|
|
288
|
+
params = request.get("params", {})
|
|
289
|
+
|
|
290
|
+
if method == "launch":
|
|
291
|
+
result = self.launch(
|
|
292
|
+
params["argv"],
|
|
293
|
+
params["cwd"],
|
|
294
|
+
params.get("stdin_path", os.devnull),
|
|
295
|
+
params.get("stdout_path", os.devnull),
|
|
296
|
+
params.get("stderr_path", os.devnull),
|
|
297
|
+
params.get("env"),
|
|
298
|
+
)
|
|
299
|
+
elif method == "list":
|
|
300
|
+
result = self.list_processes()
|
|
301
|
+
elif method == "terminate":
|
|
302
|
+
result = self.terminate(params["pid"])
|
|
303
|
+
else:
|
|
304
|
+
raise ValueError("unknown method: {}".format(method))
|
|
305
|
+
|
|
306
|
+
return {"jsonrpc": JSONRPC_VERSION, "id": request_id, "result": result}
|
|
307
|
+
|
|
308
|
+
def launch(self, argv, cwd, stdin_path, stdout_path, stderr_path, env):
|
|
309
|
+
# type: (List[str], str, str, str, str, Optional[Dict[str, str]]) -> Dict[str, Any]
|
|
310
|
+
ready = threading.Event()
|
|
311
|
+
outcome = {} # type: Dict[str, Any]
|
|
312
|
+
|
|
313
|
+
def launcher_target():
|
|
314
|
+
# type: () -> None
|
|
315
|
+
try:
|
|
316
|
+
pid = self._launch_process(
|
|
317
|
+
argv,
|
|
318
|
+
cwd,
|
|
319
|
+
stdin_path,
|
|
320
|
+
stdout_path,
|
|
321
|
+
stderr_path,
|
|
322
|
+
env,
|
|
323
|
+
)
|
|
324
|
+
outcome["pid"] = pid
|
|
325
|
+
except Exception as exc: # noqa: BLE001
|
|
326
|
+
outcome["exception"] = exc
|
|
327
|
+
finally:
|
|
328
|
+
ready.set()
|
|
329
|
+
|
|
330
|
+
launcher_thread = threading.Thread(target=launcher_target, daemon=True)
|
|
331
|
+
launcher_thread.start()
|
|
332
|
+
ready.wait()
|
|
333
|
+
|
|
334
|
+
if "exception" in outcome:
|
|
335
|
+
raise outcome["exception"]
|
|
336
|
+
|
|
337
|
+
process = self.processes.get(outcome["pid"])
|
|
338
|
+
return process.to_dict()
|
|
339
|
+
|
|
340
|
+
def _launch_process(self, argv, cwd, stdin_path, stdout_path, stderr_path, env):
|
|
341
|
+
# type: (List[str], str, str, str, str, Optional[Dict[str, str]]) -> int
|
|
342
|
+
stdin_file = None # type: Any
|
|
343
|
+
stdout_file = None # type: Any
|
|
344
|
+
stderr_file = None # type: Any
|
|
345
|
+
try:
|
|
346
|
+
stdin_file = open(stdin_path, "rb")
|
|
347
|
+
stdout_file = open(stdout_path, "wb")
|
|
348
|
+
stderr_file = open(stderr_path, "wb")
|
|
349
|
+
|
|
350
|
+
with TempCwd(cwd):
|
|
351
|
+
raw_pid = ctypes_unicode_proclaunch.launch(
|
|
352
|
+
argv,
|
|
353
|
+
environment=env,
|
|
354
|
+
stdin_file_descriptor=stdin_file.fileno(),
|
|
355
|
+
stdout_file_descriptor=stdout_file.fileno(),
|
|
356
|
+
stderr_file_descriptor=stderr_file.fileno(),
|
|
357
|
+
)
|
|
358
|
+
|
|
359
|
+
pid = int(raw_pid)
|
|
360
|
+
started_at = time.time()
|
|
361
|
+
process = Process(
|
|
362
|
+
pid=pid,
|
|
363
|
+
argv=list(argv),
|
|
364
|
+
cwd=cwd,
|
|
365
|
+
stdin_path=stdin_path,
|
|
366
|
+
stdout_path=stdout_path,
|
|
367
|
+
stderr_path=stderr_path,
|
|
368
|
+
finished=False,
|
|
369
|
+
started_at=started_at,
|
|
370
|
+
finished_at=None,
|
|
371
|
+
exit_code=None,
|
|
372
|
+
)
|
|
373
|
+
wait_thread = threading.Thread(
|
|
374
|
+
target=self._wait_for_process,
|
|
375
|
+
args=(pid,),
|
|
376
|
+
daemon=True,
|
|
377
|
+
name="wait-pid-{}".format(pid),
|
|
378
|
+
)
|
|
379
|
+
process.wait_thread = wait_thread
|
|
380
|
+
self.processes.add(process)
|
|
381
|
+
wait_thread.start()
|
|
382
|
+
logging.info("launched pid=%s argv=%r cwd=%s", pid, argv, cwd)
|
|
383
|
+
return pid
|
|
384
|
+
finally:
|
|
385
|
+
for file_obj in (stdin_file, stdout_file, stderr_file):
|
|
386
|
+
if file_obj is not None:
|
|
387
|
+
file_obj.close()
|
|
388
|
+
|
|
389
|
+
def _wait_for_process(self, pid):
|
|
390
|
+
# type: (int) -> None
|
|
391
|
+
try:
|
|
392
|
+
exit_code = int(ctypes_unicode_proclaunch.wait(pid))
|
|
393
|
+
self.processes.mark_finished(pid, exit_code)
|
|
394
|
+
logging.info("process finished pid=%s exit_code=%s", pid, exit_code)
|
|
395
|
+
except Exception as exc: # noqa: BLE001
|
|
396
|
+
logging.exception("wait failed for pid=%s", pid)
|
|
397
|
+
self.processes.mark_finished(pid, None)
|
|
398
|
+
|
|
399
|
+
def list_processes(self):
|
|
400
|
+
# type: () -> List[Dict[str, Any]]
|
|
401
|
+
return [process.to_dict() for process in self.processes.list()]
|
|
402
|
+
|
|
403
|
+
def terminate(self, pid):
|
|
404
|
+
# type: (int) -> Dict[str, Any]
|
|
405
|
+
process = self.processes.get(pid)
|
|
406
|
+
if not process.finished:
|
|
407
|
+
ctypes_unicode_proclaunch.terminate(process.pid)
|
|
408
|
+
logging.info("terminate requested pid=%s", process.pid)
|
|
409
|
+
return process.to_dict()
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
def make_error_response(request_id, exc):
|
|
413
|
+
# type: (Any, Exception) -> Dict[str, Any]
|
|
414
|
+
return {
|
|
415
|
+
"jsonrpc": JSONRPC_VERSION,
|
|
416
|
+
"id": request_id,
|
|
417
|
+
"error": {
|
|
418
|
+
"type": type(exc).__name__,
|
|
419
|
+
"message": str(exc),
|
|
420
|
+
},
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
_REQUEST_IDS = count(1)
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
def send_jsonrpc_request(host, port, method, params):
|
|
428
|
+
# type: (str, int, str, Dict[str, Any]) -> Any
|
|
429
|
+
request = {
|
|
430
|
+
"jsonrpc": JSONRPC_VERSION,
|
|
431
|
+
"id": next(_REQUEST_IDS),
|
|
432
|
+
"method": method,
|
|
433
|
+
"params": params,
|
|
434
|
+
}
|
|
435
|
+
with socket.create_connection((host, port)) as conn:
|
|
436
|
+
send_json(conn.send, request)
|
|
437
|
+
response = recv_json(conn.recv)
|
|
438
|
+
if "error" in response:
|
|
439
|
+
error = response["error"]
|
|
440
|
+
error_type = error.get("type", "Exception")
|
|
441
|
+
error_message = error.get("message", "")
|
|
442
|
+
raise RuntimeError("{}: {}".format(error_type, error_message))
|
|
443
|
+
return response["result"]
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
def request_launch(host, port, argv, cwd, stdin_path, stdout_path, stderr_path, env):
|
|
447
|
+
# type: (str, int, List[str], str, str, str, str, Dict[str, str]) -> Any
|
|
448
|
+
return send_jsonrpc_request(
|
|
449
|
+
host,
|
|
450
|
+
port,
|
|
451
|
+
"launch",
|
|
452
|
+
{
|
|
453
|
+
"argv": argv,
|
|
454
|
+
"cwd": cwd,
|
|
455
|
+
"stdin_path": stdin_path,
|
|
456
|
+
"stdout_path": stdout_path,
|
|
457
|
+
"stderr_path": stderr_path,
|
|
458
|
+
"env": env,
|
|
459
|
+
},
|
|
460
|
+
)
|
|
461
|
+
|
|
462
|
+
|
|
463
|
+
def request_list(host, port):
|
|
464
|
+
# type: (str, int) -> Any
|
|
465
|
+
return send_jsonrpc_request(host, port, "list", {})
|
|
466
|
+
|
|
467
|
+
|
|
468
|
+
def request_terminate(host, port, pid):
|
|
469
|
+
# type: (str, int, int) -> Any
|
|
470
|
+
return send_jsonrpc_request(host, port, "terminate", {"pid": pid})
|
|
471
|
+
|
|
472
|
+
|
|
473
|
+
def main(argv=None):
|
|
474
|
+
# type: (Optional[List[str]]) -> int
|
|
475
|
+
logging.basicConfig(
|
|
476
|
+
level=logging.INFO,
|
|
477
|
+
format="%(asctime)s %(levelname)s %(name)s: %(message)s",
|
|
478
|
+
)
|
|
479
|
+
|
|
480
|
+
parser = argparse.ArgumentParser(prog="procsock")
|
|
481
|
+
subparsers = parser.add_subparsers(dest="command")
|
|
482
|
+
|
|
483
|
+
server_parser = subparsers.add_parser("server", help="run the procsock server")
|
|
484
|
+
server_parser.add_argument("--host", default=DEFAULT_HOST)
|
|
485
|
+
server_parser.add_argument("--port", type=int, required=True)
|
|
486
|
+
|
|
487
|
+
launch_parser = subparsers.add_parser("launch", help="launch a process")
|
|
488
|
+
launch_parser.add_argument("--host", default=DEFAULT_HOST)
|
|
489
|
+
launch_parser.add_argument("--port", type=int, required=True)
|
|
490
|
+
launch_parser.add_argument("--cwd", default=None)
|
|
491
|
+
launch_parser.add_argument("--stdin", dest="stdin_path", default=os.devnull)
|
|
492
|
+
launch_parser.add_argument("--stdout", dest="stdout_path", default=os.devnull)
|
|
493
|
+
launch_parser.add_argument("--stderr", dest="stderr_path", default=os.devnull)
|
|
494
|
+
launch_parser.add_argument("argv", nargs=argparse.REMAINDER)
|
|
495
|
+
|
|
496
|
+
list_parser = subparsers.add_parser("list", help="list tracked processes")
|
|
497
|
+
list_parser.add_argument("--host", default=DEFAULT_HOST)
|
|
498
|
+
list_parser.add_argument("--port", type=int, required=True)
|
|
499
|
+
|
|
500
|
+
terminate_parser = subparsers.add_parser("terminate", help="terminate a process")
|
|
501
|
+
terminate_parser.add_argument("--host", default=DEFAULT_HOST)
|
|
502
|
+
terminate_parser.add_argument("--port", type=int, required=True)
|
|
503
|
+
terminate_parser.add_argument("pid", type=int)
|
|
504
|
+
args = parser.parse_args(argv)
|
|
505
|
+
|
|
506
|
+
if args.command == "server":
|
|
507
|
+
server = ProcSockServer(host=args.host, port=args.port)
|
|
508
|
+
server.serve_forever()
|
|
509
|
+
|
|
510
|
+
elif args.command == "launch":
|
|
511
|
+
launch_argv = args.argv[args.argv.index("--") + 1 :]
|
|
512
|
+
if not launch_argv:
|
|
513
|
+
raise ValueError("missing command after '--'")
|
|
514
|
+
|
|
515
|
+
result = request_launch(
|
|
516
|
+
args.host,
|
|
517
|
+
args.port,
|
|
518
|
+
launch_argv,
|
|
519
|
+
args.cwd if args.cwd is not None else os.getcwd(),
|
|
520
|
+
args.stdin_path,
|
|
521
|
+
args.stdout_path,
|
|
522
|
+
args.stderr_path,
|
|
523
|
+
read_unicode_environment_variables_dictionary(),
|
|
524
|
+
)
|
|
525
|
+
print(json.dumps(result, ensure_ascii=False, indent=2))
|
|
526
|
+
|
|
527
|
+
elif args.command == "list":
|
|
528
|
+
result = request_list(args.host, args.port)
|
|
529
|
+
print(json.dumps(result, ensure_ascii=False, indent=2))
|
|
530
|
+
|
|
531
|
+
elif args.command == "terminate":
|
|
532
|
+
result = request_terminate(args.host, args.port, args.pid)
|
|
533
|
+
print(json.dumps(result, ensure_ascii=False, indent=2))
|
|
534
|
+
|
|
535
|
+
|
|
536
|
+
if __name__ == "__main__":
|
|
537
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "procsock"
|
|
7
|
+
version = "0.1.0a0"
|
|
8
|
+
description = "A small tool for running and tracking background processes over a local TCP port."
|
|
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
|
+
"ctypes-unicode-proclaunch",
|
|
22
|
+
"read-unicode-environment-variables-dictionary",
|
|
23
|
+
"send-recv-json",
|
|
24
|
+
"typing; python_version < '3.5'"
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
[project.scripts]
|
|
28
|
+
procsock = "procsock:main"
|
|
29
|
+
|
|
30
|
+
[project.urls]
|
|
31
|
+
"Homepage" = "https://github.com/jifengwu2k/procsock"
|
|
32
|
+
"Bug Tracker" = "https://github.com/jifengwu2k/procsock/issues"
|