commit db8b40733bf247ef7fa53525599a6a59d999f522 Author: wangbing Date: Mon Mar 17 09:58:17 2025 +0800 开源 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d39192b --- /dev/null +++ b/.gitignore @@ -0,0 +1,20 @@ +target/ +pom.xml.tag +pom.xml.releaseBackup +pom.xml.versionsBackup +pom.xml.next +release.properties +/.idea +*.iml +/.settings +/bin +/gen +/build +/gradle +/classes +.classpath +.project +*.gradle +gradlew +local.properties +node_modules/ \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..f02aae5 --- /dev/null +++ b/README.md @@ -0,0 +1,45 @@ +#### 使用方法 + +```java + + + ${artifactId} + + src/main/java + + src/test/java + + + + xyz.wbsite + deployee-maven-plugin + latest + + ${build.directory} + ${build.finalName}.${project.packaging} + + + + + something + + 10.0.0.1 + + 22 + + root + + *** + + test + + /www + + 8080 + + + + + + +``` \ No newline at end of file diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..48e17f6 --- /dev/null +++ b/pom.xml @@ -0,0 +1,129 @@ + + + 4.0.0 + + xyz.wbsite + deployee-maven-plugin + 2.0.4-SNAPSHOT + maven-plugin + deployee + + + UTF-8 + UTF-8 + 1.8 + true + + + + + + aliyun + Aliyun Repository + default + https://maven.aliyun.com/repository/public + + + + + + aliyun + Aliyun Repository + https://maven.aliyun.com/repository/public + default + + + + + + + repository + snapshots + http://*.*.*.*:8081/repository/maven-snapshots/ + + + repository + releases + http://*.*.*.*:8081/repository/maven-releases/ + + + + + + org.apache.maven + maven-plugin-api + 3.6.3 + + + + org.apache.maven.plugin-tools + maven-plugin-annotations + 3.5.2 + + + + + ch.ethz.ganymed + ganymed-ssh2 + 262 + + + + com.jcraft + jsch + 0.1.54 + + + + + + cn.hutool + hutool-all + 5.8.0.M3 + + + + + + ${artifactId} + + src/main/java + + + src/main/resources + + + + + maven-plugin-plugin + 3.6.0 + + + default-addPluginArtifactMetadata + package + + addPluginArtifactMetadata + + + + default-descriptor + process-classes + + descriptor + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + 1.8 + 1.8 + + + + + diff --git a/src/main/java/xyz/wbsite/deployee/Deploy.java b/src/main/java/xyz/wbsite/deployee/Deploy.java new file mode 100644 index 0000000..b761b69 --- /dev/null +++ b/src/main/java/xyz/wbsite/deployee/Deploy.java @@ -0,0 +1,259 @@ +package xyz.wbsite.deployee; + +import cn.hutool.core.convert.Convert; +import cn.hutool.core.io.resource.ResourceUtil; +import cn.hutool.core.util.StrUtil; +import xyz.wbsite.deployee.config.Server; +import xyz.wbsite.deployee.util.IOUtil; +import xyz.wbsite.deployee.util.NetUtil; +import xyz.wbsite.deployee.util.TaskUtil; +import xyz.wbsite.deployee.util.ssh.JSshClient; +import xyz.wbsite.deployee.util.ssh.SshSimpleProgress; +import org.apache.maven.plugin.AbstractMojo; +import org.apache.maven.plugin.MojoExecutionException; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.concurrent.Callable; +import java.util.concurrent.TimeUnit; + +/** + * @goal deploy + * @execute phase="package" + */ +public class Deploy extends AbstractMojo { + + /** + * @parameter defaultValue="${build.directory}" + */ + private String targetPath; + + /** + * @parameter defaultValue="${build.finalName}.${packaging}" + */ + private String targetFile; + + /** + * @parameter + */ + private Server[] servers; + + public void execute() throws MojoExecutionException { + printLog("------------------------------------------------------------------------"); + + printLog("TargetPath {} ", targetPath); + printLog("TargetFile {} ", targetFile); + + if (servers == null || servers.length == 0) { + throw new MojoExecutionException("Server information not configured"); + } + + for (Server server : servers) { + printLog("Server[{}:{}] start deploying", server.getIp(), server.getDeployPort()); + + File target = new File(targetPath, targetFile); + if (!target.exists()) { + throw new MojoExecutionException("FileNotFoundException: " + target.getAbsolutePath()); + } + + JSshClient client = new JSshClient(server.getIp(), Convert.toInt(server.getPort()), server.getUsername(), server.getPassword()); + if (client.isLogin()) { + printLog("server[{}] login success.", server.getIp()); + } else { + throw new MojoExecutionException(StrUtil.format("server[{}] login failed.", server.getIp())); + } + + // 检测环境是否安装 + printLog("test environment"); + String serverPath = server.getDeployPath() + "/" + server.getName(); + printLog(serverPath); + if (!client.exist(serverPath)) { + try { + printLog("start install jre"); + + printLog("create dir " + serverPath); + boolean dirs = client.createDirs(serverPath); + if (dirs) { + printLog("create dirs[{}] success.", serverPath); + } else { + throw new MojoExecutionException(StrUtil.format("create dirs[{}] failed.", serverPath)); + } + + InputStream stream = ResourceUtil.getStream("jre.tar.gz"); + + client.putFile(stream, serverPath + "/jre.tar.gz", new SshSimpleProgress() { + @Override + public void update(int process) { + System.out.print(StrUtil.format("[INFO] upload jre {}", process + "%\r")); + } + }); + printLog(StrUtil.format("upload jre {}", "finish")); + + printLog("unpackage [jre.tar.gz]"); + client.exec("cd {}", serverPath); + client.exec("tar -zxvf {} -C {}", serverPath + "/jre.tar.gz", serverPath); + + printLog("delete [jre.tar.gz]"); + client.deleteFile(serverPath + "/jre.tar.gz"); + + printLog("create dir [jar]"); + client.createDirs(serverPath + "/jar"); + client.createFile(serverPath + "/jar/" + targetFile); + + printLog("create sh script"); + String sh = IOUtil.toString(ResourceUtil.getStream("service.sh"), StandardCharsets.UTF_8); + sh = StrUtil.replace(sh, "{0}", serverPath); + sh = StrUtil.replace(sh, "{1}", targetFile); + sh = StrUtil.replace(sh, "{2}", server.getDeployActive()); + sh = StrUtil.replace(sh, "{3}", server.getDeployPort()); + if (StrUtil.isNotBlank(server.getDeployActive())) { + client.write(serverPath + "/service-" + server.getDeployActive() + ".sh", sh); + client.exec("chmod +x {}", serverPath + "/service-" + server.getDeployActive() + ".sh"); + // 修复win和unix不兼容字符 + client.win2unix(serverPath + "/service-" + server.getDeployActive() + ".sh"); + } else { + client.write(serverPath + "/service.sh", sh); + client.exec("chmod +x {}", serverPath + "/service.sh"); + // 修复win和unix不兼容字符 + client.win2unix(serverPath + "/service.sh"); + } + + printLog("create service script"); + String ss = IOUtil.toString(ResourceUtil.getStream("service.service"), StandardCharsets.UTF_8); + ss = StrUtil.replace(ss, "{0}", serverPath); + ss = StrUtil.replace(ss, "{1}", targetFile); + if (StrUtil.isNotBlank(server.getDeployActive())) { + ss = StrUtil.replace(ss, "{2}", "-" + server.getDeployActive()); + } else { + ss = StrUtil.replace(ss, "{2}", ""); + } + ss = StrUtil.replace(ss, "{3}", server.getDeployPort()); + client.write("/usr/lib/systemd/system/" + server.getName() + ".service", ss); + // 修复win和unix不兼容字符 + client.win2unix("/usr/lib/systemd/system/" + server.getName() + ".service"); + printLog("create /usr/lib/systemd/system/{}.service success", server.getName()); + + client.exec("systemctl daemon-reload"); + printLog("daemon-reload"); + + client.exec("systemctl start {}.service", server.getName()); + printLog("start {}.service", server.getName()); + + client.exec("systemctl enable {}.service", server.getName()); + printLog("enable {}.service", server.getName()); + + client.exec("firewall-cmd --add-service=http --permanent"); + printLog("enable http service"); + + client.exec("firewall-cmd --zone=public --add-port={}/tcp --permanent", server.getDeployPort()); + printLog("release port {}", server.getDeployPort()); + + client.exec("firewall-cmd --reload"); + printLog("firewall-cmd --reload"); + + printLog("jre[{}] install success", server.getName()); + } catch (IOException e) { + e.printStackTrace(); + throw new MojoExecutionException("install jre error"); + } + } else { + printLog("jre[{}] has been installed", server.getName()); + } + + // 上传jar包 + client.putFile(target.getAbsolutePath(), serverPath + "/jar/" + targetFile, new SshSimpleProgress() { + @Override + public void update(int process) { + System.out.print(StrUtil.format("[INFO] upload package {}", process + "%\r")); + } + }); + printLog(StrUtil.format("upload package {}", "finish")); + + printLog("stop service {}", server.getName()); + client.exec("systemctl stop {}.service", server.getName()); + + try { + TimeUnit.SECONDS.sleep(1); + } catch (InterruptedException e) { + e.printStackTrace(); + } + + printLog("start service {}", server.getName()); + client.exec("systemctl start {}.service", server.getName()); + + final int[] tagIndex = {0}; + TaskUtil.scheduleAtFixedRate(new Runnable() { + @Override + public void run() { + System.out.print("[INFO] check service " + StrUtil.repeat('.', 1 + tagIndex[0] % 3) + "\r"); + tagIndex[0]++; + } + }, 0, 500); + + Object o = TaskUtil.retryTask(new Callable() { + @Override + public Object call() throws Exception { + if (NetUtil.telnetPort(server.getIp(), Convert.toInt(server.getDeployPort()))) { + TaskUtil.cancel(); + System.out.println("[INFO] check service finish"); + printLog("[{}] deploy success", server.getName()); + + printLog("You can execute the following command to operate the service"); + + printLog("========================================"); + printLog(StrUtil.format("systemctl start {}.service", server.getName())); + printLog(StrUtil.format("systemctl stop {}.service", server.getName())); + printLog(StrUtil.format("systemctl status {}.service", server.getName())); + printLog(StrUtil.format("systemctl reload {}.service", server.getName())); + printLog("========================================"); + return new Object(); + } + return null; + } + }, 100); + if (o == null) { + printLog("[{}] deploy failed", server.getName()); + } + } + + printLog("all deploy has finish"); + printLog("------------------------------------------------------------------------"); + } + + public static void main(String[] args) { + System.out.println(StrUtil.format("systemctl start {}.service", "tzwy")); + System.out.println(StrUtil.format("systemctl stop {}.service", "tzwy")); + System.out.println(StrUtil.format("systemctl status {}.service", "tzwy")); + System.out.println(StrUtil.format("systemctl reload {}.service", "tzwy")); + } + + private void printLog(String context, Object... args) { + getLog().info(StrUtil.format(context, args)); + } + + public String getTargetPath() { + return targetPath; + } + + public void setTargetPath(String targetPath) { + this.targetPath = targetPath; + } + + public String getTargetFile() { + return targetFile; + } + + public void setTargetFile(String targetFile) { + this.targetFile = targetFile; + } + + public Server[] getServers() { + return servers; + } + + public void setServers(Server[] servers) { + this.servers = servers; + } +} diff --git a/src/main/java/xyz/wbsite/deployee/Remove.java b/src/main/java/xyz/wbsite/deployee/Remove.java new file mode 100644 index 0000000..56c45a2 --- /dev/null +++ b/src/main/java/xyz/wbsite/deployee/Remove.java @@ -0,0 +1,96 @@ +package xyz.wbsite.deployee; + +import cn.hutool.core.convert.Convert; +import cn.hutool.core.util.StrUtil; +import xyz.wbsite.deployee.config.Server; +import xyz.wbsite.deployee.util.ssh.SshClient; +import org.apache.maven.plugin.AbstractMojo; +import org.apache.maven.plugin.MojoExecutionException; + +/** + * @goal remove + */ +public class Remove extends AbstractMojo { + + /** + * @parameter defaultValue="${build.directory}" + */ + private String targetPath; + + /** + * @parameter defaultValue="${build.finalName}.${packaging}" + */ + private String targetFile; + + /** + * @parameter + */ + private Server[] servers; + + public void execute() throws MojoExecutionException { + printLog("------------------------------------------------------------------------"); + printLog("TargetPath " + targetPath); + printLog("TargetFile " + targetFile); + + if (servers == null || servers.length == 0) { + throw new MojoExecutionException("Server information not configured"); + } + + for (Server server : servers) { + printLog("Server[{}:{}] start remove", server.getIp(), server.getDeployPort()); + + SshClient client = new SshClient(server.getIp(), Convert.toInt(server.getPort()), server.getUsername(), server.getPassword()); + + printLog("stop service {}", server.getName()); + client.exec("systemctl stop {}.service", server.getName()); + + printLog("disable service {}", server.getName()); + client.exec("systemctl disable {}.service", server.getName()); + + printLog("remove service {}", server.getName()); + client.deleteFile("/usr/lib/systemd/system/" + server.getName() + ".service"); + + printLog("daemon-reload"); + client.exec("systemctl daemon-reload"); + + printLog("remove service {}", server.getName()); + + String serverPath = server.getDeployPath() + "/" + server.getName(); + printLog("delete dir {}", serverPath); + client.deleteDirs(serverPath); + + printLog("[{}] remove success", server.getName()); + } + + printLog("all remove has finish"); + printLog("------------------------------------------------------------------------"); + } + + private void printLog(String context, Object... args) { + getLog().info(StrUtil.format(context, args)); + } + + public String getTargetPath() { + return targetPath; + } + + public void setTargetPath(String targetPath) { + this.targetPath = targetPath; + } + + public String getTargetFile() { + return targetFile; + } + + public void setTargetFile(String targetFile) { + this.targetFile = targetFile; + } + + public Server[] getServers() { + return servers; + } + + public void setServers(Server[] servers) { + this.servers = servers; + } +} diff --git a/src/main/java/xyz/wbsite/deployee/config/Server.java b/src/main/java/xyz/wbsite/deployee/config/Server.java new file mode 100644 index 0000000..19c7932 --- /dev/null +++ b/src/main/java/xyz/wbsite/deployee/config/Server.java @@ -0,0 +1,108 @@ +package xyz.wbsite.deployee.config; + +public class Server { + + /** + * @parameter defaultValue="server" + */ + private String name; + + /** + * @parameter defaultValue="127.0.0.1" + */ + private String ip; + + /** + * @parameter defaultValue="22" + */ + private String port; + + /** + * @parameter defaultValue="root" + */ + private String username; + + /** + * @parameter + */ + private String password; + + /** + * @parameter defaultValue="test" + */ + private String deployActive; + + /** + * @parameter defaultValue="/www" + */ + private String deployPath; + + /** + * @parameter defaultValue="8080" + */ + private String deployPort; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getIp() { + return ip; + } + + public void setIp(String ip) { + this.ip = ip; + } + + public String getPort() { + return port; + } + + public void setPort(String port) { + this.port = port; + } + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + + public String getDeployActive() { + return deployActive; + } + + public void setDeployActive(String deployActive) { + this.deployActive = deployActive; + } + + public String getDeployPath() { + return deployPath; + } + + public void setDeployPath(String deployPath) { + this.deployPath = deployPath; + } + + public String getDeployPort() { + return deployPort; + } + + public void setDeployPort(String deployPort) { + this.deployPort = deployPort; + } +} diff --git a/src/main/java/xyz/wbsite/deployee/util/IOUtil.java b/src/main/java/xyz/wbsite/deployee/util/IOUtil.java new file mode 100644 index 0000000..2e7b5e5 --- /dev/null +++ b/src/main/java/xyz/wbsite/deployee/util/IOUtil.java @@ -0,0 +1,38 @@ +package xyz.wbsite.deployee.util; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.Charset; + +/** + * IO流相关操作工具 + * + * @author wangbing + * @version 0.0.1 + * @since 1.8 + */ +public class IOUtil extends cn.hutool.core.io.IoUtil { + + public static String toString(InputStream input, String encoding) throws IOException { + return toString(input, Charset.forName(encoding)); + } + + public static String toString(InputStream input, Charset encoding) throws IOException { + byte[] bytes = toByteArray(input); + return new String(bytes, encoding); + } + + public static byte[] toByteArray(InputStream input) throws IOException { + ByteArrayOutputStream output = new ByteArrayOutputStream(); + + try { + copy(input, output); + return output.toByteArray(); + } catch (Throwable e) { + throw e; + } finally { + output.close(); + } + } +} diff --git a/src/main/java/xyz/wbsite/deployee/util/NetUtil.java b/src/main/java/xyz/wbsite/deployee/util/NetUtil.java new file mode 100644 index 0000000..ae9c93a --- /dev/null +++ b/src/main/java/xyz/wbsite/deployee/util/NetUtil.java @@ -0,0 +1,67 @@ +package xyz.wbsite.deployee.util; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.net.Socket; + +/** + * 网络相关工具 + * + * @author wangbing + * @version 0.0.1 + * @since 1.8 + */ +public class NetUtil extends cn.hutool.core.net.NetUtil { + + /** + * Ping网络地址 + * + * @param ip 地址 + * @return 是否ping通 + */ + public static boolean ping(String ip) { + return cn.hutool.core.net.NetUtil.ping(ip, 200); + } + + /** + * Ping网络地址 + * + * @param ip 地址 + * @return 是否ping通 + */ + public static boolean ping(String ip, int timeout) { + return cn.hutool.core.net.NetUtil.ping(ip, timeout); + } + + /** + * 检测远程主机的端口是否通 + * + * @param ip IP + * @param port 端口 + */ + public static boolean telnetPort(String ip, int port) { + Socket connect = new Socket(); + boolean res = false; + try { + connect.connect(new InetSocketAddress(ip, port), 1000);//建立连接 + //能telnet通返回true,否则返回false + res = connect.isConnected();//通过现有方法查看连通状态 + } catch (IOException e) { + + } finally { + try { + connect.close(); + } catch (IOException e) { + + } + } + return res; + } + + /** + * 获取主机地址 + */ + public static String getHostAddress() { + return getLocalhost().getHostAddress(); + } +} diff --git a/src/main/java/xyz/wbsite/deployee/util/TaskUtil.java b/src/main/java/xyz/wbsite/deployee/util/TaskUtil.java new file mode 100644 index 0000000..b1384e9 --- /dev/null +++ b/src/main/java/xyz/wbsite/deployee/util/TaskUtil.java @@ -0,0 +1,228 @@ +package xyz.wbsite.deployee.util; + +import java.util.Date; +import java.util.Timer; +import java.util.TimerTask; +import java.util.concurrent.Callable; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; + +/** + * 任务/定时任务工具 + * + * @author wangbing + * @version 0.0.1 + * @since 1.8 + */ +public class TaskUtil { + + private static Timer timer = new Timer("TaskUtil", true); + private static ScheduledExecutorService service = Executors.newSingleThreadScheduledExecutor(); + + /** + * @param task 定时任务 + * @param delay 延迟时间 + */ + public static void schedule(TimerTask task, long delay) { + timer.schedule(task, delay); + } + + /** + * @param task 定时任务 + * @param time 执行时间 + */ + public static void schedule(TimerTask task, Date time) { + timer.schedule(task, time); + } + + /** + * @param task, 定时任务 + * @param delay 延迟时间 + * @param period 循环周期(前任务结束-新任务开始) + */ + public static void schedule(TimerTask task, long delay, long period) { + timer.schedule(task, delay, period); + } + + /** + * @param task, 定时任务 + * @param firstTime 执行时间 + * @param period 循环周期 + */ + public static void schedule(TimerTask task, Date firstTime, long period) { + timer.schedule(task, firstTime, period); + } + + /** + * @param task, 定时任务 + * @param delay 延迟时间 + * @param period 循环周期(前任务开始-新任务开始) + */ + public static void scheduleAtFixedRate(TimerTask task, long delay, long period) { + timer.scheduleAtFixedRate(task, delay, period); + } + + /** + * @param task, 定时任务 + * @param firstTime 执行时间 + * @param period 循环周期(前任务开始-新任务开始) + */ + public static void scheduleAtFixedRate(TimerTask task, Date firstTime, long period) { + timer.scheduleAtFixedRate(task, firstTime, period); + } + + /** + * 取消所有任务 + */ + public static void cancel() { + timer.cancel(); + } + + /** + * 从任务中删除已取消 + */ + public static int purge() { + return timer.purge(); + } + + /** + * 执行Runnable任务 + * + * @param runnable Runnable任务 + * @param delay 延时(ms) + */ + public static void schedule(Runnable runnable, long delay) { + service.schedule(runnable, delay, TimeUnit.MILLISECONDS); + } + + /** + * 执行Runnable任务 + * + * @param runnable Runnable任务 + * @param delay 延时 + * @param unit 时间单位 + */ + public static void schedule(Runnable runnable, long delay, TimeUnit unit) { + service.schedule(runnable, delay, unit); + } + + /** + * 执行Runnable任务 + * + * @param callable Callable任务 + * @param delay 延时(ms) + */ + public static ScheduledFuture schedule(Callable callable, long delay) { + return service.schedule(callable, delay, TimeUnit.MILLISECONDS); + } + + /** + * 执行Runnable任务 + * + * @param callable Callable任务 + * @param delay 延时 + * @param unit 时间单位 + */ + public static ScheduledFuture schedule(Callable callable, long delay, TimeUnit unit) { + return service.schedule(callable, delay, unit); + } + + /** + * 执行Runnable任务 + * + * @param runnable Runnable任务 + * @param initialDelay 延迟执行时间 + * @param period 循环周期(前任务开始-新任务开始) + * @param unit 时间单位 + */ + public static ScheduledFuture scheduleAtFixedRate(Runnable runnable, long initialDelay, long period, TimeUnit unit) { + return service.scheduleAtFixedRate(runnable, initialDelay, period, unit); + } + + /** + * 执行Runnable任务 + * + * @param runnable Runnable任务 + * @param initialDelay 延迟执行时间 + * @param period 循环周期(前任务开始-新任务开始)ms + */ + public static ScheduledFuture scheduleAtFixedRate(Runnable runnable, long initialDelay, long period) { + return service.scheduleAtFixedRate(runnable, initialDelay, period, TimeUnit.MILLISECONDS); + } + + /** + * 执行Runnable任务 + * + * @param runnable Runnable任务 + * @param initialDelay 延迟执行时间 + * @param delay 循环周期(前任务结束-新任务开始) + * @param unit 时间单位 + */ + public static ScheduledFuture scheduleWithFixedDelay(Runnable runnable, long initialDelay, long delay, TimeUnit unit) { + return service.scheduleAtFixedRate(runnable, initialDelay, delay, unit); + } + + /** + * 执行Runnable任务 + * + * @param runnable Runnable任务 + * @param initialDelay 延迟执行时间 + * @param delay 循环周期(前任务结束-新任务开始)ms + */ + public static ScheduledFuture scheduleWithFixedDelay(Runnable runnable, long initialDelay, long delay) { + return service.scheduleAtFixedRate(runnable, initialDelay, delay, TimeUnit.MILLISECONDS); + } + + /** + * 以获取结果为目的并支持重试的任务,通常为有失败风险的IO或异步操作 + * + * @param runnable 任务 + * @param 结果 + * @param interval 重试间隔 + */ + public static T retryTask(Callable runnable, int maxTryCount, long interval) { + for (int i = 0; i < maxTryCount; i++) { + Future submit = service.submit(runnable); + try { + T t = submit.get(); + if (t != null) { + return t; + } + // 控制重试时间间隔 + if (i < maxTryCount - 1) { + TimeUnit.MILLISECONDS.sleep(interval); + } + } catch (Exception e) { + e.printStackTrace(); + } + } + + return null; + } + + public static T retryTask(Callable runnable) { + return retryTask(runnable, 3, 3000); + } + + public static T retryTask(Callable runnable, int maxTryCount) { + return retryTask(runnable, maxTryCount, 3000); + } + + public static void repeatTask(Runnable runnable, int repeatCount) { + repeatTask(runnable, repeatCount, 0); + } + + public static void repeatTask(Runnable runnable, int repeatCount, long interval) { + for (int i = 0; i < repeatCount; i++) { + service.submit(runnable); + try { + TimeUnit.MILLISECONDS.sleep(interval); + } catch (Exception e) { + e.printStackTrace(); + } + } + } +} diff --git a/src/main/java/xyz/wbsite/deployee/util/ssh/JSshClient.java b/src/main/java/xyz/wbsite/deployee/util/ssh/JSshClient.java new file mode 100644 index 0000000..d528f14 --- /dev/null +++ b/src/main/java/xyz/wbsite/deployee/util/ssh/JSshClient.java @@ -0,0 +1,606 @@ +package xyz.wbsite.deployee.util.ssh; + +import cn.hutool.core.io.FileUtil; +import cn.hutool.core.io.IoUtil; +import cn.hutool.core.io.StreamProgress; +import cn.hutool.core.util.IdUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.extra.ssh.ChannelType; +import cn.hutool.log.StaticLog; +import com.jcraft.jsch.*; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.util.Properties; + +/** + * ssh连接客户端 + */ +public class JSshClient { + private static final long DEFAULT_TIMEOUT = 5 * 60 * 1000; + /** + * 服务器IP + */ + private String ip; + /** + * 服务器远程端口 + */ + private int port; + /** + * 登录名称 + */ + private String username; + /** + * 登录密码 + */ + private String password; + /** + * 是否登录 + */ + private boolean isLogin; + /** + * 是否打印调试日志 + */ + private boolean debug; + + private String CHAR_SET = "UTF-8"; + private Session session; + + private ChannelSftp channelSftp; + + public JSshClient(String ip, int port) { + this.ip = ip; + this.port = port; + this.login(); + } + + public JSshClient(String ip, int port, String username, String password) { + this(ip, port, username, password, false); + } + + public JSshClient(String ip, int port, String username, String password, boolean debug) { + this.ip = ip; + this.port = port; + this.username = username; + this.password = password; + this.debug = debug; + this.login(); + } + + public boolean login(String username, String password) { + this.username = username; + this.password = password; + return login(); + } + + private boolean login() { + try { + if (isLogin) return true; + JSch jsch = new JSch(); + session = jsch.getSession(username, ip, port); + session.setPassword(password); + Properties config = new Properties(); + config.put("StrictHostKeyChecking", "no"); + session.setConfig(config); + printLog("Login..."); + try { + session.connect(); + isLogin = true; + } catch (JSchException e) { + e.printStackTrace(); + printLog("Connect time out"); + isLogin = false; + } + return isLogin; + } catch (Exception e) { + printLog("Login failed"); + return false; + } + } + + public ChannelSftp getScpClient() { + if (channelSftp == null) { + try { + channelSftp = (ChannelSftp) session.openChannel(ChannelType.SFTP.getValue()); + channelSftp.connect(); + } catch (Exception e) { + e.printStackTrace(); + } + } + return channelSftp; + } + + public synchronized String exec(String cmd, String... arg) { + return exec(cmd, DEFAULT_TIMEOUT, arg); + } + + public synchronized String exec(String cmd, long timeout, String... arg) { + if (!isLogin) return "[error]客户端未登录"; + try { + printLog("执行命令:" + StrUtil.format(cmd, arg)); + // 打开执行shell指令的通道 + ChannelExec channelExec = (ChannelExec) session.openChannel(ChannelType.EXEC.getValue()); + channelExec.setCommand(StrUtil.format(cmd, arg)); + channelExec.connect(); + return getResult(channelExec); + } catch (Exception e) { + e.printStackTrace(); + return "[error]"; + } + } + + /** + * 上传文件 + * + * @param localFile 本地文件 + * @param remoteFile 远程文件 + */ + public boolean putFile(String localFile, String remoteFile) { + return putFile(localFile, remoteFile, new SshSimpleProgress() { + @Override + public void update(int process) { + + } + }); + } + + /** + * 上传文件 + * + * @param localFile 本地文件 + * @param remoteFile 远程文件 + * @param progress 进度监听 + */ + public boolean putFile(String localFile, String remoteFile, SshSimpleProgress progress) { + return putFile(localFile, remoteFile, new SshStreamProgressWrapper(progress)); + } + + /** + * 上传文件 + * + * @param localFile 本地文件 + * @param remoteFile 远程文件 + * @param progress 进度监听 + */ + public boolean putFile(String localFile, String remoteFile, StreamProgress progress) { + if (!isLogin()) return false; + try { + getScpClient().put(localFile, remoteFile, new SftpProgressMonitor() { + long max; + long sum; + + @Override + public void init(int op, String src, String dest, long max) { + progress.start(); + this.max = max; + } + + @Override + public boolean count(long count) { + sum += count; + progress.progress(this.max, sum); + return sum < max; + } + + @Override + public void end() { + progress.finish(); + } + }, ChannelSftp.OVERWRITE); + return true; + } catch (SftpException e) { + e.printStackTrace(); + System.err.println("upload file failed"); + return false; + } + } + + /** + * 上传文件流 + * + * @param inputStream 本地文件流 + * @param remoteFile 远程文件 + */ + public boolean putFile(InputStream inputStream, String remoteFile) { + return putFile(inputStream, remoteFile, new SshSimpleProgress() { + @Override + public void update(int process) { + + } + }); + } + + /** + * 上传文件流 + * + * @param inputStream 本地文件流 + * @param remoteFile 远程文件 + * @param progress 进度监听 + */ + public boolean putFile(InputStream inputStream, String remoteFile, SshSimpleProgress progress) { + return putFile(inputStream, remoteFile, new SshStreamProgressWrapper(progress)); + } + + /** + * 上传文件流 + * + * @param inputStream 本地文件流 + * @param remoteFile 远程文件 + * @param progress 进度监听 + */ + public boolean putFile(InputStream inputStream, String remoteFile, StreamProgress progress) { + if (!isLogin()) return false; + try { + if (this.exist(remoteFile)) this.createFile(remoteFile); + getScpClient().put(inputStream, remoteFile, new SftpProgressMonitor() { + long max; + long sum; + + @Override + public void init(int op, String src, String dest, long max) { + progress.start(); + this.max = max; + if (max == -1) { + try { + this.max = inputStream.available(); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + + @Override + public boolean count(long count) { + sum += count; + progress.progress(this.max, sum); + return sum < max; + } + + @Override + public void end() { + progress.finish(); + } + }, + ChannelSftp.OVERWRITE); + return true; + } catch (Exception e) { + e.printStackTrace(); + System.err.println("upload file failed"); + return false; + } + } + + /** + * 上传文件夹 + * + * @param localPath 本地文件夹 + * @param remotePath 远程文件夹 + */ + public boolean putFiles(String localPath, String remotePath) { + if (!createDirs(remotePath)) return false; + + File local = new File(localPath); + for (File file : local.listFiles()) { + File target = new File(remotePath, file.getName()); + String targetPath = target.getPath().replace('\\', '/'); + if (file.isFile()) { + if (!putFile(file.getAbsolutePath(), targetPath)) return false; + } else { + if (!putFiles(file.getAbsolutePath(), targetPath)) return false; + } + } + return true; + } + + /** + * 下载文件 + * + * @param remoteFile 远程文件 + * @param localFile 本地文件 + */ + public boolean downFile(String remoteFile, String localFile) { + return downFile(remoteFile, new File(localFile)); + } + + /** + * 下载文件 + * + * @param remoteFile 远程文件 + * @param localFile 本地文件 + */ + public boolean downFile(String remoteFile, File localFile) { + return downFile(remoteFile, localFile, new SshSimpleProgress() { + @Override + public void update(int process) { + + } + }); + } + + /** + * 下载文件 + * + * @param remoteFile 远程文件 + * @param localFile 本地文件 + */ + public boolean downFile(String remoteFile, File localFile, SshSimpleProgress progress) { + return downFile(remoteFile, localFile, new SshStreamProgressWrapper(progress)); + } + + /** + * 下载文件 + * + * @param remoteFile 远程文件 + * @param localFile 本地文件 + */ + public boolean downFile(String remoteFile, String localFile, SshSimpleProgress progress) { + return downFile(remoteFile, new File(localFile), progress); + } + + /** + * 下载文件 + * + * @param remoteFile 远程文件 + * @param localFile 本地文件 + */ + public boolean downFile(String remoteFile, String localFile, StreamProgress progress) { + return downFile(remoteFile, new File(localFile), progress); + } + + /** + * 下载文件 + * + * @param remoteFile 远程文件 + * @param localFile 本地文件 + */ + public boolean downFile(String remoteFile, File localFile, StreamProgress progress) { + if (!isLogin()) return false; + try { + InputStream inputStream = getScpClient().get(remoteFile); + // 传输流 + IoUtil.copy(inputStream, FileUtil.getOutputStream(localFile), 0, inputStream.available(), progress); + printLog("下载文件" + localFile.getAbsolutePath()); + return true; + } catch (Exception e) { + e.printStackTrace(); + return false; + } + } + + public boolean exist(String fileOrPath) { + String exec = this.exec("ls -l " + fileOrPath); + printLog("执行结果:" + exec); + return !exec.startsWith("[error]"); + } + + public boolean createFile(String file) { + String exec = this.exec("touch " + file); + printLog("执行结果:" + exec); + return !exec.startsWith("[error]"); + } + + public boolean createDirs(String path) { + String exec = this.exec("mkdir -p " + path); + printLog("执行结果:" + exec); + return !exec.startsWith("[error]"); + } + + public boolean deleteDirs(String path) { + String exec = this.exec("rm -rf " + path); + printLog("执行结果:" + exec); + return !exec.startsWith("[error]"); + } + + public boolean deleteFile(String file) { + return deleteDirs(file); + } + + public boolean moveFile(String srcPathOrFile, String targetPathOrFile) { + String exec = this.exec(String.format("mv -f %s %s", srcPathOrFile, targetPathOrFile)); + printLog("执行结果:" + exec); + return !exec.startsWith("[error]"); + } + + /** + * 通过关键字关闭程序 + */ + public boolean killByKeyword(String keyword) { + String exec = this.exec("ps -ef | grep " + keyword + " | grep -v grep |awk '{print $2}'| xargs kill -9"); + printLog("执行结果:" + exec); + return !exec.startsWith("[error]"); + } + + /** + * 通过端口关闭程序 + */ + public boolean killByPort(int port) { + String exec = this.exec("kill -9 $(lsof -i:" + port + " -t)"); + printLog("执行结果:" + exec); + return !exec.startsWith("[error]"); + } + + /** + * 文件执行/读写授权 + */ + public boolean chown(String fileOrPath) { + String exec = this.exec("chmod -R 777 " + fileOrPath); + printLog("执行结果:" + exec); + return !exec.startsWith("[error]"); + } + + /** + * 执行shell脚本 + */ + public boolean shell(String shellFile) { + this.exec("chmod +x {}", shellFile); + String exec = this.exec("{}", shellFile); + printLog("执行结果:" + exec); + return !exec.startsWith("[error]"); + } + + /** + * 读取文件并返回内容字符串 + * + * @param remoteFile 远程文件 + */ + public String readAsString(String remoteFile) { + return readAsString(remoteFile, StandardCharsets.UTF_8); + } + + /** + * 读取文件并返回内容字符串 + * + * @param remoteFile 远程文件 + * @param charset 字符编码 + */ + public String readAsString(String remoteFile, Charset charset) { + printLog("下载文件:" + remoteFile); + File tempFile = new File(FileUtil.getTmpDir(), IdUtil.fastSimpleUUID()); + downFile(remoteFile, tempFile); + return FileUtil.readString(tempFile, charset); + } + + /** + * 写入文件 + * + * @param file 文件 + * @param content 内容 + */ + public boolean write(String file, String content) { + return write(file, content, Charset.defaultCharset()); + } + + /** + * 写入文件 + * + * @param file 文件 + * @param content 内容 + */ + public boolean write(String file, String content, Charset charset) { + printLog("写入文件:" + file); + File tempFile = new File(FileUtil.getTmpDir(), IdUtil.fastSimpleUUID()); + FileUtil.writeString(content, tempFile, charset); + try { + return this.putFile(tempFile.getAbsolutePath(), file); + } finally { + tempFile.delete(); + } + } + + /** + * 打包压缩 + * + * @param remoteTar 压缩后的文件名称 通常以.tar.gz结尾 + * @param remoteFile 远程文件或文件夹,支持通配符例如:*.jpg + */ + public boolean tar(String remoteTar, String remoteFile) { + String exec = this.exec("tar -czf {} {}", remoteTar, remoteFile); + printLog("执行结果:" + exec); + return !exec.startsWith("[error]"); + } + + /** + * 压解包解压 + * + * @param remoteTar 压缩后的文件名称 通常以.tar.gz结尾 + * @param path 解压目录 + */ + public boolean untar(String remoteTar, String path) { + String exec = this.exec("cd {} && tar -xzf {}", path, remoteTar); + printLog("执行结果:" + exec); + return !exec.startsWith("[error]"); + } + + /** + * Windows文件格式转Unix文件格式 + * + * @param remoteFile 远程文件 + */ + public boolean win2unix(String remoteFile) { + String exec = this.exec("sed -i 's/\r$//' {}", remoteFile); + printLog("执行结果:" + exec); + return !exec.startsWith("[error]"); + } + + /** + * 获取会话执行结果 + */ + private String getResult(ChannelExec channelExec) { + StringBuffer result = new StringBuffer(); + try { + byte[] tmp = new byte[1024]; + byte[] btmp = new byte[1024]; + StringBuffer iptsb = new StringBuffer(); + StringBuffer errsb = new StringBuffer(); + InputStream iptStream = channelExec.getInputStream(); + // 返回结果流(命令执行错误的信息通过getErrStream获取) + InputStream errStream = channelExec.getErrStream(); + try { + while (true) { + //获取命令执行正确的返回信息 + while (iptStream.available() > 0) { + int i = iptStream.read(btmp, 0, 1024); + if (i < 0) { + break; + } + iptsb.append(new String(btmp, 0, i)); + } + //开始获得SSH命令错误的结果 + while (errStream.available() > 0) { + int i = errStream.read(tmp, 0, 1024); + if (i < 0) { + break; + } + errsb.append(new String(tmp, 0, i)); + } + if (channelExec.isClosed()) { + break; + } + try { + Thread.sleep(200); + } catch (Exception e) { + e.printStackTrace(); + } + } + } finally { + //关闭连接 + channelExec.disconnect(); + } + if (errsb.length() > 0) { + return "[error]" + errsb.toString(); + } else { + return iptsb.toString(); + } + } catch (Exception e) { + System.err.println("解析出错:" + e.getMessage()); + e.printStackTrace(); + } + return result.toString(); + } + + public boolean close() { + if (channelSftp != null) { + channelSftp.disconnect(); + channelSftp = null; + } + if (session != null) { + session.disconnect(); + session = null; + } + return true; + } + + public boolean isLogin() { + return this.isLogin; + } + + private void printLog(String log, String... arg) { + if (debug) { + StaticLog.info("@[{}:{}] " + log, ip, port, arg); + } + } + +} diff --git a/src/main/java/xyz/wbsite/deployee/util/ssh/SshClient.java b/src/main/java/xyz/wbsite/deployee/util/ssh/SshClient.java new file mode 100644 index 0000000..9c34709 --- /dev/null +++ b/src/main/java/xyz/wbsite/deployee/util/ssh/SshClient.java @@ -0,0 +1,547 @@ +package xyz.wbsite.deployee.util.ssh; + +import ch.ethz.ssh2.ChannelCondition; +import ch.ethz.ssh2.Connection; +import ch.ethz.ssh2.SCPClient; +import ch.ethz.ssh2.SCPInputStream; +import ch.ethz.ssh2.SCPOutputStream; +import ch.ethz.ssh2.Session; +import ch.ethz.ssh2.StreamGobbler; +import cn.hutool.core.convert.Convert; +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.io.FileUtil; +import cn.hutool.core.io.IoUtil; +import cn.hutool.core.io.StreamProgress; +import cn.hutool.core.util.ClassUtil; +import cn.hutool.core.util.IdUtil; +import cn.hutool.core.util.StrUtil; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.lang.reflect.Field; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; + +public class SshClient { + private static final long DEFAULT_TIMEOUT = 5 * 60 * 1000; + /** + * 服务器IP + */ + private String ip; + /** + * 服务器远程端口 + */ + private int port; + /** + * 登录名称 + */ + private String username; + /** + * 登录密码 + */ + private String password; + /** + * 是否登录 + */ + private boolean isLogin; + /** + * 是否打印调试日志 + */ + private boolean debug; + + private String CHAR_SET = "UTF-8"; + + private Connection connection; + + private SCPClient scpClient; + + public SshClient(String ip, int port) { + this.ip = ip; + this.port = port; + this.login(); + } + + public SshClient(String ip, int port, String username, String password) { + this(ip, port, username, password, false); + } + + public SshClient(String ip, int port, String username, String password, boolean debug) { + this.ip = ip; + this.port = port; + this.username = username; + this.password = password; + this.debug = debug; + this.login(); + } + + public boolean login(String username, String password) { + this.username = username; + this.password = password; + return login(); + } + + private boolean login() { + try { + if (connection != null) close(); + connection = new Connection(ip, port); + connection.connect();// 连接 + isLogin = connection.authenticateWithPassword(username, password);// 认证 + printLog(isLogin ? "登录成功" : "登录失败"); + return isLogin; + } catch (IOException e) { + printLog("登录失败"); + connection.close(); + return false; + } + } + + public SCPClient getScpClient() { + if (scpClient == null) { + try { + scpClient = connection.createSCPClient(); + } catch (IOException e) { + e.printStackTrace(); + } + } + return scpClient; + } + + public synchronized String exec(String cmd, String... arg) { + return exec(cmd, DEFAULT_TIMEOUT, arg); + } + + public synchronized String exec(String cmd, long timeout, String... arg) { + if (!isLogin) return "[error]客户端未登录"; + try { + printLog("执行命令:" + StrUtil.format(cmd, arg)); + String execResult = null; + Session session = connection.openSession(); + session.execCommand(StrUtil.format(cmd, arg)); + session.waitForCondition(ChannelCondition.EXIT_STATUS, timeout); + execResult = getResult(session); + session.close(); + return execResult; + } catch (IOException e) { + return "[error]"; + } + } + + /** + * 上传文件 + * + * @param localFile 本地文件 + * @param remoteFile 远程文件 + */ + public boolean putFile(String localFile, String remoteFile) { + return putFile(localFile, remoteFile, new SshSimpleProgress() { + @Override + public void update(int process) { + + } + }); + } + + /** + * 上传文件 + * + * @param localFile 本地文件 + * @param remoteFile 远程文件 + */ + public boolean putFile(String localFile, String remoteFile, SshSimpleProgress progress) { + return putFile(localFile, remoteFile, new SshStreamProgressWrapper(progress)); + } + + /** + * 上传文件 + * + * @param localFile 本地文件 + * @param remoteFile 远程文件 + */ + public boolean putFile(String localFile, String remoteFile, StreamProgress progress) { + if (!isLogin()) return false; + try { + File localFileObject = new File(localFile); + File remoteFileObject = new File(remoteFile); + String targetPath = remoteFileObject.getParent().replace('\\', '/'); + SCPOutputStream scpOutputStream = getScpClient().put( + remoteFileObject.getName(), + localFileObject.length(), + targetPath, + "0600"); + IoUtil.copy(FileUtil.getInputStream(localFileObject), scpOutputStream, 0, localFileObject.length(), progress); + return true; + } catch (IOException e) { + System.err.println("创建 SCPClient 错误"); + e.printStackTrace(); + return false; + } + } + + /** + * 上传文件流 + * + * @param inputStream 本地文件流 + * @param remoteFile 远程文件 + */ + public boolean putFile(InputStream inputStream, String remoteFile) { + return putFile(inputStream, remoteFile, new SshSimpleProgress() { + @Override + public void update(int process) { + + } + }); + } + + /** + * 上传文件流 + * + * @param inputStream 本地文件流 + * @param remoteFile 远程文件 + * @param progress 进度监听 + */ + public boolean putFile(InputStream inputStream, String remoteFile, SshSimpleProgress progress) { + return putFile(inputStream, remoteFile, new SshStreamProgressWrapper(progress)); + } + + /** + * 上传文件流 + * + * @param inputStream 本地文件流 + * @param remoteFile 远程文件 + * @param progress 进度监听 + */ + public boolean putFile(InputStream inputStream, String remoteFile, StreamProgress progress) { + if (!isLogin()) return false; + try { + File remoteFileObject = new File(remoteFile); + String targetPath = remoteFileObject.getParent().replace('\\', '/'); + SCPOutputStream scpOutputStream = getScpClient().put( + remoteFileObject.getName(), + inputStream.available(), + targetPath, + "0600"); + IoUtil.copy(inputStream, scpOutputStream, 0, inputStream.available(), progress); + return true; + } catch (IOException e) { + System.err.println("创建 SCPClient 错误"); + e.printStackTrace(); + return false; + } + } + + /** + * 上传文件夹 + * + * @param localPath 本地文件夹 + * @param remotePath 远程文件夹 + */ + public boolean putFiles(String localPath, String remotePath) { + if (!createDirs(remotePath)) return false; + + File local = new File(localPath); + for (File file : local.listFiles()) { + File target = new File(remotePath, file.getName()); + String targetPath = target.getPath().replace('\\', '/'); + if (file.isFile()) { + if (!putFile(file.getAbsolutePath(), targetPath)) return false; + } else { + if (!putFiles(file.getAbsolutePath(), targetPath)) return false; + } + } + return true; + } + + /** + * 下载文件 + * + * @param remoteFile 远程文件 + * @param localFile 本地文件 + */ + public boolean downFile(String remoteFile, String localFile) { + return downFile(remoteFile, new File(localFile)); + } + + /** + * 下载文件 + * + * @param remoteFile 远程文件 + * @param localFile 本地文件 + */ + public boolean downFile(String remoteFile, File localFile) { + return downFile(remoteFile, localFile, new SshSimpleProgress() { + @Override + public void update(int process) { + + } + }); + } + + /** + * 下载文件 + * + * @param remoteFile 远程文件 + * @param localFile 本地文件 + */ + public boolean downFile(String remoteFile, File localFile, SshSimpleProgress progress) { + return downFile(remoteFile, localFile, new SshStreamProgressWrapper(progress)); + } + + /** + * 下载文件 + * + * @param remoteFile 远程文件 + * @param localFile 本地文件 + */ + public boolean downFile(String remoteFile, String localFile, SshSimpleProgress progress) { + return downFile(remoteFile, new File(localFile), progress); + } + + /** + * 下载文件 + * + * @param remoteFile 远程文件 + * @param localFile 本地文件 + */ + public boolean downFile(String remoteFile, String localFile, StreamProgress progress) { + return downFile(remoteFile, new File(localFile), progress); + } + + /** + * 下载文件 + * + * @param remoteFile 远程文件 + * @param localFile 本地文件 + */ + public boolean downFile(String remoteFile, File localFile, StreamProgress progress) { + if (!isLogin()) return false; + try { + SCPInputStream scpInputStream = getScpClient().get(remoteFile); + // 文件有效大小默认为0 + long available = 0; + // SCPInputStream.available()永远为0 + // 因此尝试通过反射获取文件大小 + try { + Field remaining = ClassUtil.getDeclaredField(SCPInputStream.class, "remaining"); + remaining.setAccessible(true); + available = Convert.toLong(remaining.get(scpInputStream), 0L); + } catch (Exception ignored) { + + } + // 传输流 + IoUtil.copy(scpInputStream, FileUtil.getOutputStream(localFile), 0, available, progress); + printLog("下载文件" + localFile.getAbsolutePath()); + return true; + } catch (IOException ioe) { + ioe.printStackTrace(); + return false; + } + } + + public boolean exist(String fileOrPath) { + String exec = this.exec("ls -l " + fileOrPath); + printLog("执行结果:" + exec); + return !exec.startsWith("[error]"); + } + + public boolean createFile(String file) { + String exec = this.exec("touch " + file); + printLog("执行结果:" + exec); + return !exec.startsWith("[error]"); + } + + public boolean createDirs(String path) { + String exec = this.exec("mkdir -p " + path); + printLog("执行结果:" + exec); + return !exec.startsWith("[error]"); + } + + public boolean deleteDirs(String path) { + String exec = this.exec("rm -rf " + path); + printLog("执行结果:" + exec); + return !exec.startsWith("[error]"); + } + + public boolean deleteFile(String file) { + return deleteDirs(file); + } + + public boolean moveFile(String srcPathOrFile, String targetPathOrFile) { + String exec = this.exec(String.format("mv -f %s %s", srcPathOrFile, targetPathOrFile)); + printLog("执行结果:" + exec); + return !exec.startsWith("[error]"); + } + + /** + * 通过关键字关闭程序 + */ + public boolean killByKeyword(String keyword) { + String exec = this.exec("ps -ef | grep " + keyword + " | grep -v grep |awk '{print $2}'| xargs kill -9"); + printLog("执行结果:" + exec); + return !exec.startsWith("[error]"); + } + + /** + * 通过端口关闭程序 + */ + public boolean killByPort(int port) { + String exec = this.exec("kill -9 $(lsof -i:" + port + " -t)"); + printLog("执行结果:" + exec); + return !exec.startsWith("[error]"); + } + + /** + * 文件执行/读写授权 + */ + public boolean chown(String fileOrPath) { + String exec = this.exec("chmod -R 777 " + fileOrPath); + printLog("执行结果:" + exec); + return !exec.startsWith("[error]"); + } + + /** + * 执行shell脚本 + */ + public boolean shell(String shellFile) { + this.exec("chmod +x {}", shellFile); + String exec = this.exec("{}", shellFile); + printLog("执行结果:" + exec); + return !exec.startsWith("[error]"); + } + + /** + * 读取文件并返回内容字符串 + * + * @param remoteFile 远程文件 + */ + public String readAsString(String remoteFile) { + return readAsString(remoteFile, StandardCharsets.UTF_8); + } + + /** + * 读取文件并返回内容字符串 + * + * @param remoteFile 远程文件 + * @param charset 字符编码 + */ + public String readAsString(String remoteFile, Charset charset) { + printLog("下载文件:" + remoteFile); + File tempFile = new File(FileUtil.getTmpDir(), IdUtil.fastSimpleUUID()); + downFile(remoteFile, tempFile); + return FileUtil.readString(tempFile, charset); + } + + /** + * 写入文件 + * + * @param file 文件 + * @param content 内容 + */ + public boolean write(String file, String content) { + return write(file, content, Charset.defaultCharset()); + } + + /** + * 写入文件 + * + * @param file 文件 + * @param content 内容 + */ + public boolean write(String file, String content, Charset charset) { + printLog("写入文件:" + file); + File tempFile = new File(FileUtil.getTmpDir(), IdUtil.fastSimpleUUID()); + FileUtil.writeString(content, tempFile, charset); + try { + return this.putFile(tempFile.getAbsolutePath(), file); + } finally { + tempFile.delete(); + } + } + + /** + * 打包压缩 + * + * @param remoteTar 压缩后的文件名称 通常以.tar.gz结尾 + * @param remoteFile 远程文件或文件夹,支持通配符例如:*.jpg + */ + public boolean tar(String remoteTar, String remoteFile) { + String exec = this.exec("tar -czf {} {}", remoteTar, remoteFile); + printLog("执行结果:" + exec); + return !exec.startsWith("[error]"); + } + + /** + * 压解包解压 + * + * @param remoteTar 压缩后的文件名称 通常以.tar.gz结尾 + * @param path 解压目录 + */ + public boolean untar(String remoteTar, String path) { + String exec = this.exec("cd {} && tar -xzf {}", path, remoteTar); + printLog("执行结果:" + exec); + return !exec.startsWith("[error]"); + } + + /** + * Windows文件格式转Unix文件格式 + * + * @param remoteFile 远程文件 + */ + public boolean win2unix(String remoteFile) { + String exec = this.exec("sed -i 's/\r$//' {}", remoteFile); + printLog("执行结果:" + exec); + return !exec.startsWith("[error]"); + } + + /** + * 获取会话执行结果 + */ + private String getResult(Session session) { + Integer exitStatus = session.getExitStatus(); + StringBuffer result = new StringBuffer(); + InputStream in; + if (exitStatus != null && exitStatus != 0) { // 执行失败 + result.append("[error]"); + in = session.getStderr(); + } else { // 执行成功 + in = session.getStdout(); + } + InputStream stdout = new StreamGobbler(in); + try { + InputStreamReader isr = new InputStreamReader(stdout, CHAR_SET); + char[] bytes = new char[1024]; + int bc; + while ((bc = isr.read(bytes)) != -1) { + result.append(bytes, 0, bc); + } + isr.close(); + } catch (Exception e) { + System.err.println("解析出错:" + e.getMessage()); + e.printStackTrace(); + } + return result.toString(); + } + + public boolean close() { + if (scpClient != null) { + scpClient = null; + } + if (connection != null) { + connection.close(); + connection = null; + } + return true; + } + + public boolean isLogin() { + return this.isLogin; + } + + private void printLog(String log, String... arg) { + if (debug) { + String logPre = StrUtil.format("LOG-[{}:{}] [{}] ", ip, port, DateUtil.now()); + System.out.println(logPre + StrUtil.format(log, arg)); + } + } +} diff --git a/src/main/java/xyz/wbsite/deployee/util/ssh/SshSimpleProgress.java b/src/main/java/xyz/wbsite/deployee/util/ssh/SshSimpleProgress.java new file mode 100644 index 0000000..179a19e --- /dev/null +++ b/src/main/java/xyz/wbsite/deployee/util/ssh/SshSimpleProgress.java @@ -0,0 +1,11 @@ +package xyz.wbsite.deployee.util.ssh; + +public interface SshSimpleProgress { + + /** + * 进度更新(0~100) + * + * @param process 进度 + */ + void update(int process); +} diff --git a/src/main/java/xyz/wbsite/deployee/util/ssh/SshStreamProgressWrapper.java b/src/main/java/xyz/wbsite/deployee/util/ssh/SshStreamProgressWrapper.java new file mode 100644 index 0000000..882b298 --- /dev/null +++ b/src/main/java/xyz/wbsite/deployee/util/ssh/SshStreamProgressWrapper.java @@ -0,0 +1,33 @@ +package xyz.wbsite.deployee.util.ssh; + +import cn.hutool.core.io.StreamProgress; + +/** + * SimpleProgress包装类 + */ +public class SshStreamProgressWrapper implements StreamProgress { + private int val; + private SshSimpleProgress progress; + + public SshStreamProgressWrapper(SshSimpleProgress progress) { + this.progress = progress; + } + + @Override + public void start() { + progress.update(val); + } + + @Override + public void progress(long total, long progressSize) { + int newVal = (int) (progressSize * 100.0 / total); + if (newVal > val) { + progress.update(val = newVal); + } + } + + @Override + public void finish() { + + } +} \ No newline at end of file diff --git a/src/main/resources/jre.tar.gz b/src/main/resources/jre.tar.gz new file mode 100644 index 0000000..66689ca Binary files /dev/null and b/src/main/resources/jre.tar.gz differ diff --git a/src/main/resources/service.service b/src/main/resources/service.service new file mode 100644 index 0000000..452a759 --- /dev/null +++ b/src/main/resources/service.service @@ -0,0 +1,13 @@ +[Unit] +Description={0} Service +After=network.target remote-fs.target nss-lookup.target + +[Service] +Type=forking +ExecStart={0}/service{2}.sh start +ExecReload={0}/service{2}.sh restart +ExecStop={0}/service{2}.sh stop +PrivateTmp=true + +[Install] +WantedBy=multi-user.target diff --git a/src/main/resources/service.sh b/src/main/resources/service.sh new file mode 100644 index 0000000..3f70d66 --- /dev/null +++ b/src/main/resources/service.sh @@ -0,0 +1,77 @@ +#!/bin/bash +# work path +cd $(dirname $0) +#java home +JAVA_HOME=jre +#jar file +APP_NAME=jar/{1} +#active +ACTIVE={2} +#port +PORT={3} +#more arg +APP_ARG= + +usage() { + echo "Usage: sh service-*.sh [start|stop|restart|status]" + exit 1 +} + +is_exist(){ + pid=`ps -ef|grep $APP_NAME|grep -v grep|grep -v /bin/bash|awk '{print $2}' ` + if [ -z "${pid}" ]; then + return 1 + else + return 0 + fi +} + +start(){ + is_exist + if [ $? -eq "0" ]; then + echo "${APP_NAME} is already running. pid=${pid} ." + else + nohup ${JAVA_HOME}/bin/java -Dfile.encoding=UTF-8 -jar $APP_NAME --spring.profiles.active=$ACTIVE --server.port=${PORT} $APP_ARG > /dev/null 2>&1 & + fi +} + +stop(){ + is_exist + if [ $? -eq "0" ]; then + kill -9 $pid + else + echo "${APP_NAME} is not running" + fi +} + +status(){ + is_exist + if [ $? -eq "0" ]; then + echo "${APP_NAME} is running. Pid is ${pid}" + else + echo "${APP_NAME} is NOT running." + fi +} + +restart(){ + stop + start +} + +case "$1" in + "start") + start + ;; + "stop") + stop + ;; + "status") + status + ;; + "restart") + restart + ;; + *) + usage + ;; +esac \ No newline at end of file