mirror of
https://github.com/uqbar-dao/nectar.git
synced 2024-12-22 16:11:38 +03:00
Merge pull request #568 from kinode-dao/bp/autoupdatesfix
app_store: fix auto_updates
This commit is contained in:
commit
c91a8898b7
@ -184,6 +184,8 @@ interface downloads {
|
||||
auto-update(auto-update-request),
|
||||
/// Notify that a download is complete
|
||||
download-complete(download-complete-request),
|
||||
/// Auto-update-download complete
|
||||
auto-download-complete(auto-download-complete-request),
|
||||
/// Get files for a package
|
||||
get-files(option<package-id>),
|
||||
/// Remove a file
|
||||
@ -243,6 +245,12 @@ interface downloads {
|
||||
err: option<download-error>,
|
||||
}
|
||||
|
||||
/// Request for an auto-download complete
|
||||
record auto-download-complete-request {
|
||||
download-info: download-complete-request,
|
||||
manifest-hash: string,
|
||||
}
|
||||
|
||||
/// Represents a hash mismatch error
|
||||
record hash-mismatch {
|
||||
desired: string,
|
||||
|
@ -30,7 +30,7 @@
|
||||
//! It delegates these responsibilities to the downloads and chain processes respectively.
|
||||
//!
|
||||
use crate::kinode::process::downloads::{
|
||||
DownloadCompleteRequest, DownloadResponses, ProgressUpdate,
|
||||
AutoDownloadCompleteRequest, DownloadCompleteRequest, DownloadResponses, ProgressUpdate,
|
||||
};
|
||||
use crate::kinode::process::main::{
|
||||
ApisResponse, GetApiResponse, InstallPackageRequest, InstallResponse, LocalRequest,
|
||||
@ -65,6 +65,7 @@ pub enum Req {
|
||||
LocalRequest(LocalRequest),
|
||||
Progress(ProgressUpdate),
|
||||
DownloadComplete(DownloadCompleteRequest),
|
||||
AutoDownloadComplete(AutoDownloadCompleteRequest),
|
||||
Http(http::server::HttpServerRequest),
|
||||
}
|
||||
|
||||
@ -161,6 +162,40 @@ fn handle_message(
|
||||
},
|
||||
);
|
||||
}
|
||||
Req::AutoDownloadComplete(req) => {
|
||||
if !message.is_local(&our) {
|
||||
return Err(anyhow::anyhow!(
|
||||
"auto download complete from non-local node"
|
||||
));
|
||||
}
|
||||
// auto_install case:
|
||||
// the downloads process has given us the new package manifest's
|
||||
// capability hashes, and the old package's capability hashes.
|
||||
// we can use these to determine if the new package has the same
|
||||
// capabilities as the old one, and if so, auto-install it.
|
||||
|
||||
let manifest_hash = req.manifest_hash;
|
||||
let package_id = req.download_info.package_id;
|
||||
let version_hash = req.download_info.version_hash;
|
||||
|
||||
if let Some(package) = state.packages.get(&package_id.clone().to_process_lib()) {
|
||||
if package.manifest_hash == Some(manifest_hash) {
|
||||
print_to_terminal(1, "auto_install:main, manifest_hash match");
|
||||
if let Err(e) =
|
||||
utils::install(&package_id, None, &version_hash, state, &our.node)
|
||||
{
|
||||
print_to_terminal(1, &format!("error auto_installing package: {e}"));
|
||||
} else {
|
||||
println!(
|
||||
"auto_installed update for package: {:?}",
|
||||
&package_id.to_process_lib()
|
||||
);
|
||||
}
|
||||
} else {
|
||||
print_to_terminal(1, "auto_install:main, manifest_hash do not match");
|
||||
}
|
||||
}
|
||||
}
|
||||
Req::DownloadComplete(req) => {
|
||||
if !message.is_local(&our) {
|
||||
return Err(anyhow::anyhow!("download complete from non-local node"));
|
||||
@ -182,41 +217,6 @@ fn handle_message(
|
||||
.unwrap(),
|
||||
},
|
||||
);
|
||||
|
||||
// auto_install case:
|
||||
// the downloads process has given us the new package manifest's
|
||||
// capability hashes, and the old package's capability hashes.
|
||||
// we can use these to determine if the new package has the same
|
||||
// capabilities as the old one, and if so, auto-install it.
|
||||
if let Some(context) = message.context() {
|
||||
let manifest_hash = String::from_utf8(context.to_vec())?;
|
||||
if let Some(package) =
|
||||
state.packages.get(&req.package_id.clone().to_process_lib())
|
||||
{
|
||||
if package.manifest_hash == Some(manifest_hash) {
|
||||
print_to_terminal(1, "auto_install:main, manifest_hash match");
|
||||
if let Err(e) = utils::install(
|
||||
&req.package_id,
|
||||
None,
|
||||
&req.version_hash,
|
||||
state,
|
||||
&our.node,
|
||||
) {
|
||||
print_to_terminal(
|
||||
1,
|
||||
&format!("error auto_installing package: {e}"),
|
||||
);
|
||||
} else {
|
||||
println!(
|
||||
"auto_installed update for package: {:?}",
|
||||
&req.package_id.to_process_lib()
|
||||
);
|
||||
}
|
||||
} else {
|
||||
print_to_terminal(1, "auto_install:main, manifest_hash do not match");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
@ -42,9 +42,9 @@
|
||||
//! mechanism is implemented in the FT worker for improved modularity and performance.
|
||||
//!
|
||||
use crate::kinode::process::downloads::{
|
||||
AutoUpdateRequest, DirEntry, DownloadCompleteRequest, DownloadError, DownloadRequests,
|
||||
DownloadResponses, Entry, FileEntry, HashMismatch, LocalDownloadRequest, RemoteDownloadRequest,
|
||||
RemoveFileRequest,
|
||||
AutoDownloadCompleteRequest, AutoUpdateRequest, DirEntry, DownloadCompleteRequest,
|
||||
DownloadError, DownloadRequests, DownloadResponses, Entry, FileEntry, HashMismatch,
|
||||
LocalDownloadRequest, RemoteDownloadRequest, RemoveFileRequest,
|
||||
};
|
||||
use std::{collections::HashSet, io::Read, str::FromStr};
|
||||
|
||||
@ -245,7 +245,7 @@ fn handle_message(
|
||||
// if we have a pending auto_install, forward that context to the main process.
|
||||
// it will check if the caps_hashes match (no change in capabilities), and auto_install if it does.
|
||||
|
||||
let context = if auto_updates.remove(&(
|
||||
let manifest_hash = if auto_updates.remove(&(
|
||||
req.package_id.clone().to_process_lib(),
|
||||
req.version_hash.clone(),
|
||||
)) {
|
||||
@ -253,7 +253,7 @@ fn handle_message(
|
||||
req.package_id.clone().to_process_lib(),
|
||||
req.version_hash.clone(),
|
||||
) {
|
||||
Ok(manifest_hash) => Some(manifest_hash.as_bytes().to_vec()),
|
||||
Ok(manifest_hash) => Some(manifest_hash),
|
||||
Err(e) => {
|
||||
print_to_terminal(
|
||||
1,
|
||||
@ -267,13 +267,26 @@ fn handle_message(
|
||||
};
|
||||
|
||||
// pushed to UI via websockets
|
||||
let mut request = Request::to(("our", "main", "app_store", "sys"))
|
||||
.body(serde_json::to_vec(&req)?);
|
||||
Request::to(("our", "main", "app_store", "sys"))
|
||||
.body(serde_json::to_vec(&req)?)
|
||||
.send()?;
|
||||
|
||||
if let Some(ctx) = context {
|
||||
request = request.context(ctx);
|
||||
// trigger auto-update install trigger to main:app_store:sys
|
||||
if let Some(manifest_hash) = manifest_hash {
|
||||
let auto_download_complete_req = AutoDownloadCompleteRequest {
|
||||
download_info: req.clone(),
|
||||
manifest_hash,
|
||||
};
|
||||
print_to_terminal(
|
||||
1,
|
||||
&format!(
|
||||
"auto_update download complete: triggering install on main:app_store:sys"
|
||||
),
|
||||
);
|
||||
Request::to(("our", "main", "app_store", "sys"))
|
||||
.body(serde_json::to_vec(&auto_download_complete_req)?)
|
||||
.send()?;
|
||||
}
|
||||
request.send()?;
|
||||
}
|
||||
DownloadRequests::GetFiles(maybe_id) => {
|
||||
// if not local, throw to the boonies.
|
||||
|
@ -6,7 +6,7 @@ interface PackageSelectorProps {
|
||||
}
|
||||
|
||||
const PackageSelector: React.FC<PackageSelectorProps> = ({ onPackageSelect }) => {
|
||||
const { installed } = useAppsStore();
|
||||
const { installed, fetchInstalled } = useAppsStore();
|
||||
const [selectedPackage, setSelectedPackage] = useState<string>("");
|
||||
const [customPackage, setCustomPackage] = useState<string>("");
|
||||
const [isCustomPackageSelected, setIsCustomPackageSelected] = useState(false);
|
||||
@ -18,6 +18,10 @@ const PackageSelector: React.FC<PackageSelectorProps> = ({ onPackageSelect }) =>
|
||||
}
|
||||
}, [selectedPackage, onPackageSelect]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchInstalled();
|
||||
}, []);
|
||||
|
||||
const handlePackageChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
const value = e.target.value;
|
||||
if (value === "custom") {
|
||||
|
@ -1,11 +1,10 @@
|
||||
import React, { useState, useEffect, useCallback, useMemo } from "react";
|
||||
import { useParams, useNavigate } from "react-router-dom";
|
||||
import { useParams } from "react-router-dom";
|
||||
import { FaDownload, FaSpinner, FaChevronDown, FaChevronUp, FaRocket, FaTrash, FaPlay } from "react-icons/fa";
|
||||
import useAppsStore from "../store";
|
||||
import { MirrorSelector } from '../components';
|
||||
|
||||
export default function DownloadPage() {
|
||||
const navigate = useNavigate();
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const {
|
||||
listings,
|
||||
@ -28,6 +27,9 @@ export default function DownloadPage() {
|
||||
const [isMirrorOnline, setIsMirrorOnline] = useState<boolean | null>(null);
|
||||
const [showCapApproval, setShowCapApproval] = useState(false);
|
||||
const [manifest, setManifest] = useState<any>(null);
|
||||
const [isInstalling, setIsInstalling] = useState(false);
|
||||
const [isCheckingLaunch, setIsCheckingLaunch] = useState(false);
|
||||
const [launchPath, setLaunchPath] = useState<string | null>(null);
|
||||
|
||||
const app = useMemo(() => listings[id || ""], [listings, id]);
|
||||
const appDownloads = useMemo(() => downloads[id || ""] || [], [downloads, id]);
|
||||
@ -101,6 +103,36 @@ export default function DownloadPage() {
|
||||
return versionData ? installedApp.our_version_hash === versionData.hash : false;
|
||||
}, [app, selectedVersion, installedApp, sortedVersions]);
|
||||
|
||||
const checkLaunchPath = useCallback(() => {
|
||||
if (!app) return;
|
||||
setIsCheckingLaunch(true);
|
||||
const appId = `${app.package_id.package_name}:${app.package_id.publisher_node}`;
|
||||
fetchHomepageApps().then(() => {
|
||||
const path = getLaunchUrl(appId);
|
||||
setLaunchPath(path);
|
||||
setIsCheckingLaunch(false);
|
||||
if (path) {
|
||||
setIsInstalling(false);
|
||||
}
|
||||
});
|
||||
}, [app, fetchHomepageApps, getLaunchUrl]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isInstalling) {
|
||||
const checkInterval = setInterval(checkLaunchPath, 500);
|
||||
const timeout = setTimeout(() => {
|
||||
clearInterval(checkInterval);
|
||||
setIsInstalling(false);
|
||||
setIsCheckingLaunch(false);
|
||||
}, 5000);
|
||||
|
||||
return () => {
|
||||
clearInterval(checkInterval);
|
||||
clearTimeout(timeout);
|
||||
};
|
||||
}
|
||||
}, [isInstalling, checkLaunchPath]);
|
||||
|
||||
const handleDownload = useCallback(() => {
|
||||
if (!id || !selectedMirror || !app || !selectedVersion) return;
|
||||
const versionData = sortedVersions.find(v => v.version === selectedVersion);
|
||||
@ -130,36 +162,87 @@ export default function DownloadPage() {
|
||||
}
|
||||
}, [id, app, appDownloads]);
|
||||
|
||||
const canDownload = useMemo(() => {
|
||||
return selectedMirror && (isMirrorOnline === true || selectedMirror.startsWith('http')) && !isDownloading && !isDownloaded;
|
||||
}, [selectedMirror, isMirrorOnline, isDownloading, isDownloaded]);
|
||||
|
||||
const confirmInstall = useCallback(() => {
|
||||
if (!id || !selectedVersion) return;
|
||||
const versionData = sortedVersions.find(v => v.version === selectedVersion);
|
||||
if (versionData) {
|
||||
setIsInstalling(true);
|
||||
setLaunchPath(null);
|
||||
installApp(id, versionData.hash).then(() => {
|
||||
fetchData(id);
|
||||
setShowCapApproval(false);
|
||||
setManifest(null);
|
||||
fetchData(id);
|
||||
});
|
||||
}
|
||||
}, [id, selectedVersion, sortedVersions, installApp, fetchData]);
|
||||
|
||||
const handleLaunch = useCallback(() => {
|
||||
if (app) {
|
||||
const launchUrl = getLaunchUrl(`${app.package_id.package_name}:${app.package_id.publisher_node}`);
|
||||
if (launchUrl) {
|
||||
window.location.href = launchUrl;
|
||||
if (launchPath) {
|
||||
window.location.href = launchPath;
|
||||
}
|
||||
}
|
||||
}, [app, getLaunchUrl]);
|
||||
}, [launchPath]);
|
||||
|
||||
const canLaunch = useMemo(() => {
|
||||
if (!app) return false;
|
||||
return !!getLaunchUrl(`${app.package_id.package_name}:${app.package_id.publisher_node}`);
|
||||
}, [app, getLaunchUrl]);
|
||||
|
||||
const canDownload = useMemo(() => {
|
||||
return selectedMirror && (isMirrorOnline === true || selectedMirror.startsWith('http')) && !isDownloading && !isDownloaded;
|
||||
}, [selectedMirror, isMirrorOnline, isDownloading, isDownloaded]);
|
||||
|
||||
const renderActionButton = () => {
|
||||
if (isCurrentVersionInstalled || launchPath) {
|
||||
return (
|
||||
<button className="action-button installed-button" disabled>
|
||||
<FaRocket /> Installed
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
if (isInstalling || isCheckingLaunch) {
|
||||
return (
|
||||
<button className="action-button installing-button" disabled>
|
||||
<FaSpinner className="fa-spin" /> Installing...
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
if (isDownloaded) {
|
||||
return (
|
||||
<button
|
||||
onClick={() => {
|
||||
const versionData = sortedVersions.find(v => v.version === selectedVersion);
|
||||
if (versionData) {
|
||||
handleInstall(versionData.version, versionData.hash);
|
||||
}
|
||||
}}
|
||||
className="action-button install-button"
|
||||
>
|
||||
<FaRocket /> Install
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={handleDownload}
|
||||
disabled={!canDownload}
|
||||
className="action-button download-button"
|
||||
>
|
||||
{isDownloading ? (
|
||||
<>
|
||||
<FaSpinner className="fa-spin" /> Downloading... {downloadProgress}%
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<FaDownload /> Download
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
if (!app) {
|
||||
return <div className="downloads-page"><h4>Loading app details...</h4></div>;
|
||||
}
|
||||
@ -176,15 +259,22 @@ export default function DownloadPage() {
|
||||
<p className="app-id">{`${app.package_id.package_name}.${app.package_id.publisher_node}`}</p>
|
||||
</div>
|
||||
</div>
|
||||
{installedApp && (
|
||||
{launchPath ? (
|
||||
<button
|
||||
onClick={handleLaunch}
|
||||
className="launch-button"
|
||||
disabled={!canLaunch}
|
||||
>
|
||||
<FaPlay /> {canLaunch ? 'Launch' : 'No UI found for app'}
|
||||
<FaPlay /> Launch
|
||||
</button>
|
||||
)}
|
||||
) : isInstalling || isCheckingLaunch ? (
|
||||
<button className="launch-button" disabled>
|
||||
<FaSpinner className="fa-spin" /> Checking...
|
||||
</button>
|
||||
) : installedApp ? (
|
||||
<button className="launch-button" disabled>
|
||||
No UI found for app
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
<p className="app-description">{app.metadata?.description}</p>
|
||||
|
||||
@ -207,39 +297,7 @@ export default function DownloadPage() {
|
||||
onMirrorSelect={handleMirrorSelect}
|
||||
/>
|
||||
|
||||
{isCurrentVersionInstalled ? (
|
||||
<button className="action-button installed-button" disabled>
|
||||
<FaRocket /> Installed
|
||||
</button>
|
||||
) : isDownloaded ? (
|
||||
<button
|
||||
onClick={() => {
|
||||
const versionData = sortedVersions.find(v => v.version === selectedVersion);
|
||||
if (versionData) {
|
||||
handleInstall(versionData.version, versionData.hash);
|
||||
}
|
||||
}}
|
||||
className="action-button install-button"
|
||||
>
|
||||
<FaRocket /> Install
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
onClick={handleDownload}
|
||||
disabled={!canDownload}
|
||||
className="action-button download-button"
|
||||
>
|
||||
{isDownloading ? (
|
||||
<>
|
||||
<FaSpinner className="fa-spin" /> Downloading... {downloadProgress}%
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<FaDownload /> Download
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
{renderActionButton()}
|
||||
</div>
|
||||
|
||||
<div className="my-downloads">
|
||||
|
@ -12,7 +12,7 @@ const NAME_INVALID = "Package name must contain only valid characters (a-z, 0-9,
|
||||
|
||||
export default function PublishPage() {
|
||||
const { openConnectModal } = useConnectModal();
|
||||
const { ourApps, fetchOurApps, installed, downloads } = useAppsStore();
|
||||
const { ourApps, fetchOurApps, downloads } = useAppsStore();
|
||||
const publicClient = usePublicClient();
|
||||
|
||||
const { address, isConnected, isConnecting } = useAccount();
|
||||
|
@ -218,12 +218,6 @@ const useAppsStore = create<AppsStore>()((set, get) => ({
|
||||
});
|
||||
if (res.status === HTTP_STATUS.CREATED) {
|
||||
await get().fetchInstalled();
|
||||
|
||||
// hacky: a small delay (500ms) before fetching homepage apps
|
||||
// to give the app time to add itself to the homepage
|
||||
// might make sense to add more state and do retry logic instead.
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
|
||||
await get().fetchHomepageApps();
|
||||
}
|
||||
} catch (error) {
|
||||
|
Loading…
Reference in New Issue
Block a user