API
nsh/updater
nsh/updater 为打包后的应用检查新版本、下载并替换应用。更新从哪里来由 nsh.json 的 "updater" 决定,调用模块的代码无法指定。每一步都在 Rust 中校验:清单签名、应用 id、版本更新、压缩包的大小和 SHA-256。界面由你自己画,模块负责返回状态和完成工作。从设置来源到发布版本的完整流程见应用更新。
它也能只更新应用的代码:一个更新构建号的签名包,应用在所有原生平台(包括 iOS 和 Android)下次启动时运行它(见 OTA 更新)。
import { checkForUpdate, downloadUpdate, installAndRestart } from "nsh/updater";
import { checkForPackage, downloadPackage, restartToPackage, runningPackage, shellInfo } from "nsh/updater";
这个模块在所有平台上都可以导入:
| 平台 | checkForUpdate |
downloadUpdate、installAndRestart |
包 |
|---|---|---|---|
| macOS、Windows、Linux(打包后) | 检查来源 | 下载并替换应用 | 检查、下载,restartToPackage 重启进入新包 |
| iOS、Android | 检查来源;url 是应用商店链接(nsh update-manifest --link) |
拒绝:引导用户打开 url |
检查并下载;包在下次启动时运行 |
Web、开发运行、没有 "updater" 的应用 |
返回 null |
拒绝 | checkForPackage 返回 null,runningPackage() 为 null,shellInfo() 没有 version |
它不需要任何能力声明,capabilities.network 对它不生效,因为应用代码无法决定这些请求发往哪里。请求走平台自己的网络栈,和 fetch 一样。
#checkForUpdate
function checkForUpdate(): Promise<AppUpdate | null>;
interface AppUpdate {
version: string;
notes?: string | null;
url: string;
canInstall: boolean;
}
从 nsh.json 中的来源获取清单,返回其中描述的新版本;应用已是最新、没有 "updater",或运行在不更新的环境(Web、nsh run)时返回 null。正在运行该版本或更高版本的包时也算最新:应用本身的版本仍是安装时的版本,但代码已经是该版本的。
| 字段 | 含义 |
|---|---|
version |
新版本号,semver |
notes |
更新说明,即清单的 --notes 文件内容 |
url |
用户手动获取的地址:压缩包,或 iOS、Android 上的商店页面。canInstall 为 false 时交给 openUrl |
canInstall |
本次运行中 downloadUpdate 和 installAndRestart 是否可用:装在用户可写位置的桌面打包应用为 true。iOS、Android 上为 false,装在只有系统可写的目录(系统包)时也为 false |
来源无法访问或清单不可接受时会 reject:设置了 publicKey 但签名错误或缺失、清单属于其他应用、URL 不是 HTTPS。版本相同或更旧的清单不算更新,因此过期的清单无法让应用降级。
#downloadUpdate
function downloadUpdate(onProgress?: ((progress: UpdateProgress) => void) | null): Promise<void>;
interface UpdateProgress {
received: number;
total?: number | null;
}
把上一次 checkForUpdate 找到的更新压缩包下载到应用的数据目录,收到数据时调用 onProgress(服务器未给出大小时没有 total),然后按清单校验大小和 SHA-256。没有找到更新或 canInstall 为 false 时以 ERR_FEATURE_UNAVAILABLE_ON_PLATFORM reject;下载失败或校验不符时也会 reject。
const progress = signal(0);
await downloadUpdate(({ received, total }) => progress.set(total ? received / total : 0));
#installAndRestart
function installAndRestart(): Promise<void>;
把下载好的压缩包解压到应用旁边,校验新应用(id、可执行文件,macOS 上还有代码签名),替换进去,然后像 Application.restart() 一样重启应用:当前进程像用户退出时那样退出,进程结束后新版本启动。Promise 只在失败时才会结束,失败时已安装的应用保持原样:没有下载过更新或应用无法替换时 reject。
Linux 和 macOS 上整个目录或 .app 一起替换,保留用户给它起的名字;Windows 上逐个文件替换,有一个失败就全部恢复。
#OTA 更新
OTA(over-the-air,空中下发)更新把打包应用的 TSX 作为签名包(nsh publish)下发,而不必发新应用。设置、发布和设备上的行为见 OTA 更新。nsh.json 信任包根密钥("updater": { "packages": { "roots": [...] } },见下文)的应用会在同一份清单里找到它,下载后从下次启动开始运行。NSH 运行时本身的改动仍然需要新的应用构建:每个包都写明能运行它的最低运行时,较旧的应用不会收到它。
#checkForPackage
function checkForPackage(): Promise<PackageUpdate | null>;
interface PackageUpdate {
version: string;
build: number;
notes?: string | null;
size?: number | null;
}
像 checkForUpdate 一样获取清单,当其中的包构建号比当前运行的更新(正在运行的包的构建号,否则是应用自带的构建号),并且当前运行时能运行它时,返回这个包。没有新包、应用没有信任任何包根密钥,或运行在不更新的环境(Web、nsh run)时返回 null。出错时与 checkForUpdate 一样拒绝。
#downloadPackage
function downloadPackage(onProgress?: ((progress: UpdateProgress) => void) | null): Promise<void>;
下载上一次 checkForPackage 找到的包,按清单核对大小和 SHA-256,并按应用下次启动时的方式验证它:由某个根密钥认证的发布密钥签名、属于本应用、构建号比当前运行的以及之前运行过的都新、适用于当前运行时。之后它在应用的数据目录中等待下次启动。上次检查没有找到包时以 ERR_FEATURE_UNAVAILABLE_ON_PLATFORM 拒绝;下载失败或包被拒绝时以说明原因的错误(package refused: …)拒绝,且不留下任何文件。
#restartToPackage
function restartToPackage(): Promise<void>;
在 macOS、Windows 和 Linux 上,像 Application.restart() 一样重启应用并进入下载好的包;只有失败时 promise 才会结束。没有下载任何包时拒绝;在 iOS 和 Android 上以 ERR_FEATURE_UNAVAILABLE_ON_PLATFORM 拒绝,包在应用下次启动时运行。
#runningPackage
function runningPackage(): RunningPackage | null;
interface RunningPackage {
version: string;
build: number;
}
本次运行的代码所来自的包;运行应用自带的代码时为 null。
#shellInfo
function shellInfo(): ShellInfo;
interface ShellInfo {
version: string | null;
build: number | null;
nshVersion: string;
packages: boolean;
}
本次代码所运行在的已安装应用,即它的外壳:打包时的 version 和 build(来自 nsh.json;在 iOS 上就是它的
CFBundleShortVersionString 和 CFBundleVersion)、外壳内置的 NSH 运行时版本,以及它能否运行签名包。包替换的是应用的代码,
不是外壳:runningPackage() 说明运行的是哪份代码,shellInfo() 说明由什么来运行它,所以需要更新运行时(minRuntime)的包,
可以据此提示用户安装完整的新应用。没有在 nsh.json 中配置 "updater" 的应用,以及 Web 和开发运行时,version 和
build 为 null,packages 为 false。
#下次启动时
应用运行通过验证、且比自带构建号更新的最新包。新包第一次运行是试运行:从第一帧起运行 10 秒,或用户退出应用后,它成为已安装的包。如果它启动失败、VM 被停止,或在此之前应用崩溃,应用会回到之前运行的包(或自带的代码),并删除新包。包以已安装应用的身份运行:使用它的 id、URL scheme 和更新来源,能力最多为已安装应用声明的能力。构建号更新的应用更新会替换旧构建的包。
import { checkForPackage, downloadPackage, restartToPackage } from "nsh/updater";
const found = await checkForPackage();
if (found) {
await downloadPackage();
// 桌面端:立即重启。iOS 和 Android:下次启动时运行。
await restartToPackage().catch(() => {});
}
#进行中的调用
同一时间只运行一个检查、下载或安装:其中一个进行时再调用任何一个都会 reject。进行期间请禁用按钮。
import { openUrl, signal } from "nsh";
import { checkForUpdate, downloadUpdate, installAndRestart, type AppUpdate } from "nsh/updater";
const update = signal<AppUpdate | null>(null);
const busy = signal(false);
async function run<T>(work: () => Promise<T>) {
busy.set(true);
try {
return await work();
} finally {
busy.set(false);
}
}
export const check = () => run(async () => update.set(await checkForUpdate()));
export const install = () =>
run(async () => {
const found = update();
if (!found) return;
if (!found.canInstall) return openUrl(found.url);
await downloadUpdate();
await installAndRestart();
});
#nsh.json
"updater" 指定一个来源:github,即最新 GitHub Release 中带有 latest.json 的 owner/repo;或 url,即自己服务器上的清单,此时必须配 publicKey。publicKey 是 nsh keygen 生成的 .pub 文件内容;配了它,未签名的清单会被拒绝。
{
"id": "com.example.notes",
"version": "1.2.0",
"updater": { "github": "example/notes", "publicKey": "<keys/update.pub 的内容>" }
}
| 键 | 类型 | 含义 |
|---|---|---|
github |
string | owner/repo:清单是其最新 Release 的附件 latest.json,跳过 draft 和 prerelease |
url |
string | 自己服务器上清单的 HTTPS 地址;压缩包的相对地址相对它解析 |
publicKey |
string | 给清单签名(latest.json.sig)的 Ed25519 公钥。使用 url 时必填 |
packages.roots |
string[] | 根公钥(nsh keygen 的 root.pub),由它们认证的发布密钥为应用的 OTA 包签名。没有它时应用不运行任何包 |
nsh package 会校验这些键,并把它们连同应用的 build 放进包里,所以来源在构建应用时就已确定。version 必须是 semver。nsh update-manifest 为一个版本的压缩包和包生成 latest.json 和签名(CLI)。
#Rust
Rust 应用(nsh.json 的 "root")直接使用 nsh-updater crate,并自带 HTTP 客户端:Config::check 校验清单,Download 校验压缩包,Installation::install 替换应用,nsh_core::element::desktop::restart() 启动新版本(应用更新)。