Please wait. This can take some minutes ...
Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance.
Project price only 1 $
You can buy this project and download/modify it how often you want.
com.wl4g.component.common.cli.ProcessUtils Maven / Gradle / Ivy
/*
* Copyright 2017 ~ 2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.wl4g.component.common.cli;
import static java.util.Arrays.asList;
import static java.util.Objects.isNull;
import static java.util.Objects.nonNull;
import static java.lang.Runtime.*;
import static java.lang.String.format;
import static java.lang.System.currentTimeMillis;
import static org.apache.commons.lang3.StringUtils.isBlank;
import static org.apache.commons.lang3.SystemUtils.IS_OS_LINUX;
import static org.apache.commons.lang3.SystemUtils.IS_OS_MAC;
import static org.apache.commons.lang3.SystemUtils.IS_OS_WINDOWS;
import static org.apache.commons.lang3.SystemUtils.JAVA_IO_TMPDIR;
import static org.apache.commons.lang3.SystemUtils.USER_NAME;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
import static java.lang.System.*;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.wl4g.component.common.log.SmartLogger;
import static com.wl4g.component.common.io.ByteStreamUtils.*;
import static com.wl4g.component.common.io.FileIOUtils.ensureFile;
import static com.wl4g.component.common.io.FileIOUtils.writeFile;
import static com.wl4g.component.common.lang.Assert2.*;
import static com.wl4g.component.common.lang.SystemUtils2.LOCAL_PROCESS_ID;
import static com.wl4g.component.common.log.SmartLoggerFactory.getLogger;
/**
* Local command process tools.
*
* @author Wangl.sir
* @version v1.0.0 2019-09-08
* @since
*/
public abstract class ProcessUtils {
final protected static SmartLogger log = getLogger(ProcessUtils.class);
/**
* Progress animations chars.
*/
final protected static String[] ANIMATIONS = { "|", "/", "-", "\\" };
/**
* Progress show whole.
*/
final protected static int SHOW_WHOLE = 50;
/**
* Print progress bar.
*
* [progress_demo.sh]:
*
*
* #!/bin/bash
*
* processBar() {
* process=$1 # 当前进度
* whole=$2 # 总进度数
* # 百分比比值(小数)
* percent_ratio=`awk BEGIN'{printf "%.2f", ('$process'/'$whole')}'`
* # 百分比数值
* percent=`awk BEGIN'{printf "%d", (100*'$percent_ratio')}'`
* let index=$((${process}%4))
* arr=( "|" "/" "-" "\\" )
* bar='>'
* for((i=0;i<($percent-1)/2;i++))
* do
* bar="="$bar
* done
* printf "[%-50s][%d%%][%3d/%03d][%c]\r" $bar $percent $process $whole "${arr[$index]}"
* }
*
* whole=200
* process=0
* while [ $process -lt $whole ]
* do
* let process++
* processBar $process $whole
* sleep 0.1
* done
* printf "\n"
*
* [Output]:
* [=================================================>][100%][200/200][|]
*
*
*
* @param title
* Current process show title.
* @param progress
* Current processed number.
* @param whole
* Total process number.
* @param barChar
* Progress bar char.
* @throws Exception
*/
public final static void printProgress(final String title, final int progress, final int whole, final char barChar) {
hasTextOf(title, "title");
notNullOf(barChar, "barChar");
isTrue(progress >= 0 && whole >= 0, format("Illegal arguments, progress: %s, whole: %s", progress, whole));
isTrue(progress <= whole, format("Progress number out of bounds, current progress: %s, whole: %s", progress, whole));
try {
// Progress percent/animation.
Float percent = (float) progress / whole;
String percentStr = new DecimalFormat("0.0").format(percent * 100);
String animation = ANIMATIONS[progress % 4];
// (Linux shell) Use char '\r' beautiful to draw progress
if (IS_OS_LINUX || IS_OS_MAC) {
String bar = ">"; // Progress bar
int showProgress = (int) (percent * SHOW_WHOLE);
for (int i = 0; i < SHOW_WHOLE; i++) {
if (i <= showProgress) {
bar = barChar + bar;
} else {
bar = bar + " ";
}
}
out.printf("[%s][%s][%s%%][%s/%s][%s]\r", title, bar, percentStr, progress, whole, animation);
} else { // (Windows) Simple output progress
out.printf("[%s][%s%%][%s]\r\n", title, percentStr, animation);
}
if (progress == whole) { // Completed?
out.println();
}
} catch (Exception e) {
throw new IllegalStateException(e);
}
}
/**
* Execution multiple row command-line.
*
* @param multiCmd
* multi command string.
* @param pwdDir
* execute context directory.
* @param stdout
* Standard output file.
* @param stderr
* Standard error output file.
* @param append
* It takes effect when one of stdout and stderr exists. It is
* used to set whether output mode will be appended
* @param redirectToNullIfNecessary
* It takes effect when both stdout and stderr are empty, and is
* used to set whether to redirect standard and exception output
* to the operating system virtual (null) file
*
* @return
* @throws IOException
*/
public final static DelegateProcess execMulti(final String multiCmd, File pwdDir, final File stdout, final File stderr,
final boolean append, final boolean redirectToNullIfNecessary) throws IOException {
pwdDir = isNull(pwdDir) ? execScriptTmpDir : pwdDir;
File tmpScript = new File(pwdDir.getAbsoluteFile(),
currentTimeMillis() + ".tmpscript" + "." + (IS_OS_WINDOWS ? "bat" : "sh"));
// Write temporary script.
writeFile(tmpScript, multiCmd, false);
// Processing windows permission is not implemented yet!!!
String callTmpScriptCmd = tmpScript.getAbsolutePath();
if (!IS_OS_WINDOWS) {
callTmpScriptCmd = format("chmod 700 %s && %s", tmpScript.getAbsolutePath(), tmpScript.getAbsolutePath());
}
return execSingle(callTmpScriptCmd, null, stdout, stderr, append, redirectToNullIfNecessary);
}
/**
* Execution single row command-line.
*
* @param singleCmd
* Single row command string.
* @param pwdDir
* execute context directory.
* @param stdout
* Standard output file.
* @param stderr
* Standard error output file.
* @param append
* It takes effect when one of stdout and stderr exists. It is
* used to set whether output mode will be appended
* @param redirectToNullIfNecessary
* It takes effect when both stdout and stderr are empty, and is
* used to set whether to redirect standard and exception output
* to the operating system virtual (null) file
* @return
* @throws IOException
*/
public final static DelegateProcess execSingle(final String singleCmd, final File pwdDir, final File stdout,
final File stderr, final boolean append, final boolean redirectToNullIfNecessary) throws IOException {
Process ps = null;
String[] cmdarray = buildCrossSingleCommands(singleCmd, stdout, stderr, append, redirectToNullIfNecessary);
if (nonNull(pwdDir)) {
state(pwdDir.exists(), format("No such directory for pwdDir:[%s]", pwdDir));
ps = getRuntime().exec(cmdarray, null, pwdDir);
} else {
ps = getRuntime().exec(cmdarray);
}
return new DelegateProcess(pwdDir, asList(cmdarray), stdout, stderr, ps);
}
/**
* Execution simple single row command-line get stdout to string.
*
* @param cmdarray
* @param timeoutMs
* @return
* @throws Exception
*/
public final static String execSimpleString(final String[] cmdarray, long timeoutMs) throws Exception {
Process ps = getRuntime().exec(cmdarray);
ps.waitFor(timeoutMs, TimeUnit.MILLISECONDS);
// Reading stderr & check.
Integer exitValue = null;
String errmsg = null;
try {
exitValue = ps.exitValue();
} catch (Exception e) {
errmsg = format("Exec process timeout for: %sMs, %s", timeoutMs, e.getMessage());
}
if (nonNull(exitValue) && exitValue != 0) {
errmsg = readFullyToString(ps.getErrorStream());
}
if (!isBlank(errmsg)) {
throw new IllegalStateException(errmsg);
}
// Reading stdout
return readFullyToString(ps.getInputStream());
}
/**
* Execution single row command-line.
*
* @param singleCmd
* Single row command string.
* @return
* @throws IOException
*/
public final static Process execSingle(final String singleCmd) throws IOException {
String[] cmdarray = buildCrossSingleCommands(singleCmd, null, null, false, false);
return getRuntime().exec(cmdarray);
}
/**
* Build cross platform single row wide fully qualified command line.
*
* @param cmd
* Execution command string.
* @param append
* Append write?
* @return
*/
public final static String[] buildCrossSingleCommands(final String cmd, final boolean append) {
return buildCrossSingleCommands(cmd, null, null, append, true);
}
/**
* Build cross platform single row wide fully qualified command line.
* Note: Please note the usage order of 2 > & 1 and > out. The following are
* the test results under Ubuntu 19.x/CentOS 7.x/CentOS 6.x testing
* example:
*
*
*
* Negative
* Command
* Run in Ubuntu 19.x
* Run in CentOS 6.x
* Run in CentOS 7.x
*
*
* ech "This a wrong test command" 2>&1 >
* out
* ×
* ×
* ×
*
*
* ech "This a wrong test command" > out 2>&1
* ✅
* ✅
* ✅
*
*
*
* @param cmd
* Execution command string.
* @param stdout
* Standard output file.
* @param stderr
* Standard error output file.
* @param append
* It takes effect when one of stdout and stderr exists. It is
* used to set whether output mode will be appended
* @param redirectToNullIfNecessary
* It takes effect when both stdout and stderr are empty, and is
* used to set whether to redirect standard and exception output
* to the operating system virtual (null) file
* @return
*/
public final static String[] buildCrossSingleCommands(final String cmd, final File stdout, final File stderr,
final boolean append, boolean redirectToNullIfNecessary) {
hasText(cmd, "Execute command can't empty.");
StringBuffer cmdStr = new StringBuffer(cmd);
String mode = append ? ">>" : ">";
List cmdarray = new ArrayList<>(8);
if (IS_OS_WINDOWS) {
cmdarray.add("C:\\Windows\\System32\\cmd.exe");
cmdarray.add("/c");
// Stdout/Stderr
if (nonNull(stdout)) {
ensureFile(stdout);
// e.g: echo "hello" 1>out.log
cmdStr.append(format(" 1%s%s", mode, stdout.getAbsolutePath()));
redirectToNullIfNecessary = false;
}
if (nonNull(stderr)) {
ensureFile(stderr);
// e.g: echo "hello" 2>err.log
cmdStr.append(format(" 2%s%s", mode, stderr.getAbsolutePath()));
redirectToNullIfNecessary = false;
}
if (redirectToNullIfNecessary) {
// e.g: echo "hello" >>C:\\nul
// To use in poweshell: $null
cmdStr.append(format(" %s C:\\nul", mode));
}
} else {
cmdarray.add("/bin/bash");
cmdarray.add("-c");
// Stdout/Stderr
if (nonNull(stdout)) {
ensureFile(stdout);
// e.g: echo "hello" 1>out.log
cmdStr.append(format(" 1%s%s", mode, stdout.getAbsolutePath()));
redirectToNullIfNecessary = false;
}
if (nonNull(stderr)) {
ensureFile(stderr);
// e.g: echo "hello" 2>err.log
cmdStr.append(format(" 2%s%s", mode, stderr.getAbsolutePath()));
redirectToNullIfNecessary = false;
}
if (redirectToNullIfNecessary) {
// e.g: echo "hello" >>/dev/null
cmdStr.append(format(" %s /dev/null", mode));
}
}
cmdarray.add(cmdStr.toString());
return cmdarray.toArray(new String[] {});
}
/**
* Delegate command process information bean.
*
* @author Wangl.sir <[email protected] , [email protected] >
* @version v1.0.0 2019-10-20
* @since
*/
public static class DelegateProcess extends Process {
/** Process commands */
final private List cmds;
/** Process context directory */
final private File pwdDir;
/** Process commands standard output file */
final private File stdout;
/** Process commands standard error output file */
final private File stderr;
/** Process object */
@JsonIgnore
final transient private Process process;
public DelegateProcess(File pwdDir, List cmds, File stdout, File stderr, Process process) {
notEmpty(cmds, "Execution cmdarray must not be empty");
notNull(process, "Execution process must not be null");
this.pwdDir = pwdDir;
this.cmds = cmds;
this.stdout = stdout;
this.stderr = stderr;
this.process = process;
}
public File getPwdDir() {
return pwdDir;
}
public List getCmds() {
return cmds;
}
public File getStdout() {
return stdout;
}
public File getStderr() {
return stderr;
}
public Process getProcess() {
return process;
}
@Override
public OutputStream getOutputStream() {
return process.getOutputStream();
}
@Override
public InputStream getInputStream() {
return process.getInputStream();
}
@Override
public InputStream getErrorStream() {
return process.getErrorStream();
}
@Override
public int waitFor() throws InterruptedException {
return process.waitFor();
}
@Override
public int exitValue() {
return process.exitValue();
}
@Override
public void destroy() {
process.destroy();
}
}
/**
* Get or create a Java command line execution temporary parent directory.
*
* @param path
* @return
* @throws IOException
*/
private final synchronized static File execScriptTmpDirectory0(String path) {
File scriptTmpDir = null;
try {
scriptTmpDir = new File(JAVA_IO_TMPDIR, path);
if (!scriptTmpDir.exists()) {
state(scriptTmpDir.mkdirs(), "Failed to create temp directory [" + scriptTmpDir.getName() + "]");
}
return scriptTmpDir;
} finally {
if (nonNull(scriptTmpDir)) {
scriptTmpDir.deleteOnExit();
}
}
}
/**
* Java command line execution temporary directory.
*/
final private static File execScriptTmpDir = execScriptTmpDirectory0(
"java_exec_tmpscript_" + USER_NAME + "/" + LOCAL_PROCESS_ID + "." + System.nanoTime());
}