Skip to content

Commit ffb6f38

Browse files
committed
subprocess_run doesn't use java anymore
Had to omit timeout= option subprocess_run: use string internally for expediency subprocess_run: add echo= option test:subprocess: use existing rather than created dir this may avoid a race condition on Windows CI
1 parent 439f6e7 commit ffb6f38

File tree

4 files changed

+304
-123
lines changed

4 files changed

+304
-123
lines changed

+stdlib/java_run.m

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
%% JAVA_RUN run process for Matlab only
2+
% requires: java
3+
%
4+
% with optional cwd, env. vars, stdin, timeout
5+
%
6+
% handles command lines with spaces
7+
% input each segment of the command as an element in a string array
8+
% this is how python subprocess.run works
9+
%
10+
%%% Inputs
11+
% * cmd_array: vector of string to compose a command line
12+
% * opt.env: environment variable struct to set
13+
% * opt.cwd: working directory to use while running command
14+
% * opt.stdin: string to pass to subprocess stdin pipe - OK to use with timeout
15+
% * opt.timeout: time to wait for process to complete before erroring (seconds)
16+
% * opt.stdout: logical to indicate whether to use pipe for stdout
17+
% * opt.stderr: logical to indicate whether to use pipe for stderr
18+
%%% Outputs
19+
% * status: 0 is generally success. -1 if timeout. Other codes as per the
20+
% program / command run
21+
% * stdout: stdout from process
22+
% * stderr: stderr from process
23+
%
24+
%% Example
25+
% subprocess_run(["mpiexec", "-help2"])
26+
% subprocess_run(["sh", "-c", "ls", "-l"])
27+
% subprocess_run(["cmd", "/c", "dir", "/Q", "/L"])
28+
%
29+
% NOTE: if cwd option used, any paths must be absolute or relative to cwd.
30+
% otherwise, they are relative to pwd.
31+
%
32+
% uses Matlab Java ProcessBuilder interface to run subprocess and use stdin/stdout pipes
33+
% https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/lang/ProcessBuilder.html
34+
35+
function [status, stdout, stderr] = java_run(cmd, opt)
36+
arguments
37+
cmd (1,:) string
38+
opt.env (1,1) struct = struct()
39+
opt.cwd (1,1) string = ""
40+
opt.stdin (1,1) string = ""
41+
opt.timeout (1,1) int64 = 0
42+
opt.stdout (1,1) logical = true
43+
opt.stderr (1,1) logical = true
44+
end
45+
46+
if (opt.stdout || opt.stderr) && opt.timeout > 0
47+
error("stderr or stdout and timeout options are mutually exclusive")
48+
end
49+
50+
%% process instantiation
51+
% https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/lang/ProcessBuilder.html#command(java.lang.String...)
52+
proc = java.lang.ProcessBuilder(cmd);
53+
54+
if ~isempty(fieldnames(opt.env))
55+
% https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/lang/ProcessBuilder.html#environment()
56+
env = proc.environment();
57+
fields = fieldnames(opt.env);
58+
for i = 1:length(fields)
59+
env.put(fields{i}, opt.env.(fields{i}));
60+
end
61+
end
62+
63+
if ~strempty(opt.cwd)
64+
% https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/lang/ProcessBuilder.html#directory(java.io.File)
65+
mustBeFolder(opt.cwd)
66+
proc.directory(java.io.File(opt.cwd));
67+
end
68+
%% Gfortran streams
69+
% https://www.mathworks.com/matlabcentral/answers/91919-why-does-the-output-of-my-fortran-script-not-show-up-in-the-matlab-command-window-when-i-execute-it#answer_101270
70+
% Matlab grabs the stdout, stderr, stdin handles of a Gfortran program, even when it's using Java.
71+
% We must disable this behavior for the duration the running process.
72+
73+
outold = getenv("GFORTRAN_STDOUT_UNIT");
74+
setenv("GFORTRAN_STDOUT_UNIT", "6");
75+
errold = getenv("GFORTRAN_STDERR_UNIT");
76+
setenv("GFORTRAN_STDERR_UNIT", "0");
77+
inold = getenv("GFORTRAN_STDIN_UNIT");
78+
setenv("GFORTRAN_STDIN_UNIT", "5");
79+
%% start process
80+
% https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/lang/ProcessBuilder.html#start()
81+
h = proc.start();
82+
83+
%% stdin pipe
84+
if ~strempty(opt.stdin)
85+
writer = java.io.BufferedWriter(java.io.OutputStreamWriter(h.getOutputStream()));
86+
stdin_text = opt.stdin;
87+
if ~endsWith(stdin_text, newline)
88+
% Fortran (across compilers) needs a \n at the end of stdin.
89+
stdin_text = stdin_text + newline;
90+
end
91+
writer.write(stdin_text);
92+
writer.flush()
93+
writer.close()
94+
end
95+
96+
%% read stdout, stderr pipes
97+
% like Python subprocess.run, this may block or deadlock if the process writes
98+
% large amounts of data to stdout or stderr.
99+
% A better approach is to read each of the streams in a separate thread.
100+
101+
stdout = "";
102+
stderr = "";
103+
if opt.stdout && nargout > 1
104+
stdout = read_stream(h.getInputStream());
105+
end
106+
if opt.stderr && nargout > 2
107+
stderr = read_stream(h.getErrorStream());
108+
end
109+
%% wait for process to complete
110+
% https://docs.oracle.com/en/java/javase/23/docs/api/java.base/java/lang/Process.html#waitFor()
111+
112+
if opt.timeout > 0
113+
% returns true if process completed successfully
114+
% returns false if process did not complete within timeout
115+
b = h.waitFor(opt.timeout, java.util.concurrent.TimeUnit.SECONDS);
116+
if b
117+
status = 0;
118+
else
119+
stderr = "Subprocess timeout";
120+
status = -1;
121+
end
122+
else
123+
% returns 0 if process completed successfully
124+
status = h.waitFor();
125+
end
126+
127+
%% close process and restore Gfortran streams
128+
h.destroy()
129+
130+
setenv("GFORTRAN_STDOUT_UNIT", outold);
131+
setenv("GFORTRAN_STDERR_UNIT", errold);
132+
setenv("GFORTRAN_STDIN_UNIT", inold);
133+
134+
if nargout < 2 && opt.stdout && ~strempty(stdout)
135+
disp(stdout)
136+
end
137+
if nargout < 3 && opt.stderr && ~strempty(stderr)
138+
warning(stderr)
139+
end
140+
141+
end % function subprocess_run
142+
143+
144+
function msg = read_stream(stream)
145+
146+
% https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/io/BufferedReader.html
147+
148+
msg = "";
149+
150+
% don't check stream.available() as it may arbitrarily return 0
151+
152+
reader = java.io.BufferedReader(java.io.InputStreamReader(stream));
153+
154+
line = reader.readLine();
155+
while ~isempty(line)
156+
msg = append(msg, string(line), newline);
157+
line = reader.readLine();
158+
end
159+
msg = strip(msg);
160+
reader.close()
161+
stream.close()
162+
163+
end
164+
165+
%!testif 0

