|
1 | | -%% SUBPROCESS_RUN run process for Matlab only |
2 | | -% requires: java |
| 1 | +%% SUBPROCESS_RUN run process |
3 | 2 | % |
4 | 3 | % with optional cwd, env. vars, stdin, timeout |
5 | 4 | % |
|
8 | 7 | % this is how python subprocess.run works |
9 | 8 | % |
10 | 9 | %%% Inputs |
11 | | -% * cmd_array: vector of string to compose a command line |
| 10 | +% * cmd: command line. Windows paths should use filesep '\' |
12 | 11 | % * opt.env: environment variable struct to set |
13 | 12 | % * 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 |
16 | 14 | % * opt.stdout: logical to indicate whether to use pipe for stdout |
17 | 15 | % * opt.stderr: logical to indicate whether to use pipe for stderr |
18 | 16 | %%% 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 |
20 | 18 | % program / command run |
21 | | -% * stdout: stdout from process |
22 | | -% * stderr: stderr from process |
| 19 | +% * msg: combined stdout and stderr from process |
23 | 20 | % |
24 | 21 | %% 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"'); |
28 | 25 | % |
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. |
34 | 27 |
|
35 | | -function [status, stdout, stderr] = subprocess_run(cmd, opt) |
| 28 | +function [status, msg] = subprocess_run(cmd, opt) |
36 | 29 | 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 |
| 30 | + cmd {mustBeTextScalar} |
| 31 | + opt.env struct {mustBeScalarOrEmpty} = struct.empty |
| 32 | + opt.cwd {mustBeTextScalar} = '' |
| 33 | + opt.stdin {mustBeTextScalar} = '' |
42 | 34 | opt.stdout (1,1) logical = true |
43 | 35 | opt.stderr (1,1) logical = true |
44 | 36 | end |
45 | 37 |
|
46 | | -if (opt.stdout || opt.stderr) && opt.timeout > 0 |
47 | | - error("stderr or stdout and timeout options are mutually exclusive") |
| 38 | +cmd = char(cmd); |
| 39 | + |
| 40 | +if ~strempty(opt.cwd) |
| 41 | + cmd = strjoin({'cd', char(opt.cwd), '&&', cmd}); |
48 | 42 | end |
49 | 43 |
|
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); |
| 44 | +if ~strempty(opt.stdin) |
| 45 | + cmd = strjoin({'echo', char(opt.stdin), '|', cmd}); |
| 46 | +end |
53 | 47 |
|
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})); |
| 48 | + |
| 49 | +if ~opt.stderr |
| 50 | + if ispc |
| 51 | + cmd = strjoin({cmd, '2> nul'}); |
| 52 | + else |
| 53 | + cmd = strjoin({cmd, '2> /dev/null'}); |
60 | 54 | end |
61 | 55 | end |
62 | 56 |
|
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)); |
| 57 | +if ~opt.stdout |
| 58 | + if ispc |
| 59 | + cmd = strjoin({cmd, '> nul'}); |
| 60 | + else |
| 61 | + cmd = strjoin({cmd, '> /dev/null'}); |
| 62 | + end |
| 63 | +end |
| 64 | + |
| 65 | +% deal struct into name, value pairs for system() |
| 66 | +if isempty(opt.env) |
| 67 | + env_pairs = {}; |
| 68 | +else |
| 69 | + f = fieldnames(opt.env); |
| 70 | + env_pairs = cell(1, 2 * numel(f)); |
| 71 | + for i = 1:numel(f) |
| 72 | + env_pairs{2*i-1} = f{i}; |
| 73 | + env_pairs{2*i} = opt.env.(f{i}); |
| 74 | + end |
67 | 75 | end |
| 76 | + |
68 | 77 | %% Gfortran streams |
69 | 78 | % 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 | 79 | % Matlab grabs the stdout, stderr, stdin handles of a Gfortran program, even when it's using Java. |
|
76 | 85 | setenv("GFORTRAN_STDERR_UNIT", "0"); |
77 | 86 | inold = getenv("GFORTRAN_STDIN_UNIT"); |
78 | 87 | 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 | 88 |
|
127 | | -%% close process and restore Gfortran streams |
128 | | -h.destroy() |
| 89 | +[status, msg] = system(cmd, env_pairs{:}); |
129 | 90 |
|
130 | 91 | setenv("GFORTRAN_STDOUT_UNIT", outold); |
131 | 92 | setenv("GFORTRAN_STDERR_UNIT", errold); |
132 | 93 | setenv("GFORTRAN_STDIN_UNIT", inold); |
133 | 94 |
|
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 | 95 |
|
146 | | -% https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/io/BufferedReader.html |
| 96 | +msg = strtrim(msg); |
147 | 97 |
|
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 | 98 | end |
159 | | -msg = strip(msg); |
160 | | -reader.close() |
161 | | -stream.close() |
162 | 99 |
|
163 | | -end |
164 | 100 |
|
165 | 101 | %!testif 0 |
0 commit comments