diff --git a/app/src/main/java/ru/kirillius/pf/sdn/App.java b/app/src/main/java/ru/kirillius/pf/sdn/App.java index 1d6b1c5..6b32ae6 100644 --- a/app/src/main/java/ru/kirillius/pf/sdn/App.java +++ b/app/src/main/java/ru/kirillius/pf/sdn/App.java @@ -22,9 +22,12 @@ import java.io.Closeable; import java.io.File; import java.io.IOException; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import java.util.concurrent.atomic.AtomicBoolean; import java.util.logging.Level; +import java.util.regex.Pattern; +import java.util.stream.Collectors; import static ru.kirillius.pf.sdn.core.Util.CommandLineUtils.getArgument; @@ -106,6 +109,41 @@ public class App implements Context, Closeable { serviceManager.getService(ComponentHandlerService.class).syncComponentsWithConfig(); serviceManager.getService(ResourceUpdateService.class).start(); + + removeObsoleteVersions(); + } + + private void removeObsoleteVersions() { + var appVersion = serviceManager.getService(AppUpdateService.class).getAppVersion(); + var listedFiles = launcherConfig.getAppLibrary().listFiles(); + if (listedFiles == null) { + SystemLogger.error("Failed to list files in library path", CTX); + return; + } + + + var versions = Arrays.stream(listedFiles) + .filter(file -> file.getName().endsWith(AppUpdateService.EXTENSION)) + .collect(Collectors.toMap(file -> file.getName().replace(Pattern.quote(AppUpdateService.EXTENSION), ""), file -> file)); + + if (!versions.containsKey(appVersion + AppUpdateService.EXTENSION)) { + SystemLogger.error("Unable to remove obsolete version because current version file is not found!", CTX); + return; + } + + versions.forEach((key, file) -> { + if (key.equals(appVersion + AppUpdateService.EXTENSION)) { + return; + } + try { + SystemLogger.message("Removing obsolete version " + key, CTX); + if (!file.delete()) { + throw new RuntimeException("Delete failed"); + } + } catch (Exception ex) { + SystemLogger.error("Failed to delete file " + file.getName(), CTX, ex); + } + }); } /** diff --git a/app/src/main/java/ru/kirillius/pf/sdn/External/API/Components/TDNS.java b/app/src/main/java/ru/kirillius/pf/sdn/External/API/Components/TDNS.java index 4c834ee..c3dc3aa 100644 --- a/app/src/main/java/ru/kirillius/pf/sdn/External/API/Components/TDNS.java +++ b/app/src/main/java/ru/kirillius/pf/sdn/External/API/Components/TDNS.java @@ -57,11 +57,14 @@ public final class TDNS extends AbstractComponent { api.createForwarderZone(zoneName, instance.forwarder); } }); - } catch (IOException e) { + SystemLogger.message("Instance is updated", CTX); + } catch (Exception e) { SystemLogger.error("Error happened on DNS server " + instance.server + " sync", CTX, e); } } + SystemLogger.message("Update is completed", CTX); + } diff --git a/app/src/main/java/ru/kirillius/pf/sdn/web/RPC/System.java b/app/src/main/java/ru/kirillius/pf/sdn/web/RPC/System.java index b7ff545..2641bef 100644 --- a/app/src/main/java/ru/kirillius/pf/sdn/web/RPC/System.java +++ b/app/src/main/java/ru/kirillius/pf/sdn/web/RPC/System.java @@ -56,9 +56,15 @@ public class System implements RPC { @ProtectedMethod @JRPCMethod - public void setConfig(@JRPCArgument(name = "config") JSONObject json) { + public void setConfig(@JRPCArgument(name = "config") JSONObject json) throws Exception { var config = JSONUtility.deserializeStructure(json, Config.class); + var initial = new Config(); + initial.merge(context.getConfig()); context.getConfig().merge(config); + context.getEventsHandler().getConfigChangeEvent().invoke(ContextEventsHandler.ConfigChangeContext.builder() + .initial(initial) + .current(config) + .build()); } diff --git a/app/src/test/java/ru/kirillius/pf/sdn/External/API/TDNS.java b/app/src/test/java/ru/kirillius/pf/sdn/External/API/TDNS.java new file mode 100644 index 0000000..c3a5c8a --- /dev/null +++ b/app/src/test/java/ru/kirillius/pf/sdn/External/API/TDNS.java @@ -0,0 +1,14 @@ +package ru.kirillius.pf.sdn.External.API; + +import org.junit.jupiter.api.Test; + +import java.io.IOException; + +public class TDNS { + @Test + public void t() throws IOException { + try (TDNSAPI t = new TDNSAPI("http://8.8.8.8", "sdfdfdsf")) { + t.getZones(); + } + } +} diff --git a/core/src/main/java/ru/kirillius/pf/sdn/core/AppUpdateService.java b/core/src/main/java/ru/kirillius/pf/sdn/core/AppUpdateService.java index 4508a9b..7821025 100644 --- a/core/src/main/java/ru/kirillius/pf/sdn/core/AppUpdateService.java +++ b/core/src/main/java/ru/kirillius/pf/sdn/core/AppUpdateService.java @@ -25,6 +25,7 @@ import java.util.regex.Pattern; * Handles application update discovery by polling a repository and downloading new packages. */ public class AppUpdateService extends AppService { + public final static String EXTENSION = ".pfapp"; private static final String CTX = AppUpdateService.class.getSimpleName(); private static final Pattern VERSION_LINK_PATTERN = Pattern.compile("]*href=\"([0-9]+(?:\\.[0-9]+)*\\.pfapp)\"", Pattern.CASE_INSENSITIVE); @@ -140,7 +141,7 @@ public class AppUpdateService extends AppService { return; } - var fileName = version + ".pfapp"; + var fileName = version + EXTENSION; var targetDirectory = appLibraryPath; var tempFile = targetDirectory.resolve(fileName + ".download"); var targetFile = targetDirectory.resolve(fileName); @@ -207,7 +208,7 @@ public class AppUpdateService extends AppService { */ private URI buildVersionUri(String version) { var base = repository.endsWith("/") ? repository : repository + "/"; - return URI.create(base + version + ".pfapp"); + return URI.create(base + version + EXTENSION); } /** @@ -276,7 +277,7 @@ public class AppUpdateService extends AppService { if (href == null || href.isBlank()) { continue; } - var version = href.endsWith(".pfapp") ? href.substring(0, href.length() - 6) : href; + var version = href.endsWith(EXTENSION) ? href.substring(0, href.length() - 6) : href; if (version.isEmpty()) { continue; } diff --git a/core/src/main/java/ru/kirillius/pf/sdn/core/ContextEventsHandler.java b/core/src/main/java/ru/kirillius/pf/sdn/core/ContextEventsHandler.java index 680ba2a..16dae10 100644 --- a/core/src/main/java/ru/kirillius/pf/sdn/core/ContextEventsHandler.java +++ b/core/src/main/java/ru/kirillius/pf/sdn/core/ContextEventsHandler.java @@ -1,5 +1,6 @@ package ru.kirillius.pf.sdn.core; +import lombok.Builder; import lombok.Getter; import ru.kirillius.java.utils.events.ConcurrentEventHandler; import ru.kirillius.java.utils.events.EventHandler; @@ -25,4 +26,15 @@ public final class ContextEventsHandler { */ @Getter private final EventHandler RPCInitEvent = new ConcurrentEventHandler<>(); + + @Getter + private final EventHandler configChangeEvent = new ConcurrentEventHandler<>(); + + @Builder + public final static class ConfigChangeContext { + @Getter + private Config initial; + @Getter + private Config current; + } } diff --git a/core/src/main/java/ru/kirillius/pf/sdn/core/Networking/NetworkResourceBundle.java b/core/src/main/java/ru/kirillius/pf/sdn/core/Networking/NetworkResourceBundle.java index d4f2cbf..7992d3b 100644 --- a/core/src/main/java/ru/kirillius/pf/sdn/core/Networking/NetworkResourceBundle.java +++ b/core/src/main/java/ru/kirillius/pf/sdn/core/Networking/NetworkResourceBundle.java @@ -7,6 +7,7 @@ import ru.kirillius.json.JSONSerializable; import java.util.ArrayList; import java.util.List; +import java.util.Objects; /** * Container for grouped network identifiers used to configure filtering and subscriptions. @@ -33,6 +34,17 @@ public class NetworkResourceBundle { @JSONArrayProperty(type = String.class) private List domains = new ArrayList<>(); + @Override + public boolean equals(Object o) { + if (!(o instanceof NetworkResourceBundle that)) return false; + return Objects.equals(ASN, that.ASN) && Objects.equals(subnets, that.subnets) && Objects.equals(domains, that.domains); + } + + @Override + public int hashCode() { + return Objects.hash(ASN, subnets, domains); + } + /** * Clears all stored network identifiers. */ diff --git a/core/src/main/java/ru/kirillius/pf/sdn/core/Networking/NetworkingService.java b/core/src/main/java/ru/kirillius/pf/sdn/core/Networking/NetworkingService.java index 8580185..77a8e81 100644 --- a/core/src/main/java/ru/kirillius/pf/sdn/core/Networking/NetworkingService.java +++ b/core/src/main/java/ru/kirillius/pf/sdn/core/Networking/NetworkingService.java @@ -7,12 +7,15 @@ import ru.kirillius.java.utils.events.EventListener; import ru.kirillius.json.JSONUtility; import ru.kirillius.pf.sdn.core.AppService; import ru.kirillius.pf.sdn.core.Context; +import ru.kirillius.pf.sdn.core.ContextEventsHandler; +import ru.kirillius.pf.sdn.core.Subscription.SubscriptionService; import ru.kirillius.pf.sdn.core.Util.IPv4Util; import ru.kirillius.utils.logging.SystemLogger; import java.io.*; import java.util.*; import java.util.concurrent.*; +import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; /** @@ -23,7 +26,15 @@ public class NetworkingService extends AppService { private final static String CTX = NetworkingService.class.getSimpleName(); private final File cacheFile; - private final EventListener subscription; + private final EventListener resourceUpdateSubscription; + private final EventListener configChangeSubscription; + + private void rebuildInputs() { + inputResources.clear(); + inputResources.add(context.getConfig().getCustomResources()); + inputResources.add(context.getServiceManager().getService(SubscriptionService.class).getOutputResources()); + triggerUpdate(false); + } /** * Creates the networking service, wiring subscriptions and restoring cached state. @@ -32,12 +43,13 @@ public class NetworkingService extends AppService { super(context); inputResources.clear(); inputResources.add(context.getConfig().getCustomResources()); - subscription = context.getEventsHandler().getSubscriptionsUpdateEvent().add(bundle -> { - var config = context.getConfig(); - inputResources.clear(); - inputResources.add(config.getCustomResources()); - inputResources.add(bundle); - triggerUpdate(false); + resourceUpdateSubscription = context.getEventsHandler().getSubscriptionsUpdateEvent().add(bundle -> rebuildInputs()); + configChangeSubscription = context.getEventsHandler().getConfigChangeEvent().add(changeContext -> { + var filtersChanges = !changeContext.getCurrent().getFilteredResources().equals(changeContext.getInitial().getFilteredResources()); + var resChanges = !changeContext.getCurrent().getCustomResources().equals(changeContext.getInitial().getCustomResources()); + if (resChanges || filtersChanges) { + NetworkingService.this.rebuildInputs(); + } }); cacheFile = new File(context.getConfig().getCacheDirectory(), "as-cache.json"); if (cacheFile.exists() && context.getConfig().isCachingAS()) { @@ -119,8 +131,14 @@ public class NetworkingService extends AppService { SystemLogger.message("Trying to summary " + subnets.size() + " subnets...", CTX); var merged = IPv4Util.summarySubnets(subnets, config.getMergeSubnetsWithUsage()); + var unmerged = new AtomicInteger(); + subnets.forEach(subnet -> { + if (!merged.getMergedSubnets().contains(subnet)) { + unmerged.getAndIncrement(); + } + }); - SystemLogger.message(merged.getMergedSubnets().size() + " subnets has been merged to " + merged.getResult().size() + " new subnets", CTX); + SystemLogger.message(subnets.size() + " subnets has been summarized and merged to " + merged.getResult().size() + " new subnets. Unmerged: " + unmerged.get(), CTX); var domains = new HashSet<>(inputResources.getDomains()); filteredResources.getDomains().forEach(domains::remove); @@ -147,7 +165,7 @@ public class NetworkingService extends AppService { try { context.getEventsHandler().getNetworkManagerUpdateEvent().invoke(outputResources); } catch (Exception e) { - throw new RuntimeException(e); + SystemLogger.error("Unable to invoke update event", CTX, e); } })); } @@ -181,7 +199,9 @@ public class NetworkingService extends AppService { */ @Override public void close() throws IOException { - context.getEventsHandler().getSubscriptionsUpdateEvent().remove(subscription); + context.getEventsHandler().getSubscriptionsUpdateEvent().remove(resourceUpdateSubscription); + context.getEventsHandler().getConfigChangeEvent().remove(configChangeSubscription); + executor.shutdown(); } } diff --git a/webui/src/modules/router.js b/webui/src/modules/router.js index 4944f55..e61332a 100644 --- a/webui/src/modules/router.js +++ b/webui/src/modules/router.js @@ -11,6 +11,7 @@ import { TDNSConfig } from '../pages/TDNS.js'; import { FRRConfig } from '../pages/FRR.js'; import { SettingsPage } from '../pages/Settings.js'; import { LogsPage } from '../pages/Logs.js'; +import { NetworkResourcesPage } from '../pages/NetworkResources.js'; // Переменная для отслеживания текущего активного хеша (для корректного unmount) @@ -28,6 +29,7 @@ const allMenuItems = [ { label: 'Настройка TDNS', path: 'tdns', component: 'ru.kirillius.pf.sdn.External.API.Components.TDNS' }, { label: 'Настройка FRR', path: 'frr', component: 'ru.kirillius.pf.sdn.External.API.Components.FRR' }, { label: 'Журнал', path: 'logs', component: null }, + { label: 'Сетевые ресурсы', path: 'network-resources', component: null }, ]; // 2. Определение страниц @@ -76,6 +78,11 @@ const routes = { render: LogsPage.render, mount: LogsPage.mount, unmount: LogsPage.unmount + }, + '#network-resources': { + render: NetworkResourcesPage.render, + mount: NetworkResourcesPage.mount, + unmount: NetworkResourcesPage.unmount } }; diff --git a/webui/src/styles/app.css b/webui/src/styles/app.css index d0719ef..5a2e064 100644 --- a/webui/src/styles/app.css +++ b/webui/src/styles/app.css @@ -399,6 +399,18 @@ html.dark-theme, body { margin-right: 8px; } +#network-resources-content .network-resource-link, +#network-resources-content .network-resource-link:visited { + color: var(--color-text); + text-decoration: none; +} + +#network-resources-content .network-resource-link:hover, +#network-resources-content .network-resource-link:focus { + color: var(--color-text); + text-decoration: underline; +} + /* 🔥 ОБНОВЛЕНО: Сделаем resource-key (используется для shortName) крупнее */ .resource-key { font-weight: 600; /* Жирный шрифт */