WIP: Patch 4 #5

Closed
SerinaNya wants to merge 4 commits from SerinaNya/KLALB:patch-4 into master
7 changed files with 577 additions and 130 deletions
Showing only changes of commit b6acc6d509 - Show all commits
+2 -1
View File
@@ -46,8 +46,9 @@ pnpm dev
- `KLALBConfigItem` is a polymorphic JSON array keyed by `Type`. Adding a type requires a subclass and cases in both default config serializer and deserializer; unknown types must remain preserved. - `KLALBConfigItem` is a polymorphic JSON array keyed by `Type`. Adding a type requires a subclass and cases in both default config serializer and deserializer; unknown types must remain preserved.
- `/api/config` is field-by-field parsing, not whole-object Gson mapping. Keep legacy key aliases in sync with new fields. - `/api/config` is field-by-field parsing, not whole-object Gson mapping. Keep legacy key aliases in sync with new fields.
- Vendored Gson is `2.1`: responses that are `JsonElement` instances must be serialized with `JsonElement.toString()`, not reflective `gson.toJson(Object)`. - Vendored Gson is `2.1`: HTTP responses that are `JsonElement` instances must be serialized with `JsonElement.toString()`, not reflective `gson.toJson(Object)`; configuration files must use the configured pretty-print Gson path rather than `JsonElement.toString()`.
- UI strings use `UIEnv.getRsb()`; add keys to both `src/klalb_zh_CN.properties` and `src/klalb_en_US.properties`. - UI strings use `UIEnv.getRsb()`; add keys to both `src/klalb_zh_CN.properties` and `src/klalb_en_US.properties`.
- Web and Swing configuration writes must use `KLALBProxySystem`'s revisioned detached-candidate commit/event path; do not mutate the canonical config object directly.
- `KLALBController.PublishedNodeInfo` is the thread-safe source for Tiny/Full node-info responses. Publish name, description, external endpoints, and Extra Routes through the controller method so snapshots and Tiny/Full update flags stay consistent. - `KLALBController.PublishedNodeInfo` is the thread-safe source for Tiny/Full node-info responses. Publish name, description, external endpoints, and Extra Routes through the controller method so snapshots and Tiny/Full update flags stay consistent.
- `RouterInfo` no longer carries a device name. Its wire format retains an empty legacy UTF slot and `RouterInfoPacket` has optional Tiny/Full invalidation flags. Treat codec changes as compatibility work: preserve old-reader behavior and review a whole-mesh rollout. - `RouterInfo` no longer carries a device name. Its wire format retains an empty legacy UTF slot and `RouterInfoPacket` has optional Tiny/Full invalidation flags. Treat codec changes as compatibility work: preserve old-reader behavior and review a whole-mesh rollout.
- Full node-info carries `extraRoutes` separately from the endpoint `data` list. Keep absent fields compatible with older peers. - Full node-info carries `extraRoutes` separately from the endpoint `data` list. Keep absent fields compatible with older peers.
+6
View File
@@ -35,6 +35,12 @@ reconnectall=Reconnect All
remotelines=Remote lines remotelines=Remote lines
settings=Settings settings=Settings
saveconfigsuccess=Save config success saveconfigsuccess=Save config success
configexternalupdate=The configuration was updated by another source.
configreload=Reload
configcontinue=Continue editing
configsaveconflict=Save failed: the configuration was updated. Reload and try again.
configsavefailed=Failed to save configuration.
configconflicttitle=Configuration conflict
warning=Warning warning=Warning
invaildipv6addr=IPv6 address:Invaild Input invaildipv6addr=IPv6 address:Invaild Input
invailddnsserver=DNS server:Invaild Input invailddnsserver=DNS server:Invaild Input
+6
View File
@@ -35,6 +35,12 @@ reconnectall=全部重连
remotelines=远程链路 remotelines=远程链路
settings=设置 settings=设置
saveconfigsuccess=保存配置成功 saveconfigsuccess=保存配置成功
configexternalupdate=配置已被其他来源更新。
configreload=重新加载
configcontinue=继续编辑
configsaveconflict=保存失败:配置已被更新,请重新加载后再试。
configsavefailed=保存配置失败。
configconflicttitle=配置冲突
warning=警告 warning=警告
invaildipv6addr=IPv6地址:非法输入 invaildipv6addr=IPv6地址:非法输入
invailddnsserver=DNS服务器:非法输入 invailddnsserver=DNS服务器:非法输入
@@ -580,7 +580,63 @@ public class KLALBController {
private KLALBRoutingProtocolAPIClient apiClient; private KLALBRoutingProtocolAPIClient apiClient;
private KLALBControllerConfigItem configItem; private volatile KLALBControllerConfigItem configItem;
private static <T> List<T> copyConfigList(List<T> values) {
return values == null ? null : new ArrayList<T>(values);
}
private static KLALBControllerConfigItem copyConfigItem(KLALBControllerConfigItem source) {
KLALBControllerConfigItem copy = new KLALBControllerConfigItem();
copy.setLanguage(source.getLanguage());
copy.setNogui(source.isNogui());
copy.setVirtualAddress(source.getVirtualAddress());
copy.setVirtualASN(source.getVirtualASN());
copy.setDNS(copyConfigList(source.getDNS()));
copy.setTCPListen(source.getTCPListen());
copy.setUDPListen(source.getUDPListen());
copy.setVirtualSocketName(source.getVirtualSocketName());
copy.setExternalEndpoints(copyConfigList(source.getExternalEndpoints()));
copy.setAutoConnections(copyConfigList(source.getAutoConnections()));
copy.setNtpServers(copyConfigList(source.getNtpServers()));
copy.setExtraRoutes(copyConfigList(source.getExtraRoutes()));
copy.setDenyExternalEndpointQuery(source.isDenyExternalEndpointQuery());
copy.setDenyExternalEndpointBroadcast(source.isDenyExternalEndpointBroadcast());
copy.setCongestionAlgorithm(source.getCongestionAlgorithm());
copy.setBurstLimit(source.getBurstLimit());
copy.setDelayUpperBound(source.getDelayUpperBound());
copy.setDelayLowerBound(source.getDelayLowerBound());
copy.setNagleDelayTime(source.getNagleDelayTime());
copy.setLinkNagleDelayTime(source.getLinkNagleDelayTime());
copy.setLinkConnectionsCount(source.getLinkConnectionsCount());
copy.setEnableTUN(source.isEnableTUN());
copy.setTUNName(source.getTUNName());
copy.setPerformanceStrategy(source.getPerformanceStrategy());
copy.setDeviceName(source.getDeviceName());
copy.setDeviceDescription(source.getDeviceDescription());
copy.setNetworkInterfaceExcepts(copyConfigList(source.getNetworkInterfaceExcepts()));
copy.setWebUI(source.isWebUI());
copy.setWebListen(source.getWebListen());
return copy;
}
public void applyConfigItem(KLALBControllerConfigItem committedConfigItem) {
if (committedConfigItem == null) {
throw new IllegalArgumentException("Controller configuration is required");
}
KLALBControllerConfigItem detachedConfigItem = copyConfigItem(committedConfigItem);
synchronized (externalEndpoints) {
configItem = detachedConfigItem;
if (srv6Router != null) {
PerformanceStrategy strategy = PerformanceStrategy.fromDescription(detachedConfigItem.getPerformanceStrategy());
if (strategy != null) {
srv6Router.setPerformanceStrategy(strategy);
}
}
publishNodeInfoLocked(detachedConfigItem.getDeviceName(), detachedConfigItem.getDeviceDescription(),
detachedConfigItem.getExternalEndpoints(), detachedConfigItem.getExtraRoutes());
}
}
public void addRemoteLines(List<MultiProtocolSocketAddress> select) { public void addRemoteLines(List<MultiProtocolSocketAddress> select) {
for (MultiProtocolSocketAddress target : select) { for (MultiProtocolSocketAddress target : select) {
@@ -8,7 +8,17 @@ import java.io.Reader;
import java.lang.reflect.Type; import java.lang.reflect.Type;
import java.net.InetAddress; import java.net.InetAddress;
import java.net.InetSocketAddress; import java.net.InetSocketAddress;
import java.nio.charset.StandardCharsets;
import java.nio.file.AtomicMoveNotSupportedException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.nio.file.StandardOpenOption;
import java.util.ArrayList;
import java.util.HashSet; import java.util.HashSet;
import java.util.List;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.function.Consumer;
import org.kne.cloud.network.*; import org.kne.cloud.network.*;
import org.kne.cloud.network.klalb.ui.KLALBStateGUI3; import org.kne.cloud.network.klalb.ui.KLALBStateGUI3;
@@ -20,6 +30,7 @@ import java.util.Set;
import com.google.gson.Gson; import com.google.gson.Gson;
import com.google.gson.GsonBuilder; import com.google.gson.GsonBuilder;
import com.google.gson.JsonArray;
import com.google.gson.JsonDeserializationContext; import com.google.gson.JsonDeserializationContext;
import com.google.gson.JsonDeserializer; import com.google.gson.JsonDeserializer;
import com.google.gson.JsonElement; import com.google.gson.JsonElement;
@@ -30,12 +41,81 @@ import com.google.gson.JsonSerializer;
import com.google.gson.JsonParser; import com.google.gson.JsonParser;
public class KLALBProxySystem { public class KLALBProxySystem {
public enum ConfigChangeSource {
WEB, SWING
}
public static final class ControllerConfigSnapshot {
private final long revision;
private final String controllerJson;
private ControllerConfigSnapshot(long revision, String controllerJson) {
this.revision = revision;
this.controllerJson = controllerJson;
}
public long getRevision() {
return revision;
}
public String getControllerJson() {
return controllerJson;
}
}
public static final class ConfigChangeEvent {
private final ControllerConfigSnapshot snapshot;
private final ConfigChangeSource source;
private ConfigChangeEvent(ControllerConfigSnapshot snapshot, ConfigChangeSource source) {
this.snapshot = snapshot;
this.source = source;
}
public ControllerConfigSnapshot getSnapshot() {
return snapshot;
}
public ConfigChangeSource getSource() {
return source;
}
}
public static final class CommitResult {
private final boolean success;
private final long revision;
private final ControllerConfigSnapshot snapshot;
private CommitResult(boolean success, long revision, ControllerConfigSnapshot snapshot) {
this.success = success;
this.revision = revision;
this.snapshot = snapshot;
}
public boolean isSuccess() {
return success;
}
public long getRevision() {
return revision;
}
public ControllerConfigSnapshot getSnapshot() {
return snapshot;
}
}
private Set<Proxy> proxys=new HashSet<>(); private Set<Proxy> proxys=new HashSet<>();
private KLALBController klalbController; private KLALBController klalbController;
private KLALBWebServer webServer; private KLALBWebServer webServer;
private KLALBConfig config; private KLALBConfig config;
private Gson gson; private Gson gson;
private File jsonFile; private File jsonFile;
private final Object configLock = new Object();
private long configRevision;
private volatile ControllerConfigSnapshot controllerConfigSnapshot = new ControllerConfigSnapshot(0L, null);
private final CopyOnWriteArraySet<Consumer<ConfigChangeEvent>> configChangeListeners = new CopyOnWriteArraySet<>();
private JsonArray rawConfigJson;
{ {
GsonBuilder gb=new GsonBuilder().setPrettyPrinting(); GsonBuilder gb=new GsonBuilder().setPrettyPrinting();
MultiProtocolSocketAddress.registerToGsonBuilder(gb); MultiProtocolSocketAddress.registerToGsonBuilder(gb);
@@ -142,11 +222,25 @@ public class KLALBProxySystem {
loadConfigJson(new JsonParser().parse(json)); loadConfigJson(new JsonParser().parse(json));
} }
public void loadConfigJson(JsonElement json) { public void loadConfigJson(JsonElement json) {
JsonElement rawJson = new JsonParser().parse(json.toString());
KLALBConfig config= gson.fromJson(json, KLALBConfig.class); KLALBConfig config= gson.fromJson(json, KLALBConfig.class);
loadConfig(config); JsonArray rawArray = rawJson instanceof JsonArray ? (JsonArray) rawJson : null;
synchronized (configLock) {
installConfigLocked(config, rawArray);
}
} }
public void loadConfig(KLALBConfig config) { public void loadConfig(KLALBConfig config) {
JsonElement rawJson = gson.toJsonTree(config);
JsonArray rawArray = rawJson instanceof JsonArray ? (JsonArray) rawJson : null;
synchronized (configLock) {
installConfigLocked(config, rawArray);
}
}
private void installConfigLocked(KLALBConfig config, JsonArray rawArray) {
this.config=config; this.config=config;
this.configRevision=0L;
rawConfigJson=rawArray;
for(KLALBConfigItem item:config) { for(KLALBConfigItem item:config) {
if(item instanceof KLALBControllerConfigItem) { if(item instanceof KLALBControllerConfigItem) {
KLALBControllerConfigItem kcci=(KLALBControllerConfigItem) item; KLALBControllerConfigItem kcci=(KLALBControllerConfigItem) item;
@@ -229,28 +323,171 @@ public class KLALBProxySystem {
} }
} }
} }
controllerConfigSnapshot = createControllerConfigSnapshotLocked();
} }
public void saveConfigToFile() { public void saveConfigToFile() {
synchronized (configLock) {
if (jsonFile != null && config != null) { if (jsonFile != null && config != null) {
String json = gson.toJson(config); try {
try (FileWriter fw = new FileWriter(jsonFile)) { KLALBControllerConfigItem item = findControllerConfigItemLocked();
fw.write(json); if (item == null) {
persistConfigJsonLocked(gson.toJson(config));
} else {
String controllerJson = gson.toJson(item);
JsonArray completeConfig = createCompleteConfigJsonLocked(controllerJson);
persistConfigJsonLocked(gson.toJson(completeConfig));
rawConfigJson = completeConfig;
}
} catch (IOException e) { } catch (IOException e) {
e.printStackTrace(); e.printStackTrace();
} }
} }
} }
}
private KLALBControllerConfigItem findControllerConfigItemLocked() {
if (config == null) {
return null;
}
for (KLALBConfigItem item : config) {
if (item instanceof KLALBControllerConfigItem) {
return (KLALBControllerConfigItem) item;
}
}
return null;
}
private int findControllerConfigIndexLocked() {
if (config == null) {
return -1;
}
for (int i = 0; i < config.size(); i++) {
if (config.get(i) instanceof KLALBControllerConfigItem) {
return i;
}
}
return -1;
}
private ControllerConfigSnapshot createControllerConfigSnapshotLocked() {
KLALBControllerConfigItem item = findControllerConfigItemLocked();
return new ControllerConfigSnapshot(configRevision, item == null ? null : gson.toJson(item));
}
public ControllerConfigSnapshot getControllerConfigSnapshot() {
synchronized (configLock) {
controllerConfigSnapshot = createControllerConfigSnapshotLocked();
return controllerConfigSnapshot;
}
}
public KLALBControllerConfigItem parseControllerConfigCandidate(ControllerConfigSnapshot snapshot) {
if (snapshot == null || snapshot.getControllerJson() == null) {
return null;
}
return gson.fromJson(snapshot.getControllerJson(), KLALBControllerConfigItem.class);
}
private void persistConfigJsonLocked(String json) throws IOException {
if (jsonFile == null) {
return;
}
Path target = jsonFile.toPath().toAbsolutePath();
Path parent = target.getParent();
Path temporary = Files.createTempFile(parent, target.getFileName().toString(), ".tmp");
try {
Files.write(temporary, json.getBytes(StandardCharsets.UTF_8), StandardOpenOption.TRUNCATE_EXISTING,
StandardOpenOption.WRITE);
try {
Files.move(temporary, target, StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING);
} catch (AtomicMoveNotSupportedException e) {
Files.move(temporary, target, StandardCopyOption.REPLACE_EXISTING);
}
} finally {
Files.deleteIfExists(temporary);
}
}
private JsonArray createCompleteConfigJsonLocked(String controllerJson) throws IOException {
JsonElement source = rawConfigJson == null ? gson.toJsonTree(config) : rawConfigJson;
if (!(source instanceof JsonArray)) {
throw new IOException("Configuration is not an array");
}
JsonArray sourceArray = (JsonArray) source;
int index = findControllerConfigIndexLocked();
if (index < 0 || index >= sourceArray.size() || sourceArray.size() != config.size()) {
throw new IOException("Configuration item layout changed");
}
JsonArray mergedConfig = new JsonArray();
JsonElement controllerElement = new JsonParser().parse(controllerJson);
for (int i = 0; i < sourceArray.size(); i++) {
mergedConfig.add(i == index ? controllerElement : sourceArray.get(i));
}
return mergedConfig;
}
public void addConfigChangeListener(Consumer<ConfigChangeEvent> listener) {
configChangeListeners.add(listener);
}
public void removeConfigChangeListener(Consumer<ConfigChangeEvent> listener) {
configChangeListeners.remove(listener);
}
public CommitResult commitControllerConfig(long expectedRevision, KLALBControllerConfigItem candidate,
ConfigChangeSource source) throws IOException {
if (candidate == null || source == null) {
throw new IllegalArgumentException("Candidate and source are required");
}
ConfigChangeEvent event;
CommitResult result;
synchronized (configLock) {
if (expectedRevision != configRevision) {
ControllerConfigSnapshot currentSnapshot = createControllerConfigSnapshotLocked();
controllerConfigSnapshot = currentSnapshot;
return new CommitResult(false, configRevision, currentSnapshot);
}
int index = findControllerConfigIndexLocked();
if (index < 0) {
throw new IOException("Controller configuration is missing");
}
String candidateJson = gson.toJson(candidate);
KLALBControllerConfigItem committedCandidate = gson.fromJson(candidateJson,
KLALBControllerConfigItem.class);
if (PerformanceStrategy.fromDescription(committedCandidate.getPerformanceStrategy()) == null) {
throw new IllegalArgumentException("Unknown performanceStrategy: "
+ committedCandidate.getPerformanceStrategy());
}
com.google.gson.JsonArray mergedConfig = createCompleteConfigJsonLocked(candidateJson);
persistConfigJsonLocked(gson.toJson(mergedConfig));
rawConfigJson = mergedConfig;
config.set(index, committedCandidate);
if (klalbController != null) {
klalbController.applyConfigItem(committedCandidate);
}
configRevision++;
controllerConfigSnapshot = createControllerConfigSnapshotLocked();
event = new ConfigChangeEvent(controllerConfigSnapshot, source);
result = new CommitResult(true, configRevision, controllerConfigSnapshot);
}
for (Consumer<ConfigChangeEvent> listener : configChangeListeners) {
try {
listener.accept(event);
} catch (Throwable e) {
e.printStackTrace();
}
}
return result;
}
private KLALBStateGUI3 kgui; private KLALBStateGUI3 kgui;
public KLALBStateGUI3 getKLALBGUI() { public KLALBStateGUI3 getKLALBGUI() {
if(kgui==null) { if(kgui==null) {
kgui=new KLALBStateGUI3(klalbController); kgui=new KLALBStateGUI3(klalbController);
kgui.loadConfig(config); kgui.bindConfigSystem(this);
kgui.setSaveComsumer((cfg)->{
saveConfigToFile();
});
} }
return kgui; return kgui;
} }
@@ -258,10 +495,6 @@ public class KLALBProxySystem {
return config; return config;
} }
public KLALBControllerConfigItem getControllerConfig() { public KLALBControllerConfigItem getControllerConfig() {
for(KLALBConfigItem item:config) { return parseControllerConfigCandidate(getControllerConfigSnapshot());
if(item instanceof KLALBControllerConfigItem)
return (KLALBControllerConfigItem) item;
}
return null;
} }
} }
@@ -106,6 +106,80 @@ public class KLALBStateGUI3 extends XFrame {
// ==================== 配置和回调 ==================== // ==================== 配置和回调 ====================
private KLALBConfig config; // 配置文件 private KLALBConfig config; // 配置文件
private Consumer<KLALBConfig> saveComsumer; // 保存配置的回调函数 private Consumer<KLALBConfig> saveComsumer; // 保存配置的回调函数
private KLALBProxySystem configSystem;
private KLALBProxySystem.ControllerConfigSnapshot loadedSnapshot;
private long loadedRevision;
private SettingsFormState loadedFormState;
private KLALBProxySystem.ControllerConfigSnapshot pendingExternalSnapshot;
private long pendingExternalRevision;
private boolean configSystemClosed;
private long bindingGeneration;
private Consumer<KLALBProxySystem.ConfigChangeEvent> boundConfigChangeListener;
private static final class SettingsFormState {
private final String language, deviceName, deviceDescription, address, dns, extraRoutes;
private final String asn, tunName, webListen, tcpListen, udpListen, openLines, connectLines, ntp;
private final String performance, congestion;
private final boolean enableTun, webApi, nogui, denyQuery, denyBroadcast;
private final List<String> interfaces;
private final int connections, burst, upper, lower, nagle, linkNagle;
private SettingsFormState(KLALBStateGUI3 gui) {
Language languageItem = (Language) gui.comboLang.getSelectedItem();
language = languageItem == null ? null : languageItem.name();
deviceName = gui.deviceNameSet.getText();
deviceDescription = gui.deviceDescriptionSet.getText();
address = gui.addressFieldSet.getText();
dns = gui.dnsAreaSet.getText();
extraRoutes = gui.extraRoutesSet.getText();
asn = gui.asnFieldSet.getText();
tunName = gui.tunDeviceName.getText();
webListen = gui.webListenSet.getText();
tcpListen = gui.tcpListeningSet.getText();
udpListen = gui.udpListeningSet.getText();
openLines = gui.openLineTabelSet.getText();
connectLines = gui.connectLineTabelSet.getText();
ntp = gui.ntpServerSet.getText();
PerformanceStrategyItem performanceItem = (PerformanceStrategyItem) gui.comboPerformance.getSelectedItem();
performance = performanceItem == null ? null : performanceItem.getStrategy().toString();
congestion = String.valueOf(gui.congestions.getComboBox().getSelectedItem());
enableTun = gui.enableTUN.isSelected();
webApi = gui.webApiEnabled.isSelected();
nogui = gui.nogui.isSelected();
denyQuery = gui.denyQuery.isSelected();
denyBroadcast = gui.denyBroadcast.isSelected();
interfaces = new ArrayList<>();
for (int i = 0; i < gui.nilsimdl.getSize(); i++) interfaces.add(gui.nilsimdl.getElementAt(i).getName());
connections = gui.linkConnectionsCount.getSlider().getValue();
burst = gui.burstLimit.getSlider().getValue();
upper = gui.delayHbound.getSlider().getValue();
lower = gui.delayLbound.getSlider().getValue();
nagle = gui.nagleDelayTime.getSlider().getValue();
linkNagle = gui.linkNagleDelayTime.getSlider().getValue();
}
@Override public boolean equals(Object obj) {
if (!(obj instanceof SettingsFormState)) return false;
SettingsFormState o = (SettingsFormState) obj;
return enableTun == o.enableTun && webApi == o.webApi && nogui == o.nogui
&& denyQuery == o.denyQuery && denyBroadcast == o.denyBroadcast
&& connections == o.connections && burst == o.burst && upper == o.upper
&& lower == o.lower && nagle == o.nagle && linkNagle == o.linkNagle
&& Objects.equals(language, o.language) && Objects.equals(deviceName, o.deviceName)
&& Objects.equals(deviceDescription, o.deviceDescription) && Objects.equals(address, o.address)
&& Objects.equals(dns, o.dns) && Objects.equals(extraRoutes, o.extraRoutes)
&& Objects.equals(asn, o.asn) && Objects.equals(tunName, o.tunName)
&& Objects.equals(webListen, o.webListen) && Objects.equals(tcpListen, o.tcpListen)
&& Objects.equals(udpListen, o.udpListen) && Objects.equals(openLines, o.openLines)
&& Objects.equals(connectLines, o.connectLines) && Objects.equals(ntp, o.ntp)
&& Objects.equals(performance, o.performance) && Objects.equals(congestion, o.congestion)
&& Objects.equals(interfaces, o.interfaces);
}
@Override public int hashCode() { return Objects.hash(language, deviceName, deviceDescription, address, dns,
extraRoutes, asn, tunName, webListen, tcpListen, udpListen, openLines, connectLines, ntp,
performance, congestion, enableTun, webApi, nogui, denyQuery, denyBroadcast, interfaces,
connections, burst, upper, lower, nagle, linkNagle); }
}
// ==================== 尺寸常量 ==================== // ==================== 尺寸常量 ====================
private Dimension dashSize = new Dimension((int) (145 * 0.7), (int) (165 * 0.7)); // 仪表盘尺寸 private Dimension dashSize = new Dimension((int) (145 * 0.7), (int) (165 * 0.7)); // 仪表盘尺寸
@@ -1056,14 +1130,9 @@ public class KLALBStateGUI3 extends XFrame {
* 保存配置到文件 * 保存配置到文件
*/ */
private void saveConfig() { private void saveConfig() {
if (config == null) { if (configSystem == null || loadedSnapshot == null) return;
config = new KLALBConfig(); KLALBControllerConfigItem kck = configSystem.parseControllerConfigCandidate(loadedSnapshot);
config.add(new KLALBControllerConfigItem()); if (kck == null) return;
}
for (KLALBConfigItem item : config) {
if (item instanceof KLALBControllerConfigItem) {
KLALBControllerConfigItem kck = (KLALBControllerConfigItem) item;
String oldDeviceName = kck.getDeviceName(); String oldDeviceName = kck.getDeviceName();
String oldDeviceDescription = kck.getDeviceDescription(); String oldDeviceDescription = kck.getDeviceDescription();
List<MultiProtocolSocketAddress> oldExternalEndpoints = kck.getExternalEndpoints() == null List<MultiProtocolSocketAddress> oldExternalEndpoints = kck.getExternalEndpoints() == null
@@ -1314,28 +1383,30 @@ public class KLALBStateGUI3 extends XFrame {
kck.setLinkNagleDelayTime(linkNagleDelayTime.getSlider().getValue()*100000L); kck.setLinkNagleDelayTime(linkNagleDelayTime.getSlider().getValue()*100000L);
boolean deviceNameChanged = !Objects.equals(oldDeviceName, newDeviceName);
boolean deviceDescriptionChanged = !Objects.equals(oldDeviceDescription, newDeviceDescription);
boolean externalEndpointsChanged = !Objects.equals(oldExternalEndpoints, newExternalEndpoints);
boolean extraRoutesChanged = !Objects.equals(oldExtraRoutes, newExtraRoutes);
if (deviceNameChanged || deviceDescriptionChanged || externalEndpointsChanged || extraRoutesChanged) {
if (kcontroller != null) {
kcontroller.publishNodeInfo(newDeviceName, newDeviceDescription, newExternalEndpoints,
newExtraRoutes);
} else {
kck.setDeviceName(newDeviceName); kck.setDeviceName(newDeviceName);
kck.setDeviceDescription(newDeviceDescription); kck.setDeviceDescription(newDeviceDescription);
kck.setExternalEndpoints(newExternalEndpoints); kck.setExternalEndpoints(newExternalEndpoints);
kck.setExtraRoutes(newExtraRoutes); kck.setExtraRoutes(newExtraRoutes);
try {
KLALBProxySystem.CommitResult result = configSystem.commitControllerConfig(loadedRevision, kck,
KLALBProxySystem.ConfigChangeSource.SWING);
if (!result.isSuccess()) {
resolveConfigConflict(result.getSnapshot(), "configsaveconflict");
return;
} }
} loadedSnapshot = result.getSnapshot();
} loadedRevision = result.getRevision();
} loadedFormState = new SettingsFormState(this);
pendingExternalSnapshot = null;
// 调用保存回调
if (saveComsumer != null) {
saveComsumer.accept(config);
JOptionPane.showMessageDialog(this, UIEnv.getRsb().getString("saveconfigsuccess")); JOptionPane.showMessageDialog(this, UIEnv.getRsb().getString("saveconfigsuccess"));
} catch (IOException e) {
e.printStackTrace();
JOptionPane.showMessageDialog(this, UIEnv.getRsb().getString("configsavefailed"),
UIEnv.getRsb().getString("warning"), JOptionPane.WARNING_MESSAGE);
} catch (RuntimeException e) {
e.printStackTrace();
JOptionPane.showMessageDialog(this, UIEnv.getRsb().getString("configsavefailed"),
UIEnv.getRsb().getString("warning"), JOptionPane.WARNING_MESSAGE);
} }
} }
@@ -1565,11 +1636,7 @@ public class KLALBStateGUI3 extends XFrame {
/** /**
* 加载配置文件 * 加载配置文件
*/ */
public void loadConfig(KLALBConfig config) { private void loadConfigCandidate(KLALBControllerConfigItem kck) {
this.config = config;
for (KLALBConfigItem item : config) {
if (item instanceof KLALBControllerConfigItem) {
KLALBControllerConfigItem kck = (KLALBControllerConfigItem) item;
// 加载语言设置 // 加载语言设置
String lg = kck.getLanguage(); String lg = kck.getLanguage();
@@ -1696,8 +1763,91 @@ public class KLALBStateGUI3 extends XFrame {
long delr=kck.getLinkNagleDelayTime(); long delr=kck.getLinkNagleDelayTime();
linkNagleDelayTime.getSlider().setValue((int)(delr/100000L)); linkNagleDelayTime.getSlider().setValue((int)(delr/100000L));
} }
public void loadConfig(KLALBConfig config) {
if (configSystem != null) return;
this.config = config;
if (config == null) return;
for (KLALBConfigItem item : config) {
if (item instanceof KLALBControllerConfigItem) {
loadConfigCandidate((KLALBControllerConfigItem) item);
break;
} }
} }
}
public void bindConfigSystem(KLALBProxySystem system) {
if (configSystemClosed || system == null) return;
final long generation = ++bindingGeneration;
Runnable bind = () -> {
if (configSystemClosed || generation != bindingGeneration) return;
if (configSystem != null && boundConfigChangeListener != null)
configSystem.removeConfigChangeListener(boundConfigChangeListener);
configSystem = system;
final Consumer<KLALBProxySystem.ConfigChangeEvent> listener = event ->
SwingUtilities.invokeLater(() -> handleConfigChange(system, generation, event));
boundConfigChangeListener = listener;
pendingExternalSnapshot = null;
pendingExternalRevision = 0L;
loadedSnapshot = null;
loadedRevision = 0L;
loadedFormState = null;
system.addConfigChangeListener(listener);
loadedSnapshot = system.getControllerConfigSnapshot();
KLALBControllerConfigItem candidate = system.parseControllerConfigCandidate(loadedSnapshot);
if (candidate != null) {
loadConfigCandidate(candidate);
loadedRevision = loadedSnapshot.getRevision();
loadedFormState = new SettingsFormState(this);
}
};
if (SwingUtilities.isEventDispatchThread()) bind.run();
else SwingUtilities.invokeLater(bind);
}
private void handleConfigChange(KLALBProxySystem sourceSystem, long generation,
KLALBProxySystem.ConfigChangeEvent event) {
if (configSystemClosed || generation != bindingGeneration || sourceSystem != configSystem
|| event.getSource() == KLALBProxySystem.ConfigChangeSource.SWING) return;
KLALBProxySystem.ControllerConfigSnapshot snapshot = event.getSnapshot();
if (snapshot == null || snapshot.getRevision() <= Math.max(loadedRevision, pendingExternalRevision)) return;
if (loadedFormState != null && loadedFormState.equals(new SettingsFormState(this))) {
applyExternalSnapshot(snapshot);
return;
}
if (pendingExternalSnapshot == null || snapshot.getRevision() > pendingExternalRevision) {
boolean hadPending = pendingExternalSnapshot != null;
pendingExternalSnapshot = snapshot;
pendingExternalRevision = snapshot.getRevision();
if (!hadPending) resolveConfigConflict(snapshot, "configexternalupdate");
}
}
private void resolveConfigConflict(KLALBProxySystem.ControllerConfigSnapshot snapshot, String messageKey) {
if (snapshot != null && snapshot.getRevision() > Math.max(loadedRevision, pendingExternalRevision)) {
pendingExternalSnapshot = snapshot;
pendingExternalRevision = snapshot.getRevision();
}
Object[] options = { UIEnv.getRsb().getString("configreload"), UIEnv.getRsb().getString("configcontinue") };
int choice = JOptionPane.showOptionDialog(this, UIEnv.getRsb().getString(messageKey),
UIEnv.getRsb().getString("configconflicttitle"), JOptionPane.DEFAULT_OPTION,
JOptionPane.WARNING_MESSAGE, null, options, options[0]);
if (choice == 0 && pendingExternalSnapshot != null
&& pendingExternalSnapshot.getRevision() >= loadedRevision) {
applyExternalSnapshot(pendingExternalSnapshot);
}
}
private void applyExternalSnapshot(KLALBProxySystem.ControllerConfigSnapshot snapshot) {
KLALBControllerConfigItem candidate = configSystem.parseControllerConfigCandidate(snapshot);
if (candidate == null) return;
loadConfigCandidate(candidate);
loadedSnapshot = snapshot;
loadedRevision = snapshot.getRevision();
loadedFormState = new SettingsFormState(this);
pendingExternalSnapshot = null;
pendingExternalRevision = 0L;
}
// ==================== 辅助方法 ==================== // ==================== 辅助方法 ====================
/** /**
@@ -1773,6 +1923,7 @@ public class KLALBStateGUI3 extends XFrame {
* 设置保存配置的回调函数 * 设置保存配置的回调函数
*/ */
public void setSaveComsumer(Consumer<KLALBConfig> saveComsumer) { public void setSaveComsumer(Consumer<KLALBConfig> saveComsumer) {
if (configSystem != null) return;
this.saveComsumer = saveComsumer; this.saveComsumer = saveComsumer;
} }
@@ -1788,6 +1939,11 @@ public class KLALBStateGUI3 extends XFrame {
* 关闭窗口并清理资源 * 关闭窗口并清理资源
*/ */
public void close() { public void close() {
bindingGeneration++;
configSystemClosed = true;
if (configSystem != null && boundConfigChangeListener != null)
configSystem.removeConfigChangeListener(boundConfigChangeListener);
boundConfigChangeListener = null;
for (int i = 0; i < tabbedPane.getTabCount(); i++) { for (int i = 0; i < tabbedPane.getTabCount(); i++) {
Component component = tabbedPane.getComponentAt(i); Component component = tabbedPane.getComponentAt(i);
if (component instanceof NodeInformationPanel) { if (component instanceof NodeInformationPanel) {
@@ -869,17 +869,11 @@ public class KLALBWebServer {
String body = readRequestBody(exchange); String body = readRequestBody(exchange);
try { try {
JsonObject json = new JsonParser().parse(body).getAsJsonObject(); JsonObject json = new JsonParser().parse(body).getAsJsonObject();
KLALBControllerConfigItem current = proxySystem.getControllerConfig(); KLALBProxySystem.ControllerConfigSnapshot baseSnapshot = proxySystem.getControllerConfigSnapshot();
KLALBControllerConfigItem current = proxySystem.parseControllerConfigCandidate(baseSnapshot);
if (current != null) { if (current != null) {
String oldDeviceName = current.getDeviceName(); String newDeviceName = current.getDeviceName();
String oldDeviceDescription = current.getDeviceDescription(); String newDeviceDescription = current.getDeviceDescription();
List<MultiProtocolSocketAddress> oldExternalEndpoints = current.getExternalEndpoints() == null
? null
: new ArrayList<MultiProtocolSocketAddress>(current.getExternalEndpoints());
List<String> oldExtraRoutes = current.getExtraRoutes() == null
? null : new ArrayList<String>(current.getExtraRoutes());
String newDeviceName = oldDeviceName;
String newDeviceDescription = oldDeviceDescription;
List<MultiProtocolSocketAddress> newExternalEndpoints = current.getExternalEndpoints(); List<MultiProtocolSocketAddress> newExternalEndpoints = current.getExternalEndpoints();
List<String> newExtraRoutes = current.getExtraRoutes(); List<String> newExtraRoutes = current.getExtraRoutes();
if (json.has("deviceName") && !json.get("deviceName").isJsonNull()) { if (json.has("deviceName") && !json.get("deviceName").isJsonNull()) {
@@ -1078,37 +1072,32 @@ public class KLALBWebServer {
current.setDenyExternalEndpointBroadcast(json.get("denyLineTableBroadcast").getAsBoolean()); current.setDenyExternalEndpointBroadcast(json.get("denyLineTableBroadcast").getAsBoolean());
} }
boolean deviceNameChanged = !Objects.equals(oldDeviceName, newDeviceName);
boolean deviceDescriptionChanged = !Objects.equals(oldDeviceDescription, newDeviceDescription);
boolean externalEndpointsChanged = !Objects.equals(oldExternalEndpoints, newExternalEndpoints);
boolean extraRoutesChanged = !Objects.equals(oldExtraRoutes, newExtraRoutes);
if (deviceNameChanged || deviceDescriptionChanged || externalEndpointsChanged || extraRoutesChanged) {
KLALBController kc = proxySystem.getKlalbController();
if (kc != null) {
kc.publishNodeInfo(newDeviceName, newDeviceDescription, newExternalEndpoints,
newExtraRoutes);
} else {
current.setDeviceName(newDeviceName); current.setDeviceName(newDeviceName);
current.setDeviceDescription(newDeviceDescription); current.setDeviceDescription(newDeviceDescription);
current.setExternalEndpoints(newExternalEndpoints); current.setExternalEndpoints(newExternalEndpoints);
current.setExtraRoutes(newExtraRoutes); current.setExtraRoutes(newExtraRoutes);
} KLALBProxySystem.CommitResult commitResult = proxySystem.commitControllerConfig(
} baseSnapshot.getRevision(), current,
KLALBProxySystem.ConfigChangeSource.WEB);
// Trigger GUI save consumer or save directly if (!commitResult.isSuccess()) {
if (proxySystem.getKLALBGUI() != null && proxySystem.getKLALBGUI().getSaveComsumer() != null) { JsonObject conflict = new JsonObject();
proxySystem.getKLALBGUI().getSaveComsumer().accept(proxySystem.getConfig()); conflict.addProperty("success", false);
} else { conflict.addProperty("revision", commitResult.getRevision());
proxySystem.saveConfigToFile(); conflict.addProperty("error", "Configuration revision conflict");
sendJsonResponse(exchange, 409, conflict);
return;
} }
JsonObject resp = new JsonObject(); JsonObject resp = new JsonObject();
resp.addProperty("success", true); resp.addProperty("success", true);
resp.addProperty("revision", commitResult.getRevision());
resp.addProperty("message", "Configuration updated successfully"); resp.addProperty("message", "Configuration updated successfully");
sendJsonResponse(exchange, 200, resp); sendJsonResponse(exchange, 200, resp);
} else { } else {
sendError(exchange, 500, "Current configuration is null"); sendError(exchange, 500, "Current configuration is null");
} }
} catch (IOException e) {
sendError(exchange, 500, "Failed to persist configuration: " + e.getMessage());
} catch (Exception e) { } catch (Exception e) {
sendError(exchange, 400, "Failed to update configuration: " + e.getMessage()); sendError(exchange, 400, "Failed to update configuration: " + e.getMessage());
} }