This commit is contained in:
+68
-7
@@ -34,6 +34,7 @@ import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Objects;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import org.slf4j.Logger;
|
||||
@@ -47,6 +48,7 @@ public class SystemUpdateApplicationService {
|
||||
private static final Logger log = LoggerFactory.getLogger(SystemUpdateApplicationService.class);
|
||||
|
||||
private static final int MAX_MANIFEST_BYTES = 64 * 1024;
|
||||
private static final int MAX_RELEASE_METADATA_BYTES = 16 * 1024;
|
||||
private static final int MAX_RELEASE_API_BYTES = 256 * 1024;
|
||||
private static final int MAX_STATUS_BYTES = 64 * 1024;
|
||||
private static final int MAX_EVENT_LOG_BYTES = 256 * 1024;
|
||||
@@ -418,15 +420,71 @@ public class SystemUpdateApplicationService {
|
||||
}
|
||||
|
||||
private UpdateStatus readStatus() {
|
||||
UpdateStatus resolved;
|
||||
if (!effectiveEnabled()) {
|
||||
return new UpdateStatus("DISABLED", "在线更新未配置", null, null);
|
||||
resolved = new UpdateStatus("DISABLED", "在线更新未配置", null, null);
|
||||
} else {
|
||||
UpdateStatus persisted = readPersistedStatus();
|
||||
if (persisted != null && BUSY_STATES.contains(persisted.state())) {
|
||||
resolved = persisted;
|
||||
} else if (persisted != null && "FAILED".equals(persisted.state()) && hasProcessingRequest()) {
|
||||
resolved = persisted;
|
||||
} else {
|
||||
UpdateStatus queued = pendingStatus();
|
||||
resolved = queued != null
|
||||
? queued
|
||||
: (persisted == null ? new UpdateStatus("IDLE", "尚未执行在线更新", null, null) : persisted);
|
||||
}
|
||||
}
|
||||
return enrichWithEmbeddedReleaseMetadata(resolved);
|
||||
}
|
||||
|
||||
/**
|
||||
* The updater which performed an older release switch may not have known about release notes yet.
|
||||
* New artifacts therefore carry the signed notes inside the release directory as well. Reading that
|
||||
* immutable, artifact-hashed file lets the first application process after an upgrade reconstruct the
|
||||
* terminal audit snapshot without a network request or a database migration.
|
||||
*/
|
||||
private UpdateStatus enrichWithEmbeddedReleaseMetadata(UpdateStatus status) {
|
||||
if (status == null) return null;
|
||||
ReleaseMetadata metadata = readEmbeddedReleaseMetadata();
|
||||
if (metadata == null) return status;
|
||||
String current = currentVersion();
|
||||
boolean matchesCurrent = metadata.version().equals(current);
|
||||
boolean matchesTarget = status.targetVersion() != null && metadata.version().equals(status.targetVersion());
|
||||
if (!matchesCurrent && !matchesTarget) return status;
|
||||
String notes = status.releaseNotes() == null || status.releaseNotes().isBlank()
|
||||
? metadata.releaseNotes() : status.releaseNotes();
|
||||
Instant publishedAt = status.publishedAt() == null ? metadata.publishedAt() : status.publishedAt();
|
||||
if (notes.equals(status.releaseNotes()) && Objects.equals(publishedAt, status.publishedAt())) return status;
|
||||
return new UpdateStatus(status.state(), status.message(), status.targetVersion(), status.updatedAt(),
|
||||
status.downloadedBytes(), status.totalBytes(), status.bytesPerSecond(), status.downloadPercent(),
|
||||
status.restartExpectedSeconds(), status.requestId(), status.action(), notes, publishedAt);
|
||||
}
|
||||
|
||||
private ReleaseMetadata readEmbeddedReleaseMetadata() {
|
||||
Path versionFile = properties.currentVersionFile();
|
||||
if (versionFile == null) return null;
|
||||
Path releaseDirectory = versionFile.toAbsolutePath().normalize().getParent();
|
||||
if (releaseDirectory == null) return null;
|
||||
Path metadataFile = releaseDirectory.resolve("release-metadata.json").normalize();
|
||||
if (!metadataFile.startsWith(releaseDirectory)
|
||||
|| !Files.isRegularFile(metadataFile, LinkOption.NOFOLLOW_LINKS)
|
||||
|| Files.isSymbolicLink(metadataFile)) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
if (Files.size(metadataFile) > MAX_RELEASE_METADATA_BYTES) return null;
|
||||
JsonNode root = objectMapper.readTree(Files.readAllBytes(metadataFile));
|
||||
String version = blank(root.path("version").asText(null));
|
||||
String notes = boundedText(root, "releaseNotes", 4000);
|
||||
Instant publishedAt = parseInstant(root.path("publishedAt").asText(null));
|
||||
if (version == null || !VERSION.matcher(version).matches() || notes == null) return null;
|
||||
return new ReleaseMetadata(version, notes, publishedAt);
|
||||
} catch (IOException | RuntimeException exception) {
|
||||
log.debug("嵌入式 Release 更新日志读取失败:{}", metadataFile, exception);
|
||||
return null;
|
||||
}
|
||||
UpdateStatus persisted = readPersistedStatus();
|
||||
if (persisted != null && BUSY_STATES.contains(persisted.state())) return persisted;
|
||||
if (persisted != null && "FAILED".equals(persisted.state()) && hasProcessingRequest()) return persisted;
|
||||
UpdateStatus queued = pendingStatus();
|
||||
if (queued != null) return queued;
|
||||
return persisted == null ? new UpdateStatus("IDLE", "尚未执行在线更新", null, null) : persisted;
|
||||
}
|
||||
|
||||
private UpdateStatus readPersistedStatus() {
|
||||
@@ -702,6 +760,9 @@ public class SystemUpdateApplicationService {
|
||||
String releaseNotes) {
|
||||
}
|
||||
|
||||
private record ReleaseMetadata(String version, String releaseNotes, Instant publishedAt) {
|
||||
}
|
||||
|
||||
private record UpdateStatus(String state, String message, String targetVersion, Instant updatedAt,
|
||||
Long downloadedBytes, Long totalBytes, Long bytesPerSecond,
|
||||
Integer downloadPercent, Integer restartExpectedSeconds,
|
||||
|
||||
+37
-1
@@ -336,8 +336,44 @@ class SystemUpdateApplicationServiceTest {
|
||||
"Release 1.0.0-preview.43 is running", java.time.Instant.parse("2026-08-18T23:50:00Z"), null, null);
|
||||
}
|
||||
|
||||
@Test
|
||||
void restoresEmbeddedReleaseNotesAfterLegacyUpdaterSwitch() throws Exception {
|
||||
Path inbox = Files.createDirectory(tempDir.resolve("embedded-notes-inbox"));
|
||||
Path versionFile = tempDir.resolve("current/VERSION");
|
||||
Files.createDirectories(versionFile.getParent());
|
||||
Files.writeString(versionFile, "1.0.0-preview.47\n");
|
||||
Files.writeString(versionFile.resolveSibling("release-metadata.json"), """
|
||||
{"version":"1.0.0-preview.47","publishedAt":"2026-08-19T00:28:41.701Z",
|
||||
"releaseNotes":"从已签名制品恢复的更新日志"}
|
||||
""");
|
||||
Path statusFile = tempDir.resolve("embedded-notes-status.json");
|
||||
Files.writeString(statusFile, """
|
||||
{"state":"SUCCEEDED","message":"Release 1.0.0-preview.47 is running",
|
||||
"targetVersion":"1.0.0-preview.47","updatedAt":"2026-08-19T00:30:00Z"}
|
||||
""");
|
||||
AuditService auditService = mock(AuditService.class);
|
||||
when(auditService.recordSystemUpdateTerminal(null, null, "SUCCEEDED", "1.0.0-preview.47",
|
||||
"Release 1.0.0-preview.47 is running", java.time.Instant.parse("2026-08-19T00:30:00Z"),
|
||||
"从已签名制品恢复的更新日志", java.time.Instant.parse("2026-08-19T00:28:41.701Z")))
|
||||
.thenReturn("SYSUPD:embedded-notes");
|
||||
SystemUpdateApplicationService service = serviceForStatus(inbox, statusFile, auditService, versionFile);
|
||||
|
||||
var status = service.status();
|
||||
|
||||
assertEquals("从已签名制品恢复的更新日志", status.releaseNotes());
|
||||
assertEquals(java.time.Instant.parse("2026-08-19T00:28:41.701Z"), status.publishedAt());
|
||||
verify(auditService).recordSystemUpdateTerminal(null, null, "SUCCEEDED", "1.0.0-preview.47",
|
||||
"Release 1.0.0-preview.47 is running", java.time.Instant.parse("2026-08-19T00:30:00Z"),
|
||||
"从已签名制品恢复的更新日志", java.time.Instant.parse("2026-08-19T00:28:41.701Z"));
|
||||
}
|
||||
|
||||
private SystemUpdateApplicationService serviceForStatus(Path inbox, Path statusFile, AuditService auditService) {
|
||||
SystemUpdateProperties properties = new SystemUpdateProperties(true, "1.0.0-preview.43", null,
|
||||
return serviceForStatus(inbox, statusFile, auditService, null);
|
||||
}
|
||||
|
||||
private SystemUpdateApplicationService serviceForStatus(Path inbox, Path statusFile, AuditService auditService,
|
||||
Path versionFile) {
|
||||
SystemUpdateProperties properties = new SystemUpdateProperties(true, "1.0.0-preview.43", versionFile,
|
||||
"https://release.fixture.invalid/", null, null, inbox.resolve("request.json"), statusFile,
|
||||
Duration.ofSeconds(2), Duration.ofSeconds(2), true);
|
||||
AuthorizationService authorization = mock(AuthorizationService.class);
|
||||
|
||||
Reference in New Issue
Block a user