从 Kafka 中提取一个执行命令的工具类

背景

  1. 之前在做视频处理相关需求时,经常遇到一些视频存在编解码的问题,需要分析视频的相关信息,因此每次都需要从点播平台找到视频地址,然后再通过本地 ffprobe 命令进行分析
  2. 另外很多视频在封装成标准的 hls 之前,需要转码成 mp4[lib264, acc] 格式,需要通过 ffmpeg 命令进行转码,第一次写转码工具时自己实现了一个执行命令的方法,但是感觉不是很通用。

记得之前看过一些源代码,在kafka 的源代码中有一个 shell 工具类,我觉得可以抽出来放到自己的项目里,只要稍微修改一些代码就行

下面是从 kafka 工具类中抽离出来的执行命令的工具类,包含超时执行命令处理

/**
 * @author fenglh
 * @version 1.0
 * @description 执行命令工具
 * @date 2022/10/21 14:24:07
 */

@Slf4j
public abstract class ExecuteCommandUtil {

    /**
     * 执行命令的名称和参数, 需要子类实现
     */
    protected abstract String[] execString();

    /**
     * 执行结果解析,比如直接转换未字符串输出
     */
    protected abstract void parseExecResult(BufferedReader lines) throws IOException;

    /**
     * 执行命令的超时时间
     */
    private final long timeout;

    /**
     * 命令推出 code 码
     */
    private int exitCode;

    /**
     * 用于执行命令的子进程
     */
    private Process process;

    /**
     *  判断命令是否执行完成的标志
     */
    private volatile AtomicBoolean completed;

    /**
     * @param timeout 毫秒时间,超过设置时间的命令会被杀掉进程,  -1 表示不设置超时时间.
     */
    public ExecuteCommandUtil(long timeout) {
        this.timeout = timeout;
    }

    /**
     * @return 进程退出的 code 码
     */
    public int exitCode() {
        return exitCode;
    }

    /**
     * 获取当前执行命令的子进程
     *
     * @return 执行命令的进程
     */
    public Process process() {
        return process;
    }

    protected void run() throws IOException {
        exitCode = 0; // reset for next run
        runCommand();
    }