+stdlib/subprocess_run_octave.m renamed to +stdlib/java_run_octave.m

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
%% SUBPROCESS_RUN_OCTAVE run process for GNU Octave only
1+
%% JAVA_RUN_OCTAVE run process for GNU Octave only
22
% requires: java
33
%
44
% with optional cwd, env. vars, stdin, timeout
@@ -28,7 +28,7 @@
2828
% uses Java ProcessBuilder interface to run subprocess and use stdin/stdout pipes
2929
% https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/lang/ProcessBuilder.html
3030

31-
function [status, stdout, stderr] = subprocess_run_octave(cmd, env, cwd, stdin, timeout)
31+
function [status, stdout, stderr] = java_run_octave(cmd, env, cwd, stdin, timeout)
3232
if ischar(cmd), cmd = {cmd}; end
3333
if nargin < 2 || isempty(env), env = struct(); end
3434
if nargin < 3, cwd = ''; end

+stdlib/subprocess_run.m

Lines changed: 50 additions & 109 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
%% SUBPROCESS_RUN run process for Matlab only
2-
% requires: java
1+
%% SUBPROCESS_RUN run process
32
%
43
% with optional cwd, env. vars, stdin, timeout
54
%
@@ -8,63 +7,74 @@
87
% this is how python subprocess.run works
98
%
109
%%% Inputs
11-
% * cmd_array: vector of string to compose a command line
10+
% * cmd: command line. Windows paths should use filesep '\'
1211
% * opt.env: environment variable struct to set
1312
% * opt.cwd: working directory to use while running command
14-
% * opt.stdin: string to pass to subprocess stdin pipe - OK to use with timeout
15-
% * opt.timeout: time to wait for process to complete before erroring (seconds)
13+
% * opt.stdin: string to pass to subprocess stdin pipe
1614
% * opt.stdout: logical to indicate whether to use pipe for stdout
1715
% * opt.stderr: logical to indicate whether to use pipe for stderr
1816
%%% Outputs
19-
% * status: 0 is generally success. -1 if timeout. Other codes as per the
17+
% * status: 0 is generally success. Other codes as per the
2018
% program / command run
21-
% * stdout: stdout from process
22-
% * stderr: stderr from process
19+
% * msg: combined stdout and stderr from process
2320
%
2421
%% Example
25-
% subprocess_run(["mpiexec", "-help2"])
26-
% subprocess_run(["sh", "-c", "ls", "-l"])
27-
% subprocess_run(["cmd", "/c", "dir", "/Q", "/L"])
22+
% subprocess_run('mpiexec -help2');
23+
% subprocess_run('sh -c "ls -l"');
24+
% subprocess_run('cmd /c "dir /Q /L"');
2825
%
29-
% NOTE: if cwd option used, any paths must be absolute or relative to cwd.
30-
% otherwise, they are relative to pwd.
31-
%
32-
% uses Matlab Java ProcessBuilder interface to run subprocess and use stdin/stdout pipes
33-
% https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/lang/ProcessBuilder.html
26+
% NOTE: if cwd option used, any paths must be absolute, or they are relative to pwd.
3427

