XuqmGroup-Server/tenant-service/src/main/java/com/xuqm/tenant/service/SystemUpdateService.java

160 行
6.7 KiB
Java

package com.xuqm.tenant.service;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.util.List;
import java.util.function.Consumer;
@Service
public class SystemUpdateService {
private static final Logger log = LoggerFactory.getLogger(SystemUpdateService.class);
private static final List<String> OTHER_SERVICES = List.of(
"file-service", "tenant-web", "im-service", "push-service", "update-service", "license-service"
);
@Value("${PRIVATE_DEPLOY_ROOT:/opt/xuqm-private}")
private String deployRoot;
public void runUpdate(Consumer<String> emit) {
String composeFile = deployRoot + "/docker-compose.yml";
emit.accept(">>> 拉取最新镜像...");
for (String svc : OTHER_SERVICES) {
if (isRunning(svc)) {
emit.accept(" pulling " + svc + " ...");
exec(emit, "docker-compose", "-f", composeFile, "-p", "xuqm", "pull", "--quiet", svc);
}
}
emit.accept(" pulling tenant-service ...");
exec(emit, "docker-compose", "-f", composeFile, "-p", "xuqm", "pull", "--quiet", "tenant-service");
emit.accept(">>> 镜像拉取完成");
emit.accept(">>> 重启各服务...");
for (String svc : OTHER_SERVICES) {
if (isRunning(svc)) {
emit.accept(" restarting " + svc + " ...");
exec(emit, "docker-compose", "-f", composeFile, "-p", "xuqm",
"up", "-d", "--no-deps", "--force-recreate", svc);
emit.accept(" " + svc + "");
}
}
// tenant-service 自身的重建:不能直接用 docker-compose,因为一旦发出 stop 指令,
// 当前容器(含 docker-compose 进程)会立即被杀死,后续的 rm/create/start 步骤不会执行。
// 解决方案:先用 docker run -d 启动一个独立助手容器,它不依附于 tenant-service,
// 能在 tenant-service 停止后继续完成重建。
emit.accept(">>> 启动自更新助手容器...");
// Resolve the image by inspecting the running container via compose labels,
// so we don't depend on REGISTRY/IMAGE_TAG being present in the container env.
String selfImage = getCurrentImage();
if (selfImage == null) {
emit.accept(">>> [错误] 无法获取当前 tenant-service 镜像名,请检查容器标签或手动执行更新。");
emit.accept("DONE");
return;
}
boolean helperStarted = spawnSelfUpdater(composeFile, selfImage);
if (helperStarted) {
emit.accept(">>> 助手容器已就绪,tenant-service 即将重建(连接将短暂中断)...");
emit.accept("RESTART_SELF");
} else {
emit.accept(">>> [警告] 助手容器启动失败,请手动执行:");
emit.accept(">>> docker-compose -f " + composeFile + " -p xuqm up -d --no-deps --force-recreate tenant-service");
emit.accept("DONE");
}
}
/**
* 启动一个独立的 detached 容器 tenant-service 被停止后重建它
* 助手容器与 tenant-service 无父子关系不会随 tenant-service 终止
*/
private boolean spawnSelfUpdater(String composeFile, String image) {
try {
// 清理上次残留(若有)
new ProcessBuilder("docker", "rm", "-f", "xuqm-self-updater")
.redirectErrorStream(true).start().waitFor();
// 等待 8 秒确保 tenant-service 已完全停止,然后执行 force-recreate
String shellCmd = "sleep 8 && docker-compose -f " + composeFile
+ " -p xuqm up -d --no-deps --force-recreate tenant-service";
Process p = new ProcessBuilder(
"docker", "run", "-d", "--rm",
"--name", "xuqm-self-updater",
"-v", "/var/run/docker.sock:/var/run/docker.sock",
"-v", deployRoot + ":" + deployRoot,
"--entrypoint", "sh",
image,
"-c", shellCmd
).redirectErrorStream(true).start();
String out = new String(p.getInputStream().readAllBytes()).trim();
int code = p.waitFor();
log.info("self-updater spawn: code={} containerId={}", code, out);
return code == 0;
} catch (Exception e) {
log.error("failed to spawn self-updater", e);
return false;
}
}
// Use compose labels instead of container name — works with both Compose v1 (xuqm_svc_1)
// and Compose v2 (xuqm-svc-1) naming conventions.
private boolean isRunning(String service) {
try {
Process p = new ProcessBuilder(
"docker", "ps", "-q",
"--filter", "label=com.docker.compose.project=xuqm",
"--filter", "label=com.docker.compose.service=" + service
).redirectErrorStream(true).start();
String out = new String(p.getInputStream().readAllBytes()).trim();
p.waitFor();
return !out.isEmpty();
} catch (Exception e) {
return false;
}
}
private String getCurrentImage() {
try {
Process p = new ProcessBuilder(
"docker", "ps",
"--filter", "label=com.docker.compose.project=xuqm",
"--filter", "label=com.docker.compose.service=tenant-service",
"--format", "{{.Image}}"
).redirectErrorStream(true).start();
String out = new String(p.getInputStream().readAllBytes()).trim();
p.waitFor();
return out.isEmpty() ? null : out;
} catch (Exception e) {
return null;
}
}
private void exec(Consumer<String> emit, String... cmd) {
try {
Process p = new ProcessBuilder(cmd)
.redirectErrorStream(true)
.start();
try (BufferedReader reader = new BufferedReader(new InputStreamReader(p.getInputStream()))) {
String line;
while ((line = reader.readLine()) != null) {
if (!line.isBlank()) emit.accept(" " + line);
}
}
int code = p.waitFor();
if (code != 0) emit.accept(" [warn] exit code " + code);
} catch (Exception e) {
emit.accept(" [error] " + e.getMessage());
log.error("exec failed: {}", String.join(" ", cmd), e);
}
}
}