|
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 | 30 | 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} = '' |
42 | 34 | opt.stdout (1,1) logical = true
|
43 | 35 | opt.stderr (1,1) logical = true
|
| 36 | + opt.echo (1,1) logical = false |
44 | 37 | end
|
45 | 38 |
|
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]); |
48 | 47 | end
|
49 | 48 |
|
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 | 49 |
|
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"]); |
60 | 55 | end
|
61 | 56 | end
|
62 | 57 |
|
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 |
67 | 64 | 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 | + |
68 | 78 | %% Gfortran streams
|
69 | 79 | % 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 | 80 | % Matlab grabs the stdout, stderr, stdin handles of a Gfortran program, even when it's using Java.
|
|
76 | 86 | setenv("GFORTRAN_STDERR_UNIT", "0");
|
77 | 87 | inold = getenv("GFORTRAN_STDIN_UNIT");
|
78 | 88 | 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 | 89 |
|
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) |
125 | 92 | end
|
126 | 93 |
|
127 |
| -%% close process and restore Gfortran streams |
128 |
| -h.destroy() |
| 94 | +[status, msg] = system(join(cmd), env_pairs{:}); |
129 | 95 |
|
130 | 96 | setenv("GFORTRAN_STDOUT_UNIT", outold);
|
131 | 97 | setenv("GFORTRAN_STDERR_UNIT", errold);
|
132 | 98 | setenv("GFORTRAN_STDIN_UNIT", inold);
|
133 | 99 |
|
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 | 100 |
|
146 |
| -% https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/io/BufferedReader.html |
| 101 | +msg = strtrim(msg); |
147 | 102 |
|
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 | 103 | end
|
159 |
| -msg = strip(msg); |
160 |
| -reader.close() |
161 |
| -stream.close() |
162 | 104 |
|
163 |
| -end |
164 | 105 |
|
165 | 106 | %!testif 0
|
0 commit comments