Merge pull request #568 from kinode-dao/bp/autoupdatesfix

app_store: fix auto_updates
This commit is contained in:
doria 2024-10-11 01:36:56 +09:00 committed by GitHub
commit c91a8898b7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 181 additions and 104 deletions

View File

@ -184,6 +184,8 @@ interface downloads {
auto-update(auto-update-request), auto-update(auto-update-request),
/// Notify that a download is complete /// Notify that a download is complete
download-complete(download-complete-request), download-complete(download-complete-request),
/// Auto-update-download complete
auto-download-complete(auto-download-complete-request),
/// Get files for a package /// Get files for a package
get-files(option<package-id>), get-files(option<package-id>),
/// Remove a file /// Remove a file
@ -243,6 +245,12 @@ interface downloads {
err: option<download-error>, 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 /// Represents a hash mismatch error
record hash-mismatch { record hash-mismatch {
desired: string, desired: string,

View File

@ -30,7 +30,7 @@
//! It delegates these responsibilities to the downloads and chain processes respectively. //! It delegates these responsibilities to the downloads and chain processes respectively.
//! //!
use crate::kinode::process::downloads::{ use crate::kinode::process::downloads::{
DownloadCompleteRequest, DownloadResponses, ProgressUpdate, AutoDownloadCompleteRequest, DownloadCompleteRequest, DownloadResponses, ProgressUpdate,
}; };
use crate::kinode::process::main::{ use crate::kinode::process::main::{
ApisResponse, GetApiResponse, InstallPackageRequest, InstallResponse, LocalRequest, ApisResponse, GetApiResponse, InstallPackageRequest, InstallResponse, LocalRequest,
@ -65,6 +65,7 @@ pub enum Req {
LocalRequest(LocalRequest), LocalRequest(LocalRequest),
Progress(ProgressUpdate), Progress(ProgressUpdate),
DownloadComplete(DownloadCompleteRequest), DownloadComplete(DownloadCompleteRequest),
AutoDownloadComplete(AutoDownloadCompleteRequest),
Http(http::server::HttpServerRequest), 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) => { Req::DownloadComplete(req) => {
if !message.is_local(&our) { if !message.is_local(&our) {
return Err(anyhow::anyhow!("download complete from non-local node")); return Err(anyhow::anyhow!("download complete from non-local node"));
@ -182,41 +217,6 @@ fn handle_message(
.unwrap(), .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 { } else {

View File

@ -42,9 +42,9 @@
//! mechanism is implemented in the FT worker for improved modularity and performance. //! mechanism is implemented in the FT worker for improved modularity and performance.
//! //!
use crate::kinode::process::downloads::{ use crate::kinode::process::downloads::{
AutoUpdateRequest, DirEntry, DownloadCompleteRequest, DownloadError, DownloadRequests, AutoDownloadCompleteRequest, AutoUpdateRequest, DirEntry, DownloadCompleteRequest,
DownloadResponses, Entry, FileEntry, HashMismatch, LocalDownloadRequest, RemoteDownloadRequest, DownloadError, DownloadRequests, DownloadResponses, Entry, FileEntry, HashMismatch,
RemoveFileRequest, LocalDownloadRequest, RemoteDownloadRequest, RemoveFileRequest,
}; };
use std::{collections::HashSet, io::Read, str::FromStr}; 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. // 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. // 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.package_id.clone().to_process_lib(),
req.version_hash.clone(), req.version_hash.clone(),
)) { )) {
@ -253,7 +253,7 @@ fn handle_message(
req.package_id.clone().to_process_lib(), req.package_id.clone().to_process_lib(),
req.version_hash.clone(), req.version_hash.clone(),
) { ) {
Ok(manifest_hash) => Some(manifest_hash.as_bytes().to_vec()), Ok(manifest_hash) => Some(manifest_hash),
Err(e) => { Err(e) => {
print_to_terminal( print_to_terminal(
1, 1,
@ -267,13 +267,26 @@ fn handle_message(
}; };
// pushed to UI via websockets // pushed to UI via websockets
let mut request = Request::to(("our", "main", "app_store", "sys")) Request::to(("our", "main", "app_store", "sys"))
.body(serde_json::to_vec(&req)?); .body(serde_json::to_vec(&req)?)
.send()?;
if let Some(ctx) = context { // trigger auto-update install trigger to main:app_store:sys
request = request.context(ctx); 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) => { DownloadRequests::GetFiles(maybe_id) => {
// if not local, throw to the boonies. // if not local, throw to the boonies.

View File

@ -6,7 +6,7 @@ interface PackageSelectorProps {
} }
const PackageSelector: React.FC<PackageSelectorProps> = ({ onPackageSelect }) => { const PackageSelector: React.FC<PackageSelectorProps> = ({ onPackageSelect }) => {
const { installed } = useAppsStore(); const { installed, fetchInstalled } = useAppsStore();
const [selectedPackage, setSelectedPackage] = useState<string>(""); const [selectedPackage, setSelectedPackage] = useState<string>("");
const [customPackage, setCustomPackage] = useState<string>(""); const [customPackage, setCustomPackage] = useState<string>("");
const [isCustomPackageSelected, setIsCustomPackageSelected] = useState(false); const [isCustomPackageSelected, setIsCustomPackageSelected] = useState(false);
@ -18,6 +18,10 @@ const PackageSelector: React.FC<PackageSelectorProps> = ({ onPackageSelect }) =>
} }
}, [selectedPackage, onPackageSelect]); }, [selectedPackage, onPackageSelect]);
useEffect(() => {
fetchInstalled();
}, []);
const handlePackageChange = (e: React.ChangeEvent<HTMLSelectElement>) => { const handlePackageChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
const value = e.target.value; const value = e.target.value;
if (value === "custom") { if (value === "custom") {

View File

@ -1,11 +1,10 @@
import React, { useState, useEffect, useCallback, useMemo } from "react"; 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 { FaDownload, FaSpinner, FaChevronDown, FaChevronUp, FaRocket, FaTrash, FaPlay } from "react-icons/fa";
import useAppsStore from "../store"; import useAppsStore from "../store";
import { MirrorSelector } from '../components'; import { MirrorSelector } from '../components';
export default function DownloadPage() { export default function DownloadPage() {
const navigate = useNavigate();
const { id } = useParams<{ id: string }>(); const { id } = useParams<{ id: string }>();
const { const {
listings, listings,
@ -28,6 +27,9 @@ export default function DownloadPage() {
const [isMirrorOnline, setIsMirrorOnline] = useState<boolean | null>(null); const [isMirrorOnline, setIsMirrorOnline] = useState<boolean | null>(null);
const [showCapApproval, setShowCapApproval] = useState(false); const [showCapApproval, setShowCapApproval] = useState(false);
const [manifest, setManifest] = useState<any>(null); 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 app = useMemo(() => listings[id || ""], [listings, id]);
const appDownloads = useMemo(() => downloads[id || ""] || [], [downloads, 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; return versionData ? installedApp.our_version_hash === versionData.hash : false;
}, [app, selectedVersion, installedApp, sortedVersions]); }, [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(() => { const handleDownload = useCallback(() => {
if (!id || !selectedMirror || !app || !selectedVersion) return; if (!id || !selectedMirror || !app || !selectedVersion) return;
const versionData = sortedVersions.find(v => v.version === selectedVersion); const versionData = sortedVersions.find(v => v.version === selectedVersion);
@ -130,36 +162,87 @@ export default function DownloadPage() {
} }
}, [id, app, appDownloads]); }, [id, app, appDownloads]);
const canDownload = useMemo(() => {
return selectedMirror && (isMirrorOnline === true || selectedMirror.startsWith('http')) && !isDownloading && !isDownloaded;
}, [selectedMirror, isMirrorOnline, isDownloading, isDownloaded]);
const confirmInstall = useCallback(() => { const confirmInstall = useCallback(() => {
if (!id || !selectedVersion) return; if (!id || !selectedVersion) return;
const versionData = sortedVersions.find(v => v.version === selectedVersion); const versionData = sortedVersions.find(v => v.version === selectedVersion);
if (versionData) { if (versionData) {
setIsInstalling(true);
setLaunchPath(null);
installApp(id, versionData.hash).then(() => { installApp(id, versionData.hash).then(() => {
fetchData(id);
setShowCapApproval(false); setShowCapApproval(false);
setManifest(null); setManifest(null);
fetchData(id);
}); });
} }
}, [id, selectedVersion, sortedVersions, installApp, fetchData]); }, [id, selectedVersion, sortedVersions, installApp, fetchData]);
const handleLaunch = useCallback(() => { const handleLaunch = useCallback(() => {
if (app) { if (launchPath) {
const launchUrl = getLaunchUrl(`${app.package_id.package_name}:${app.package_id.publisher_node}`); window.location.href = launchPath;
if (launchUrl) {
window.location.href = launchUrl;
}
} }
}, [app, getLaunchUrl]); }, [launchPath]);
const canLaunch = useMemo(() => { const canLaunch = useMemo(() => {
if (!app) return false; if (!app) return false;
return !!getLaunchUrl(`${app.package_id.package_name}:${app.package_id.publisher_node}`); return !!getLaunchUrl(`${app.package_id.package_name}:${app.package_id.publisher_node}`);
}, [app, getLaunchUrl]); }, [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) { if (!app) {
return <div className="downloads-page"><h4>Loading app details...</h4></div>; 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> <p className="app-id">{`${app.package_id.package_name}.${app.package_id.publisher_node}`}</p>
</div> </div>
</div> </div>
{installedApp && ( {launchPath ? (
<button <button
onClick={handleLaunch} onClick={handleLaunch}
className="launch-button" className="launch-button"
disabled={!canLaunch}
> >
<FaPlay /> {canLaunch ? 'Launch' : 'No UI found for app'} <FaPlay /> Launch
</button> </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> </div>
<p className="app-description">{app.metadata?.description}</p> <p className="app-description">{app.metadata?.description}</p>
@ -207,39 +297,7 @@ export default function DownloadPage() {
onMirrorSelect={handleMirrorSelect} onMirrorSelect={handleMirrorSelect}
/> />
{isCurrentVersionInstalled ? ( {renderActionButton()}
<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>
)}
</div> </div>
<div className="my-downloads"> <div className="my-downloads">

View File

@ -12,7 +12,7 @@ const NAME_INVALID = "Package name must contain only valid characters (a-z, 0-9,
export default function PublishPage() { export default function PublishPage() {
const { openConnectModal } = useConnectModal(); const { openConnectModal } = useConnectModal();
const { ourApps, fetchOurApps, installed, downloads } = useAppsStore(); const { ourApps, fetchOurApps, downloads } = useAppsStore();
const publicClient = usePublicClient(); const publicClient = usePublicClient();
const { address, isConnected, isConnecting } = useAccount(); const { address, isConnected, isConnecting } = useAccount();

View File

@ -218,12 +218,6 @@ const useAppsStore = create<AppsStore>()((set, get) => ({
}); });
if (res.status === HTTP_STATUS.CREATED) { if (res.status === HTTP_STATUS.CREATED) {
await get().fetchInstalled(); 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(); await get().fetchHomepageApps();
} }
} catch (error) { } catch (error) {