|
| 1 | +%% SUBPROCESS_RUN run process for GNU Octave only |
| 2 | +% with optional cwd, env. vars, stdin, timeout |
| 3 | +% |
| 4 | +% handles command lines with spaces |
| 5 | +% input each segment of the command as an element in a string array |
| 6 | +% this is how python subprocess.run works |
| 7 | +% |
| 8 | +%%% Inputs |
| 9 | +% * cmd_array: cell of char to compose a command line |
| 10 | +% * env: environment variable struct to set |
| 11 | +% * cwd: working directory to use while running command |
| 12 | +% * stdin: string to pass to subprocess stdin pipe |
| 13 | +% * timeout: time to wait for process to complete before erroring (seconds) |
| 14 | +%%% Outputs |
| 15 | +% * status: 0 is generally success. -1 if timeout. Other codes as per the |
| 16 | +% program / command run |
| 17 | +% * stdout: stdout from process |
| 18 | +% * stderr: stderr from process |
| 19 | +% |
| 20 | +%% Example |
| 21 | +% subprocess_run({'mpiexec', '-help2'}) |
| 22 | +% |
| 23 | +% NOTE: if cwd option used, any paths must be absolute or relative to cwd. |
| 24 | +% otherwise, they are relative to pwd. |
| 25 | +% |
| 26 | +% uses Java ProcessBuilder interface to run subprocess and use stdin/stdout pipes |
| 27 | +% https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/lang/ProcessBuilder.html |
| 28 | + |
| 29 | +function [status, stdout, stderr] = subprocess_run_octave(cmd, env, cwd, stdin, timeout) |
| 30 | +if ischar(cmd), cmd = {cmd}; end |
| 31 | +if nargin < 2 || isempty(env), env = struct(); end |
| 32 | +if nargin < 3, cwd = ''; end |
| 33 | +if nargin < 4, stdin = ''; end |
| 34 | +if nargin < 5, timeout = 0; end |
| 35 | + |
| 36 | +%% process instantiation |
| 37 | +% https://docs.oracle.com/en/java/javase/23/docs/api/java.base/java/lang/ProcessBuilder.html#command(java.lang.String...) |
| 38 | +jcary = javaArray("java.lang.String", length(cmd)); |
| 39 | +for i = 1:length(cmd) |
| 40 | + jcary(i) = javaObject("java.lang.String", cmd{i}); |
| 41 | +end |
| 42 | +proc = javaObject("java.lang.ProcessBuilder", jcary); |
| 43 | + |
| 44 | +if ~isempty(fieldnames(env)) |
| 45 | + % https://docs.oracle.com/en/java/javase/23/docs/api/java.base/java/lang/ProcessBuilder.html#environment() |
| 46 | + jenv = proc.environment(); |
| 47 | + fields = fieldnames(env); |
| 48 | + for i = 1:length(fields) |
| 49 | + jenv.put(fields{i}, env.(fields{i})); |
| 50 | + % jenv.put(fields{i}, javaObject("java.lang.String", env.(fields{i}))); |
| 51 | + end |
| 52 | +end |
| 53 | + |
| 54 | +if stdlib.len(cwd) > 0 |
| 55 | + % https://docs.oracle.com/en/java/javase/23/docs/api/java.base/java/lang/ProcessBuilder.html#directory(java.io.File) |
| 56 | + o = javaObject("java.io.File", cwd); |
| 57 | + proc.directory(o); |
| 58 | +end |
| 59 | + |
| 60 | +%% start process |
| 61 | +% https://docs.oracle.com/en/java/javase/23/docs/api/java.base/java/lang/ProcessBuilder.html#start() |
| 62 | +h = proc.start(); |
| 63 | + |
| 64 | +%% stdin pipe |
| 65 | +if stdlib.len(stdin) > 0 |
| 66 | + os = javaObject("java.io.OutputStream", h.getOutputStream()); |
| 67 | + writer = javaObject("java.io.BufferedWriter", os); |
| 68 | + writer.write(stdin); |
| 69 | + writer.flush() |
| 70 | + writer.close() |
| 71 | +end |
| 72 | + |
| 73 | +%% wait for process to complete |
| 74 | +% https://docs.oracle.com/en/java/javase/23/docs/api/java.base/java/lang/Process.html#waitFor() |
| 75 | + |
| 76 | +tmsg = ''; |
| 77 | +if timeout > 0 |
| 78 | + % returns true if process completed successfully |
| 79 | + % returns false if process did not complete within timeout |
| 80 | + sec = javaMethod("valueOf", "java.util.concurrent.TimeUnit", "SECONDS"); |
| 81 | + b = h.waitFor(timeout, sec); |
| 82 | + if b |
| 83 | + status = 0; |
| 84 | + else |
| 85 | + tmsg = 'Subprocess timeout'; |
| 86 | + status = -1; |
| 87 | + end |
| 88 | +else |
| 89 | + % returns 0 if process completed successfully |
| 90 | + status = h.waitFor(); |
| 91 | +end |
| 92 | + |
| 93 | +%% read stdout, stderr pipes |
| 94 | +stdout = read_stream(h.getInputStream()); |
| 95 | +stderr = read_stream(h.getErrorStream()); |
| 96 | + |
| 97 | +%% close process |
| 98 | +h.destroy(); |
| 99 | + |
| 100 | +stderr = strcat(tmsg, stderr); |
| 101 | + |
| 102 | +if nargout < 2 && stdlib.len(stdout) > 0 |
| 103 | + disp(stdout) |
| 104 | +end |
| 105 | +if nargout < 3 && stdlib.len(stderr) > 0 |
| 106 | + warning(stderr) |
| 107 | +end |
| 108 | + |
| 109 | +end % function subprocess_run |
| 110 | + |
| 111 | + |
| 112 | +function msg = read_stream(stream) |
| 113 | + |
| 114 | +% https://docs.oracle.com/en/java/javase/23/docs/api/java.base/java/io/BufferedReader.html |
| 115 | +reader = javaObject("java.io.BufferedReader", javaObject("java.io.InputStreamReader", stream)); |
| 116 | + |
| 117 | +line = reader.readLine(); |
| 118 | +msg = ''; |
| 119 | +while ~isempty(line) |
| 120 | + msg = strcat(msg, line, newline); |
| 121 | + |
| 122 | + line = reader.readLine(); |
| 123 | +end |
| 124 | + |
| 125 | +if stdlib.len(msg) > 0 && msg(end) == newline |
| 126 | + msg(end) = []; |
| 127 | +end |
| 128 | + |
| 129 | +reader.close(); |
| 130 | + |
| 131 | +end |
| 132 | + |
| 133 | +%!test |
| 134 | +%! if ispc, c = "dir"; else, c = "ls"; end |
| 135 | +%! [r, m, e] = subprocess_run_octave(c); |
| 136 | +%! assert(r == 0) |
| 137 | +%! assert(length(m) > 0) |
| 138 | +%! assert(length(e) == 0) |
| 139 | +%! [r, mc, e] = subprocess_run_octave(c, [], '/'); |
| 140 | +%! assert(r == 0) |
| 141 | +%! assert(!strcmp(m, mc)) |
| 142 | +%!testif 0 |
| 143 | +%! names = {'TEST1', 'TEST2'}; |
| 144 | +%! vals = {'test123', 'test321'}; |
| 145 | +%! env = struct(names{1}, vals{1}, names{2}, vals{2}); |
| 146 | +%! for i = 1:length(names) |
| 147 | +%! if ispc |
| 148 | +%! c = {"cmd", "/c", "echo", strcat('%', names{i}, '%')}; |
| 149 | +%! else |
| 150 | +%! c = {"echo", strcat('$', names{i})}; |
| 151 | +%! end |
| 152 | +%! [r, m, e] = subprocess_run_octave(c, env); |
| 153 | +%! assert(r == 0) |
| 154 | +%! assert(strcmp(m, vals{i}), '%s != %s', m, vals{i}) |
| 155 | +%! assert(length(e) == 0) |
| 156 | +%! end |
| 157 | +%!test |
| 158 | +%! if ispc, c = {'powershell', '-command', 'Start-Sleep -s 3'}; else, c = {'sleep', '3'}; end |
| 159 | +%! [r, m, e] = subprocess_run_octave(c, [], [], [], 1); |
| 160 | +%! assert(r == -1) |
| 161 | +%! assert(length(m) == 0) |
| 162 | +%! assert(strncmp(e, 'Subprocess timeout', 17)) |
0 commit comments