Новая обработка обновлений в соответствии с новым протоколом предоставляемым SDU серверами

This commit is contained in:
RoyceDa
2026-02-12 12:19:32 +02:00
parent ccec7d9446
commit b38918cb6d
7 changed files with 270 additions and 232 deletions

View File

@@ -1,29 +1,36 @@
import { Button, MantineRadius } from "@mantine/core"; import { Button, MantineRadius } from "@mantine/core";
import { IconRefresh } from "@tabler/icons-react"; import { IconRefresh } from "@tabler/icons-react";
import { AnimatedRoundedProgress } from "../AnimatedRoundedProgress/AnimatedRoundedProgress"; import { AnimatedRoundedProgress } from "../AnimatedRoundedProgress/AnimatedRoundedProgress";
import { UpdateStatus, useUpdater } from "@/app/hooks/useUpdater"; import { useUpdater } from "@/app/providers/UpdateProvider/useUpdater";
import { UpdateStatus } from "@/app/providers/UpdateProvider/UpdateProvider";
import { useEffect } from "react";
interface UpdateAlertProps { interface UpdateAlertProps {
radius?: MantineRadius; radius?: MantineRadius;
} }
/**
* Компонент для отображения кнопки обновлений если оно доступно, и прогресса загрузки если обновление уже скачивается
*/
export function UpdateAlert(props : UpdateAlertProps) { export function UpdateAlert(props : UpdateAlertProps) {
const radius = props.radius || 0; const radius = props.radius || 0;
const { const {
appUpdateUrl,
kernelUpdateUrl, kernelUpdateUrl,
downloadProgress, downloadProgress,
updateStatus, updateStatus,
kernelOutdatedForNextAppUpdates,
downloadLastApplicationUpdate, downloadLastApplicationUpdate,
restartAppForUpdateApply, restartAppForUpdateApply,
checkForUpdates
} = useUpdater(); } = useUpdater();
useEffect(() => {
checkForUpdates();
}, []);
return ( return (
<> <>
{updateStatus == UpdateStatus.IDLE && <> {updateStatus == UpdateStatus.KERNEL_UPDATE_NEED && <>
{kernelOutdatedForNextAppUpdates && <>
<Button h={45} leftSection={ <Button h={45} leftSection={
<IconRefresh size={15}/> <IconRefresh size={15}/>
} onClick={() => { } onClick={() => {
@@ -32,14 +39,13 @@ export function UpdateAlert(props : UpdateAlertProps) {
Kernel update required Kernel update required
</Button> </Button>
</>} </>}
{!kernelOutdatedForNextAppUpdates && appUpdateUrl != "" && <> {updateStatus == UpdateStatus.APP_UPDATE_AVAILABLE && <>
<Button h={45} onClick={downloadLastApplicationUpdate} leftSection={ <Button h={45} onClick={downloadLastApplicationUpdate} leftSection={
<IconRefresh size={15}/> <IconRefresh size={15}/>
} fullWidth variant={'gradient'} gradient={{ from: 'blue', to: 'green', deg: 233 }} radius={radius}> } fullWidth variant={'gradient'} gradient={{ from: 'blue', to: 'green', deg: 233 }} radius={radius}>
New version available New version available
</Button> </Button>
</>} </>}
</>}
{updateStatus == UpdateStatus.DOWNLOADING && <> {updateStatus == UpdateStatus.DOWNLOADING && <>
<Button h={45} leftSection={ <Button h={45} leftSection={
<AnimatedRoundedProgress value={downloadProgress} /> <AnimatedRoundedProgress value={downloadProgress} />

View File

@@ -1,124 +0,0 @@
import { useEffect, useState } from "react";
import { PacketRequestUpdate } from "../providers/ProtocolProvider/protocol/packets/packet.requestupdate";
import { APPLICATION_ARCH, APPLICATION_PLATFROM, CORE_VERSION } from "../constants";
import { PacketKernelUpdate } from "../providers/ProtocolProvider/protocol/packets/packet.kernelupdate";
import { usePacket } from "../providers/ProtocolProvider/usePacket";
import { PacketAppUpdate } from "../providers/ProtocolProvider/protocol/packets/packet.appupdate";
import { compareVersions } from "../utils/update";
import { useSender } from "../providers/ProtocolProvider/useSender";
import { useConsoleLogger } from "./useConsoleLogger";
import { useFileStorage } from "./useFileStorage";
import { APP_VERSION } from "../version";
import { useMemory } from "../providers/MemoryProvider/useMemory";
export enum UpdateStatus {
IDLE,
DOWNLOADING,
COMPILE,
READY_FOR_RESTART
}
export function useUpdater() {
const send = useSender();
const [kernelOutdatedForNextAppUpdates,
setKernelOutdatedForNextAppUpdates] = useState(false);
const [kernelUpdateUrl, setKernelUpdateUrl] = useState("");
const [appUpdateUrl, setAppUpdateUrl] = useState("");
const [appActualVersion, setAppActualVersion] = useState("");
const [kernelActualVersion, setKernelActualVersion] = useState("");
const [downloadProgress, setDownloadProgress] = useMemory<number>("dp", 0, true);
const [updateStatus, setUpdateStatus] = useMemory<UpdateStatus>("us", UpdateStatus.IDLE, true);
const {error, info} = useConsoleLogger('useUpdater');
const {writeFile} = useFileStorage();
useEffect(() => {
let packet = new PacketRequestUpdate();
packet.setAppVersion(APP_VERSION);
packet.setKernelVersion(CORE_VERSION);
packet.setArch(APPLICATION_ARCH);
packet.setPlatform(APPLICATION_PLATFROM);
send(packet);
}, []);
usePacket(0x0D, (packet : PacketKernelUpdate) => {
let url = packet.getUrl();
let version = packet.getVersion();
setKernelActualVersion(version);
setKernelUpdateUrl(url);
console.info("Kernel update available: ", version, url);
}, []);
usePacket(0x0E, (packet : PacketAppUpdate) => {
let url = packet.getUrl();
let version = packet.getVersion();
let kernelVersionRequired = packet.getKernelVersionRequired();
if(compareVersions(CORE_VERSION, kernelVersionRequired) < 0){
error("Kernel version is outdated. Cannot update app.");
setKernelOutdatedForNextAppUpdates(true);
return;
}
setAppActualVersion(version);
setAppUpdateUrl(url);
}, []);
const downloadLastApplicationUpdate = () => {
if(appUpdateUrl == ""){
return;
}
if(updateStatus != UpdateStatus.IDLE){
return;
}
setUpdateStatus(UpdateStatus.DOWNLOADING);
const xhr = new XMLHttpRequest();
xhr.open("GET", appUpdateUrl, true);
xhr.responseType = "blob";
xhr.onprogress = (event) => {
if (event.lengthComputable) {
const percentComplete = (event.loaded / event.total) * 100;
setDownloadProgress(Math.round(percentComplete));
}
};
xhr.onload = async () => {
if (xhr.status === 200) {
setUpdateStatus(UpdateStatus.COMPILE);
const blob : Blob = xhr.response;
let bundleName = `bundle ${appActualVersion}.zip`;
await writeFile(bundleName, Buffer.from(await blob.arrayBuffer()));
info("Update downloaded, starting compiler...");
await window.electron.ipcRenderer.invoke('update:installServiceUpdate', bundleName);
info("Update compiled successfully.");
setTimeout(() => {
setUpdateStatus(UpdateStatus.READY_FOR_RESTART);
}, 10000);
}
}
xhr.onerror = () => {
error("Error downloading update");
setUpdateStatus(UpdateStatus.IDLE);
}
xhr.send();
}
const restartAppForUpdateApply = () => {
if(updateStatus != UpdateStatus.READY_FOR_RESTART){
return;
}
window.electron.ipcRenderer.invoke('update:restartApp');
}
return {
appUpdateUrl,
kernelUpdateUrl,
appActualVersion,
kernelActualVersion,
kernelOutdatedForNextAppUpdates,
downloadProgress,
updateStatus,
downloadLastApplicationUpdate,
restartAppForUpdateApply
}
}

View File

@@ -2,64 +2,34 @@ import Packet from "../packet";
import Stream from "../stream"; import Stream from "../stream";
/**
* Запрашивает сервер обновлений
*/
export class PacketRequestUpdate extends Packet { export class PacketRequestUpdate extends Packet {
private kernelVersion: string = ""; private updateServer: string = "";
private appVersion: string = "";
private arch: string = "";
private platform: string = "";
public getPacketId(): number { public getPacketId(): number {
return 0xA; return 0xA;
} }
public _receive(stream: Stream): void { public _receive(stream: Stream): void {
this.kernelVersion = stream.readString(); this.updateServer = stream.readString();
this.appVersion = stream.readString();
this.arch = stream.readString();
this.platform = stream.readString();
} }
public _send(): Promise<Stream> | Stream { public _send(): Promise<Stream> | Stream {
let stream = new Stream(); let stream = new Stream();
stream.writeInt16(this.getPacketId()); stream.writeInt16(this.getPacketId());
stream.writeString(this.kernelVersion); stream.writeString(this.updateServer);
stream.writeString(this.appVersion);
stream.writeString(this.arch);
stream.writeString(this.platform);
return stream; return stream;
} }
public setKernelVersion(version: string): void { public setUpdateServer(updateServer: string) {
this.kernelVersion = version; this.updateServer = updateServer;
} }
public getKernelVersion(): string { public getUpdateServer(): string {
return this.kernelVersion; return this.updateServer;
}
public setAppVersion(version: string): void {
this.appVersion = version;
}
public getAppVersion(): string {
return this.appVersion;
}
public setArch(arch: string): void {
this.arch = arch;
}
public getArch(): string {
return this.arch;
}
public setPlatform(platform: string): void {
this.platform = platform;
}
public getPlatform(): string {
return this.platform;
} }
} }

View File

@@ -0,0 +1,172 @@
import { createContext, useEffect, useRef, useState } from "react";
import { PacketRequestUpdate } from "../ProtocolProvider/protocol/packets/packet.requestupdate";
import { useSender } from "../ProtocolProvider/useSender";
import { usePacket } from "../ProtocolProvider/usePacket";
import { useConsoleLogger } from "@/app/hooks/useConsoleLogger";
import { useFileStorage } from "@/app/hooks/useFileStorage";
import { APPLICATION_ARCH, APPLICATION_PLATFROM, CORE_VERSION } from "@/app/constants";
import { APP_VERSION } from "@/app/version";
export interface UpdateProviderProps {
children: React.ReactNode;
}
export enum UpdateStatus {
DOWNLOADING,
COMPILE,
READY_FOR_RESTART,
NO_UPDATES,
KERNEL_UPDATE_NEED,
APP_UPDATE_AVAILABLE
}
/**
* Ответ от сервера обновлений на запрос наличия обновлений. Содержит информацию о том, какие обновления есть, и ссылки на них
*/
export interface UpdateServerResponse {
version: string;
platform: string;
arch: string;
kernel_update_required: boolean;
service_pack_url: string;
kernel_url: string;
}
export interface UpdateContextValue {
updateStatus: UpdateStatus;
downloadProgress: number;
kernelUpdateUrl: string;
checkForUpdates: () => void;
downloadLastApplicationUpdate: () => void;
restartAppForUpdateApply: () => void;
}
export const UpdateProviderContext = createContext<UpdateContextValue | null>(null);
/**
* Провайдер для управления обновлениями приложения. При инициализации
* запрашвиает сервер обновлений с основного сервера и получает актуальные версии с сервера обновлений
*/
export function UpdateProvider(props: UpdateProviderProps) {
const send = useSender();
const {info, error} = useConsoleLogger('UpdateProvider');
const updateServerRef = useRef<string | null>(null);
const [updateStatus, setUpdateStatus] = useState<UpdateStatus>(UpdateStatus.NO_UPDATES);
const [downloadProgress, setDownloadProgress] = useState<number>(0);
const [kernelUpdateUrl, setKernelUpdateUrl] = useState<string>("");
const [appUpdateUrl, setAppUpdateUrl] = useState<string>("");
const [appActualVersion, setAppActualVersion] = useState<string>("");
const {writeFile} = useFileStorage();
useEffect(() => {
let packet = new PacketRequestUpdate();
/**
* Клиент хочет получить сервер обновлений с основного сервера
*/
packet.setUpdateServer("");
send(packet);
}, []);
usePacket(0xA, (packet : PacketRequestUpdate) => {
updateServerRef.current = packet.getUpdateServer();
info(`Update server ${updateServerRef.current}`);
checkForUpdates();
}, []);
const checkForUpdates = async () => {
if(updateServerRef.current == null){
/**
* SDU еще не определен
*/
return;
}
/**
* Запрашиваем обновления с SDU сервера
*/
let response = await fetch(`${updateServerRef.current}/updates/get?app=${APP_VERSION}&kernel=${CORE_VERSION}&arch=${APPLICATION_ARCH}&platform=${APPLICATION_PLATFROM}`);
if(response.status != 200){
error("Failed to check for updates: " + response.statusText);
return;
}
let updateInfo : UpdateServerResponse = await response.json();
console.info("Update info: ", updateInfo);
if(updateInfo.kernel_update_required){
/**
* Чтобы дальше получать обновления приложения нужно сначала обновить ядро
*/
setUpdateStatus(UpdateStatus.KERNEL_UPDATE_NEED);
setKernelUpdateUrl(updateInfo.kernel_url);
info("Kernel update needed for next application updates");
return;
}
if(updateInfo.service_pack_url != null){
/**
* Доступно обновление приложения, которое можно скачать и установить
*/
setUpdateStatus(UpdateStatus.APP_UPDATE_AVAILABLE);
setAppUpdateUrl(updateInfo.service_pack_url);
setAppActualVersion(updateInfo.version);
info("Application update available");
return;
}
}
const downloadLastApplicationUpdate = async () => {
if(updateStatus != UpdateStatus.APP_UPDATE_AVAILABLE){
return;
}
setUpdateStatus(UpdateStatus.DOWNLOADING);
const xhr = new XMLHttpRequest();
xhr.open("GET", updateServerRef.current + appUpdateUrl, true);
xhr.responseType = "blob";
xhr.onprogress = (event) => {
if (event.lengthComputable) {
const percentComplete = (event.loaded / event.total) * 100;
setDownloadProgress(Math.round(percentComplete));
}
};
xhr.onload = async () => {
if (xhr.status === 200) {
setUpdateStatus(UpdateStatus.COMPILE);
const blob : Blob = xhr.response;
let bundleName = `bundle ${appActualVersion}.zip`;
await writeFile(bundleName, Buffer.from(await blob.arrayBuffer()));
info("Update downloaded, starting compiler...");
await window.electron.ipcRenderer.invoke('update:installServiceUpdate', bundleName);
info("Update compiled successfully.");
setTimeout(() => {
setUpdateStatus(UpdateStatus.READY_FOR_RESTART);
}, 10000);
}
}
xhr.onerror = () => {
error("Error downloading update");
setUpdateStatus(UpdateStatus.APP_UPDATE_AVAILABLE);
}
xhr.send();
}
const restartAppForUpdateApply = () => {
if(updateStatus != UpdateStatus.READY_FOR_RESTART){
return;
}
window.electron.ipcRenderer.invoke('update:restartApp');
}
return (
<UpdateProviderContext.Provider value={{
updateStatus,
downloadProgress,
kernelUpdateUrl,
checkForUpdates,
downloadLastApplicationUpdate,
restartAppForUpdateApply
}}>
{props.children}
</UpdateProviderContext.Provider>
);
}

View File

@@ -0,0 +1,10 @@
import { useContext } from "react";
import { UpdateContextValue, UpdateProviderContext } from "./UpdateProvider";
export function useUpdater() : UpdateContextValue {
const context = useContext(UpdateProviderContext);
if (!context) {
throw new Error("useUpdater must be used within an UpdateProvider");
}
return context;
}

View File

@@ -29,6 +29,7 @@ import { useAccountProvider } from "@/app/providers/AccountProvider/useAccountPr
import { useLogout } from "@/app/providers/AccountProvider/useLogout"; import { useLogout } from "@/app/providers/AccountProvider/useLogout";
import { useUpdateMessage } from "@/app/hooks/useUpdateMessage"; import { useUpdateMessage } from "@/app/hooks/useUpdateMessage";
import { useDeviceMessage } from "@/app/hooks/useDeviceMessage"; import { useDeviceMessage } from "@/app/hooks/useDeviceMessage";
import { UpdateProvider } from "@/app/providers/UpdateProvider/UpdateProvider";
export function Main() { export function Main() {
const { mainColor, borderColor } = useRosettaColors(); const { mainColor, borderColor } = useRosettaColors();
@@ -146,6 +147,7 @@ export function Main() {
<PrivateView> <PrivateView>
<SystemAccountProvider> <SystemAccountProvider>
<TransportProvider> <TransportProvider>
<UpdateProvider>
<Flex direction={'row'} style={{ <Flex direction={'row'} style={{
height: '100%', height: '100%',
width: '100vw', width: '100vw',
@@ -192,6 +194,7 @@ export function Main() {
</Flex> </Flex>
</Overlay> </Overlay>
)} )}
</UpdateProvider>
</TransportProvider> </TransportProvider>
</SystemAccountProvider> </SystemAccountProvider>
</PrivateView>); </PrivateView>);

View File

@@ -5,18 +5,19 @@ import { SettingsAlert } from "@/app/components/SettingsAlert/SettingsAlert";
import { SettingsInput } from "@/app/components/SettingsInput/SettingsInput"; import { SettingsInput } from "@/app/components/SettingsInput/SettingsInput";
import { UpdateAlert } from "@/app/components/UpdateAlert/UpdateAlert"; import { UpdateAlert } from "@/app/components/UpdateAlert/UpdateAlert";
import { CORE_VERSION } from "@/app/constants"; import { CORE_VERSION } from "@/app/constants";
import { useUpdater } from "@/app/hooks/useUpdater"; import { UpdateStatus } from "@/app/providers/UpdateProvider/UpdateProvider";
import { useUpdater } from "@/app/providers/UpdateProvider/useUpdater";
import { APP_VERSION } from "@/app/version"; import { APP_VERSION } from "@/app/version";
import { Box, Text } from "@mantine/core"; import { Box, Text } from "@mantine/core";
export function Update() { export function Update() {
const {appUpdateUrl, kernelUpdateUrl, kernelOutdatedForNextAppUpdates} = useUpdater(); const {updateStatus} = useUpdater();
return ( return (
<> <>
<Breadcrumbs text="Updates"></Breadcrumbs> <Breadcrumbs text="Updates"></Breadcrumbs>
<InternalScreen> <InternalScreen>
{(kernelUpdateUrl != "" || appUpdateUrl != "" || kernelOutdatedForNextAppUpdates) && ( {(updateStatus != UpdateStatus.NO_UPDATES) && (
<SettingsAlert type="error" text="We recommend always using the latest version of the application. You can also update the app using the button in the left menu below the list of dialogs."></SettingsAlert> <SettingsAlert type="error" text="We recommend always using the latest version of the application. You can also update the app using the button in the left menu below the list of dialogs."></SettingsAlert>
)} )}
<Box mt={'sm'}> <Box mt={'sm'}>