java 执行shell命令及日志收集避坑指南

import org.apache.commons.io.FileUtils;

import java.io.BufferedReader;

import java.io.File;

import java.io.IOException;

import java.io.InputStreamReader;

import java.nio.charset.Charset;

import java.util.List;

import java.util.Map;

import java.util.concurrent.ConcurrentHashMap;

import java.util.concurrent.ExecutorService;

import java.util.concurrent.Executors;

/**

  • 功能描述: Shell命令运行工具类封装

*/

@Log4j2

public class ShellCommandExecUtil {

/**

  • @see #runShellCommandSync(String, String[], Charset, String)

*/

public static int runShellCommandSync(String baseShellDir, String[] cmd,

Charset outputCharset) throws IOException {

return runShellCommandSync(baseShellDir, cmd, outputCharset, null);

}

/**

  • 真正运行shell命令

  • @param baseShellDir 运行命令所在目录(先切换到该目录后再运行命令)

  • @param cmd 命令数组

  • @param outputCharset 日志输出字符集,一般windows为GBK, linux为utf8

  • @param logFilePath 日志输出文件路径, 为空则直接输出到当前应用日志中,否则写入该文件

  • @return 进程退出码, 0: 成功, 其他:失败

  • @throws IOException 执行异常时抛出

*/

public static int runShellCommandSync(String baseShellDir, String[] cmd,

Charset outputCharset, String logFilePath)

throws IOException {

long startTime = System.currentTimeMillis();

boolean needReadProcessOutLogStreamByHand = true;

log.info(“【cli】receive new Command. baseDir: {}, cmd: {}, logFile:{}”,

baseShellDir, String.join(" ", cmd), logFilePath);

ProcessBuilder pb = new ProcessBuilder(cmd);

pb.directory(new File(baseShellDir));

initErrorLogHolder(logFilePath, outputCharset);

int exitCode = 0;

try {

if(logFilePath != null) {

ensureFilePathExists(logFilePath);

// String redirectLogInfoAndErrCmd = " > " + logFilePath + " 2>&1 ";

// cmd = mergeTwoArr(cmd, redirectLogInfoAndErrCmd.split(“\s+”));

pb.redirectErrorStream(true);

pb.redirectOutput(new File(logFilePath));

needReadProcessOutLogStreamByHand = false;

}

Process p = pb.start();

if(needReadProcessOutLogStreamByHand) {

readProcessOutLogStream(p, outputCharset);

}

try {

p.waitFor();

}

catch (InterruptedException e) {

log.error(“进程被中断”, e);

setProcessLastError(“中断异常:” + e.getMessage());

}

finally {

exitCode = p.exitValue();

log.info(“【cli】process costTime:{}ms, exitCode:{}”,

System.currentTimeMillis() - startTime, exitCode);

}

if(exitCode != 0) {

throw new ShellProcessExecException(exitCode,

“进程返回异常信息, returnCode:” + exitCode

  • “, lastError:” + getProcessLastError());

}

return exitCode;

}

finally {

removeErrorLogHolder();

}

}

/**

  • 使用 Runtime.exec() 运行shell

*/

public static int runShellWithRuntime(String baseShellDir,

String[] cmd,

Charset outputCharset) throws IOException {

long startTime = System.currentTimeMillis();

initErrorLogHolder(null, outputCharset);

Process p = Runtime.getRuntime().exec(cmd, null, new File(baseShellDir));

readProcessOutLogStream(p, outputCharset);

int exitCode;

try {

p.waitFor();

}

catch (InterruptedException e) {

log.error(“进程被中断”, e);

setProcessLastError(“中断异常:” + e.getMessage());

}

catch (Throwable e) {

log.error(“其他异常”, e);

setProcessLastError(e.getMessage());

}

finally {

exitCode = p.exitValue();

log.info(“【cli】process costTime:{}ms, exitCode:{}”,

System.currentTimeMillis() - startTime, exitCode);

}

if(exitCode != 0) {

throw new ShellProcessExecException(exitCode,

“进程返回异常信息, returnCode:” + exitCode

  • “, lastError:” + getProcessLastError());

}

return exitCode;

}

/**

  • 确保文件夹存在

  • @param filePath 文件路径

  • @throws IOException 创建文件夹异常抛出

*/

public static void ensureFilePathExists(String filePath) throws IOException {

File path = new File(filePath);

if(path.exists()) {

return;

}

File p = path.getParentFile();

if(p.mkdirs()) {

log.info(“为文件创建目录: {} 成功”, p.getPath());

return;

}

log.warn(“创建目录:{} 失败”, p.getPath());

}

/**

  • 合并两个数组数据

  • @param arrFirst 左边数组

  • @param arrAppend 要添加的数组

  • @return 合并后的数组

*/

public static String[] mergeTwoArr(String[] arrFirst, String[] arrAppend) {

String[] merged = new String[arrFirst.length + arrAppend.length];

System.arraycopy(arrFirst, 0,

merged, 0, arrFirst.length);

System.arraycopy(arrAppend, 0,

merged, arrFirst.length, arrAppend.length);

return merged;

}

/**

  • 删除以某字符结尾的字符

  • @param originalStr 原始字符

  • @param toTrimChar 要检测的字

  • @return 裁剪后的字符串

*/

public static String trimEndsWith(String originalStr, char toTrimChar) {

char[] value = originalStr.toCharArray();

int i = value.length - 1;

while (i > 0 && value[i] == toTrimChar) {

i–;

}

return new String(value, 0, i + 1);

}

/**

  • 错误日志读取线程池(不设上限)

*/

private static final ExecutorService errReadThreadPool = Executors.newCachedThreadPool(

new NamedThreadFactory(“ReadProcessErrOut”));

/**

  • 最后一次异常信息

*/

private static final Map<Thread, ProcessErrorLogDescriptor>

lastErrorHolder = new ConcurrentHashMap<>();

/**

  • 主动读取进程的标准输出信息日志

  • @param process 进程实体

  • @param outputCharset 日志字符集

  • @throws IOException 读取异常时抛出

*/

private static void readProcessOutLogStream(Process process,

Charset outputCharset) throws IOException {

try (BufferedReader stdInput = new BufferedReader(new InputStreamReader(

process.getInputStream(), outputCharset))) {

Thread parentThread = Thread.currentThread();

// 另起一个线程读取错误消息,必须先启该线程

errReadThreadPool.submit(() -> {

try {

try (BufferedReader stdError = new BufferedReader(

new InputStreamReader(process.getErrorStream(), outputCharset))) {

String err;

while ((err = stdError.readLine()) != null) {

log.error(“【cli】{}”, err);

setProcessLastError(parentThread, err);

}

}

}

catch (IOException e) {

log.error(“读取进程错误日志输出时发生了异常”, e);

setProcessLastError(parentThread, e.getMessage());

}

});

// 外部线程读取标准输出消息

String stdOut;

while ((stdOut = stdInput.readLine()) != null) {

log.info(“【cli】{}”, stdOut);

}

}

}

/**

  • 新建一个进程错误信息容器

  • @param logFilePath 日志文件路径,如无则为 null

*/

private static void initErrorLogHolder(String logFilePath, Charset outputCharset) {

lastErrorHolder.put(Thread.currentThread(),

new ProcessErrorLogDescriptor(logFilePath, outputCharset));

}

/**

  • 移除错误日志监听

*/

private static void removeErrorLogHolder() {

lastErrorHolder.remove(Thread.currentThread());

}

/**

  • 获取进程的最后错误信息

  •  注意: 该方法只会在父线程中调用
    

*/

private static String getProcessLastError() {

Thread thread = Thread.currentThread();

return lastErrorHolder.get(thread).getLastError();

}

/**

  • 设置最后一个错误信息描述

  •  使用当前线程或自定义
    

*/

private static void setProcessLastError(String lastError) {

lastErrorHolder.get(Thread.currentThread()).setLastError(lastError);

}

private static void setProcessLastError(Thread thread, String lastError) {

lastErrorHolder.get(thread).setLastError(lastError);

}

/**

  • 判断当前系统是否是 windows

*/

public static boolean isWindowsSystemOs() {

return System.getProperty(“os.name”).toLowerCase()

.startsWith(“win”);

}

/**

  • 进程错误信息描述封装类

*/

private static class ProcessErrorLogDescriptor {

/**

  • 错误信息记录文件

*/

private String logFile;

/**

  • 最后一行错误信息

*/

private String lastError;

private Charset charset;

ProcessErrorLogDescriptor(String logFile, Charset outputCharset) {

this.logFile = logFile;

charset = outputCharset;

}

String getLastError() {

if(lastError != null) {

return lastError;

}

try{

if(logFile == null) {

return null;

}

List lines = FileUtils.readLines(

new File(logFile), charset);

StringBuilder sb = new StringBuilder();

for (int i = lines.size() - 1; i >= 0; i–) {

sb.insert(0, lines.get(i) + “\n”);

if(sb.length() > 200) {

break;

}

}

return sb.toString();

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值