From adbd4ea6d49e50629f9042a225152bb43d35f150 Mon Sep 17 00:00:00 2001 From: kirillius Date: Sat, 4 Oct 2025 22:46:12 +0300 Subject: [PATCH] =?UTF-8?q?=D1=80=D0=B5=D0=B0=D0=BB=D0=B8=D0=B7=D0=BE?= =?UTF-8?q?=D0=B2=D0=B0=D0=BB=20=D0=B2=D1=8D=D0=B1=D0=BA=D1=83=20=D0=BE?= =?UTF-8?q?=D0=B1=D0=BD=D0=BE=D0=B2=D0=BB=D0=B5=D0=BD=D0=B8=D1=8F=20=D0=9F?= =?UTF-8?q?=D0=9E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/java/ru/kirillius/pf/sdn/App.java | 10 +- .../pf/sdn/External/API/Components/OVPN.java | 31 +++- .../ru/kirillius/pf/sdn/web/RPC/System.java | 34 +++- .../pf/sdn/core/AppUpdateService.java | 22 ++- .../java/ru/kirillius/pf/sdn/core/Config.java | 11 +- .../pf/sdn/core/ResourceUpdateService.java | 5 + .../pf/sdn/core/Util/CommandLineUtils.java | 2 +- webui/src/pages/Statistics.js | 152 +++++++++++++++--- 8 files changed, 221 insertions(+), 46 deletions(-) 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 75fc31a..81f80c5 100644 --- a/app/src/main/java/ru/kirillius/pf/sdn/App.java +++ b/app/src/main/java/ru/kirillius/pf/sdn/App.java @@ -33,7 +33,6 @@ public class App implements Context, Closeable { static { SystemLogger.initializeLogging(Level.INFO, List.of(InMemoryLogHandler.class)); - SystemLogger.setExceptionDumping(true); } private final AtomicBoolean shouldRestart = new AtomicBoolean(false); @@ -71,8 +70,6 @@ public class App implements Context, Closeable { private ServiceManager loadServiceManager() { var manager = new ServiceManager(this, List.of(AuthManager.class, ComponentHandlerService.class, TokenService.class, AppUpdateService.class, BGPInfoService.class, NetworkingService.class, SubscriptionService.class, ResourceUpdateService.class, WebService.class)); manager.getService(BGPInfoService.class).setProvider(new HEInfoProvider()); - manager.getService(ResourceUpdateService.class).start(); - manager.getService(ComponentHandlerService.class).syncComponentsWithConfig(); return manager; } @@ -93,9 +90,16 @@ public class App implements Context, Closeable { public App(LauncherConfig launcherConfig) { this.launcherConfig = launcherConfig; config = loadConfig(); + if (config.isDisplayDebuggingInfo()) { + SystemLogger.setExceptionDumping(true); + } + serviceManager = loadServiceManager(); serviceManager.getService(SubscriptionService.class).triggerUpdate(); checkDefaultPassword(); + + serviceManager.getService(ComponentHandlerService.class).syncComponentsWithConfig(); + serviceManager.getService(ResourceUpdateService.class).start(); } /** diff --git a/app/src/main/java/ru/kirillius/pf/sdn/External/API/Components/OVPN.java b/app/src/main/java/ru/kirillius/pf/sdn/External/API/Components/OVPN.java index 77f0978..a2b4cbe 100644 --- a/app/src/main/java/ru/kirillius/pf/sdn/External/API/Components/OVPN.java +++ b/app/src/main/java/ru/kirillius/pf/sdn/External/API/Components/OVPN.java @@ -12,6 +12,7 @@ import ru.kirillius.json.rpc.Servlet.JSONRPCServlet; import ru.kirillius.pf.sdn.External.API.ShellExecutor; import ru.kirillius.pf.sdn.core.AbstractComponent; import ru.kirillius.pf.sdn.core.Context; +import ru.kirillius.pf.sdn.core.Networking.NetworkResourceBundle; import ru.kirillius.pf.sdn.core.Networking.NetworkingService; import ru.kirillius.pf.sdn.core.Util.IPv4Util; import ru.kirillius.pf.sdn.web.ProtectedMethod; @@ -25,21 +26,30 @@ import java.io.IOException; */ public final class OVPN extends AbstractComponent { private final static String CTX = OVPN.class.getSimpleName(); - private final EventListener subscription; + private final EventListener rpcEvent; + private final EventListener updateEvent; /** * Registers the component with the JSON-RPC servlet or defers until it becomes available. */ public OVPN(Context context) { super(context); + var eventsHandler = context.getEventsHandler(); + if (config.restartOnUpdate) { + updateEvent = eventsHandler.getNetworkManagerUpdateEvent().add(bundle -> restartSystemService()); + } else { + updateEvent = null; + } + + var RPC = context.getServiceManager().getService(WebService.class).getJSONRPC(); if (RPC != null) { RPC.addTargetInstance(OVPN.class, this); - subscription = null; + rpcEvent = null; return; } - subscription = context.getEventsHandler().getRPCInitEvent() - .add(servlet -> servlet.addTargetInstance(OVPN.class, OVPN.this)); + rpcEvent = eventsHandler.getRPCInitEvent() + .add(servlet -> servlet.addTargetInstance(OVPN.class, OVPN.this)); //TODO поисследовать. Возможно событие уже не нужно } /** @@ -78,8 +88,12 @@ public final class OVPN extends AbstractComponent { */ @Override public void close() { - if (subscription != null) { - context.getEventsHandler().getRPCInitEvent().remove(subscription); + var eventsHandler = context.getEventsHandler(); + if (rpcEvent != null) { + eventsHandler.getRPCInitEvent().remove(rpcEvent); + } + if (updateEvent != null) { + eventsHandler.getNetworkManagerUpdateEvent().remove(updateEvent); } } @@ -97,5 +111,10 @@ public final class OVPN extends AbstractComponent { @Setter @JSONProperty private volatile String restartCommand = "rc-service openvpn restart"; + + @Getter + @Setter + @JSONProperty + private volatile boolean restartOnUpdate = true; } } 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 e7b9f26..65b3680 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 @@ -5,12 +5,10 @@ import org.json.JSONObject; import ru.kirillius.json.JSONUtility; import ru.kirillius.json.rpc.Annotations.JRPCArgument; import ru.kirillius.json.rpc.Annotations.JRPCMethod; -import ru.kirillius.pf.sdn.core.AppUpdateService; -import ru.kirillius.pf.sdn.core.Component; -import ru.kirillius.pf.sdn.core.ComponentHandlerService; -import ru.kirillius.pf.sdn.core.Context; +import ru.kirillius.pf.sdn.core.*; import ru.kirillius.pf.sdn.web.ProtectedMethod; +import java.io.IOException; import java.util.stream.Collectors; /** @@ -25,6 +23,7 @@ public class System implements RPC { public System(Context context) { this.context = context; } + /** * Requests an application restart. */ @@ -61,6 +60,16 @@ public class System implements RPC { return context.getConfig().isModified(); } + @ProtectedMethod + @JRPCMethod + public void saveConfig() { + try { + Config.store(context.getConfig(), context.getLauncherConfig().getConfigFile()); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + /** * Retrieves the latest version available for update. */ @@ -85,11 +94,20 @@ public class System implements RPC { @ProtectedMethod @JRPCMethod public JSONObject getVersionInfo() { - var available = context.getServiceManager().getService(AppUpdateService.class).checkVersionForUpdate(); - - return null; + var updateService = context.getServiceManager().getService(AppUpdateService.class); + var json = new JSONObject(); + json.put("available", updateService.getLatestVersion()); + json.put("current", updateService.getAppVersion()); + json.put("downloaded", updateService.getDownloadedVersion()); + return json; } + @ProtectedMethod + @JRPCMethod + public void doUpdate() { + var updateService = context.getServiceManager().getService(AppUpdateService.class); + updateService.updateApp(); + } /** * Returns the list of enabled component class names. @@ -141,7 +159,7 @@ public class System implements RPC { /** - Updates the configuration of the specified component and reloads it. + * Updates the configuration of the specified component and reloads it. */ @SuppressWarnings({"rawtypes", "unchecked"}) @ProtectedMethod 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 c5c53f3..5412d99 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 @@ -1,5 +1,6 @@ package ru.kirillius.pf.sdn.core; +import lombok.Getter; import org.w3c.dom.Element; import ru.kirillius.utils.logging.SystemLogger; @@ -29,7 +30,11 @@ public class AppUpdateService extends AppService { private final Path appLibraryPath; private final Class anchorClass; private final HttpClient httpClient; - private volatile String cachedLatestVersion; + @Getter + private volatile String latestVersion; + @Getter + private volatile String downloadedVersion; + /** * Creates the service bound to the provided context and initialises HTTP access helpers. @@ -94,19 +99,20 @@ public class AppUpdateService extends AppService { * @return newest version string or the previously cached value when fetch fails. */ public synchronized String checkVersionForUpdate() { + SystemLogger.message("Checking application version", CTX); var latest = fetchLatestVersion(); if (latest != null) { - cachedLatestVersion = latest; + latestVersion = latest; return latest; } - return cachedLatestVersion; + return latestVersion; } /** * Downloads the application package corresponding to the latest known version. */ public void updateApp() { - var version = cachedLatestVersion; + var version = latestVersion; if (version == null || version.isBlank()) { version = checkVersionForUpdate(); } @@ -128,6 +134,11 @@ public class AppUpdateService extends AppService { var tempFile = targetDirectory.resolve(fileName + ".download"); var targetFile = targetDirectory.resolve(fileName); + if (targetFile.toFile().exists()) { + SystemLogger.error("Latest version is downloaded already", CTX); + return; + } + try { Files.createDirectories(targetDirectory); Files.deleteIfExists(tempFile); @@ -142,7 +153,8 @@ public class AppUpdateService extends AppService { return; } Files.move(tempFile, targetFile, StandardCopyOption.REPLACE_EXISTING); - cachedLatestVersion = version; + latestVersion = version; + downloadedVersion = version; } catch (Exception e) { SystemLogger.error("Failed to download update", CTX, e); } finally { diff --git a/core/src/main/java/ru/kirillius/pf/sdn/core/Config.java b/core/src/main/java/ru/kirillius/pf/sdn/core/Config.java index e368062..5e716a5 100644 --- a/core/src/main/java/ru/kirillius/pf/sdn/core/Config.java +++ b/core/src/main/java/ru/kirillius/pf/sdn/core/Config.java @@ -92,6 +92,11 @@ public class Config { @JSONProperty private volatile boolean mergeSubnets = true; + @Setter + @Getter + @JSONProperty + private volatile boolean displayDebuggingInfo = true; + @Setter @Getter @JSONProperty @@ -113,8 +118,10 @@ public class Config { public static void store(Config config, File file) throws IOException { try (var fileInputStream = new FileOutputStream(file)) { try (var writer = new BufferedWriter(new OutputStreamWriter(fileInputStream))) { - writer.write(serialize(config).toString()); + var json = serialize(config); + writer.write(json.toString()); writer.flush(); + config.initialJSON = json; } } } @@ -152,6 +159,6 @@ public class Config { * Indicates whether the in-memory configuration diverges from the initially loaded snapshot. */ public boolean isModified() { - return !initialJSON.equals(serialize(this)); + return !initialJSON.toString().equals(serialize(this).toString()); } } diff --git a/core/src/main/java/ru/kirillius/pf/sdn/core/ResourceUpdateService.java b/core/src/main/java/ru/kirillius/pf/sdn/core/ResourceUpdateService.java index 5c7f0d5..5e19bbf 100644 --- a/core/src/main/java/ru/kirillius/pf/sdn/core/ResourceUpdateService.java +++ b/core/src/main/java/ru/kirillius/pf/sdn/core/ResourceUpdateService.java @@ -56,12 +56,17 @@ public class ResourceUpdateService extends AppService { @SneakyThrows @Override public void run() { + var updateService = context.getServiceManager().getService(AppUpdateService.class); var uptime = 0L; var config = context.getConfig(); + updateService.checkVersionForUpdate(); while (!Thread.currentThread().isInterrupted()) { Thread.sleep(Duration.ofMinutes(1)); uptime++; + if (uptime % 15 == 0) { + updateService.checkVersionForUpdate(); + } if (config.getUpdateSubscriptionsInterval() > 0 && uptime % (config.getUpdateSubscriptionsInterval() * 60L) == 0) { SystemLogger.message("Updating subscriptions", CTX); diff --git a/core/src/main/java/ru/kirillius/pf/sdn/core/Util/CommandLineUtils.java b/core/src/main/java/ru/kirillius/pf/sdn/core/Util/CommandLineUtils.java index f5afdfe..bf8c01c 100644 --- a/core/src/main/java/ru/kirillius/pf/sdn/core/Util/CommandLineUtils.java +++ b/core/src/main/java/ru/kirillius/pf/sdn/core/Util/CommandLineUtils.java @@ -17,6 +17,6 @@ public final class CommandLineUtils { if (first.isEmpty()) { throw new IllegalArgumentException("Missing required argument: -" + argname); } - return first.get(); + return first.get().substring(argname.length() + 2); } } diff --git a/webui/src/pages/Statistics.js b/webui/src/pages/Statistics.js index 60d1eb2..3604b78 100644 --- a/webui/src/pages/Statistics.js +++ b/webui/src/pages/Statistics.js @@ -6,18 +6,27 @@ let updateInterval = null; const $content = () => $('#statistics-content'); -const createStatusCard = (title, status, buttonId, buttonLabel, isUpdating, resourceStatsHtml = '') => { - const statusClass = status === 'Обновляется' || status === 'Обнаружено' ? 'text-yellow-500' : 'text-green-500'; +const createStatusCard = (title, status, buttons = [], resourceStatsHtml = '', customStatusContent = null) => { + const warningStatuses = ['Обновляется', 'Обнаружено', 'Готово к установке']; + const statusClass = warningStatuses.includes(status) ? 'text-yellow-500' : 'text-green-500'; + const statusSection = customStatusContent || ` +

+ Статус: ${status} +

+ `; + const buttonsHtml = buttons.length + ? `
${buttons.map(({ id, label, disabled, isLoading = false, loadingLabel = 'Обновление...' }) => ` + + `).join('')}
` + : ''; return `

${title}

-

- Статус: ${status} -

- + ${statusSection} + ${buttonsHtml} ${resourceStatsHtml}
`; @@ -72,7 +81,32 @@ async function getNetworkResources() { async function renderSystemAndManagerStatus() { const dataHtml = []; - const hasUpdates = await JSONRPC.System.hasUpdates(); + const versionInfo = await JSONRPC.System.getVersionInfo(); + const availableVersion = (versionInfo && versionInfo.available) || ''; + const currentVersion = (versionInfo && versionInfo.current) || ''; + const downloadedVersion = (versionInfo && versionInfo.downloaded) || ''; + const isConfigChanged = await JSONRPC.System.isConfigChanged(); + const hasAvailableUpdate = availableVersion && availableVersion !== currentVersion; + const isDownloaded = hasAvailableUpdate && downloadedVersion === availableVersion; + const updateButtonDisabled = !hasAvailableUpdate || isDownloaded; + const systemStatus = (() => { + if (!hasAvailableUpdate) { + return 'Нет обновлений'; + } + if (isDownloaded) { + return 'Готово к установке'; + } + return 'Обнаружено'; + })(); + const versionStatusHtml = ` +
+ Текущая версия: ${currentVersion || '-'}
+ Последняя версия: ${availableVersion || '-'} +
+

+ Конфигурация: ${isConfigChanged ? 'изменена' : 'актуальна'} +

+ `; const isSubUpdating = await JSONRPC.SubscriptionManager.isUpdating(); const isNetUpdating = await JSONRPC.NetworkManager.isUpdating(); const subCount = await getSubscriptionCount(); @@ -80,10 +114,27 @@ async function renderSystemAndManagerStatus() { dataHtml.push(createStatusCard( 'Обновление ПО', - hasUpdates ? 'Обнаружено' : 'Нет обновлений', - 'update-software-btn', - 'Проверить', - false + systemStatus, + [ + { + id: 'update-software-btn', + label: 'Обновить', + disabled: updateButtonDisabled + }, + { + id: 'restart-system-btn', + label: 'Перезагрузить', + loadingLabel: 'Перезагрузка...' + }, + { + id: 'save-config-btn', + label: 'Сохранить', + disabled: !isConfigChanged, + loadingLabel: 'Сохранение...' + } + ], + '', + versionStatusHtml )); const subStatsHtml = `
@@ -95,9 +146,14 @@ async function renderSystemAndManagerStatus() { dataHtml.push(createStatusCard( 'Менеджер Подписок', isSubUpdating ? 'Обновляется' : 'Активен', - 'update-sub-btn', - 'Обновить', - isSubUpdating, + [ + { + id: 'update-sub-btn', + label: 'Обновить', + isLoading: isSubUpdating, + loadingLabel: 'Обновление...' + } + ], subStatsHtml )); @@ -105,9 +161,14 @@ async function renderSystemAndManagerStatus() { dataHtml.push(createStatusCard( 'Менеджер Сетей', isNetUpdating ? 'Обновляется' : 'Активен', - 'update-net-btn', - 'Обновить', - isNetUpdating, + [ + { + id: 'update-net-btn', + label: 'Обновить', + isLoading: isNetUpdating, + loadingLabel: 'Обновление...' + } + ], netResourcesHtml )); @@ -153,8 +214,57 @@ function attachEventHandlers() { } }); - $('#update-software-btn').off('click').on('click', function () { - alert('Проверка обновлений ПО запущена...'); + $('#update-software-btn').off('click').on('click', async function () { + const $btn = $(this); + if ($btn.prop('disabled')) { + return; + } + + $btn.prop('disabled', true).text('Обновление...'); + + try { + await JSONRPC.System.doUpdate(); + alert('Обновление ПО запущено!'); + await renderSystemAndManagerStatus(); + } catch (e) { + alert('Ошибка при запуске обновления ПО!'); + $btn.prop('disabled', false).text('Обновить'); + } + }); + + $('#restart-system-btn').off('click').on('click', async function () { + const $btn = $(this); + + $btn.prop('disabled', true).text('Перезагрузка...'); + + try { + await JSONRPC.System.restart(); + alert('Перезагрузка системы инициирована!'); + setTimeout(() => { + window.location.reload(); + }, 10000); + } catch (e) { + alert('Ошибка при перезагрузке системы!'); + $btn.prop('disabled', false).text('Перезагрузить'); + } + }); + + $('#save-config-btn').off('click').on('click', async function () { + const $btn = $(this); + if ($btn.prop('disabled')) { + return; + } + + $btn.prop('disabled', true).text('Сохранение...'); + + try { + await JSONRPC.System.saveConfig(); + alert('Конфигурация сохранена!'); + await renderSystemAndManagerStatus(); + } catch (e) { + alert('Ошибка при сохранении конфигурации!'); + $btn.prop('disabled', false).text('Сохранить'); + } }); }