Skip to content

Commit 80868e6

Browse files
committed
subprocess_run timeout mutually exclusive outpipe
1 parent 91704f0 commit 80868e6

File tree

6 files changed

+113
-13
lines changed

6 files changed

+113
-13
lines changed

+stdlib/subprocess_run.m

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,9 @@
1111
% * cmd_array: vector of string to compose a command line
1212
% * opt.env: environment variable struct to set
1313
% * opt.cwd: working directory to use while running command
14-
% * opt.stdin: string to pass to subprocess stdin pipe
14+
% * opt.stdin: string to pass to subprocess stdin pipe - OK to use with timeout
1515
% * opt.timeout: time to wait for process to complete before erroring (seconds)
16+
% * opt.outpipe: logical to indicate whether to use pipe for stdout and stderr (mutually exclusive with timeout option)
1617
%%% Outputs
1718
% * status: 0 is generally success. -1 if timeout. Other codes as per the
1819
% program / command run
@@ -37,6 +38,11 @@
3738
opt.cwd (1,1) string = ""
3839
opt.stdin (1,1) string = ""
3940
opt.timeout (1,1) int64 = 0
41+
opt.outpipe (1,1) logical = true
42+
end
43+
44+
if opt.outpipe && opt.timeout > 0
45+
error("outpipe and timeout options are mutually exclusive")
4046
end
4147

4248
%% process instantiation
@@ -81,9 +87,16 @@
8187
end
8288

8389
%% read stdout, stderr pipes
84-
stdout = read_stream(h.getInputStream());
85-
stderr = read_stream(h.getErrorStream());
86-
90+
% like Python subprocess.run, this may block or deadlock if the process writes
91+
% large amounts of data to stdout or stderr.
92+
% A better approach is to read each of the streams in a separate thread.
93+
if opt.outpipe
94+
stdout = read_stream(h.getInputStream());
95+
stderr = read_stream(h.getErrorStream());
96+
else
97+
stdout = "";
98+
stderr = "";
99+
end
87100
%% wait for process to complete
88101
% https://docs.oracle.com/en/java/javase/23/docs/api/java.base/java/lang/Process.html#waitFor()
89102

@@ -139,6 +152,7 @@
139152
end
140153
msg = strip(msg);
141154
reader.close()
155+
stream.close()
142156

143157
end
144158

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,4 @@ docs/
1212

1313
test/printer_c.exe
1414
test/printer_fortran.exe
15+
test/sleep.exe

buildfile.m

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,9 @@
2424
end
2525

2626
plan("build_c") = matlab.buildtool.Task(Actions=@subprocess_build_c);
27+
plan("build_cpp") = matlab.buildtool.Task(Actions=@subprocess_build_cpp);
2728
plan("build_fortran") = matlab.buildtool.Task(Actions=@subprocess_build_fortran);
28-
plan("test").Dependencies = ["build_c", "build_fortran"];
29+
plan("test").Dependencies = ["build_c", "build_cpp", "build_fortran"];
2930

3031
if ~isMATLABReleaseOlderThan("R2024a")
3132
plan("check") = matlab.buildtool.tasks.CodeIssuesTask(pkg_name, IncludeSubfolders=true, ...
@@ -128,6 +129,39 @@ function subprocess_build_c(context)
128129
end
129130

130131

132+
function subprocess_build_cpp(context)
133+
134+
td = context.Plan.RootFolder + "/test";
135+
src = td + "/sleep.cpp";
136+
exe = td + "/sleep.exe";
137+
138+
co = mex.getCompilerConfigurations('c++');
139+
140+
cpp = co.Details.CompilerExecutable;
141+
142+
outFlag = "-o";
143+
shell = "";
144+
shell_arg = "";
145+
msvcLike = ispc && endsWith(cpp, "cl");
146+
if msvcLike
147+
shell = strtrim(co.Details.CommandLineShell);
148+
shell_arg = co.Details.CommandLineShellArg;
149+
outFlag = "/link /out:";
150+
end
151+
152+
cmd = join([cpp, src, outFlag, exe]);
153+
if shell ~= ""
154+
cmd = join([shell, shell_arg, cmd]);
155+
end
156+
157+
[r, m] = system(cmd);
158+
if r ~= 0
159+
warning("failed to build TestSubprocess " + exe + " " + m)
160+
end
161+
162+
end
163+
164+
131165
function subprocess_build_fortran(context)
132166

133167
td = context.Plan.RootFolder + "/test";

example/subprocess_run.java

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import java.io.BufferedReader;
2+
import java.io.InputStreamReader;
3+
import java.io.IOException;
4+
5+
public class Filesystem {
6+
public static void main(String[] argv) {
7+
try {
8+
9+
ProcessBuilder builder = new ProcessBuilder(argv[0]);
10+
Process process = builder.start();
11+
12+
BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
13+
String line;
14+
while ((line = reader.readLine()) != null) {
15+
System.out.println(line);
16+
}
17+
18+
19+
BufferedReader errorReader = new BufferedReader(new InputStreamReader(process.getErrorStream()));
20+
while ((line = errorReader.readLine()) != null) {
21+
System.err.println(line);
22+
}
23+
24+
int exitCode = process.waitFor();
25+
System.out.println("Process exited with code: " + exitCode);
26+
27+
process.destroy();
28+
29+
} catch (IOException | InterruptedException e) {
30+
// ignore
31+
}
32+
33+
}
34+
35+
}

test/TestSubprocess.m

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -89,16 +89,13 @@ function test_env_run(tc)
8989

9090
function test_timeout(tc)
9191
import matlab.unittest.constraints.StartsWithSubstring
92+
import matlab.unittest.constraints.IsFile
9293

93-
timeout = 1;
94-
95-
if ispc
96-
c = ["powershell", "-command", "Start-Sleep -s 3"];
97-
else
98-
c = ["sleep", "3"];
99-
end
94+
cwd = fileparts(mfilename('fullpath'));
95+
exe = cwd + "/sleep.exe";
96+
tc.assumeThat(exe, IsFile, exe + " not found")
10097

101-
[ret, ~, err] = stdlib.subprocess_run(c, "timeout", timeout);
98+
[ret, ~, err] = stdlib.subprocess_run(exe, timeout=1, outpipe=false);
10299

103100
tc.verifyNotEqual(ret, 0, err)
104101
tc.verifyThat(err, StartsWithSubstring("Subprocess timeout"))

test/sleep.cpp

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
#include <chrono>
2+
#include <thread>
3+
4+
#include <cstdlib>
5+
#include <iostream>
6+
7+
8+
int main(int argc, char *argv[]) {
9+
10+
int milliseconds = (argc > 1) ? std::atoi(argv[1]) : 2000;
11+
12+
std::cout << "Sleeping for " << milliseconds << " milliseconds..." << std::endl;
13+
14+
std::this_thread::sleep_for(std::chrono::milliseconds(milliseconds));
15+
16+
std::cout << "Awake!" << std::endl;
17+
18+
return EXIT_SUCCESS;
19+
}

0 commit comments

Comments
 (0)