Exercise Forge round-trip through plugin transfer API

This commit is contained in:
layue13 committed 2026-09-26 01:41:20 +08:00
1 parent f0e9abdd57
commit 2eeb342d74
5 files changed
+146 -29

No files matched your search

+2 -2
View File
@@ -5,7 +5,7 @@ StrataProxy 是面向 Minecraft 1.7.10 Forge 的玩家会话代理。项目正
## 核心边界 ## 核心边界
- **玩家会话**:代理负责入服、连接后端、转发、切换后端和断线清理。玩家身份包含 UUID 与连接代次,避免旧连接的异步结果作用到新连接。在线玩家视图由 Session 持有,监听器只保留按 UUID 查找会话的索引。 - **玩家会话**:代理负责入服、连接后端、转发、切换后端和断线清理。玩家身份包含 UUID 与连接代次,避免旧连接的异步结果作用到新连接。在线玩家视图由 Session 持有,监听器只保留按 UUID 查找会话的索引。
- **服务器目录**:静态配置与插件调用同一个注册接口。目录维护地址、声明的容量,以及本代理连接和入服预留的数量;它不采集后端 CPU、TPS、内存等负载。同名重注册生成新句柄,旧异步结果不能修改新条目;同名同地址的旧连接仍占用容量,地址变更后旧连接只计入旧地址。容量预留同时确定本次连接使用的地址,避免动态更新夹在查询和预留之间时把会话计入错误后端。 - **服务器目录**:静态配置与插件调用同一个注册接口。目录维护地址、声明的容量,以及本代理连接和入服预留的数量;它不采集后端 CPU、TPS、内存等负载,也不提供后端负载观测服务。玩法插件可自行获取其选服所需的数据。同名重注册生成新句柄,旧异步结果不能修改新条目;同名同地址的旧连接仍占用容量,地址变更后旧连接只计入旧地址。容量预留同时确定本次连接使用的地址,避免动态更新夹在查询和预留之间时把会话计入错误后端。
- **插件 API**:插件可查询 `PlayerView`、`ServerView`,动态注册后端,并以一个异步回调决定初始落点。空岛分配、实例唤醒和玩法数据由插件自行实现。单次选服失败或超时只结束当前请求,不停用插件;玩家断线会取消未决请求并移除尚未执行的回调,已启动的插件异步任务只做尽力取消。宿主关闭时会结束未决选服请求并撤销插件注册。插件返回的拒绝原因会作为登录断开消息发给玩家。 - **插件 API**:插件可查询 `PlayerView`、`ServerView`,动态注册后端,并以一个异步回调决定初始落点。空岛分配、实例唤醒和玩法数据由插件自行实现。单次选服失败或超时只结束当前请求,不停用插件;玩家断线会取消未决请求并移除尚未执行的回调,已启动的插件异步任务只做尽力取消。宿主关闭时会结束未决选服请求并撤销插件注册。插件返回的拒绝原因会作为登录断开消息发给玩家。
- **发现方式**:DNS 是独立插件,核心不认识 DNS 或 Agent。配置只启用列出的插件;插件关闭时其注册会清理。普通 DNS 主机名通过独立解析器查询 A 与 AAAA 地址,不使用 JVM 的进程级地址缓存;`localhost` 按系统回环地址解析。最小默认选路按注册顺序选取后端,因此 DNS 插件先注册 IPv4 地址。一次查询失败会保留已有注册,连续三次失败会移除旧地址,恢复解析后重新注册。 - **发现方式**:DNS 是独立插件,核心不认识 DNS 或 Agent。配置只启用列出的插件;插件关闭时其注册会清理。普通 DNS 主机名通过独立解析器查询 A 与 AAAA 地址,不使用 JVM 的进程级地址缓存;`localhost` 按系统回环地址解析。最小默认选路按注册顺序选取后端,因此 DNS 插件先注册 IPv4 地址。一次查询失败会保留已有注册,连续三次失败会移除旧地址,恢复解析后重新注册。
- **协议热路径**:同一玩家的前端和后端通道绑定同一个 Netty 事件循环,会话控制状态在该循环上串行更新。会话始终保留协议 5 的帧边界,普通数据包不重新编码并尽量复用 Netty `ByteBuf`;Keep Alive ID 在会话内转换,可同时跟踪多个未回复请求,转服后旧后端的迟到回复会被丢弃;转服后只改写少数与玩家实体 ID 相关的包,并按目标通道可写状态控制读取。后端主机名由 Netty 异步解析,避免把 DNS 等待放在玩家 I/O 线程上。 - **协议热路径**:同一玩家的前端和后端通道绑定同一个 Netty 事件循环,会话控制状态在该循环上串行更新。会话始终保留协议 5 的帧边界,普通数据包不重新编码并尽量复用 Netty `ByteBuf`;Keep Alive ID 在会话内转换,可同时跟踪多个未回复请求,转服后旧后端的迟到回复会被丢弃;转服后只改写少数与玩家实体 ID 相关的包,并按目标通道可写状态控制读取。后端主机名由 Netty 异步解析,避免把 DNS 等待放在玩家 I/O 线程上。
@@ -76,7 +76,7 @@ PR 的 `.gitea/workflows/verify.yml` 独立运行 `gradlew check`,不使用 Ma
如果本地已有 Uranium 1.7.10 可运行包及其编译好的 `MinecraftProtocolProbe`,可运行 `smoke/local-uranium.ps1 -BundlePath <包目录> -ProbeClassesPath <探针类目录>`。脚本复制服务端到忽略目录,启动 Java 8 后端与当前安装包,再让探针经代理完成状态查询、FML 登录、Join Game 和持续 Keep Alive,结束时停止两个进程。本地一次结果与具体前提见 [最小 Uranium 联机记录](smoke/results/2026-09-25-local-uranium.md)。这项测试使用协议探针,目标整合包客户端与实服转服仍待验证。 如果本地已有 Uranium 1.7.10 可运行包及其编译好的 `MinecraftProtocolProbe`,可运行 `smoke/local-uranium.ps1 -BundlePath <包目录> -ProbeClassesPath <探针类目录>`。脚本复制服务端到忽略目录,启动 Java 8 后端与当前安装包,再让探针经代理完成状态查询、FML 登录、Join Game 和持续 Keep Alive,结束时停止两个进程。本地一次结果与具体前提见 [最小 Uranium 联机记录](smoke/results/2026-09-25-local-uranium.md)。这项测试使用协议探针,目标整合包客户端与实服转服仍待验证。
`smoke/local-uranium-transfer.ps1 -BundlePath <包目录>` 复制并启动两台 Uranium,然后编译仓库中的协议探针,通过实际 `ProxySessionListener` 请求从旧服切换到新服。加上 `-InstalledPlugin` 则启动已安装的 `ProxyMain`:旧服由静态配置注册,临时插件用 `Servers.register` 注册目标服,在初始落点回调读取 `ServerView`,再通过 `Players.transfer` 发起转服。默认模式使用协议探针核对 FML 重置、重新握手、世界切换包和目标连接的 Keep Alive,见 [两台 Uranium 转服记录](smoke/results/2026-09-25-local-uranium-transfer.md)。`-InstalledPlugin -PrismClient` 改用本机 Prism 中的 `1.7.10` Forge 实例,要求客户端在目标服保持连接 10 秒;两个真实客户端脚本都可用 `-PrismInstance <实例名>` 选择其他实例。若实例的文件夹名与启动名不同,另传 `-PrismInstanceFolder <文件夹名>`,以便准确定位并清理这次启动的客户端;`-PrismPath` 指向其他 Prism 安装位置。`smoke/local-uranium.ps1 -PrismClient` 可单独验证首次登录。这些实测仍不能替代目标整合包验收。 `smoke/local-uranium-transfer.ps1 -BundlePath <包目录>` 复制并启动两台 Uranium,然后编译仓库中的协议探针,通过实际 `ProxySessionListener` 请求从旧服切换到新服。加上 `-InstalledPlugin` 则启动已安装的 `ProxyMain`:旧服由静态配置注册,临时插件用 `Servers.register` 注册目标服,在初始落点回调读取 `ServerView`,再通过 `Players.transfer` 发起转服。默认模式使用协议探针核对 FML 重置、重新握手、世界切换包和目标连接的 Keep Alive,见 [两台 Uranium 转服记录](smoke/results/2026-09-25-local-uranium-transfer.md)。`-InstalledPlugin -ReturnToOld` 让协议探针验证旧服→新服→旧服的两次切换。`-InstalledPlugin -PrismClient` 改用本机 Prism 中的 `1.7.10` Forge 实例,要求客户端在目标服保持连接 10 秒;可同时使用 `-ReturnToOld` 验证真实客户端往返。两个真实客户端脚本都可用 `-PrismInstance <实例名>` 选择其他实例。若实例的文件夹名与启动名不同,另传 `-PrismInstanceFolder <文件夹名>`,以便准确定位并清理这次启动的客户端;`-PrismPath` 指向其他 Prism 安装位置。`smoke/local-uranium.ps1 -PrismClient` 可单独验证首次登录。这些实测仍不能替代目标整合包验收。
## 合成 relay 基准 ## 合成 relay 基准
+25 -10
View File
@@ -20,11 +20,17 @@ import java.util.concurrent.TimeUnit;
public final class UraniumTransferPlugin implements Plugin { public final class UraniumTransferPlugin implements Plugin {
private PluginContext context; private PluginContext context;
private ServerRegistration targetRegistration; private ServerRegistration targetRegistration;
private boolean returnToOld;
private volatile Thread worker; private volatile Thread worker;
@Override public void onLoad(PluginContext loadedContext) { @Override public void onLoad(PluginContext loadedContext) {
context = loadedContext; context = loadedContext;
int port = Integer.parseInt(context.settings().get("newPort")); int port = Integer.parseInt(context.settings().get("newPort"));
String returnSetting = context.settings().getOrDefault("returnToOld", "false");
if (!returnSetting.equals("true") && !returnSetting.equals("false")) {
throw new IllegalArgumentException("returnToOld must be true or false");
}
returnToOld = Boolean.parseBoolean(returnSetting);
targetRegistration = context.servers().register(new ServerDefinition("new", targetRegistration = context.servers().register(new ServerDefinition("new",
URI.create("tcp://127.0.0.1:" + port), Map.of(), 10, URI.create("tcp://127.0.0.1:" + port), Map.of(), 10,
Map.of("source", "uranium-smoke-plugin"))); Map.of("source", "uranium-smoke-plugin")));
@@ -49,16 +55,25 @@ public final class UraniumTransferPlugin implements Plugin {
while (System.nanoTime() < deadline) { while (System.nanoTime() < deadline) {
PlayerView player = context.players().find(identity).orElse(null); PlayerView player = context.players().find(identity).orElse(null);
if (player != null && player.currentServer().filter("old"::equals).isPresent()) { if (player != null && player.currentServer().filter("old"::equals).isPresent()) {
context.players().transfer(identity, "new").whenComplete((result, failure) -> { var result = context.players().transfer(identity, "new").toCompletableFuture()
if (failure != null) { .get(30, TimeUnit.SECONDS);
context.logger().error("SMOKE_PLUGIN_TRANSFER_FAILED", failure); if (result.status() != TransferStatus.NETWORK_READY) {
} else if (result.status() == TransferStatus.NETWORK_READY) { context.logger().error("SMOKE_PLUGIN_TRANSFER_FAILED status={} detail={}",
context.logger().info("SMOKE_PLUGIN_TRANSFER_PASS status=NETWORK_READY"); result.status(), result.detail().orElse(""));
} else { return;
context.logger().error("SMOKE_PLUGIN_TRANSFER_FAILED status={} detail={}", }
result.status(), result.detail().orElse("")); context.logger().info("SMOKE_PLUGIN_TRANSFER_PASS status=NETWORK_READY");
if (returnToOld) {
Thread.sleep(2000);
var returned = context.players().transfer(identity, "old").toCompletableFuture()
.get(30, TimeUnit.SECONDS);
if (returned.status() != TransferStatus.NETWORK_READY) {
context.logger().error("SMOKE_PLUGIN_RETURN_FAILED status={} detail={}",
returned.status(), returned.detail().orElse(""));
return;
} }
}); context.logger().info("SMOKE_PLUGIN_RETURN_PASS status=NETWORK_READY");
}
return; return;
} }
Thread.sleep(20); Thread.sleep(20);
@@ -66,7 +81,7 @@ public final class UraniumTransferPlugin implements Plugin {
context.logger().error("SMOKE_PLUGIN_TRANSFER_FAILED player was not connected to old backend"); context.logger().error("SMOKE_PLUGIN_TRANSFER_FAILED player was not connected to old backend");
} catch (InterruptedException interrupted) { } catch (InterruptedException interrupted) {
Thread.currentThread().interrupt(); Thread.currentThread().interrupt();
} catch (RuntimeException failure) { } catch (Exception failure) {
context.logger().error("SMOKE_PLUGIN_TRANSFER_FAILED", failure); context.logger().error("SMOKE_PLUGIN_TRANSFER_FAILED", failure);
} }
} }
+43 -7
View File
@@ -29,8 +29,10 @@ public final class UraniumTransferProbe {
private UraniumTransferProbe() { } private UraniumTransferProbe() { }
public static void main(String[] arguments) throws Exception { public static void main(String[] arguments) throws Exception {
if (arguments.length == 2 && arguments[0].equals("--external")) { if (arguments.length == 2 && (arguments[0].equals("--external")
runClient(Integer.parseInt(arguments[1]), null); || arguments[0].equals("--external-roundtrip"))) {
runClient(Integer.parseInt(arguments[1]), null,
arguments[0].equals("--external-roundtrip"));
return; return;
} }
if (arguments.length != 2) throw new IllegalArgumentException("Expected old and new Uranium ports"); if (arguments.length != 2) throw new IllegalArgumentException("Expected old and new Uranium ports");
@@ -47,13 +49,13 @@ public final class UraniumTransferProbe {
try { try {
var channel = proxy.start().toCompletableFuture().get(10, TimeUnit.SECONDS); var channel = proxy.start().toCompletableFuture().get(10, TimeUnit.SECONDS);
int proxyPort = ((InetSocketAddress) channel.localAddress()).getPort(); int proxyPort = ((InetSocketAddress) channel.localAddress()).getPort();
runClient(proxyPort, proxy); runClient(proxyPort, proxy, false);
} finally { } finally {
proxy.close().toCompletableFuture().get(15, TimeUnit.SECONDS); proxy.close().toCompletableFuture().get(15, TimeUnit.SECONDS);
} }
} }
private static void runClient(int proxyPort, ProxySessionListener proxy) throws Exception { private static void runClient(int proxyPort, ProxySessionListener proxy, boolean roundTrip) throws Exception {
try (Socket client = new Socket(InetAddress.getLoopbackAddress(), proxyPort)) { try (Socket client = new Socket(InetAddress.getLoopbackAddress(), proxyPort)) {
client.setSoTimeout(30000); client.setSoTimeout(30000);
send(client, payload(out -> { send(client, payload(out -> {
@@ -100,7 +102,8 @@ public final class UraniumTransferProbe {
if (message == ForgeMessage.SERVER_HELLO) secondHellos++; if (message == ForgeMessage.SERVER_HELLO) secondHellos++;
} }
if (id == 7) respawns++; if (id == 7) respawns++;
if ((transfer == null || transfer.isDone()) && respawns >= 2 && keepAlivesAfterReady >= 2) break; if ((transfer == null || transfer.isDone()) && respawns >= 2
&& (roundTrip || keepAlivesAfterReady >= 2)) break;
} }
if (transfer != null) { if (transfer != null) {
TransferResult result = transfer.get(2, TimeUnit.SECONDS); TransferResult result = transfer.get(2, TimeUnit.SECONDS);
@@ -110,8 +113,10 @@ public final class UraniumTransferProbe {
require(resets == 1, "expected one Forge reset; got " + resets); require(resets == 1, "expected one Forge reset; got " + resets);
require(secondHellos == 1, "expected one replacement Forge ServerHello; got " + secondHellos); require(secondHellos == 1, "expected one replacement Forge ServerHello; got " + secondHellos);
require(respawns >= 2, "expected two world transition Respawns; got " + respawns); require(respawns >= 2, "expected two world transition Respawns; got " + respawns);
require(keepAlivesAfterReady >= 2, if (!roundTrip) {
"replacement session did not sustain two Keep Alives; got " + keepAlivesAfterReady); require(keepAlivesAfterReady >= 2,
"replacement session did not sustain two Keep Alives; got " + keepAlivesAfterReady);
}
if (proxy != null) { if (proxy != null) {
require(proxy.find(player.identity()).flatMap(PlayerView::currentServer).orElse("").equals("new"), require(proxy.find(player.identity()).flatMap(PlayerView::currentServer).orElse("").equals("new"),
"player did not move to new backend"); "player did not move to new backend");
@@ -119,6 +124,37 @@ public final class UraniumTransferProbe {
System.out.printf("REAL_URANIUM_TRANSFER_PASS mode=%s reset=%d serverHellos=%d respawns=%d " System.out.printf("REAL_URANIUM_TRANSFER_PASS mode=%s reset=%d serverHellos=%d respawns=%d "
+ "keepAlivesAfterReady=%d%n", + "keepAlivesAfterReady=%d%n",
proxy == null ? "plugin" : "direct", resets, secondHellos, respawns, keepAlivesAfterReady); proxy == null ? "plugin" : "direct", resets, secondHellos, respawns, keepAlivesAfterReady);
if (roundTrip) {
int returnResets = 0;
int returnHellos = 0;
int returnRespawns = 0;
int returnKeepAlives = 0;
long returnDeadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(30);
while (System.nanoTime() < returnDeadline) {
byte[] frame = read(client);
int id = packetId(frame);
if (id == 0x40) throw new AssertionError("disconnected during return transfer");
if (id == 0) {
send(client, frame);
if (returnRespawns >= 2) returnKeepAlives++;
}
if (id == 0x3f) {
ForgeMessage message = respondToForge(client, frame);
if (message == ForgeMessage.RESET) returnResets++;
if (message == ForgeMessage.SERVER_HELLO) returnHellos++;
}
if (id == 7) returnRespawns++;
if (returnRespawns >= 2 && returnKeepAlives >= 2) break;
}
require(returnResets == 1, "expected one return Forge reset; got " + returnResets);
require(returnHellos == 1, "expected one return Forge ServerHello; got " + returnHellos);
require(returnRespawns >= 2, "expected two return Respawns; got " + returnRespawns);
require(returnKeepAlives >= 2,
"return session did not sustain two Keep Alives; got " + returnKeepAlives);
System.out.printf("REAL_URANIUM_ROUNDTRIP_PASS resets=%d serverHellos=%d respawns=%d "
+ "keepAlivesAfterReady=%d%n",
returnResets, returnHellos, returnRespawns, returnKeepAlives);
}
} }
} }
+62 -10
View File
@@ -4,6 +4,7 @@ param(
[string]$Java8Path = 'C:\Program Files\Zulu\zulu-8\bin\java.exe', [string]$Java8Path = 'C:\Program Files\Zulu\zulu-8\bin\java.exe',
[switch]$InstalledPlugin, [switch]$InstalledPlugin,
[switch]$PrismClient, [switch]$PrismClient,
[switch]$ReturnToOld,
[string]$PrismInstance = '1.7.10', [string]$PrismInstance = '1.7.10',
[string]$PrismInstanceFolder, [string]$PrismInstanceFolder,
[string]$PrismPath = (Join-Path $env:LOCALAPPDATA 'Programs\PrismLauncher\prismlauncher.exe') [string]$PrismPath = (Join-Path $env:LOCALAPPDATA 'Programs\PrismLauncher\prismlauncher.exe')
@@ -11,6 +12,9 @@ param(
$ErrorActionPreference = 'Stop' $ErrorActionPreference = 'Stop'
if ($PrismClient -and -not $InstalledPlugin) { throw '-PrismClient requires -InstalledPlugin' } if ($PrismClient -and -not $InstalledPlugin) { throw '-PrismClient requires -InstalledPlugin' }
if ($ReturnToOld -and -not $InstalledPlugin) {
throw '-ReturnToOld requires -InstalledPlugin'
}
if (-not $PrismInstanceFolder) { $PrismInstanceFolder = $PrismInstance } if (-not $PrismInstanceFolder) { $PrismInstanceFolder = $PrismInstance }
$repoRoot = Split-Path -Parent $PSScriptRoot $repoRoot = Split-Path -Parent $PSScriptRoot
$bundle = (Resolve-Path -LiteralPath $BundlePath).Path $bundle = (Resolve-Path -LiteralPath $BundlePath).Path
@@ -123,6 +127,7 @@ $newPort = FreePort
while ($newPort -eq $oldPort) { $newPort = FreePort } while ($newPort -eq $oldPort) { $newPort = FreePort }
$servers = [System.Collections.Generic.List[object]]::new() $servers = [System.Collections.Generic.List[object]]::new()
$proxy = $null $proxy = $null
$prismLauncher = $null
$initialJavawIds = @() $initialJavawIds = @()
$clientLaunchAttempted = $false $clientLaunchAttempted = $false
try { try {
@@ -155,6 +160,7 @@ try {
while ($proxyPort -eq $oldPort -or $proxyPort -eq $newPort) { $proxyPort = FreePort } while ($proxyPort -eq $oldPort -or $proxyPort -eq $newPort) { $proxyPort = FreePort }
$config = Join-Path $runDir 'strataproxy.yml' $config = Join-Path $runDir 'strataproxy.yml'
$pluginDirYaml = $pluginDir.Replace('\', '/') $pluginDirYaml = $pluginDir.Replace('\', '/')
$returnToOldSetting = if ($ReturnToOld) { 'true' } else { 'false' }
@" @"
listen: "127.0.0.1:$proxyPort" listen: "127.0.0.1:$proxyPort"
authentication: OFFLINE authentication: OFFLINE
@@ -163,6 +169,7 @@ plugins:
enabled: enabled:
dev.strataproxy.smoke.UraniumTransferPlugin: dev.strataproxy.smoke.UraniumTransferPlugin:
newPort: "$newPort" newPort: "$newPort"
returnToOld: "$returnToOldSetting"
backends: backends:
- name: old - name: old
address: "127.0.0.1:$oldPort" address: "127.0.0.1:$oldPort"
@@ -178,12 +185,17 @@ backends:
WaitForPort $proxyPort $proxy WaitForPort $proxyPort $proxy
if ($PrismClient) { if ($PrismClient) {
if (-not (Test-Path -LiteralPath $PrismPath)) { throw "Prism Launcher not found: $PrismPath" } if (-not (Test-Path -LiteralPath $PrismPath)) { throw "Prism Launcher not found: $PrismPath" }
$existingSmokeLauncher = @(Get-CimInstance Win32_Process -Filter "Name = 'prismlauncher.exe'" |
Where-Object { $_.CommandLine -match '--offline\s+PrismSmoke' })
if ($existingSmokeLauncher.Count -ne 0) {
throw 'A PrismSmoke launcher from another run is still active'
}
$initialJavawIds = @(Get-CimInstance Win32_Process -Filter "Name = 'javaw.exe'" | $initialJavawIds = @(Get-CimInstance Win32_Process -Filter "Name = 'javaw.exe'" |
Select-Object -ExpandProperty ProcessId) Select-Object -ExpandProperty ProcessId)
Start-Process -FilePath $PrismPath -ArgumentList @( $prismLauncher = Start-Process -FilePath $PrismPath -ArgumentList @(
'--launch', $PrismInstance, '--offline', 'PrismSmoke', '--launch', $PrismInstance, '--offline', 'PrismSmoke',
'--server', "127.0.0.1:$proxyPort" '--server', "127.0.0.1:$proxyPort"
) -WindowStyle Hidden | Out-Null ) -WindowStyle Hidden -PassThru
$clientLaunchAttempted = $true $clientLaunchAttempted = $true
$deadline = [DateTime]::UtcNow.AddSeconds(120) $deadline = [DateTime]::UtcNow.AddSeconds(120)
$oldLog = Join-Path $old.Directory 'logs\latest.log' $oldLog = Join-Path $old.Directory 'logs\latest.log'
@@ -193,22 +205,41 @@ backends:
$newJoined = Select-String -LiteralPath $newLog -Pattern 'PrismSmoke.*logged in' -Quiet $newJoined = Select-String -LiteralPath $newLog -Pattern 'PrismSmoke.*logged in' -Quiet
$transferred = Select-String -LiteralPath $proxyLog ` $transferred = Select-String -LiteralPath $proxyLog `
-Pattern 'SMOKE_PLUGIN_TRANSFER_PASS status=NETWORK_READY' -Quiet -Pattern 'SMOKE_PLUGIN_TRANSFER_PASS status=NETWORK_READY' -Quiet
if ($oldJoined -and $newJoined -and $transferred) { break } $returned = $false
$secondOldLogin = $false
$newDisconnected = $false
if ($ReturnToOld) {
$returned = Select-String -LiteralPath $proxyLog `
-Pattern 'SMOKE_PLUGIN_RETURN_PASS status=NETWORK_READY' -Quiet
$secondOldLogin = @(Select-String -LiteralPath $oldLog -Pattern 'PrismSmoke.*logged in').Count -ge 2
$newDisconnected = Select-String -LiteralPath $newLog -Pattern 'PrismSmoke lost connection' -Quiet
}
if ($oldJoined -and $newJoined -and $transferred -and
(-not $ReturnToOld -or ($returned -and $secondOldLogin -and $newDisconnected))) { break }
if (Select-String -LiteralPath $proxyLog -Pattern 'SMOKE_PLUGIN_TRANSFER_FAILED' -Quiet) { if (Select-String -LiteralPath $proxyLog -Pattern 'SMOKE_PLUGIN_TRANSFER_FAILED' -Quiet) {
throw 'Proxy rejected the Prism Forge transfer; inspect proxy.stdout.log and the target server log' throw 'Proxy rejected the Prism Forge transfer; inspect proxy.stdout.log and the target server log'
} }
if (Select-String -LiteralPath $newLog -Pattern 'PrismSmoke lost connection' -Quiet) { if (Select-String -LiteralPath $proxyLog -Pattern 'SMOKE_PLUGIN_RETURN_FAILED' -Quiet) {
throw 'Proxy rejected the return Forge transfer; inspect proxy.stdout.log and the old server log'
}
if (-not $ReturnToOld -and
(Select-String -LiteralPath $newLog -Pattern 'PrismSmoke lost connection' -Quiet)) {
throw 'Target Uranium disconnected the Prism Forge client during transfer' throw 'Target Uranium disconnected the Prism Forge client during transfer'
} }
if ($proxy.HasExited) { throw "Proxy exited while Prism was transferring: $($proxy.ExitCode)" } if ($proxy.HasExited) { throw "Proxy exited while Prism was transferring: $($proxy.ExitCode)" }
if ($old.Process.HasExited -or $new.Process.HasExited) { throw 'Uranium exited during Prism transfer' } if ($old.Process.HasExited -or $new.Process.HasExited) { throw 'Uranium exited during Prism transfer' }
Start-Sleep -Milliseconds 500 Start-Sleep -Milliseconds 500
} }
if (-not $oldJoined -or -not $newJoined -or -not $transferred) { if (-not $oldJoined -or -not $newJoined -or -not $transferred -or
throw 'Prism Forge client did not finish Uranium-to-Uranium transfer within 120 seconds' ($ReturnToOld -and (-not $returned -or -not $secondOldLogin -or -not $newDisconnected))) {
throw 'Prism Forge client did not finish Uranium transfer sequence within 120 seconds'
} }
Start-Sleep -Seconds 10 Start-Sleep -Seconds 10
if (Select-String -LiteralPath $newLog -Pattern 'PrismSmoke lost connection' -Quiet) { if ($ReturnToOld) {
if (@(Select-String -LiteralPath $oldLog -Pattern 'PrismSmoke lost connection').Count -ge 2) {
throw 'Prism Forge client disconnected after returning to old during the 10-second hold'
}
} elseif (Select-String -LiteralPath $newLog -Pattern 'PrismSmoke lost connection' -Quiet) {
throw 'Prism Forge client disconnected from target during the 10-second hold' throw 'Prism Forge client disconnected from target during the 10-second hold'
} }
$newClient = @(Get-CimInstance Win32_Process -Filter "Name = 'javaw.exe'" | $newClient = @(Get-CimInstance Win32_Process -Filter "Name = 'javaw.exe'" |
@@ -216,14 +247,24 @@ backends:
if ($newClient.Count -ne 1) { if ($newClient.Count -ne 1) {
throw "Expected one new Prism Java client, found $($newClient.Count)" throw "Expected one new Prism Java client, found $($newClient.Count)"
} }
Write-Output 'REAL_PRISM_URANIUM_TRANSFER_PASS dynamicRegistration=true status=NETWORK_READY holdSeconds=10' if ($ReturnToOld) {
Write-Output 'REAL_PRISM_URANIUM_ROUNDTRIP_PASS dynamicRegistration=true transfers=2 status=NETWORK_READY holdSeconds=10'
} else {
Write-Output 'REAL_PRISM_URANIUM_TRANSFER_PASS dynamicRegistration=true status=NETWORK_READY holdSeconds=10'
}
} else { } else {
& java -cp "$classes;$classpath" dev.strataproxy.smoke.UraniumTransferProbe --external $proxyPort $probeMode = if ($ReturnToOld) { '--external-roundtrip' } else { '--external' }
& java -cp "$classes;$classpath" dev.strataproxy.smoke.UraniumTransferProbe $probeMode $proxyPort
if ($LASTEXITCODE -ne 0) { throw "Installed-plugin transfer probe failed: $LASTEXITCODE" } if ($LASTEXITCODE -ne 0) { throw "Installed-plugin transfer probe failed: $LASTEXITCODE" }
Write-Output 'REAL_URANIUM_PLUGIN_TRANSFER_PASS dynamicRegistration=true status=NETWORK_READY' if ($ReturnToOld) {
Write-Output 'REAL_URANIUM_PLUGIN_ROUNDTRIP_PASS dynamicRegistration=true transfers=2 status=NETWORK_READY'
} else {
Write-Output 'REAL_URANIUM_PLUGIN_TRANSFER_PASS dynamicRegistration=true status=NETWORK_READY'
}
} }
WaitForProxyLog $proxyLog 'SMOKE_PLUGIN_REGISTER_PASS name=new' WaitForProxyLog $proxyLog 'SMOKE_PLUGIN_REGISTER_PASS name=new'
WaitForProxyLog $proxyLog 'SMOKE_PLUGIN_TRANSFER_PASS status=NETWORK_READY' WaitForProxyLog $proxyLog 'SMOKE_PLUGIN_TRANSFER_PASS status=NETWORK_READY'
if ($ReturnToOld) { WaitForProxyLog $proxyLog 'SMOKE_PLUGIN_RETURN_PASS status=NETWORK_READY' }
} else { } else {
& java -cp "$classes;$classpath" dev.strataproxy.smoke.UraniumTransferProbe $oldPort $newPort & java -cp "$classes;$classpath" dev.strataproxy.smoke.UraniumTransferProbe $oldPort $newPort
if ($LASTEXITCODE -ne 0) { throw "Uranium transfer probe failed: $LASTEXITCODE" } if ($LASTEXITCODE -ne 0) { throw "Uranium transfer probe failed: $LASTEXITCODE" }
@@ -233,12 +274,23 @@ backends:
WaitForLog $new.Directory "$playerName.*logged in" WaitForLog $new.Directory "$playerName.*logged in"
WaitForLog $old.Directory "$playerName lost connection" WaitForLog $old.Directory "$playerName lost connection"
Write-Output 'REAL_URANIUM_BACKEND_LOGS_PASS oldLogin=true newLogin=true oldDisconnected=true' Write-Output 'REAL_URANIUM_BACKEND_LOGS_PASS oldLogin=true newLogin=true oldDisconnected=true'
if ($ReturnToOld) {
if (@(Select-String -LiteralPath (Join-Path $old.Directory 'logs\latest.log') `
-Pattern "$playerName.*logged in").Count -lt 2) {
throw 'Old Uranium did not log the returning player'
}
WaitForLog $new.Directory "$playerName lost connection"
Write-Output 'REAL_URANIUM_ROUNDTRIP_LOGS_PASS oldLogins=2 newLogin=true newDisconnected=true'
}
} finally { } finally {
if ($clientLaunchAttempted) { if ($clientLaunchAttempted) {
Get-CimInstance Win32_Process -Filter "Name = 'javaw.exe'" | Get-CimInstance Win32_Process -Filter "Name = 'javaw.exe'" |
Where-Object { $_.ProcessId -notin $initialJavawIds -and (IsPrismInstanceProcess $_) } | Where-Object { $_.ProcessId -notin $initialJavawIds -and (IsPrismInstanceProcess $_) } |
ForEach-Object { Stop-Process -Id $_.ProcessId -Force -ErrorAction SilentlyContinue } ForEach-Object { Stop-Process -Id $_.ProcessId -Force -ErrorAction SilentlyContinue }
} }
if ($prismLauncher -and -not $prismLauncher.HasExited) {
Stop-Process -Id $prismLauncher.Id -Force -ErrorAction SilentlyContinue
}
if ($proxy -and -not $proxy.HasExited) { if ($proxy -and -not $proxy.HasExited) {
Stop-Process -Id $proxy.Id -Force Stop-Process -Id $proxy.Id -Force
Wait-Process -Id $proxy.Id -ErrorAction SilentlyContinue Wait-Process -Id $proxy.Id -ErrorAction SilentlyContinue
@@ -55,3 +55,17 @@ On 2026-09-26 (Asia/Shanghai), commit `c909f6a0e2b2151dd36be765802427b03e540ab7`
The script reported `REAL_PRISM_URANIUM_TRANSFER_PASS dynamicRegistration=true status=NETWORK_READY holdSeconds=10` and `REAL_URANIUM_BACKEND_LOGS_PASS oldLogin=true newLogin=true oldDisconnected=true`. The Uranium JAR SHA-256 was `B16747D08BAD4B7C67DB8F1F41C9C31066FEAAAC22B527775F604BDFA885BAD2`. The proxy log recorded `SMOKE_PLUGIN_REGISTER_PASS` at 01:15:57 and `SMOKE_PLUGIN_TRANSFER_PASS status=NETWORK_READY` at 01:16:08. The old backend logged `PrismSmoke` in at 01:16:07 and disconnected at 01:16:08; the replacement logged the player in at 01:16:08, recorded `Server side modded connection established` in its FML log, and remained connected until the smoke script stopped the client at 01:16:19. No matching Prism game process remained after cleanup. Raw logs are in ignored local directory `build/local-uranium-transfer-a1560de67b4c477f8fd5bdd25399bdfe`. The script reported `REAL_PRISM_URANIUM_TRANSFER_PASS dynamicRegistration=true status=NETWORK_READY holdSeconds=10` and `REAL_URANIUM_BACKEND_LOGS_PASS oldLogin=true newLogin=true oldDisconnected=true`. The Uranium JAR SHA-256 was `B16747D08BAD4B7C67DB8F1F41C9C31066FEAAAC22B527775F604BDFA885BAD2`. The proxy log recorded `SMOKE_PLUGIN_REGISTER_PASS` at 01:15:57 and `SMOKE_PLUGIN_TRANSFER_PASS status=NETWORK_READY` at 01:16:08. The old backend logged `PrismSmoke` in at 01:16:07 and disconnected at 01:16:08; the replacement logged the player in at 01:16:08, recorded `Server side modded connection established` in its FML log, and remained connected until the smoke script stopped the client at 01:16:19. No matching Prism game process remained after cleanup. Raw logs are in ignored local directory `build/local-uranium-transfer-a1560de67b4c477f8fd5bdd25399bdfe`.
This checks the current branch with one real minimal Forge client and two local Uranium backends. It does not cover the target modpack, real Mojang authentication, cross-host operation, or target traffic performance. This checks the current branch with one real minimal Forge client and two local Uranium backends. It does not cover the target modpack, real Mojang authentication, cross-host operation, or target traffic performance.
## Two-hop protocol probe; Prism launcher limitation
On 2026-09-26 (Asia/Shanghai), the installed distribution was exercised with a disposable plugin and the protocol probe:
```powershell
.\smoke\local-uranium-transfer.ps1 -BundlePath 'C:\Users\layue\Documents\ChatGPT\tdlm 2\Uranium\rfg\build\rfg-netty-compat-bundle' -InstalledPlugin -ReturnToOld
```
The run reported `REAL_URANIUM_ROUNDTRIP_PASS resets=1 serverHellos=1 respawns=2 keepAlivesAfterReady=2`, `REAL_URANIUM_PLUGIN_ROUNDTRIP_PASS dynamicRegistration=true transfers=2 status=NETWORK_READY`, and `REAL_URANIUM_ROUNDTRIP_LOGS_PASS oldLogins=2 newLogin=true newDisconnected=true`. The proxy logged both `SMOKE_PLUGIN_TRANSFER_PASS` and `SMOKE_PLUGIN_RETURN_PASS`. The old backend logged two `NettyProbe` logins; both backends logged a completed FML handshake. Raw logs are in ignored local directory `build/local-uranium-transfer-354d12ea24e54f83bead8ee47f3f7e6e`.
The single-transfer installed-plugin probe also passed after these smoke-plugin changes, with `REAL_URANIUM_PLUGIN_TRANSFER_PASS` and backend login/disconnect markers. Its raw logs are in `build/local-uranium-transfer-0e15153ae1f74cfd8870fce0d58087af`.
Two attempts to run `-InstalledPlugin -PrismClient -ReturnToOld` did not start the Minecraft client. Prism Launcher reported `部分组件元数据加载失败` during component resolution, and neither proxy nor Uranium logged a client login. Those attempts do not establish whether a real Forge client can complete a round trip. Their raw proxy/backend logs are in `build/local-uranium-transfer-6f41df45b2ac499cb7e59d64e4bff96e` and `build/local-uranium-transfer-6a09156996a7448e8133b38d8e863dc2`. The smoke script now stops the Prism launcher process it starts, including after a failed launch.