Skip to content

tio-boot 案例 - 执行 shell 命令 ​

java
package nexus.io.deploy.handler;

import nexus.io.deploy.service.ShellService;
import nexus.io.tio.boot.http.TioRequestContext;
import nexus.io.tio.core.Tio;
import nexus.io.tio.http.common.HttpRequest;
import nexus.io.tio.http.common.HttpResponse;
import nexus.io.http.common.sse.SsePacket;
import nexus.io.tio.http.server.util.CORSUtils;
import nexus.io.tio.http.server.util.SseEmitter;
import nexus.io.tio.utils.SystemTimer;

public class ShellHandler {

  public HttpResponse index(HttpRequest request) {
    String shell = request.getBodyString();
    HttpResponse response = TioRequestContext.getResponse();
    CORSUtils.enableCORS(response);
    response.addServerSentEventsHeader();
    Tio.send(request.channelContext, response);
    response.setSend(false);
    Tio.send(request.channelContext, new SsePacket("start", SystemTimer.currTime + ""));
    ShellService shellService = new ShellService();
    try {
      shellService.execute(shell, request.channelContext);
    } finally {
      Tio.send(request.channelContext, new SsePacket("end", SystemTimer.currTime + ""));
      SseEmitter.closeSeeConnection(request.channelContext);
    }

    return response;

  }
}
java
package nexus.io.deploy.service;

import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;

import nexus.io.tio.core.ChannelContext;
import nexus.io.tio.core.Tio;
import nexus.io.http.common.sse.SsePacket;
import nexus.io.tio.utils.crypto.Md5Utils;
import nexus.io.tio.utils.hutool.FileUtil;
import nexus.io.tio.utils.thread.TioThreadUtils;

public class ShellService {

  // 最大允许执行时长(分钟)
  private static final long TIMEOUT_MINUTES = 10;

  public void execute(String shellCode, ChannelContext channelContext) {
    // 1. 写脚本到文件
    String md5 = Md5Utils.getMD5(shellCode);
    File scriptDir = new File("script");
    if (!scriptDir.exists()) {
      scriptDir.mkdirs();
    }
    String suffix = isWindows() ? ".bat" : ".sh";
    String scriptPath = scriptDir.getPath() + File.separator + md5 + suffix;
    File scriptFile = new File(scriptPath);
    try {
      FileUtil.writeString(shellCode, scriptPath, "UTF-8");
      if (!isWindows()) {
        scriptFile.setExecutable(true);
      }
    } catch (IOException e) {
      Tio.send(channelContext, new SsePacket("error", "Save script failed: " + e.getMessage()));
      return;
    }

    // 3. 构造命令,使用绝对路径
    String absolutePath = scriptFile.getAbsolutePath();
    List<String> command = new ArrayList<>();
    if (isWindows()) {
      command.add("cmd");
      command.add("/c");
      command.add(absolutePath);
    } else {
      command.add("sh");
      command.add(absolutePath);
    }

    Process process;
    try {
      ProcessBuilder pb = new ProcessBuilder(command);
      process = pb.start();
    } catch (IOException e) {
      Tio.send(channelContext, new SsePacket("error", "Failed to start process: " + e.getMessage()));
      return;
    }

    // 4. 异步读取 stdout 和 stderr
    Future<?> stdoutFuture = TioThreadUtils.submit(() -> streamToSse(process.getInputStream(), channelContext, "stdout"));
    Future<?> stderrFuture = TioThreadUtils.submit(() -> streamToSse(process.getErrorStream(), channelContext, "stderr"));

    // 5. 等待完成或超时
    boolean finished;
    try {
      finished = process.waitFor(TIMEOUT_MINUTES, TimeUnit.MINUTES);
      if (!finished) {
        process.destroyForcibly();
        Tio.send(channelContext, new SsePacket("error", "Execution timed out and was forcibly terminated."));
      }
    } catch (InterruptedException e) {
      Thread.currentThread().interrupt();
      Tio.send(channelContext, new SsePacket("error", "Execution interrupted: " + e.getMessage()));
      finished = true;
    } finally {
      stdoutFuture.cancel(true);
      stderrFuture.cancel(true);
    }

    // 6. 发送退出码
    try {
      int exitCode = process.exitValue();
      Tio.send(channelContext, new SsePacket("exit", String.valueOf(exitCode)));
    } catch (IllegalThreadStateException ignored) {
    }
  }

  /**
   * 将流中的每一行以 SSE 形式发给客户端
   */
  private void streamToSse(InputStream input, ChannelContext ctx, String eventType) {
    Charset charset = isWindows() ? Charset.forName("GBK") : StandardCharsets.UTF_8;
    try (BufferedReader reader = new BufferedReader(new InputStreamReader(input, charset))) {
      String line;
      while ((line = reader.readLine()) != null) {
        Tio.send(ctx, new SsePacket(eventType, line));
      }
    } catch (IOException ignored) {
    }
  }

  /** 判断是否为 Windows 环境 */
  private boolean isWindows() {
    String os = System.getProperty("os.name");
    return os != null && os.toLowerCase().contains("win");
  }
}
http
POST /api/shell

ping openai.com
curl -X POST --data-binary "ping openai.com" http://localhost:10000/api/shell

MIT Licensed