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.support.cli.GenericProcessManager 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.support.cli;
import com.wl4g.component.common.cli.ssh2.SSH2Holders;
import com.wl4g.component.common.cli.ssh2.SshjHolder.CommandSessionWrapper;
import com.wl4g.component.common.log.SmartLogger;
import com.wl4g.component.common.task.RunnerProperties;
import com.wl4g.component.support.cli.IllegalProcessStateException;
import com.wl4g.component.support.cli.NoSuchProcessException;
import com.wl4g.component.support.cli.TimeoutDestroyProcessException;
import com.wl4g.component.support.cli.command.DestroableCommand;
import com.wl4g.component.support.cli.command.LocalDestroableCommand;
import com.wl4g.component.support.cli.command.RemoteDestroableCommand;
import com.wl4g.component.support.cli.destroy.DestroySignal;
import com.wl4g.component.support.cli.process.DestroableProcess;
import com.wl4g.component.support.cli.process.EthzDestroableProcess;
import com.wl4g.component.support.cli.process.LocalDestroableProcess;
import com.wl4g.component.support.cli.process.SshdDestroableProcess;
import com.wl4g.component.support.cli.process.SshjDestroableProcess;
import com.wl4g.component.support.cli.repository.ProcessRepository;
import com.wl4g.component.support.task.ApplicationTaskRunner;
import ch.ethz.ssh2.Session;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executor;
import java.util.concurrent.TimeUnit;
import org.apache.sshd.client.channel.ChannelExec;
import static com.wl4g.component.common.cli.ProcessUtils.*;
import static com.wl4g.component.common.io.ByteStreamUtils.*;
import static com.wl4g.component.common.lang.Assert2.*;
import static com.wl4g.component.common.lang.Exceptions.getRootCausesString;
import static com.wl4g.component.common.lang.Exceptions.getStackTraceAsString;
import static com.wl4g.component.common.log.SmartLoggerFactory.getLogger;
import static java.lang.String.format;
import static java.lang.System.arraycopy;
import static java.lang.Thread.currentThread;
import static java.lang.Thread.sleep;
import static java.util.Objects.isNull;
import static java.util.Objects.nonNull;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static org.apache.commons.lang3.StringUtils.EMPTY;
import static org.apache.commons.lang3.StringUtils.isBlank;
import static org.springframework.util.ClassUtils.isPresent;
/**
* Abstract generic command-line process management implements.
*
* @author Wangl.sir <[email protected] , [email protected] >
* @version v1.0.0 2019-10-20
* @since
*/
public abstract class GenericProcessManager extends ApplicationTaskRunner implements DestroableProcessManager {
final protected SmartLogger log = getLogger(getClass());
/** Command-line process repository */
final protected ProcessRepository repository;
public GenericProcessManager(ProcessRepository repository) {
super(new RunnerProperties(false, 1));
notNull(repository, "Process repository can't null");
this.repository = repository;
}
@Override
public String execWaitForComplete(DestroableCommand cmd)
throws IllegalProcessStateException, InterruptedException, Exception {
notNull(cmd, "Execution command can't null.");
DestroableProcess dp = null;
if (cmd instanceof LocalDestroableCommand) {
dp = doExecLocal((LocalDestroableCommand) cmd);
} else if (cmd instanceof RemoteDestroableCommand) {
dp = doExecRemote((RemoteDestroableCommand) cmd);
} else {
throw new UnsupportedOperationException(String.format("Unsupported DestroableCommand[%s]", cmd));
}
notNull(dp, "Process not created? An unexpected error!");
// Register process if necessary.
if (!isBlank(cmd.getProcessId())) {
repository.register(cmd.getProcessId(), dp);
}
// Check exited?
try {
// Wait for completed.
dp.waitFor(cmd.getTimeoutMs(), MILLISECONDS);
// Waiting be completed.
Integer exitValue = null;
try {
exitValue = dp.exitValue(); // [Note]: exitCode may be null
} catch (IllegalThreadStateException e) {
throw new IllegalProcessStateException(exitValue,
format("Exec process timeout for: %sMs, %s", cmd.getTimeoutMs(), e.getMessage()));
}
if (isNull(exitValue)) {
// [Fallback]: If the output is not redirected to the local
// file, the execution fails if there is an stderr message
if (!isLocalStderr(cmd)) {
String errmsg = readFullyToString(dp.getStderr());
if (!isBlank(errmsg)) {
throw new IllegalProcessStateException(errmsg);
}
}
} else if (exitValue != 0) {
String errmsg = EMPTY;
try {
// stderr redirected? (e.g: mvn install >/mvn.out 2>&1)
// and will not get the message.
if (isLocalStderr(cmd))
errmsg = format("Could't exec command, more error info refer to: '%s'",
((LocalDestroableCommand) cmd).getStderr());
else
errmsg = readFullyToString(dp.getStderr());
} catch (Exception e) {
errmsg = getRootCausesString(e);
}
throw new IllegalProcessStateException(exitValue, errmsg);
}
return readFullyToString(dp.getStdout());
} catch (IllegalProcessStateException ex) {
throw new IllegalProcessStateException(ex.getExitValue(),
format("Failed to process(%s), commands: [%s], cause by: %s", cmd.getProcessId(), dp.getCommand().getCmd(),
getStackTraceAsString(ex)));
} finally {
// Destroy process.
destroy0(dp, DEFAULT_DESTROY_TIMEOUTMS);
// Cleanup if necessary.
if (!isBlank(cmd.getProcessId())) {
repository.cleanup(cmd.getProcessId());
}
}
}
@Override
public void exec(DestroableCommand cmd, Executor executor, ProcessCallback callback) throws Exception, InterruptedException {
notNull(cmd, "Execution command can't null.");
notNull(executor, "Process excutor can't null.");
notNull(callback, "Process callback can't null.");
DestroableProcess dp = null;
if (cmd instanceof DestroableCommand) {
dp = doExecLocal((LocalDestroableCommand) cmd);
} else if (cmd instanceof RemoteDestroableCommand) {
dp = doExecRemote((RemoteDestroableCommand) cmd);
} else {
throw new UnsupportedOperationException(String.format("Unsupported DestroableCommand[%s]", cmd));
}
notNull(dp, "Process not created? An unexpected error!");
// Register process if necessary.
if (!isBlank(cmd.getProcessId())) {
repository.register(cmd.getProcessId(), dp);
}
// Stderr/Stdout stream process.
CountDownLatch latch = new CountDownLatch(2);
try {
readInputStream(dp.getStderr(), executor, latch, callback, dp, true);
readInputStream(dp.getStdout(), executor, latch, callback, dp, false);
latch.await(cmd.getTimeoutMs(), TimeUnit.MILLISECONDS); // Await-done
} finally {
// Destroy process.
destroy0(dp, DEFAULT_DESTROY_TIMEOUTMS);
// Cleanup if necessary.
if (!isBlank(cmd.getProcessId())) {
repository.cleanup(cmd.getProcessId());
}
}
}
/**
* Execution local command line, callback standard or exception output
*
* @param cmd
* @throws InterruptedException
* @throws Exception
*/
protected DestroableProcess doExecLocal(LocalDestroableCommand cmd) throws InterruptedException, Exception {
log.info("Exec local command: {}", cmd.getCmd());
DelegateProcess ps = execMulti(cmd.getCmd(), cmd.getPwdDir(), cmd.getStdout(), cmd.getStderr(), cmd.isAppend(), false);
return new LocalDestroableProcess(cmd.getProcessId(), cmd, ps);
}
/**
* Execution remote command line, callback standard or exception output
*
* @param cmd
* @throws InterruptedException
* @throws Exception
*/
@SuppressWarnings("unchecked")
protected DestroableProcess doExecRemote(RemoteDestroableCommand cmd) throws InterruptedException, Exception {
log.info("Exec remote command: {}", cmd.getCmd());
return (DestroableProcess) SSH2Holders.getDefault().execWaitForComplete(cmd.getHost(), cmd.getUser(),
cmd.getPemPrivateKey(), cmd.getPassword(), cmd.getCmd(), s -> wrapDestroableProcess(cmd.getProcessId(), cmd, s),
cmd.getTimeoutMs());
}
@Override
public void setDestroable(String processId, boolean destroable) throws NoSuchProcessException {
repository.setDestroable(processId, destroable);
}
/**
* Destroy command-line process.
* There's no guarantee that it will be killed.
*
* @param signal
* Destruction process signal.
* @throws TimeoutDestroyProcessException
* @see {@link ch.ethz.ssh2.Session#close()}
* @see {@link ch.ethz.ssh2.channel.ChannelManager#closeChannel(Channel,String,boolean)}
*/
protected void destroy(DestroySignal signal) throws TimeoutDestroyProcessException {
notNull(signal, "Destroy signal must not be null.");
isTrue(signal.getTimeoutMs() >= DEFAULT_DESTROY_INTERVALMS,
String.format("Destroy timeoutMs must be less than or equal to %s", DEFAULT_DESTROY_INTERVALMS));
// Process wrapper.
DestroableProcess dpw = repository.get(signal.getProcessId());
// Check destroable.
state(dpw.isDestroable(),
String.format("Failed to destroy command process: (%s), because the current destroable state: %s",
signal.getProcessId(), dpw.isDestroable()));
// Destroy process.
if (nonNull(dpw)) {
destroy0(dpw, signal.getTimeoutMs());
repository.cleanup(signal.getProcessId()); // Cleanup
} else {
log.warn("Failed to destroy because processId: {} does not exist or has been destroyed!", signal.getProcessId());
}
}
/**
* Destroy process streams(IN/ERR {@link InputStream} and
* {@link OutputStream}).
*
* @param timeoutMs
*/
private final void destroy0(DestroableProcess dpw, long timeoutMs) {
notNull(dpw, "Destroable process can't null.");
try {
dpw.getStdin().close();
} catch (IOException e) {
log.error("Failed to stdin stream close", e);
}
try {
dpw.getStdout().close();
} catch (IOException e) {
log.error("Failed to stdout stream close", e);
}
try {
dpw.getStderr().close();
} catch (IOException e) {
log.error("Failed to stderr stream close", e);
}
// Destroy force.
for (long i = 0, c = (timeoutMs / DEFAULT_DESTROY_INTERVALMS); (dpw.isAlive() && i < c); i++) {
try {
dpw.destoryForcibly();
if (dpw.isAlive()) { // Failed destroy?
sleep(DEFAULT_DESTROY_INTERVALMS);
}
} catch (Exception e) {
log.error("Failed to destory process.", e);
break;
}
}
// Assertion destroyed?
if (dpw.isAlive()) {
throw new TimeoutDestroyProcessException(
String.format("Still not destroyed '%s', handling timeout", dpw.getCommand().getProcessId()));
}
}
/**
* Submit read {@link InputStream} process task.
*
* @param in
* @param executor
* @param latch
* @param callback
* @param dpw
* @param iserr
* @throws IOException
*/
private final void readInputStream(InputStream in, Executor executor, CountDownLatch latch, ProcessCallback callback,
DestroableProcess dpw, boolean iserr) {
notNull(dpw, "DestroableProcess can't null.");
notNull(in, "Process inputStream can't null");
notNull(callback, "Process callback can't null");
executor.execute(() -> {
try {
int len = 0;
byte[] buf = new byte[DEFAULT_BUFFER_SIZE];
while (dpw.isAlive() && ((len = in.read(buf)) != -1)) {
byte[] data = new byte[len];
arraycopy(buf, 0, data, 0, len);
if (iserr) {
callback.onStderr(data);
} else {
callback.onStdout(data);
}
}
} catch (IOException e) {
throw new IllegalStateException(e);
} finally {
latch.countDown();
}
});
}
/**
* Check {@link LocalDestroableCommand} and has stderr file.
*
* @param command
* @return
*/
private final boolean isLocalStderr(DestroableCommand command) {
return (command instanceof LocalDestroableCommand) && ((LocalDestroableCommand) command).hasStderr();
}
/**
* Wrapper remote destroable process.
*
* @param processId
* @param command
* @param session
* @return
*/
private final DestroableProcess wrapDestroableProcess(String processId, DestroableCommand command, Object session) {
DestroableProcess process = null;
if (isEthzClass && session instanceof Session) {
process = new EthzDestroableProcess(processId, command, (Session) session);
} else if (isSshjClass && session instanceof CommandSessionWrapper) { // Sshj
process = new SshjDestroableProcess(processId, command, (CommandSessionWrapper) session);
} else if (isSshdClass && session instanceof ChannelExec) { // Sshd
process = new SshdDestroableProcess(processId, command, (ChannelExec) session);
} else if (isJschClass && session instanceof Void) { // Jsch
// TODO
}
return notNull(process, "No supported remote process of %s", session);
}
public final static long DEFAULT_DESTROY_INTERVALMS = 200L;
public final static long DEFAULT_DESTROY_TIMEOUTMS = 30 * 1000L;
public final static int DEFAULT_BUFFER_SIZE = 1024 * 4;
public final static boolean isEthzClass;
public final static boolean isSshjClass;
public final static boolean isSshdClass;
public final static boolean isJschClass;
static {
ClassLoader classLoader = currentThread().getContextClassLoader();
isEthzClass = isPresent("ch.ethz.ssh2.Session", classLoader);
isSshjClass = isPresent("net.schmizz.sshj.connection.channel.direct.Session", classLoader);
isSshdClass = isPresent("org.apache.sshd.client.channel.ChannelExec", classLoader);
isJschClass = isPresent("com.jcraft.jsch.JSch", classLoader);
}
}