diff --git a/build.gradle.kts b/build.gradle.kts index b91965a..be0f27e 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -7,7 +7,7 @@ plugins { } group = "xyz.thewhitedog9487" -version = "0.4.4" +version = "0.5.0" java { toolchain { diff --git a/src/main/java/xyz/thewhitedog9487/WebAPI/Configuration/OpenAPIConfiguration.java b/src/main/java/xyz/thewhitedog9487/WebAPI/Configuration/OpenAPIConfiguration.java index fdddef3..b758b96 100644 --- a/src/main/java/xyz/thewhitedog9487/WebAPI/Configuration/OpenAPIConfiguration.java +++ b/src/main/java/xyz/thewhitedog9487/WebAPI/Configuration/OpenAPIConfiguration.java @@ -23,7 +23,7 @@ class OpenAPIConfiguration { .contact(new Contact() .name("TheWhiteDog9487") .url("https://www.github.com/TheWhiteDog9487/WebAPI")) - .version("0.4.4") + .version("0.5.0") .license(new License() .name("WTFPL") .url("https://spdx.org/licenses/WTFPL")) ) diff --git a/src/main/java/xyz/thewhitedog9487/WebAPI/Startup/SystemdInstallerManager.java b/src/main/java/xyz/thewhitedog9487/WebAPI/Startup/SystemdInstallerManager.java new file mode 100644 index 0000000..41fa426 --- /dev/null +++ b/src/main/java/xyz/thewhitedog9487/WebAPI/Startup/SystemdInstallerManager.java @@ -0,0 +1,161 @@ +package xyz.thewhitedog9487.WebAPI.Startup; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.CommandLineRunner; +import org.springframework.stereotype.Component; + +import java.lang.reflect.InvocationTargetException; +import java.net.URISyntaxException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; + +@Slf4j +@Component +class SystemdInstallerManager implements CommandLineRunner { + static String TemplateContent = """ + [Unit] + Description=WebAPI + After=network.target + + [Service] + User=%s + WorkingDirectory=%s + ExecStart=%s + Restart=always + Type=simple + + [Install] + WantedBy=multi-user.target + """; + + String CurrentOS = System.getProperty("os.name"); + String CurrentUser = System.getProperty("user.name"); + Path CurrentExecutableFilePath = GetExecutblePath(); + Path WorkingDirectory = CurrentExecutableFilePath.getParent(); + Path ServiceFileDirectory = Path.of("/etc/systemd/system/"); + Path ServiceFileName = Path.of("WebAPI.service"); + String SoftLinkFileName = "Current"; + Path SymbolicLinkPath = WorkingDirectory.resolve(SoftLinkFileName); + + /** + * 检测当前运行环境是否为GraalVM生成的Native Image + */ + boolean IsImageCode(){ + try { + return (boolean) Class.forName("org.graalvm.nativeimage.ImageInfo") + .getMethod("inImageCode") + .invoke(null); + } catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException | ClassNotFoundException e) { + return false; } } + + /** + * 获取当前可执行文件的路径。 + *
+ * 如果是通过GraalVM生成的Native Image,则返回可执行文件的路径 + *
+ * 如果是直接用JRE运行的JAR包,则返回JAR文件的路径 + */ + Path GetExecutblePath(){ + try { + var IsImageCode = IsImageCode(); + log.debug("IsImageCode: {}", IsImageCode); + if( IsImageCode == true ){ + Object ExecutablePath = Class.forName("org.graalvm.nativeimage.ProcessProperties") + .getMethod("getExecutableName") + .invoke(null); + log.debug("ExecutablePath: {}", ExecutablePath); + return Path.of((String) ExecutablePath); } + else{ + log.debug("IsImageCode is false"); + // ↓ 没有错误,但是获取Jar包位置的代码在catch里面,扔一个异常出去以进入catch分支 + // ↓ 这个主要是针对使用GraalVM的JVM而不是Native Image的情况 + throw new RuntimeException("Not Image Code"); } + } catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException | ClassNotFoundException | + RuntimeException e) { + try { + return Path.of(SystemdInstallerManager.class + .getProtectionDomain() + .getCodeSource() + .getLocation() + .toURI() + .getPath()); + } catch (URISyntaxException ex) { + throw new RuntimeException(ex);}}} + + @Override + public void run(String... args) throws Exception { + for (String Argument : args) { + if ( List.of("install", "--install").contains(Argument) == true ) { + /* + 在Native Image / Jar文件旁边生成指向自身的,名为Current的软连接 + 然后在软连接旁生成systemd服务文件,执行目标指向软连接 + */ + log.info("开始安装systemd服务"); + log.debug("SystemProperties.os.name: {}", CurrentOS); + if ( CurrentOS.startsWith("Linux") ) { + log.debug("SystemProperties.user.name: {}", CurrentUser); + if (CurrentUser.equals("root") == false) { + log.error("只有root才有权在系统层级安装systemd服务,但是JVM报告的当前用户是{}", CurrentUser); + throw new IllegalStateException("请使用root用户运行此命令以安装为systemd服务"); } + log.debug("CurrentExecutableFilePath: {}", CurrentExecutableFilePath); + log.debug("WorkingDirectory: {}", WorkingDirectory); + log.debug("SymbolicLinkPath: {}", SymbolicLinkPath); + Files.deleteIfExists(SymbolicLinkPath); + Files.createSymbolicLink(SymbolicLinkPath, CurrentExecutableFilePath); + log.info("成功创建指向当前程序的软链接: {} -> {}", SymbolicLinkPath, CurrentExecutableFilePath); + Path CurrentJavaPath = null; + var IsImageCode = IsImageCode(); + if ( IsImageCode == false ) { + CurrentJavaPath = Path.of("/proc/self/exe").toRealPath(); + log.debug("CurrentJavaPath: {}", CurrentJavaPath); } + var ExecCommand = (IsImageCode == true) ? + SymbolicLinkPath.toString() : + CurrentJavaPath + " -jar " + SymbolicLinkPath.toString(); + log.debug("ExecCommand: {}", ExecCommand); + var ServiceFileContent = String.format( + TemplateContent, + CurrentUser, + WorkingDirectory, + ExecCommand); + log.debug("ServiceFileContent: \n{}", ServiceFileContent); + Files.deleteIfExists( WorkingDirectory.resolve(ServiceFileName) ); + Files.writeString(WorkingDirectory.resolve(ServiceFileName), ServiceFileContent); + log.info("成功创建systemd服务文件: {}", WorkingDirectory.resolve(ServiceFileName)); + Files.deleteIfExists( ServiceFileDirectory.resolve(ServiceFileName) ); + Files.createSymbolicLink(ServiceFileDirectory.resolve(ServiceFileName), WorkingDirectory.resolve(ServiceFileName)); + log.info("成功创建指向服务文件的软链接: {} -> {}", ServiceFileDirectory.resolve(ServiceFileName), WorkingDirectory.resolve(ServiceFileName)); + new ProcessBuilder("systemctl", "daemon-reload").start().waitFor(); + log.info("已完成systemd守护进程重载"); + new ProcessBuilder("systemctl", "enable", ServiceFileName.toString()).start().waitFor(); + log.info("已启用systemd服务: {}", ServiceFileName); + new ProcessBuilder("systemctl", "start", ServiceFileName.toString()).start().waitFor(); + log.info("已启动systemd服务: {}", ServiceFileName); } + else if ( CurrentOS.startsWith("Windows") ) { + log.error("只有Linux才能使用systemd,但是JVM报告的当前系统是{}", CurrentOS); + throw new IllegalStateException("Windows系统不支持systemd服务管理,暂不支持在Windows上自动管理服务。"); } + System.exit(0); + } + else if ( List.of("uninstall", "--uninstall").contains(Argument) == true ) { + log.info("开始卸载systemd服务"); + log.debug("SystemProperties.os.name: {}", CurrentOS); + if ( CurrentOS.startsWith("Windows") ) { + log.error("只有Linux才能使用systemd,但是JVM报告的当前系统是{}", CurrentOS); + throw new IllegalStateException("Windows系统不支持systemd服务管理,暂不支持在Windows上自动管理服务。"); } + else if ( CurrentOS.startsWith("Linux") ) { + if (CurrentUser.equals("root") == false) { + log.error("只有root才有权在系统层级卸载systemd服务,但是JVM报告的当前用户是{}", CurrentUser); + throw new IllegalStateException("请使用root用户运行此命令以卸载systemd服务"); } + new ProcessBuilder("systemctl", "stop", ServiceFileName.toString()).start().waitFor(); + log.info("已停止systemd服务: {}", ServiceFileName); + new ProcessBuilder("systemctl", "disable", ServiceFileName.toString()).start().waitFor(); + log.info("已禁用systemd服务: {}", ServiceFileName); + Files.deleteIfExists(ServiceFileDirectory.resolve(ServiceFileName)); + log.info("成功删除systemd服务文件: {}", ServiceFileDirectory.resolve(ServiceFileName)); + Files.deleteIfExists(WorkingDirectory.resolve(ServiceFileName)); + log.info("成功删除程序旁的服务文件: {}", WorkingDirectory.resolve(ServiceFileName)); + Files.deleteIfExists(SymbolicLinkPath); + log.info("成功删除指向程序自身的软链接: {}", SymbolicLinkPath); + new ProcessBuilder("systemctl", "daemon-reload").start().waitFor(); + log.info("已完成systemd守护进程重载"); } + System.exit(0); } } } } \ No newline at end of file