35-
function [status, stdout, stderr] = subprocess_run(cmd, opt)
28+
function [status, msg] = subprocess_run(cmd, opt)
3629
arguments
3730
cmd (1,:) string
38-
opt.env (1,1) struct = struct()
39-
opt.cwd (1,1) string = ""
40-
opt.stdin (1,1) string = ""
41-
opt.timeout (1,1) int64 = 0
31+
opt.env struct {mustBeScalarOrEmpty} = struct.empty
32+
opt.cwd {mustBeTextScalar} = ''
33+
opt.stdin {mustBeTextScalar} = ''
4234
opt.stdout (1,1) logical = true
4335
opt.stderr (1,1) logical = true
36+
opt.echo (1,1) logical = false
4437
end
4538

46-
if (opt.stdout || opt.stderr) && opt.timeout > 0
47-
error("stderr or stdout and timeout options are mutually exclusive")
39+
40+
if ~strempty(opt.cwd)
41+
mustBeFolder(opt.cwd)
42+
cmd = join(["cd", opt.cwd, "&&", cmd]);
43+
end
44+
45+
if ~strempty(opt.stdin)
46+
cmd = join(["echo", opt.stdin, "|", cmd]);
4847
end
4948

50-
%% process instantiation
51-
% https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/lang/ProcessBuilder.html#command(java.lang.String...)
52-
proc = java.lang.ProcessBuilder(cmd);
5349

54-
if ~isempty(fieldnames(opt.env))
55-
% https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/lang/ProcessBuilder.html#environment()
56-
env = proc.environment();
57-
fields = fieldnames(opt.env);
58-
for i = 1:length(fields)
59-
env.put(fields{i}, opt.env.(fields{i}));
50+
if ~opt.stderr
51+
if ispc
52+
cmd = join([cmd, "2> nul"]);
53+
else
54+
cmd = join([cmd, "2> /dev/null"]);
6055
end
6156
end
6257

