реализовал вэбку обновления ПО
This commit is contained in:
parent
95d09e3c79
commit
adbd4ea6d4
|
|
@ -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();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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('Сохранить');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue