diff --git a/AGENTS.md b/AGENTS.md index 7d962ff..c323de6 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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. - `/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`. +- 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. - `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. diff --git a/src/klalb_en_US.properties b/src/klalb_en_US.properties index 02a5e59..cf98436 100644 --- a/src/klalb_en_US.properties +++ b/src/klalb_en_US.properties @@ -34,7 +34,13 @@ addline=Add line reconnectall=Reconnect All remotelines=Remote lines 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 invaildipv6addr=IPv6 address:Invaild Input invailddnsserver=DNS server:Invaild Input diff --git a/src/klalb_zh_CN.properties b/src/klalb_zh_CN.properties index fc1b624..ab6b6d5 100644 --- a/src/klalb_zh_CN.properties +++ b/src/klalb_zh_CN.properties @@ -34,7 +34,13 @@ addline=添加链路 reconnectall=全部重连 remotelines=远程链路 settings=设置 -saveconfigsuccess=保存配置成功 +saveconfigsuccess=保存配置成功 +configexternalupdate=配置已被其他来源更新。 +configreload=重新加载 +configcontinue=继续编辑 +configsaveconflict=保存失败:配置已被更新,请重新加载后再试。 +configsavefailed=保存配置失败。 +configconflicttitle=配置冲突 warning=警告 invaildipv6addr=IPv6地址:非法输入 invailddnsserver=DNS服务器:非法输入 diff --git a/src/org/kne/cloud/network/klalb/KLALBController.java b/src/org/kne/cloud/network/klalb/KLALBController.java index ebbbc61..e9b3e1d 100644 --- a/src/org/kne/cloud/network/klalb/KLALBController.java +++ b/src/org/kne/cloud/network/klalb/KLALBController.java @@ -580,7 +580,63 @@ public class KLALBController { private KLALBRoutingProtocolAPIClient apiClient; - private KLALBControllerConfigItem configItem; + private volatile KLALBControllerConfigItem configItem; + + private static List copyConfigList(List values) { + return values == null ? null : new ArrayList(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 select) { for (MultiProtocolSocketAddress target : select) { diff --git a/src/org/kne/cloud/network/klalb/KLALBProxySystem.java b/src/org/kne/cloud/network/klalb/KLALBProxySystem.java index 85dda3c..e17d8d6 100644 --- a/src/org/kne/cloud/network/klalb/KLALBProxySystem.java +++ b/src/org/kne/cloud/network/klalb/KLALBProxySystem.java @@ -4,11 +4,21 @@ import java.io.File; import java.io.FileReader; import java.io.FileWriter; import java.io.IOException; -import java.io.Reader; -import java.lang.reflect.Type; -import java.net.InetAddress; +import java.io.Reader; +import java.lang.reflect.Type; +import java.net.InetAddress; import java.net.InetSocketAddress; -import java.util.HashSet; +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.List; +import java.util.concurrent.CopyOnWriteArraySet; +import java.util.function.Consumer; import org.kne.cloud.network.*; import org.kne.cloud.network.klalb.ui.KLALBStateGUI3; @@ -19,8 +29,9 @@ import org.kne.cloud.network.klalb.web.KLALBWebServer; import java.util.Set; import com.google.gson.Gson; -import com.google.gson.GsonBuilder; -import com.google.gson.JsonDeserializationContext; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonArray; +import com.google.gson.JsonDeserializationContext; import com.google.gson.JsonDeserializer; import com.google.gson.JsonElement; import com.google.gson.JsonParseException; @@ -29,13 +40,82 @@ import com.google.gson.JsonSerializationContext; import com.google.gson.JsonSerializer; import com.google.gson.JsonParser; -public class KLALBProxySystem { - private Set proxys=new HashSet<>(); - private KLALBController klalbController; - private KLALBWebServer webServer; - private KLALBConfig config; - private Gson gson; - private File jsonFile; +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 proxys=new HashSet<>(); + private KLALBController klalbController; + private KLALBWebServer webServer; + private KLALBConfig config; + private Gson gson; + private File jsonFile; + private final Object configLock = new Object(); + private long configRevision; + private volatile ControllerConfigSnapshot controllerConfigSnapshot = new ControllerConfigSnapshot(0L, null); + private final CopyOnWriteArraySet> configChangeListeners = new CopyOnWriteArraySet<>(); + private JsonArray rawConfigJson; { GsonBuilder gb=new GsonBuilder().setPrettyPrinting(); MultiProtocolSocketAddress.registerToGsonBuilder(gb); @@ -141,13 +221,27 @@ public class KLALBProxySystem { public void loadConfigJson(Reader json) { loadConfigJson(new JsonParser().parse(json)); } - public void loadConfigJson(JsonElement json) { - KLALBConfig config= gson.fromJson(json, KLALBConfig.class); - loadConfig(config); - } - public void loadConfig(KLALBConfig config) { - this.config=config; - for(KLALBConfigItem item:config) { + public void loadConfigJson(JsonElement json) { + JsonElement rawJson = new JsonParser().parse(json.toString()); + KLALBConfig config= gson.fromJson(json, KLALBConfig.class); + JsonArray rawArray = rawJson instanceof JsonArray ? (JsonArray) rawJson : null; + synchronized (configLock) { + installConfigLocked(config, rawArray); + } + } + 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.configRevision=0L; + rawConfigJson=rawArray; + for(KLALBConfigItem item:config) { if(item instanceof KLALBControllerConfigItem) { KLALBControllerConfigItem kcci=(KLALBControllerConfigItem) item; String lstr=kcci.getLanguage(); @@ -227,41 +321,180 @@ public class KLALBProxySystem { } catch (IOException e) { e.printStackTrace(); } - } - } - } + } + } + controllerConfigSnapshot = createControllerConfigSnapshotLocked(); + } - public void saveConfigToFile() { - if (jsonFile != null && config != null) { - String json = gson.toJson(config); - try (FileWriter fw = new FileWriter(jsonFile)) { - fw.write(json); - } catch (IOException e) { - e.printStackTrace(); - } - } - } + public void saveConfigToFile() { + synchronized (configLock) { + if (jsonFile != null && config != null) { + try { + KLALBControllerConfigItem item = findControllerConfigItemLocked(); + 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) { + 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 listener) { + configChangeListeners.add(listener); + } + + public void removeConfigChangeListener(Consumer 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 listener : configChangeListeners) { + try { + listener.accept(event); + } catch (Throwable e) { + e.printStackTrace(); + } + } + return result; + } private KLALBStateGUI3 kgui; - public KLALBStateGUI3 getKLALBGUI() { - if(kgui==null) { - kgui=new KLALBStateGUI3(klalbController); - kgui.loadConfig(config); - kgui.setSaveComsumer((cfg)->{ - saveConfigToFile(); - }); - } + public KLALBStateGUI3 getKLALBGUI() { + if(kgui==null) { + kgui=new KLALBStateGUI3(klalbController); + kgui.bindConfigSystem(this); + } return kgui; } public KLALBConfig getConfig() { return config; } - public KLALBControllerConfigItem getControllerConfig() { - for(KLALBConfigItem item:config) { - if(item instanceof KLALBControllerConfigItem) - return (KLALBControllerConfigItem) item; - } - return null; - } -} + public KLALBControllerConfigItem getControllerConfig() { + return parseControllerConfigCandidate(getControllerConfigSnapshot()); + } +} diff --git a/src/org/kne/cloud/network/klalb/ui/KLALBStateGUI3.java b/src/org/kne/cloud/network/klalb/ui/KLALBStateGUI3.java index b110b94..c72a353 100644 --- a/src/org/kne/cloud/network/klalb/ui/KLALBStateGUI3.java +++ b/src/org/kne/cloud/network/klalb/ui/KLALBStateGUI3.java @@ -104,8 +104,82 @@ public class KLALBStateGUI3 extends XFrame { private JCheckBox nogui; // ==================== 配置和回调 ==================== - private KLALBConfig config; // 配置文件 - private Consumer saveComsumer; // 保存配置的回调函数 + private KLALBConfig config; // 配置文件 + private Consumer 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 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 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)); // 仪表盘尺寸 @@ -1055,15 +1129,10 @@ public class KLALBStateGUI3 extends XFrame { /** * 保存配置到文件 */ - private void saveConfig() { - if (config == null) { - config = new KLALBConfig(); - config.add(new KLALBControllerConfigItem()); - } - - for (KLALBConfigItem item : config) { - if (item instanceof KLALBControllerConfigItem) { - KLALBControllerConfigItem kck = (KLALBControllerConfigItem) item; + private void saveConfig() { + if (configSystem == null || loadedSnapshot == null) return; + KLALBControllerConfigItem kck = configSystem.parseControllerConfigCandidate(loadedSnapshot); + if (kck == null) return; String oldDeviceName = kck.getDeviceName(); String oldDeviceDescription = kck.getDeviceDescription(); List oldExternalEndpoints = kck.getExternalEndpoints() == null @@ -1314,30 +1383,32 @@ public class KLALBStateGUI3 extends XFrame { 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.setDeviceDescription(newDeviceDescription); - kck.setExternalEndpoints(newExternalEndpoints); - kck.setExtraRoutes(newExtraRoutes); - } - } - } - } - - // 调用保存回调 - if (saveComsumer != null) { - saveComsumer.accept(config); - JOptionPane.showMessageDialog(this, UIEnv.getRsb().getString("saveconfigsuccess")); - } - } + kck.setDeviceName(newDeviceName); + kck.setDeviceDescription(newDeviceDescription); + kck.setExternalEndpoints(newExternalEndpoints); + 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; + 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) { - this.config = config; - for (KLALBConfigItem item : config) { - if (item instanceof KLALBControllerConfigItem) { - KLALBControllerConfigItem kck = (KLALBControllerConfigItem) item; + private void loadConfigCandidate(KLALBControllerConfigItem kck) { // 加载语言设置 String lg = kck.getLanguage(); @@ -1695,9 +1762,92 @@ public class KLALBStateGUI3 extends XFrame { long delr=kck.getLinkNagleDelayTime(); 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 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; + } // ==================== 辅助方法 ==================== /** @@ -1772,8 +1922,9 @@ public class KLALBStateGUI3 extends XFrame { /** * 设置保存配置的回调函数 */ - public void setSaveComsumer(Consumer saveComsumer) { - this.saveComsumer = saveComsumer; + public void setSaveComsumer(Consumer saveComsumer) { + if (configSystem != null) return; + this.saveComsumer = saveComsumer; } /** @@ -1788,6 +1939,11 @@ public class KLALBStateGUI3 extends XFrame { * 关闭窗口并清理资源 */ public void close() { + bindingGeneration++; + configSystemClosed = true; + if (configSystem != null && boundConfigChangeListener != null) + configSystem.removeConfigChangeListener(boundConfigChangeListener); + boundConfigChangeListener = null; for (int i = 0; i < tabbedPane.getTabCount(); i++) { Component component = tabbedPane.getComponentAt(i); if (component instanceof NodeInformationPanel) { diff --git a/src/org/kne/cloud/network/klalb/web/KLALBWebServer.java b/src/org/kne/cloud/network/klalb/web/KLALBWebServer.java index 2a5dd59..430f57d 100644 --- a/src/org/kne/cloud/network/klalb/web/KLALBWebServer.java +++ b/src/org/kne/cloud/network/klalb/web/KLALBWebServer.java @@ -869,17 +869,11 @@ public class KLALBWebServer { String body = readRequestBody(exchange); try { JsonObject json = new JsonParser().parse(body).getAsJsonObject(); - KLALBControllerConfigItem current = proxySystem.getControllerConfig(); + KLALBProxySystem.ControllerConfigSnapshot baseSnapshot = proxySystem.getControllerConfigSnapshot(); + KLALBControllerConfigItem current = proxySystem.parseControllerConfigCandidate(baseSnapshot); if (current != null) { - String oldDeviceName = current.getDeviceName(); - String oldDeviceDescription = current.getDeviceDescription(); - List oldExternalEndpoints = current.getExternalEndpoints() == null - ? null - : new ArrayList(current.getExternalEndpoints()); - List oldExtraRoutes = current.getExtraRoutes() == null - ? null : new ArrayList(current.getExtraRoutes()); - String newDeviceName = oldDeviceName; - String newDeviceDescription = oldDeviceDescription; + String newDeviceName = current.getDeviceName(); + String newDeviceDescription = current.getDeviceDescription(); List newExternalEndpoints = current.getExternalEndpoints(); List newExtraRoutes = current.getExtraRoutes(); if (json.has("deviceName") && !json.get("deviceName").isJsonNull()) { @@ -1078,37 +1072,32 @@ public class KLALBWebServer { 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.setDeviceDescription(newDeviceDescription); - current.setExternalEndpoints(newExternalEndpoints); - current.setExtraRoutes(newExtraRoutes); - } - } - - // Trigger GUI save consumer or save directly - if (proxySystem.getKLALBGUI() != null && proxySystem.getKLALBGUI().getSaveComsumer() != null) { - proxySystem.getKLALBGUI().getSaveComsumer().accept(proxySystem.getConfig()); - } else { - proxySystem.saveConfigToFile(); + current.setDeviceName(newDeviceName); + current.setDeviceDescription(newDeviceDescription); + current.setExternalEndpoints(newExternalEndpoints); + current.setExtraRoutes(newExtraRoutes); + KLALBProxySystem.CommitResult commitResult = proxySystem.commitControllerConfig( + baseSnapshot.getRevision(), current, + KLALBProxySystem.ConfigChangeSource.WEB); + if (!commitResult.isSuccess()) { + JsonObject conflict = new JsonObject(); + conflict.addProperty("success", false); + conflict.addProperty("revision", commitResult.getRevision()); + conflict.addProperty("error", "Configuration revision conflict"); + sendJsonResponse(exchange, 409, conflict); + return; } JsonObject resp = new JsonObject(); resp.addProperty("success", true); + resp.addProperty("revision", commitResult.getRevision()); resp.addProperty("message", "Configuration updated successfully"); sendJsonResponse(exchange, 200, resp); } else { sendError(exchange, 500, "Current configuration is null"); } + } catch (IOException e) { + sendError(exchange, 500, "Failed to persist configuration: " + e.getMessage()); } catch (Exception e) { sendError(exchange, 400, "Failed to update configuration: " + e.getMessage()); }