63-
if ~strempty(opt.cwd)
64-
% https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/lang/ProcessBuilder.html#directory(java.io.File)
65-
mustBeFolder(opt.cwd)
66-
proc.directory(java.io.File(opt.cwd));
58+
if ~opt.stdout
59+
if ispc
60+
cmd = join([cmd, "> nul"]);
61+
else
62+
cmd = join([cmd, "> /dev/null"]);
63+
end
6764
end
65+
66+
% deal struct into name, value pairs for system()
67+
if isempty(opt.env)
68+
env_pairs = {};
69+
else
70+
f = fieldnames(opt.env);
71+
env_pairs = cell(1, 2 * numel(f));
72+
for i = 1:numel(f)
73+
env_pairs{2*i-1} = f{i};
74+
env_pairs{2*i} = opt.env.(f{i});
75+
end
76+
end
77+
6878
%% Gfortran streams
6979
% https://www.mathworks.com/matlabcentral/answers/91919-why-does-the-output-of-my-fortran-script-not-show-up-in-the-matlab-command-window-when-i-execute-it#answer_101270
7080
% Matlab grabs the stdout, stderr, stdin handles of a Gfortran program, even when it's using Java.
@@ -76,90 +86,21 @@
7686
setenv("GFORTRAN_STDERR_UNIT", "0");
7787
inold = getenv("GFORTRAN_STDIN_UNIT");
7888
setenv("GFORTRAN_STDIN_UNIT", "5");
79-
%% start process
80-
% https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/lang/ProcessBuilder.html#start()
81-
h = proc.start();
82-
83-
%% stdin pipe
84-
if ~strempty(opt.stdin)
85-
writer = java.io.BufferedWriter(java.io.OutputStreamWriter(h.getOutputStream()));
86-
stdin_text = opt.stdin;
87-
if ~endsWith(stdin_text, newline)
88-
% Fortran (across compilers) needs a \n at the end of stdin.
89-
stdin_text = stdin_text + newline;
90-
end
91-
writer.write(stdin_text);
92-
writer.flush()
93-
writer.close()
94-
end
9589

96-
%% read stdout, stderr pipes
97-
% like Python subprocess.run, this may block or deadlock if the process writes
98-
% large amounts of data to stdout or stderr.
99-
% A better approach is to read each of the streams in a separate thread.
100-
101-
stdout = "";
102-
stderr = "";
103-
if opt.stdout && nargout > 1
104-
stdout = read_stream(h.getInputStream());
105-
end
106-
if opt.stderr && nargout > 2
107-
stderr = read_stream(h.getErrorStream());
108-
end
109-
%% wait for process to complete
110-
% https://docs.oracle.com/en/java/javase/23/docs/api/java.base/java/lang/Process.html#waitFor()
111-
112-
if opt.timeout > 0
113-
% returns true if process completed successfully
114-
% returns false if process did not complete within timeout
115-
b = h.waitFor(opt.timeout, java.util.concurrent.TimeUnit.SECONDS);
116-
if b
117-
status = 0;
118-
else
119-
stderr = "Subprocess timeout";
120-
status = -1;
121-
end
122-
else
123-
% returns 0 if process completed successfully
124-
status = h.waitFor();
90+
if opt.echo
91+
disp(cmd)
12592
end
12693

127-
%% close process and restore Gfortran streams
128-
h.destroy()
94+
[status, msg] = system(join(cmd), env_pairs{:});
12995

13096
setenv("GFORTRAN_STDOUT_UNIT", outold);
13197
setenv("GFORTRAN_STDERR_UNIT", errold);
13298
setenv("GFORTRAN_STDIN_UNIT", inold);
13399

134-
if nargout < 2 && opt.stdout && ~strempty(stdout)
135-
disp(stdout)
136-
end
137-
if nargout < 3 && opt.stderr && ~strempty(stderr)
138-
warning(stderr)
139-
end
140-
141-
end % function subprocess_run
142-
143-
144-
function msg = read_stream(stream)
145100

146-
% https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/io/BufferedReader.html
101+
msg = strtrim(msg);
147102

148-
msg = "";
149-
150-
% don't check stream.available() as it may arbitrarily return 0
151-
152-
reader = java.io.BufferedReader(java.io.InputStreamReader(stream));
153-
154-
line = reader.readLine();
155-
while ~isempty(line)
156-
msg = append(msg, string(line), newline);
157-
line = reader.readLine();
158103
end
159-
msg = strip(msg);
160-
reader.close()
161-
stream.close()
162104

163-
end
164105

165106
%!testif 0

0 commit comments

Comments
 (0)