реализовал вэбку обновления ПО

This commit is contained in:
kirillius 2025-10-04 22:46:12 +03:00
parent 95d09e3c79
commit adbd4ea6d4
8 changed files with 221 additions and 46 deletions

View File

@ -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();
}
/**

View File

@ -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<OVPN.OVPNConfig> {
private final static String CTX = OVPN.class.getSimpleName();
private final EventListener<JSONRPCServlet> subscription;
private final EventListener<JSONRPCServlet> rpcEvent;
private final EventListener<NetworkResourceBundle> 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<OVPN.OVPNConfig> {
*/
@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<OVPN.OVPNConfig> {
@Setter
@JSONProperty
private volatile String restartCommand = "rc-service openvpn restart";
@Getter
@Setter
@JSONProperty
private volatile boolean restartOnUpdate = true;
}
}

View File

@ -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

View File

@ -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 {

View File

@ -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());
}
}

View File

@ -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);

View File

@ -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);
}
}

View File

@ -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 || `
<p class="status-line">
Статус: <span class="${statusClass}">${status}</span>
</p>
`;
const buttonsHtml = buttons.length
? `<div class="button-group">${buttons.map(({ id, label, disabled, isLoading = false, loadingLabel = 'Обновление...' }) => `
<button id="${id}" class="btn-secondary" ${disabled || isLoading ? 'disabled' : ''}>
${isLoading ? loadingLabel : label}
</button>
`).join('')}</div>`
: '';
return `
<div class="stat-card">
<h3>${title}</h3>
<p class="status-line">
Статус: <span class="${statusClass}">${status}</span>
</p>
<button id="${buttonId}" class="btn-secondary" ${isUpdating ? 'disabled' : ''}>
${isUpdating ? 'Обновление...' : buttonLabel}
</button>
${statusSection}
${buttonsHtml}
${resourceStatsHtml}
</div>
`;
@ -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 = `
<div class="status-line version-line">
<span>Текущая версия: <span class="${hasAvailableUpdate ? 'text-yellow-500' : 'text-green-500'}">${currentVersion || '-'}</span></span><br />
<span class="last-version">Последняя версия: <span class="${hasAvailableUpdate ? 'text-yellow-500' : 'text-green-500'}">${availableVersion || '-'}</span></span>
</div>
<p class="status-line ${isConfigChanged ? 'text-yellow-500' : 'text-green-500'}">
Конфигурация: ${isConfigChanged ? 'изменена' : 'актуальна'}
</p>
`;
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 = `<div class="resource-group" style="margin-top: 20px;">
@ -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('Сохранить');
}
});
}