From 1172f4a98c3b3623bce8c8946f8d8907e57073b4 Mon Sep 17 00:00:00 2001 From: John Niang Date: Wed, 9 Aug 2023 14:04:11 +0800 Subject: [PATCH] Support restarting Halo (#4361) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #### What type of PR is this? /kind feature /area core /milestone 2.9.x #### What this PR does / why we need it: Support restarting Halo and enable restart endpoint by default. Restart endpoint detail: request uri: `/actuator/restart` request method: `POST` Please note that memory usage may slightly increase after restarting Halo. #### Does this PR introduce a user-facing change? ```release-note 支持在线重启 Halo。 ``` --- application/build.gradle | 2 - .../halo/app/actuator/RestartEndpoint.java | 77 +++++++++++++++++++ .../src/main/resources/application.yaml | 2 +- console/src/locales/en.yaml | 8 +- console/src/locales/zh-CN.yaml | 8 +- console/src/locales/zh-TW.yaml | 8 +- .../modules/system/backup/tabs/Restore.vue | 4 +- 7 files changed, 92 insertions(+), 17 deletions(-) create mode 100644 application/src/main/java/run/halo/app/actuator/RestartEndpoint.java diff --git a/application/build.gradle b/application/build.gradle index 47d2d297c5..f3daca0956 100644 --- a/application/build.gradle +++ b/application/build.gradle @@ -52,8 +52,6 @@ dependencies { annotationProcessor "org.springframework.boot:spring-boot-configuration-processor" annotationProcessor "org.springframework:spring-context-indexer" - developmentOnly 'org.springframework.boot:spring-boot-devtools' - testImplementation 'org.springframework.boot:spring-boot-starter-test' testImplementation 'org.springframework.security:spring-security-test' testImplementation 'io.projectreactor:reactor-test' diff --git a/application/src/main/java/run/halo/app/actuator/RestartEndpoint.java b/application/src/main/java/run/halo/app/actuator/RestartEndpoint.java new file mode 100644 index 0000000000..02289ee342 --- /dev/null +++ b/application/src/main/java/run/halo/app/actuator/RestartEndpoint.java @@ -0,0 +1,77 @@ +package run.halo.app.actuator; + +import java.io.Closeable; +import java.io.IOException; +import java.util.Collections; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.actuate.endpoint.annotation.WriteOperation; +import org.springframework.boot.actuate.endpoint.web.annotation.WebEndpoint; +import org.springframework.boot.context.event.ApplicationStartedEvent; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationListener; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.stereotype.Component; +import run.halo.app.Application; + +@WebEndpoint(id = "restart") +@Component +@Slf4j +public class RestartEndpoint implements ApplicationListener { + + private SpringApplication application; + + private String[] args; + + private ConfigurableApplicationContext context; + + @WriteOperation + public Object restart() { + var threadGroup = new ThreadGroup("RestartGroup"); + var thread = new Thread(threadGroup, this::doRestart, "restartMain"); + thread.setDaemon(false); + thread.setContextClassLoader(Application.class.getClassLoader()); + thread.start(); + return Collections.singletonMap("message", "Restarting"); + } + + private synchronized void doRestart() { + log.info("Restarting..."); + if (this.context != null) { + try { + closeRecursively(this.context); + var shutdownHandlers = SpringApplication.getShutdownHandlers(); + if (shutdownHandlers instanceof Runnable runnable) { + // clear closedContext in org.springframework.boot.SpringApplicationShutdownHook + runnable.run(); + } + this.context = this.application.run(args); + log.info("Restarted"); + } catch (Throwable t) { + log.error("Failed to restart.", t); + } + } + } + + private static void closeRecursively(ApplicationContext ctx) { + while (ctx != null) { + if (ctx instanceof Closeable closeable) { + try { + closeable.close(); + } catch (IOException e) { + log.error("Cannot close context: {}", ctx.getId(), e); + } + } + ctx = ctx.getParent(); + } + } + + @Override + public void onApplicationEvent(ApplicationStartedEvent event) { + if (this.context == null) { + this.application = event.getSpringApplication(); + this.args = event.getArgs(); + this.context = event.getApplicationContext(); + } + } +} diff --git a/application/src/main/resources/application.yaml b/application/src/main/resources/application.yaml index b90cb8cb88..0bd9b2e56c 100644 --- a/application/src/main/resources/application.yaml +++ b/application/src/main/resources/application.yaml @@ -64,7 +64,7 @@ management: endpoints: web: exposure: - include: ["health", "info", "startup", "globalinfo", "logfile", "shutdown"] + include: ["health", "info", "startup", "globalinfo", "logfile", "shutdown", "restart"] endpoint: shutdown: enabled: true diff --git a/console/src/locales/en.yaml b/console/src/locales/en.yaml index 68e8aa8f80..429dd1dbbc 100644 --- a/console/src/locales/en.yaml +++ b/console/src/locales/en.yaml @@ -1003,9 +1003,9 @@ core: description: Are you sure you want to delete the backup? restore: title: Restore successfully - description: After successful restore, you need to restart Halo to load the system resources normally. After clicking OK, we will automatically stop running Halo. - shutdown: - toast_success: Requested to shutdown operation + description: After successful restore, you need to restart Halo to load the system resources normally. After clicking OK, we will automatically restart Halo. + restart: + toast_success: Requested to restart list: phases: pending: Pending @@ -1018,7 +1018,7 @@ core: tips: first: 1. The restore process may last for a long time, please do not refresh the page during this period. second: 2. During the restore process, although the existing data will not be cleaned up, if there is a conflict, the data will be overwritten. - third: 3. After the restore is completed, you will be prompted to stop running Halo, and you may need to run it manually after stopping. + third: 3. After the restore is completed, you need to restart Halo to load the system resources normally. complete: Restore completed, waiting for restart... start: Start restore exception: diff --git a/console/src/locales/zh-CN.yaml b/console/src/locales/zh-CN.yaml index 7bb1968c99..608bf39da3 100644 --- a/console/src/locales/zh-CN.yaml +++ b/console/src/locales/zh-CN.yaml @@ -1003,9 +1003,9 @@ core: description: 确定要删除该备份吗? restore: title: 恢复成功 - description: 恢复成功之后,需要重启一下 Halo 才能够正常加载系统资源,点击确定之后我们会自动停止运行 Halo - shutdown: - toast_success: 已请求停止运行 + description: 恢复成功之后,需要重启一下 Halo 才能够正常加载系统资源,点击确定之后我们会自动重启 Halo。 + restart: + toast_success: 已请求重启 list: phases: pending: 准备中 @@ -1018,7 +1018,7 @@ core: tips: first: 1. 恢复过程可能会持续较长时间,期间请勿刷新页面。 second: 2. 在恢复的过程中,虽然已有的数据不会被清理掉,但如果有冲突的数据将被覆盖。 - third: 3. 恢复完成之后会提示停止运行 Halo,停止之后可能需要手动运行。 + third: 3. 恢复完成之后需要重启 Halo 才能够正常加载系统资源。 complete: 恢复完成,等待重启... start: 开始恢复 exception: diff --git a/console/src/locales/zh-TW.yaml b/console/src/locales/zh-TW.yaml index 2c5e4a7b8e..c9118f56b5 100644 --- a/console/src/locales/zh-TW.yaml +++ b/console/src/locales/zh-TW.yaml @@ -1003,9 +1003,9 @@ core: description: 確定要刪除此備份嗎? restore: title: 還原成功 - description: 還原成功後,需要重新啟動 Halo 才能正常載入系統資源,點擊確定後我們會自動停止運行 Halo。 - shutdown: - toast_success: 已請求停止運行 + description: 還原成功後,需要重新啟動 Halo 才能正常載入系統資源,點擊確定之後,我們會自動重啟 Halo。 + restart: + toast_success: 已請求重啟 list: phases: pending: 準備中 @@ -1018,7 +1018,7 @@ core: tips: first: 1. 還原過程可能需要較長時間,期間請勿重新整理頁面。 second: 2. 在還原過程中,雖然已有的資料不會被清除,但若有衝突的資料將被覆蓋。 - third: 3. 還原完成後會提示停止運行 Halo,停止後可能需要手動啟動。 + third: 3. 還原完成後需要重新啟動 Halo 才能正常載入系統資源。 complete: 恢復完成,等待重啟... start: 開始還原 exception: diff --git a/console/src/modules/system/backup/tabs/Restore.vue b/console/src/modules/system/backup/tabs/Restore.vue index 77d02cd3a2..a64485984a 100644 --- a/console/src/modules/system/backup/tabs/Restore.vue +++ b/console/src/modules/system/backup/tabs/Restore.vue @@ -25,8 +25,8 @@ const onUploaded = () => { }; async function handleShutdown() { - await axios.post(`/actuator/shutdown`); - Toast.success(t("core.backup.operations.shutdown.toast_success")); + await axios.post(`/actuator/restart`); + Toast.success(t("core.backup.operations.restart.toast_success")); setTimeout(() => { complete.value = true;