Skip to content

Commit 1260ee6

Browse files
committed
Merge pull request godotengine#89206 from bruvzg/pipe_api
Implement pipe API for executed processes IO redirection.
2 parents d2f9245 + 082b420 commit 1260ee6

File tree

16 files changed

+790
-11
lines changed

16 files changed

+790
-11
lines changed

core/core_bind.cpp

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -283,8 +283,8 @@ String OS::read_string_from_stdin() {
283283

284284
int OS::execute(const String &p_path, const Vector<String> &p_arguments, Array r_output, bool p_read_stderr, bool p_open_console) {
285285
List<String> args;
286-
for (int i = 0; i < p_arguments.size(); i++) {
287-
args.push_back(p_arguments[i]);
286+
for (const String &arg : p_arguments) {
287+
args.push_back(arg);
288288
}
289289
String pipe;
290290
int exitcode = 0;
@@ -296,10 +296,18 @@ int OS::execute(const String &p_path, const Vector<String> &p_arguments, Array r
296296
return exitcode;
297297
}
298298

299+
Dictionary OS::execute_with_pipe(const String &p_path, const Vector<String> &p_arguments) {
300+
List<String> args;
301+
for (const String &arg : p_arguments) {
302+
args.push_back(arg);
303+
}
304+
return ::OS::get_singleton()->execute_with_pipe(p_path, args);
305+
}
306+
299307
int OS::create_instance(const Vector<String> &p_arguments) {
300308
List<String> args;
301-
for (int i = 0; i < p_arguments.size(); i++) {
302-
args.push_back(p_arguments[i]);
309+
for (const String &arg : p_arguments) {
310+
args.push_back(arg);
303311
}
304312
::OS::ProcessID pid = 0;
305313
Error err = ::OS::get_singleton()->create_instance(args, &pid);
@@ -311,8 +319,8 @@ int OS::create_instance(const Vector<String> &p_arguments) {
311319

312320
int OS::create_process(const String &p_path, const Vector<String> &p_arguments, bool p_open_console) {
313321
List<String> args;
314-
for (int i = 0; i < p_arguments.size(); i++) {
315-
args.push_back(p_arguments[i]);
322+
for (const String &arg : p_arguments) {
323+
args.push_back(arg);
316324
}
317325
::OS::ProcessID pid = 0;
318326
Error err = ::OS::get_singleton()->create_process(p_path, args, &pid, p_open_console);
@@ -587,6 +595,7 @@ void OS::_bind_methods() {
587595
ClassDB::bind_method(D_METHOD("get_executable_path"), &OS::get_executable_path);
588596
ClassDB::bind_method(D_METHOD("read_string_from_stdin"), &OS::read_string_from_stdin);
589597
ClassDB::bind_method(D_METHOD("execute", "path", "arguments", "output", "read_stderr", "open_console"), &OS::execute, DEFVAL(Array()), DEFVAL(false), DEFVAL(false));
598+
ClassDB::bind_method(D_METHOD("execute_with_pipe", "path", "arguments"), &OS::execute_with_pipe);
590599
ClassDB::bind_method(D_METHOD("create_process", "path", "arguments", "open_console"), &OS::create_process, DEFVAL(false));
591600
ClassDB::bind_method(D_METHOD("create_instance", "arguments"), &OS::create_instance);
592601
ClassDB::bind_method(D_METHOD("kill", "pid"), &OS::kill);

core/core_bind.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,7 @@ class OS : public Object {
156156
String get_executable_path() const;
157157
String read_string_from_stdin();
158158
int execute(const String &p_path, const Vector<String> &p_arguments, Array r_output = Array(), bool p_read_stderr = false, bool p_open_console = false);
159+
Dictionary execute_with_pipe(const String &p_path, const Vector<String> &p_arguments);
159160
int create_process(const String &p_path, const Vector<String> &p_arguments, bool p_open_console = false);
160161
int create_instance(const Vector<String> &p_arguments);
161162
Error kill(int p_pid);

core/io/file_access.cpp

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ thread_local Error FileAccess::last_file_open_error = OK;
4747

4848
Ref<FileAccess> FileAccess::create(AccessType p_access) {
4949
ERR_FAIL_INDEX_V(p_access, ACCESS_MAX, nullptr);
50+
ERR_FAIL_NULL_V(create_func[p_access], nullptr);
5051

5152
Ref<FileAccess> ret = create_func[p_access]();
5253
ret->_set_access_type(p_access);
@@ -75,7 +76,8 @@ Ref<FileAccess> FileAccess::create_for_path(const String &p_path) {
7576
ret = create(ACCESS_RESOURCES);
7677
} else if (p_path.begins_with("user://")) {
7778
ret = create(ACCESS_USERDATA);
78-
79+
} else if (p_path.begins_with("pipe://")) {
80+
ret = create(ACCESS_PIPE);
7981
} else {
8082
ret = create(ACCESS_FILESYSTEM);
8183
}
@@ -209,6 +211,9 @@ String FileAccess::fix_path(const String &p_path) const {
209211
}
210212

211213
} break;
214+
case ACCESS_PIPE: {
215+
return r_path;
216+
} break;
212217
case ACCESS_FILESYSTEM: {
213218
return r_path;
214219
} break;

core/io/file_access.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ class FileAccess : public RefCounted {
5050
ACCESS_RESOURCES,
5151
ACCESS_USERDATA,
5252
ACCESS_FILESYSTEM,
53+
ACCESS_PIPE,
5354
ACCESS_MAX
5455
};
5556

core/os/os.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,7 @@ class OS {
170170
virtual Vector<String> get_system_font_path_for_text(const String &p_font_name, const String &p_text, const String &p_locale = String(), const String &p_script = String(), int p_weight = 400, int p_stretch = 100, bool p_italic = false) const { return Vector<String>(); };
171171
virtual String get_executable_path() const;
172172
virtual Error execute(const String &p_path, const List<String> &p_arguments, String *r_pipe = nullptr, int *r_exitcode = nullptr, bool read_stderr = false, Mutex *p_pipe_mutex = nullptr, bool p_open_console = false) = 0;
173+
virtual Dictionary execute_with_pipe(const String &p_path, const List<String> &p_arguments) { return Dictionary(); }
173174
virtual Error create_process(const String &p_path, const List<String> &p_arguments, ProcessID *r_child_id = nullptr, bool p_open_console = false) = 0;
174175
virtual Error create_instance(const List<String> &p_arguments, ProcessID *r_child_id = nullptr) { return create_process(get_executable_path(), p_arguments, r_child_id); };
175176
virtual Error kill(const ProcessID &p_pid) = 0;

doc/classes/OS.xml

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -50,9 +50,9 @@
5050
<param index="1" name="arguments" type="PackedStringArray" />
5151
<param index="2" name="open_console" type="bool" default="false" />
5252
<description>
53-
Creates a new process that runs independently of Godot. It will not terminate when Godot terminates. The path specified in [param path] must exist and be executable file or macOS .app bundle. Platform path resolution will be used. The [param arguments] are used in the given order and separated by a space.
53+
Creates a new process that runs independently of Godot. It will not terminate when Godot terminates. The path specified in [param path] must exist and be an executable file or macOS [code].app[/code] bundle. The path is resolved based on the current platform. The [param arguments] are used in the given order and separated by a space.
5454
On Windows, if [param open_console] is [code]true[/code] and the process is a console app, a new terminal window will be opened.
55-
If the process is successfully created, this method returns its process ID, which you can use to monitor the process (and potentially terminate it with [method kill]). Otherwise this method returns [code]-1[/code].
55+
If the process is successfully created, this method returns its process ID, which you can use to monitor the process (and potentially terminate it with [method kill]). Otherwise, this method returns [code]-1[/code].
5656
For example, running another instance of the project:
5757
[codeblocks]
5858
[gdscript]
@@ -63,7 +63,7 @@
6363
[/csharp]
6464
[/codeblocks]
6565
See [method execute] if you wish to run an external command and retrieve the results.
66-
[b]Note:[/b] This method is implemented on Android, iOS, Linux, macOS and Windows.
66+
[b]Note:[/b] This method is implemented on Android, Linux, macOS, and Windows.
6767
[b]Note:[/b] On macOS, sandboxed applications are limited to run only embedded helper executables, specified during export or system .app bundle, system .app bundles will ignore arguments.
6868
</description>
6969
</method>
@@ -120,14 +120,31 @@
120120
OS.Execute("CMD.exe", new string[] {"/C", "cd %TEMP% &amp;&amp; dir"}, output);
121121
[/csharp]
122122
[/codeblocks]
123-
[b]Note:[/b] This method is implemented on Android, iOS, Linux, macOS and Windows.
123+
[b]Note:[/b] This method is implemented on Android, Linux, macOS, and Windows.
124124
[b]Note:[/b] To execute a Windows command interpreter built-in command, specify [code]cmd.exe[/code] in [param path], [code]/c[/code] as the first argument, and the desired command as the second argument.
125125
[b]Note:[/b] To execute a PowerShell built-in command, specify [code]powershell.exe[/code] in [param path], [code]-Command[/code] as the first argument, and the desired command as the second argument.
126126
[b]Note:[/b] To execute a Unix shell built-in command, specify shell executable name in [param path], [code]-c[/code] as the first argument, and the desired command as the second argument.
127127
[b]Note:[/b] On macOS, sandboxed applications are limited to run only embedded helper executables, specified during export.
128128
[b]Note:[/b] On Android, system commands such as [code]dumpsys[/code] can only be run on a rooted device.
129129
</description>
130130
</method>
131+
<method name="execute_with_pipe">
132+
<return type="Dictionary" />
133+
<param index="0" name="path" type="String" />
134+
<param index="1" name="arguments" type="PackedStringArray" />
135+
<description>
136+
Creates a new process that runs independently of Godot with redirected IO. It will not terminate when Godot terminates. The path specified in [param path] must exist and be an executable file or macOS [code].app[/code] bundle. The path is resolved based on the current platform. The [param arguments] are used in the given order and separated by a space.
137+
If the process cannot be created, this method returns an empty [Dictionary]. Otherwise, this method returns a [Dictionary] with the following keys:
138+
- [code]"stdio"[/code] - [FileAccess] to access the process stdin and stdout pipes (read/write).
139+
- [code]"stderr"[/code] - [FileAccess] to access the process stderr pipe (read only).
140+
- [code]"pid"[/code] - Process ID as an [int], which you can use to monitor the process (and potentially terminate it with [method kill]).
141+
[b]Note:[/b] This method is implemented on Android, Linux, macOS, and Windows.
142+
[b]Note:[/b] To execute a Windows command interpreter built-in command, specify [code]cmd.exe[/code] in [param path], [code]/c[/code] as the first argument, and the desired command as the second argument.
143+
[b]Note:[/b] To execute a PowerShell built-in command, specify [code]powershell.exe[/code] in [param path], [code]-Command[/code] as the first argument, and the desired command as the second argument.
144+
[b]Note:[/b] To execute a Unix shell built-in command, specify shell executable name in [param path], [code]-c[/code] as the first argument, and the desired command as the second argument.
145+
[b]Note:[/b] On macOS, sandboxed applications are limited to run only embedded helper executables, specified during export or system .app bundle, system .app bundles will ignore arguments.
146+
</description>
147+
</method>
131148
<method name="find_keycode_from_string" qualifiers="const">
132149
<return type="int" enum="Key" />
133150
<param index="0" name="string" type="String" />
Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
/**************************************************************************/
2+
/* file_access_unix_pipe.cpp */
3+
/**************************************************************************/
4+
/* This file is part of: */
5+
/* GODOT ENGINE */
6+
/* https://godotengine.org */
7+
/**************************************************************************/
8+
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
9+
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
10+
/* */
11+
/* Permission is hereby granted, free of charge, to any person obtaining */
12+
/* a copy of this software and associated documentation files (the */
13+
/* "Software"), to deal in the Software without restriction, including */
14+
/* without limitation the rights to use, copy, modify, merge, publish, */
15+
/* distribute, sublicense, and/or sell copies of the Software, and to */
16+
/* permit persons to whom the Software is furnished to do so, subject to */
17+
/* the following conditions: */
18+
/* */
19+
/* The above copyright notice and this permission notice shall be */
20+
/* included in all copies or substantial portions of the Software. */
21+
/* */
22+
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
23+
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
24+
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
25+
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
26+
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
27+
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
28+
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
29+
/**************************************************************************/
30+
31+
#include "file_access_unix_pipe.h"
32+
33+
#if defined(UNIX_ENABLED)
34+
35+
#include "core/os/os.h"
36+
#include "core/string/print_string.h"
37+
38+
#include <errno.h>
39+
#include <fcntl.h>
40+
#include <sys/stat.h>
41+
#include <sys/types.h>
42+
#include <unistd.h>
43+
44+
Error FileAccessUnixPipe::open_existing(int p_rfd, int p_wfd) {
45+
// Open pipe using handles created by pipe(fd) call in the OS.execute_with_pipe.
46+
_close();
47+
48+
path_src = String();
49+
unlink_on_close = false;
50+
ERR_FAIL_COND_V_MSG(fd[0] >= 0 || fd[1] >= 0, ERR_ALREADY_IN_USE, "Pipe is already in use.");
51+
fd[0] = p_rfd;
52+
fd[1] = p_wfd;
53+
54+
last_error = OK;
55+
return OK;
56+
}
57+
58+
Error FileAccessUnixPipe::open_internal(const String &p_path, int p_mode_flags) {
59+
_close();
60+
61+
path_src = p_path;
62+
ERR_FAIL_COND_V_MSG(fd[0] >= 0 || fd[1] >= 0, ERR_ALREADY_IN_USE, "Pipe is already in use.");
63+
64+
path = String("/tmp/") + p_path.replace("pipe://", "").replace("/", "_");
65+
struct stat st = {};
66+
int err = stat(path.utf8().get_data(), &st);
67+
if (err) {
68+
if (mkfifo(path.utf8().get_data(), 0666) != 0) {
69+
last_error = ERR_FILE_CANT_OPEN;
70+
return last_error;
71+
}
72+
unlink_on_close = true;
73+
} else {
74+
ERR_FAIL_COND_V_MSG(!S_ISFIFO(st.st_mode), ERR_ALREADY_IN_USE, "Pipe name is already used by file.");
75+
}
76+
77+
int f = ::open(path.utf8().get_data(), O_RDWR | O_CLOEXEC);
78+
if (f < 0) {
79+
switch (errno) {
80+
case ENOENT: {
81+
last_error = ERR_FILE_NOT_FOUND;
82+
} break;
83+
default: {
84+
last_error = ERR_FILE_CANT_OPEN;
85+
} break;
86+
}
87+
return last_error;
88+
}
89+
90+
// Set close on exec to avoid leaking it to subprocesses.
91+
fd[0] = f;
92+
fd[1] = f;
93+
94+
last_error = OK;
95+
return OK;
96+
}
97+
98+
void FileAccessUnixPipe::_close() {
99+
if (fd[0] < 0) {
100+
return;
101+
}
102+
103+
if (fd[1] != fd[0]) {
104+
::close(fd[1]);
105+
}
106+
::close(fd[0]);
107+
fd[0] = -1;
108+
fd[1] = -1;
109+
110+
if (unlink_on_close) {
111+
::unlink(path.utf8().ptr());
112+
}
113+
unlink_on_close = false;
114+
}
115+
116+
bool FileAccessUnixPipe::is_open() const {
117+
return (fd[0] >= 0 || fd[1] >= 0);
118+
}
119+
120+
String FileAccessUnixPipe::get_path() const {
121+
return path_src;
122+
}
123+
124+
String FileAccessUnixPipe::get_path_absolute() const {
125+
return path_src;
126+
}
127+
128+
uint8_t FileAccessUnixPipe::get_8() const {
129+
ERR_FAIL_COND_V_MSG(fd[0] < 0, 0, "Pipe must be opened before use.");
130+
131+
uint8_t b;
132+
if (::read(fd[0], &b, 1) == 0) {
133+
last_error = ERR_FILE_CANT_READ;
134+
b = '\0';
135+
} else {
136+
last_error = OK;
137+
}
138+
return b;
139+
}
140+
141+
uint64_t FileAccessUnixPipe::get_buffer(uint8_t *p_dst, uint64_t p_length) const {
142+
ERR_FAIL_COND_V(!p_dst && p_length > 0, -1);
143+
ERR_FAIL_COND_V_MSG(fd[0] < 0, -1, "Pipe must be opened before use.");
144+
145+
uint64_t read = ::read(fd[0], p_dst, p_length);
146+
if (read == p_length) {
147+
last_error = ERR_FILE_CANT_READ;
148+
} else {
149+
last_error = OK;
150+
}
151+
return read;
152+
}
153+
154+
Error FileAccessUnixPipe::get_error() const {
155+
return last_error;
156+
}
157+
158+
void FileAccessUnixPipe::store_8(uint8_t p_src) {
159+
ERR_FAIL_COND_MSG(fd[1] < 0, "Pipe must be opened before use.");
160+
if (::write(fd[1], &p_src, 1) != 1) {
161+
last_error = ERR_FILE_CANT_WRITE;
162+
} else {
163+
last_error = OK;
164+
}
165+
}
166+
167+
void FileAccessUnixPipe::store_buffer(const uint8_t *p_src, uint64_t p_length) {
168+
ERR_FAIL_COND_MSG(fd[1] < 0, "Pipe must be opened before use.");
169+
ERR_FAIL_COND(!p_src && p_length > 0);
170+
if (::write(fd[1], p_src, p_length) != (ssize_t)p_length) {
171+
last_error = ERR_FILE_CANT_WRITE;
172+
} else {
173+
last_error = OK;
174+
}
175+
}
176+
177+
void FileAccessUnixPipe::close() {
178+
_close();
179+
}
180+
181+
FileAccessUnixPipe::~FileAccessUnixPipe() {
182+
_close();
183+
}
184+
185+
#endif // UNIX_ENABLED

0 commit comments

Comments
 (0)