    /**
     * 执行命令
     */
    private void runCommand() throws IOException {
        ProcessBuilder builder = new ProcessBuilder(execString());
        Timer timeoutTimer = null;
        // 初始化执行完成状态为未完成
        completed = new AtomicBoolean(false);

        process = builder.start();
        if (timeout > -1) {
            timeoutTimer = new Timer();
            //One time scheduling.
            timeoutTimer.schedule(new ShellTimeoutTimerTask(this), timeout);
        }
        final BufferedReader errReader = new BufferedReader(
                new InputStreamReader(process.getErrorStream(), StandardCharsets.UTF_8));

        final StringBuffer errMsg = new StringBuffer();

        // 使用单独的线程读取错误流,释放缓冲区
        Thread thread = new Thread(() -> {
            try {
                String line = errReader.readLine();
                while ((line != null) && !Thread.currentThread().isInterrupted()) {
                    errMsg.append(line);
                    errMsg.append(System.getProperty("line.separator"));
                    line = errReader.readLine();
                }
            } catch (IOException ioe) {
                log.warn("Error reading the error stream", ioe);
            }
        });
        thread.start();
        // 输出流信息交给子类处理
        BufferedReader inReader = new BufferedReader(
                new InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8));
        try {
            parseExecResult(inReader);
            // 等待进程完成并检查退出状态码
            exitCode = process.waitFor();
            try {
                // 确保错误流处理线程退出
                thread.join();
            } catch (InterruptedException ie) {
                log.warn("Interrupted while reading the error stream", ie);
            }
            // 命令执行完成,设置标志位
            completed.set(true);
            //如果进程未退出,交给 finally 块中的超时线程处理
            if (exitCode != 0) {
                throw new ExitCodeException(exitCode, errMsg.toString());
            }
        } catch (InterruptedException ie) {
            throw new IOException(ie.toString());
        } finally {
            if (timeoutTimer != null) {
                timeoutTimer.cancel();
            }
            // 关闭输入流
            try {
                inReader.close();
            } catch (IOException ioe) {
                log.warn("Error while closing the input stream", ioe);
            }
            if (!completed.get())
                thread.interrupt();

            try {
                errReader.close();
            } catch (IOException ioe) {
                log.warn("Error while closing the error stream", ioe);
            }
            // 执行完命令之后,需要关闭进程
            process.destroy();
        }
    }


    /**
     * 添加了退出状态码的 IOException
     */
    @SuppressWarnings("serial")
    public static class ExitCodeException extends IOException {

        int exitCode;

        public ExitCodeException(int exitCode, String message) {
            super(message);
            this.exitCode = exitCode;
        }

        public int getExitCode() {
            return exitCode;
        }
    }

    /**
     * 一个简单的shell命令执行器。
     *
     * 在以下情况下应使用ShellCommandExecutor
     * 命令不需要显式解析,其中命令、工作目录和环境保持不变。命令的输出按原样存储,输出预计会很小。
     */
    public static class ShellCommandExecutor extends ExecuteCommandUtil {

        private final String[] command;
        private StringBuffer output;

        /**
         * 创建 ShellCommandExecutor 的新实例以执行命令。
         *
         * @param execString 要使用参数执行的命令
         * @param timeout    指定命令执行的超时时间(以毫秒为单位),-1表示不设置超时
         */
        public ShellCommandExecutor(String[] execString, long timeout) {
            super(timeout);
            command = execString.clone();
        }

        /**
         * 执行 shell 命令
         */
        public void execute() throws IOException {
            this.run();
        }

        protected String[] execString() {
            return command;
        }
        /**
         * 将命令执行的输出保存处理
         */
        protected void parseExecResult(BufferedReader reader) throws IOException {
            output = new StringBuffer();
            char[] buf = new char[512];
            int nRead;
            while ((nRead = reader.read(buf, 0, buf.length)) > 0) {
                output.append(buf, 0, nRead);
            }
        }

        /**
         * 返回命令执行输出
         */
        public String output() {
            return (output == null) ? "" : output.toString();
        }

        /**
         * 返回实际命令, 带空格的参数会用引号括起来, 其他参数按原始字符展示
         *
         * @return 执行命令的字符串形式
         */
        public String toString() {
            StringBuilder builder = new StringBuilder();
            String[] args = execString();
            for (String s : args) {
                if (s.indexOf(' ') >= 0) {
                    builder.append('"').append(s).append('"');
                } else {
                    builder.append(s);
                }
                builder.append(' ');
            }
            return builder.toString();
        }
    }

    /**
     * 执行shell命令的静态方法。
     * 大多数简单命令执行,不需要用户实现 ExecuteCommandUtil 接口
     *
     * @param cmd 执行命令
     * @return 执行命令的输出
     */
    public static String execCommand(String... cmd) throws IOException {
        return execCommand(cmd, -1);
    }

    /**
     * 与上面的执行命令方法类似,只不过提供了超时设置
     */
    public static String execCommand(String[] cmd, long timeout) throws IOException {
        ShellCommandExecutor exec = new ShellCommandExecutor(cmd, timeout);
        exec.execute();
        return exec.output();
    }

    /**
     * 计时器,用于 shell 执行超时处理。
     */
    private static class ShellTimeoutTimerTask extends TimerTask {

        private final ExecuteCommandUtil shell;

        public ShellTimeoutTimerTask(ExecuteCommandUtil shell) {
            this.shell = shell;
        }

        @Override
        public void run() {
            Process p = shell.process();
            try {
                p.exitValue();
            } catch (Exception e) {
                //如果进程还未结束执行,直接结束进程处理
                if (p != null && !shell.completed.get()) {
                    p.destroy();
                }
            }
        }
    }
}

有了这个工具类之后,实现在线分析视频信息的接口就非常简单了,这里我写了一个简单的测试 controller, 来测试我们的 ffprobe 命令

    @RequestMapping("v1/video/check/with/command")
    public String getVideoDetailsByCommand(String command, String mediaId) throws Exception {
        GetMezzanineInfoResponse response = aliVideoService.getMezzanineInfo(mediaId);
        if (null == response || null == response.getMezzanine()) {
            return "未获取到源文件";
        }
        String ffrobeCmd = String.format(command, response.getMezzanine().getFileURL());
        return ExecuteCommandUtil.execCommand(ffrobeCmd.split(" "));
    }

总结

其实有的时候,不一定什么代码都要从零开始的,如果有看过的开源代码可以借鉴,拿过来直接改一改可以用是,最好最快的方法,因为好的开源代码往往都经过千锤百炼的,而且这样的代码 bug 也比较少

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值