MATLAB的`system`命令的安全替代方案

7

我一直在使用MATLAB的system命令获取一些Linux命令的结果,就像下面这个简单的例子:

[junk, result] = system('find ~/ -type f')

这个代码在正常情况下能够按照预期工作,但是如果用户同时在MATLAB的命令窗口输入内容,则可能会出现意外情况。特别是当执行长时间的find指令时,这种情况较为普遍。如果出现这种情况,用户的输入似乎会与find指令的结果混合在一起(然后程序就崩溃了)。

例如,下面的输出:

/path/to/file/one
/path/to/file/two
/path/to/file/three
/path/to/file/four

我可能会得到:

J/path/to/file/one
u/path/to/file/two
n/path/to/file/three
k/path/to/file/four

为了更容易地演示,我们可以运行如下内容:
[junk, result] = system('cat')

在命令窗口中输入内容,然后按下CTRL+D关闭流。变量result将是您在命令窗口中输入的任何内容。

有没有更安全的方法可以在MATLAB中调用系统命令而不会冒着输入被破坏的风险?


当您使用unix而不是system时,是否观察到相同的行为? - Robert Seifert
1
system('cat') 的例子可能是一个糟糕的复制案例:cat 应该将其 stdin 回显到 stdout,而 Matlab 似乎将其命令窗口或控制台键盘输入钩子化为 system() 调用的 stdin。(就像 shell 做的那样。)让您与需要用户输入的程序进行交互。find 的例子更奇怪;它不应该随机混合它在 stdin 中得到的内容到其输出中,这看起来像是真正的交叉连线。FWIW,我可以在 OS X 上使用 R2014a 复制这两个例子。 - Andrew Janke
@thewaywewalk - 是的,它具有相同的行为。 - Samuel O'Malley
2个回答

4

哇,那个行为真是让人惊讶。听起来值得向 MathWorks 报告一个 bug。我在 OS X 上测试了一下,发现出现了同样的情况。

作为一种解决办法,你可以使用调用嵌入在 Matlab 中的 JVM 的 java.lang.Process 和相关对象来重新实现 system()

你需要:

  • 使用轮询来保持 Matlab 的输入(特别是 Ctrl-C)处于活动状态
  • 使用 shell 处理来支持 ~ 和其他变量和通配符的扩展,并支持在单个字符串中指定命令及其参数,就像 Matlab 的 system 命令一样。 ** 作为替代方案,如果你想要更低层次的控制并且不想处理字符串的转义和引号,则可以公开参数数组形式。这两种方法都非常有用。
  • 将输出重定向到文件或定期在轮询代码中清除子进程的输出缓冲区。

这是一个示例。

function [status,out,errout] = systemwithjava(cmd)
%SYSTEMCMD Version of system implemented with java.lang features
%
% [status,out,errout] = systemwithcmd(cmd)
%
% Written to work around issue with Matlab UI entry getting mixed up with 
% output captured by system().

if isunix
    % Use 'sh -s' to enable processing of single line command and expansion of ~
    % and other special characters, like the Matlab system() does
    pb = java.lang.ProcessBuilder({'bash', '-s'});
    % Redirect stdout to avoid filling up buffers
    myTempname = tempname;
    stdoutFile = [myTempname '.systemwithjava.out'];
    stderrFile = [myTempname '.systemwithjava.err'];
    pb.redirectOutput(java.io.File(stdoutFile));
    pb.redirectError(java.io.File(stderrFile));
    p = pb.start();
    RAII.cleanUpProcess = onCleanup(@() p.destroy());
    RAII.stdoutFile = onCleanup(@() delete(stdoutFile));
    RAII.stderrFile = onCleanup(@() delete(stderrFile));
    childStdin = java.io.PrintStream(p.getOutputStream());
    childStdin.println(cmd);
    childStdin.close();
else
    % TODO: Fill in Windows implementation here    
end

% Poll instead of waitFor() so Ctrl-C stays live
% This try/catch mechanism is lousy, but there is no isFinished() method.
% Could be done more cleanly with a Java worker that did waitFor() on a
% separate thread, and have the GUI event thread interrupt it on Ctrl-C.
status = [];
while true
    try
        status = p.exitValue();
        % If that returned, it means the process is finished
        break;
    catch err
        if isequal(err.identifier, 'MATLAB:Java:GenericException') ...
                && isa(err.ExceptionObject, 'java.lang.IllegalThreadStateException')
            % Means child process is still running
            % (Seriously, java.lang.Process, no "bool isFinished()"?
            % Just continue
        else
            rethrow(err);
        end
    end
    % Pause to allow UI event processing, including Ctrl-C
    pause(.01);
end

% Collect output
out = slurpfile(stdoutFile);
errout = slurpfile(stderrFile);
end

function out = slurpfile(file)
fid = fopen(file, 'r');
RAII.fid = onCleanup(@() fclose(fid));
out = fread(fid, 'char=>char')'; %'
end

我尽力测试了一下,看起来它确实将子进程的输出与键盘输入到Matlab IDE分开。键盘输入被缓存并作为额外的命令在systemwithjava()返回后执行。Ctrl-C保持活动状态并会中断函数,让子进程被杀死。

你的评论“将输出重定向到文件”给了我一个想法,可以将STDIN重定向。如果我在我的find命令末尾添加< /dev/null,似乎问题已经解决了。你得到了相同的结果吗?(除非磁盘/查找非常缓慢,否则我没有可靠的复现问题的方法) - Samuel O'Malley
啊,好主意!我觉得它有效。这里有一个更简单的重现方法:[ret,out] = system('sleep 2')。如果我这样做并乱按键,输出中会出现垃圾数据。但是使用 [ret,out] = system('sleep 2 < /dev/null'),即使乱按键,我也可以得到干净的输出。 - Andrew Janke
谢谢确认,安德鲁。请随意将其添加到您的答案中,我会将其标记为已接受的答案。 - Samuel O'Malley
安德鲁,如果你在使用 < /dev/null 后回到 [ret,out] = system('sleep 2'),它是否仍然存在相同的问题?对我来说,它似乎可以自行修复,所以我无法重现这个问题。 - Samuel O'Malley
2
哦,我也是这样:一旦我执行了 < /dev/null 一次,后来在同一个会话中的普通 [ret,out] = system('sleep 2') 调用就不再捕获用户键盘输入了。(在 OS X 上使用 R2014a。)我不知道这里发生了什么事情;看起来像是一个 bug。 - Andrew Janke
1
不,这是你的想法,而且它是一个独特的机制:我认为你应该将其作为单独的答案输入,并在最终采用它时自我接受。它可以独立存在。 - Andrew Janke

3

感谢Andrew Janke帮助我找到了这个解决方案。

为了轻松复现错误,我们可以运行以下命令:

[ret, out] = system('sleep 2');

如果我们在程序运行时输入了一些字符,out变量将会被这些字符污染。
解决这个问题的方法是将 stdin 重定向到 /dev/null,如下所示:
[ret, out] = system('sleep 2 < /dev/null');

这可以防止用户输入污染out变量。

有趣的是,这似乎修复了当前MATLAB会话的原始测试用例(在R2014a OSX和R2013b Linux上进行了测试),因此如果我们再次执行[ret, out] = system('sleep 2');,输出将不再被用户输入所污染。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接