akanjs 3.0.0-beta.28 → 3.0.0-beta.29
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/server/akanApp.ts +5 -3
- package/server/akanServer.ts +16 -6
- package/server/contentEncoding.ts +11 -0
- package/server/ops/appInfo.ts +3 -1
- package/server/selfExec.ts +12 -4
- package/types/server/ops/appInfo.d.ts +1 -0
- package/types/vendor/@akanjs/native/packages/cli/src/api.d.ts +17 -1
- package/types/vendor/@akanjs/native/packages/cli/src/lib/publish.d.ts +8 -0
- package/types/vendor/@akanjs/native/packages/cli/src/lib/updates.d.ts +2 -0
- package/types/vendor/@akanjs/native/packages/cli/src/platforms/desktop-update.d.ts +7 -1
- package/types/vendor/@akanjs/native/packages/cli/src/platforms/windows-installer.d.ts +21 -6
- package/types/vendor/@akanjs/native/packages/desktop/src/plugin.d.ts +19 -5
- package/types/vendor/@akanjs/native/packages/desktop/src/relaunch.d.ts +20 -1
- package/types/vendor/@akanjs/native/packages/desktop/src/server.d.ts +18 -0
- package/types/vendor/@akanjs/native/plugins/app/src/index.d.ts +7 -0
- package/types/vendor/@akanjs/native/plugins/updates/src/desktop.d.ts +7 -1
- package/types/vendor/@akanjs/native/plugins/updates/src/index.d.ts +18 -6
- package/vendor/@akanjs/native/packages/cli/src/api.ts +32 -12
- package/vendor/@akanjs/native/packages/cli/src/commands/update.ts +9 -2
- package/vendor/@akanjs/native/packages/cli/src/lib/publish.ts +22 -5
- package/vendor/@akanjs/native/packages/cli/src/lib/updates.ts +2 -0
- package/vendor/@akanjs/native/packages/cli/src/platforms/desktop-update.ts +51 -26
- package/vendor/@akanjs/native/packages/cli/src/platforms/windows-installer.ts +198 -33
- package/vendor/@akanjs/native/packages/desktop/src/dispatcher.ts +9 -2
- package/vendor/@akanjs/native/packages/desktop/src/host.ts +21 -9
- package/vendor/@akanjs/native/packages/desktop/src/plugin.ts +20 -5
- package/vendor/@akanjs/native/packages/desktop/src/relaunch.ts +82 -5
- package/vendor/@akanjs/native/packages/desktop/src/server.ts +124 -7
- package/vendor/@akanjs/native/plugins/app/native-plugin.json +1 -1
- package/vendor/@akanjs/native/plugins/app/src/desktop.ts +5 -0
- package/vendor/@akanjs/native/plugins/app/src/index.ts +6 -1
- package/vendor/@akanjs/native/plugins/updates/src/desktop.ts +410 -111
- package/vendor/@akanjs/native/plugins/updates/src/index.ts +18 -6
package/package.json
CHANGED
package/server/akanApp.ts
CHANGED
|
@@ -242,7 +242,7 @@ export class AkanApp {
|
|
|
242
242
|
if (this.#solo) return await this.#startSolo();
|
|
243
243
|
if (SelfExec.carried)
|
|
244
244
|
throw new Error(
|
|
245
|
-
"A desktop app's server runs in one process, and this
|
|
245
|
+
"A desktop app's server runs in one process, and this one was asked for a gateway and replicas, which would start with the `bun` on the user's PATH: by `replica` or `solo: false` in main.ts, or by AKAN_SOLO=false, an AKAN_REPLICA other than one traffic replica, or AKAN_COMMAND_TYPE=start in its env. Leave them out of the app the desktop build carries.",
|
|
246
246
|
);
|
|
247
247
|
Logger.role = "gateway";
|
|
248
248
|
await this.#prepareRuntimeDir();
|
|
@@ -943,8 +943,10 @@ export class AkanApp {
|
|
|
943
943
|
async #proxyResponse(req: Request, upstreamRes: Response): Promise<Response> {
|
|
944
944
|
const headers = new Headers(upstreamRes.headers);
|
|
945
945
|
|
|
946
|
-
headers.
|
|
947
|
-
|
|
946
|
+
if (headers.has("content-encoding")) {
|
|
947
|
+
headers.delete("content-encoding");
|
|
948
|
+
headers.delete("content-length");
|
|
949
|
+
}
|
|
948
950
|
this.#rewriteInternalLocation(headers);
|
|
949
951
|
const proxied = new Response(upstreamRes.body, {
|
|
950
952
|
status: upstreamRes.status,
|
package/server/akanServer.ts
CHANGED
|
@@ -170,6 +170,7 @@ export class AkanServer {
|
|
|
170
170
|
#ops: OpsRoute | null | undefined;
|
|
171
171
|
#lastMetrics: AkanMetricsReport = {};
|
|
172
172
|
#stopping: Promise<void> | null = null;
|
|
173
|
+
#parentGone: boolean = false;
|
|
173
174
|
#orphanExiting = false;
|
|
174
175
|
constructor(
|
|
175
176
|
name = "AkanServer",
|
|
@@ -472,7 +473,7 @@ export class AkanServer {
|
|
|
472
473
|
this.#di.registerSchedule(this.serverMode);
|
|
473
474
|
this.logger.verbose(`🚀 ${this.name} is running on ${unix ? `unix://${unix}` : `port ${port}`}`);
|
|
474
475
|
const wsPort = this.#wsServer?.port;
|
|
475
|
-
|
|
476
|
+
this.#sendReady({
|
|
476
477
|
type: "ready",
|
|
477
478
|
pid: process.pid,
|
|
478
479
|
replicaIdx: Number(process.env.AKAN_REPLICA_IDX ?? 0),
|
|
@@ -481,7 +482,7 @@ export class AkanServer {
|
|
|
481
482
|
wsUpstream: typeof wsPort === "number" ? { type: "tcp", host: "127.0.0.1", port: wsPort } : undefined,
|
|
482
483
|
healthPath: "/_akan/app/child-health",
|
|
483
484
|
crossSite: CrossSiteGuard.option(),
|
|
484
|
-
}
|
|
485
|
+
});
|
|
485
486
|
await this.#di.runSchedulerInit();
|
|
486
487
|
ShutdownManager.register(this.logger, () => this.stop());
|
|
487
488
|
return this;
|
|
@@ -490,7 +491,10 @@ export class AkanServer {
|
|
|
490
491
|
async start({ listen, web }: { listen?: boolean; web?: AkanWebOption } = {}) {
|
|
491
492
|
const isNoListenCommand = process.env.AKAN_COMMAND_TYPE === "script" || process.env.AKAN_COMMAND_TYPE === "console";
|
|
492
493
|
const shouldListen = (listen ?? !isNoListenCommand) && this.serverMode !== "batch";
|
|
494
|
+
|
|
495
|
+
if (shouldListen || !isNoListenCommand) this.#registerParentIpc();
|
|
493
496
|
await this.init({ routes: shouldListen, web });
|
|
497
|
+
if (this.#parentGone) return this;
|
|
494
498
|
if (!shouldListen) {
|
|
495
499
|
const websocket = this.#di.getWebsocketAdaptor();
|
|
496
500
|
if (websocket)
|
|
@@ -500,20 +504,18 @@ export class AkanServer {
|
|
|
500
504
|
Logger.role = this.serverMode;
|
|
501
505
|
this.#metricsTimer ??= ProcessMetricsCollector.startReporting(() => this.#reportMetrics());
|
|
502
506
|
this.#di.registerSchedule(this.serverMode);
|
|
503
|
-
this.#registerParentIpc();
|
|
504
507
|
await this.#startLogTransport();
|
|
505
|
-
|
|
508
|
+
this.#sendReady({
|
|
506
509
|
type: "ready",
|
|
507
510
|
pid: process.pid,
|
|
508
511
|
replicaIdx: Number(process.env.AKAN_REPLICA_IDX ?? 0),
|
|
509
512
|
role: this.serverMode,
|
|
510
|
-
}
|
|
513
|
+
});
|
|
511
514
|
await this.#di.runSchedulerInit();
|
|
512
515
|
ShutdownManager.register(this.logger, () => this.stop());
|
|
513
516
|
}
|
|
514
517
|
return this;
|
|
515
518
|
}
|
|
516
|
-
this.#registerParentIpc();
|
|
517
519
|
return this.listen();
|
|
518
520
|
}
|
|
519
521
|
stop(): Promise<void> {
|
|
@@ -560,9 +562,17 @@ export class AkanServer {
|
|
|
560
562
|
#registerParentIpc() {
|
|
561
563
|
process.on("message", (message) => this.#handleIpcMessage(message as AkanIpcMessage));
|
|
562
564
|
process.on("disconnect", () => this.#handleParentDisconnect());
|
|
565
|
+
|
|
566
|
+
if (process.send && !process.connected) this.#handleParentDisconnect();
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
#sendReady(message: AkanIpcMessage) {
|
|
570
|
+
if (process.send?.(message) === false) this.#handleParentDisconnect();
|
|
563
571
|
}
|
|
564
572
|
|
|
565
573
|
#handleParentDisconnect() {
|
|
574
|
+
if (this.#parentGone) return;
|
|
575
|
+
this.#parentGone = true;
|
|
566
576
|
this.logger.warn("Parent IPC channel closed; shutting down to avoid an orphaned replica");
|
|
567
577
|
setTimeout(() => this.#exitOrphaned(1), this.shutdownTimeoutMs + 1_000);
|
|
568
578
|
void this.stop()
|
|
@@ -32,6 +32,8 @@ const GZIP_LEVEL = 6;
|
|
|
32
32
|
/** Under a KB the framing bytes and the call cost more than the repetition they remove. */
|
|
33
33
|
const MIN_COMPRESS_BYTES = 1024;
|
|
34
34
|
|
|
35
|
+
const MAX_COMPRESS_BYTES = 4 * 1024 * 1024;
|
|
36
|
+
|
|
35
37
|
const compressBody = (bytes: Uint8Array, encoding: ContentEncoding) =>
|
|
36
38
|
encoding === "br"
|
|
37
39
|
? brotliCompressSync(bytes, {
|
|
@@ -42,8 +44,17 @@ const compressBody = (bytes: Uint8Array, encoding: ContentEncoding) =>
|
|
|
42
44
|
})
|
|
43
45
|
: gzipSync(bytes, { level: GZIP_LEVEL });
|
|
44
46
|
|
|
47
|
+
const isWholeBody = (req: Request, response: Response) =>
|
|
48
|
+
!req.headers.has("range") &&
|
|
49
|
+
!response.headers.has("content-range") &&
|
|
50
|
+
response.status >= 200 &&
|
|
51
|
+
response.status < 300 &&
|
|
52
|
+
response.status !== 206;
|
|
53
|
+
|
|
45
54
|
export const compressResponse = async (req: Request, response: Response): Promise<Response> => {
|
|
46
55
|
if (response.headers.has("content-encoding") || !response.body) return response;
|
|
56
|
+
if (!isWholeBody(req, response)) return response;
|
|
57
|
+
if (Number(response.headers.get("content-length")) > MAX_COMPRESS_BYTES) return response;
|
|
47
58
|
if (!isCompressibleContentType(response.headers.get("content-type") ?? "")) return response;
|
|
48
59
|
if (process.env.AKAN_HTTP_COMPRESS === "false" || process.env.AKAN_HTTP_COMPRESS === "0") return response;
|
|
49
60
|
const acceptEncoding = req.headers.get("accept-encoding") ?? "";
|
package/server/ops/appInfo.ts
CHANGED
|
@@ -7,6 +7,7 @@ export interface AppPublicInfo {
|
|
|
7
7
|
repoName: string;
|
|
8
8
|
environment: string;
|
|
9
9
|
operationMode: string;
|
|
10
|
+
workspaceRoot?: string;
|
|
10
11
|
}
|
|
11
12
|
|
|
12
13
|
export interface AppDetailInfo extends AppPublicInfo {
|
|
@@ -39,7 +40,8 @@ export class AppInfo {
|
|
|
39
40
|
|
|
40
41
|
static public(): AppPublicInfo {
|
|
41
42
|
const { appName, repoName, environment, operationMode } = getEnv();
|
|
42
|
-
|
|
43
|
+
const workspaceRoot = operationMode === "local" ? process.env.AKAN_WORKSPACE_ROOT : undefined;
|
|
44
|
+
return { appName, repoName, environment, operationMode, ...(workspaceRoot ? { workspaceRoot } : {}) };
|
|
43
45
|
}
|
|
44
46
|
|
|
45
47
|
static detail(proc: AppProcessInfo): AppDetailInfo {
|
package/server/selfExec.ts
CHANGED
|
@@ -1,12 +1,19 @@
|
|
|
1
|
-
|
|
1
|
+
interface SelfExecGlobal {
|
|
2
|
+
[key: symbol]: string | undefined;
|
|
3
|
+
}
|
|
2
4
|
|
|
3
5
|
export class SelfExec {
|
|
4
|
-
|
|
6
|
+
|
|
7
|
+
static readonly #key = Symbol.for("akan.selfExec.beBun");
|
|
8
|
+
|
|
9
|
+
static get #beBun(): string | undefined {
|
|
10
|
+
return (globalThis as unknown as SelfExecGlobal)[SelfExec.#key];
|
|
11
|
+
}
|
|
5
12
|
|
|
6
13
|
static adopt() {
|
|
7
14
|
const value = process.env.BUN_BE_BUN;
|
|
8
15
|
if (value === undefined) return;
|
|
9
|
-
SelfExec.#
|
|
16
|
+
(globalThis as unknown as SelfExecGlobal)[SelfExec.#key] = value;
|
|
10
17
|
delete process.env.BUN_BE_BUN;
|
|
11
18
|
}
|
|
12
19
|
|
|
@@ -16,7 +23,8 @@ export class SelfExec {
|
|
|
16
23
|
}
|
|
17
24
|
|
|
18
25
|
static env(env: NodeJS.ProcessEnv = process.env): NodeJS.ProcessEnv {
|
|
19
|
-
|
|
26
|
+
const beBun = SelfExec.#beBun;
|
|
27
|
+
return beBun === undefined ? env : { ...env, BUN_BE_BUN: beBun };
|
|
20
28
|
}
|
|
21
29
|
|
|
22
30
|
static command(script: string, ...args: string[]): string[] {
|
|
@@ -7,7 +7,7 @@ import { type UpdateManifest } from "./lib/updates.ts";
|
|
|
7
7
|
import { type TargetPlatform } from "./platforms/index.ts";
|
|
8
8
|
export type { AkanNativeConfig, AndroidSigning, Device, DeviceSelector, DoctorCheck, IosBuild, IosSigning, IosSigningResult, LogEvent, TargetPlatform, WindowsBuild, };
|
|
9
9
|
/** semver of this API. A caller checks the major before it relies on anything here. */
|
|
10
|
-
export declare const API_VERSION = "0.
|
|
10
|
+
export declare const API_VERSION = "0.9.0";
|
|
11
11
|
export type AkanNativeErrorCode = "CONFIG_INVALID" | "TOOLCHAIN_MISSING" | "WEB_INPUT_INVALID" | "WEB_BUILD_FAILED" | "NATIVE_BUILD_FAILED" | "SIGNING_FAILED" | "DEVICE_FAILED" | "CANCELLED" | "INTERNAL";
|
|
12
12
|
export declare class AkanNativeError extends Error {
|
|
13
13
|
readonly code: AkanNativeErrorCode;
|
|
@@ -126,6 +126,22 @@ export interface PublishResult {
|
|
|
126
126
|
/** Bytes of the files the manifest names. */
|
|
127
127
|
size: number;
|
|
128
128
|
}
|
|
129
|
+
export interface PublishCheckOptions extends Pick<TaskOptions, "appDir" | "config"> {
|
|
130
|
+
platform: Exclude<TargetPlatform, "web">;
|
|
131
|
+
channel?: string;
|
|
132
|
+
out?: string;
|
|
133
|
+
/** Whether the release will carry a server; the config's `desktop.server` by default. */
|
|
134
|
+
server?: boolean;
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* What publishUpdate() refuses before it builds, without building (CONFIG_INVALID): no updates settings, a channel no
|
|
138
|
+
* app could be configured for, no signing key on this machine or another app's, and a desktop release whose server
|
|
139
|
+
* presence is not the channel's previous release's. For a caller with a long build of its own before publishUpdate.
|
|
140
|
+
*/
|
|
141
|
+
export declare function checkPublishUpdate(options: PublishCheckOptions): {
|
|
142
|
+
channel: string;
|
|
143
|
+
out: string;
|
|
144
|
+
};
|
|
129
145
|
/**
|
|
130
146
|
* A signed release for the updates plugin (UP-1, UP-2): a release build, then `<channel>.json`, its signature and
|
|
131
147
|
* its files under `out` (default <appDir>/.akan/native/updates), signed with the key updateKeygen() made.
|
|
@@ -11,6 +11,14 @@ export interface PublishedRelease {
|
|
|
11
11
|
* sequence, so a clock behind the last publisher's would make this release look older to every one of them.
|
|
12
12
|
*/
|
|
13
13
|
export declare function nextSequence(dir: string, channel: string, now?: number): number;
|
|
14
|
+
/** `<out>/<os>-<arch>` for a desktop release, `<out>/<platform>` for a phone's web bundle. */
|
|
15
|
+
export declare function releaseDir(out: string, platform: ReleasePlatform): string;
|
|
16
|
+
/** Refuses before the build a desktop release the apps of its channel would refuse for its server (assertSameServer). */
|
|
17
|
+
export declare function assertServerOfChannel(config: {
|
|
18
|
+
desktop?: {
|
|
19
|
+
server?: unknown;
|
|
20
|
+
};
|
|
21
|
+
}, platform: ReleasePlatform, out: string, channel: string): void;
|
|
14
22
|
/** Refuses before the build a release that could not be signed: no key on this machine, or another app's. */
|
|
15
23
|
export declare function assertSigningKey(config: {
|
|
16
24
|
app: {
|
|
@@ -52,6 +52,8 @@ export interface UpdateManifest {
|
|
|
52
52
|
sha256: string;
|
|
53
53
|
size: number;
|
|
54
54
|
}[];
|
|
55
|
+
/** App updates: whether the app carries a server; an installed app refuses a release that differs from itself. */
|
|
56
|
+
server?: boolean;
|
|
55
57
|
}
|
|
56
58
|
/** Every file of a web bundle as the app serves it: index.html with the init script and CSP, public/, env.runtime.json. */
|
|
57
59
|
export declare function webFiles(webDir: string, html: string, env: Record<string, string>): {
|
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
import type { BuildContext } from "../lib/prepare.ts";
|
|
2
2
|
import { type UpdateManifest } from "../lib/updates.ts";
|
|
3
3
|
import { type DesktopOs } from "./desktop.ts";
|
|
4
|
+
/**
|
|
5
|
+
* An installed app refuses a release whose server presence is not its own (plugins/updates): published, this one would
|
|
6
|
+
* be refused by every app the channel reached, and so would every release after it. A manifest from before the field
|
|
7
|
+
* says nothing and is not compared.
|
|
8
|
+
*/
|
|
9
|
+
export declare function assertSameServer(dir: string, channel: string, server: boolean): void;
|
|
4
10
|
export declare function publishAppUpdate(ctx: BuildContext, os: DesktopOs, app: string, out: string, channel: string, sequence: number): Promise<{
|
|
5
11
|
manifest: UpdateManifest;
|
|
6
|
-
|
|
12
|
+
leftovers: string[];
|
|
7
13
|
}>;
|
|
@@ -16,6 +16,13 @@ export interface InstallerInput {
|
|
|
16
16
|
name: string;
|
|
17
17
|
dir: boolean;
|
|
18
18
|
}[];
|
|
19
|
+
/** Bytes the app folder takes on disk, and bytes the setup unpacks in all (the folder and the WebView2 bootstrapper). */
|
|
20
|
+
space: {
|
|
21
|
+
install: number;
|
|
22
|
+
unpack: number;
|
|
23
|
+
};
|
|
24
|
+
/** deepLinks.schemes: the uninstaller removes those the app registered for its own executable. */
|
|
25
|
+
schemes: string[];
|
|
19
26
|
icon?: string;
|
|
20
27
|
bootstrapper: string;
|
|
21
28
|
stopScript: string;
|
|
@@ -23,15 +30,23 @@ export interface InstallerInput {
|
|
|
23
30
|
}
|
|
24
31
|
export declare function installerScript(input: InstallerInput): string;
|
|
25
32
|
/**
|
|
26
|
-
* Stops every process running from the install folder,
|
|
27
|
-
* WMI: the installer is a 32-bit program, so its PowerShell is too, and a
|
|
28
|
-
* 64-bit one's path (Get-Process's Path comes back empty).
|
|
33
|
+
* Stops every process running from the install folder, waits (up to 10 s) until they are gone, and exits 1 when
|
|
34
|
+
* one is left or cannot be seen. Through WMI: the installer is a 32-bit program, so its PowerShell is too, and a
|
|
35
|
+
* 32-bit process cannot read a 64-bit one's path (Get-Process's Path comes back empty).
|
|
29
36
|
*/
|
|
30
|
-
export declare const STOP_SCRIPT = "param([string]$Dir)\n$prefix = [IO.Path]::GetFullPath($Dir).TrimEnd('\\') + '\\'\n$running = { Get-CimInstance Win32_Process | Where-Object { $_.ExecutablePath -and $_.ExecutablePath.StartsWith($prefix, [StringComparison]::OrdinalIgnoreCase) } }\n& $running | ForEach-Object { Stop-Process -Id $_.ProcessId -Force -ErrorAction SilentlyContinue }\nfor ($i = 0; $i -lt 50 -and (& $running); $i++) { Start-Sleep -Milliseconds 200 }\n";
|
|
37
|
+
export declare const STOP_SCRIPT = "param([string]$Dir)\n$ErrorActionPreference = 'Stop'\n$prefix = [IO.Path]::GetFullPath($Dir).TrimEnd('\\') + '\\'\n$running = { Get-CimInstance Win32_Process | Where-Object { $_.ExecutablePath -and $_.ExecutablePath.StartsWith($prefix, [StringComparison]::OrdinalIgnoreCase) } }\n& $running | ForEach-Object { Stop-Process -Id $_.ProcessId -Force -ErrorAction SilentlyContinue }\nfor ($i = 0; $i -lt 50 -and (& $running); $i++) { Start-Sleep -Milliseconds 200 }\nif (& $running) { exit 1 }\n";
|
|
31
38
|
/** The longest path under `folder`, relative and with Windows separators. Links are not followed. */
|
|
32
39
|
export declare function deepestPath(folder: string): string;
|
|
33
|
-
/**
|
|
34
|
-
export declare function
|
|
40
|
+
/** The bytes of the files under `folder`, and what they take on disk. Links are not followed. */
|
|
41
|
+
export declare function folderSize(folder: string): {
|
|
42
|
+
bytes: number;
|
|
43
|
+
disk: number;
|
|
44
|
+
};
|
|
45
|
+
/**
|
|
46
|
+
* How long `relative` gets on a typical PC while an update unpacks, the longest it ever is: root, app, then
|
|
47
|
+
* `.update-<bundle>\` and the app again.
|
|
48
|
+
*/
|
|
49
|
+
export declare function installedPathLength(appName: string, version: string, relative: string): number;
|
|
35
50
|
/** makensis: AKAN_NATIVE_MAKENSIS, PATH, then where the NSIS installer puts it. */
|
|
36
51
|
export declare function findMakensis(env?: Record<string, string | undefined>): string | null;
|
|
37
52
|
/** Writes the setup program next to the app folder and answers its path. */
|
|
@@ -12,7 +12,10 @@ export type EmitTarget = {
|
|
|
12
12
|
} | "focused";
|
|
13
13
|
export interface DesktopContext {
|
|
14
14
|
app: AppInfo;
|
|
15
|
-
/**
|
|
15
|
+
/**
|
|
16
|
+
* A debug build (boot.json `dev`: `--debug`, and every dev build, whose pages come from a dev server the shell did not
|
|
17
|
+
* start). It has the release app's id, so what it keeps apart goes in folders of its own.
|
|
18
|
+
*/
|
|
16
19
|
readonly dev: boolean;
|
|
17
20
|
/**
|
|
18
21
|
* The window whose page made this call (SH-6; 1 = the window the app opened). Only in method
|
|
@@ -51,7 +54,7 @@ export interface DesktopContext {
|
|
|
51
54
|
readonly binDir: string | null;
|
|
52
55
|
/**
|
|
53
56
|
* The server the app carries (desktop.server), or null. `ready` settles once per session: true when the server
|
|
54
|
-
* first answered ready, false when it could not start or gave up before that.
|
|
57
|
+
* first answered ready, false when it could not start or gave up before that; `state` and `onState` follow it after.
|
|
55
58
|
*/
|
|
56
59
|
readonly server: DesktopServerStatus | null;
|
|
57
60
|
/** Pushes an event of this plugin to the pages that listen. Returns the windows reached. */
|
|
@@ -108,18 +111,29 @@ export interface DesktopContext {
|
|
|
108
111
|
closeWindow(window?: number): void;
|
|
109
112
|
/**
|
|
110
113
|
* Launch phase: only while setup runs, before the window exists. The host waits for every
|
|
111
|
-
* plugin's setup (async allowed, at most 3 s each) before it creates the window.
|
|
112
|
-
* that
|
|
114
|
+
* plugin's setup (async allowed, at most 3 s each) before it creates the window. setWindow after
|
|
115
|
+
* that is ignored with a warning.
|
|
113
116
|
*/
|
|
114
117
|
readonly launch: {
|
|
115
118
|
/** Creates the window at these bounds (logical points) instead of the configured size (window-state). */
|
|
116
119
|
setWindow(bounds: LaunchWindow): void;
|
|
117
|
-
/**
|
|
120
|
+
/**
|
|
121
|
+
* Ends the app before a window is created; onQuit hooks do not run (single-instance). A setup that ran past its
|
|
122
|
+
* time quits the app instead, since its window exists by then.
|
|
123
|
+
*/
|
|
118
124
|
exit(code?: number): void;
|
|
119
125
|
};
|
|
120
126
|
}
|
|
127
|
+
/**
|
|
128
|
+
* "starting" until the server answers ready (again after a crash before that), "up" while it answers, "restarting"
|
|
129
|
+
* after a crash once it had been up, "gaveUp" when it is not started again this session, "stopped" as the app quits.
|
|
130
|
+
*/
|
|
131
|
+
export type DesktopServerState = "starting" | "up" | "restarting" | "gaveUp" | "stopped";
|
|
121
132
|
export interface DesktopServerStatus {
|
|
122
133
|
readonly ready: Promise<boolean>;
|
|
134
|
+
readonly state: DesktopServerState;
|
|
135
|
+
/** Every later change of `state`. Returns an unsubscribe function. */
|
|
136
|
+
onState(listener: (state: DesktopServerState) => void): () => void;
|
|
123
137
|
}
|
|
124
138
|
/** Which page load ended. `id` is "" for a page that sends no document id (v1 callers, tests). */
|
|
125
139
|
export interface DocumentInfo {
|
|
@@ -1,14 +1,33 @@
|
|
|
1
1
|
export interface RelaunchOptions {
|
|
2
2
|
/** How long the helper waits after this process exited before it starts the app. */
|
|
3
3
|
delayMs?: number;
|
|
4
|
+
/**
|
|
5
|
+
* Windows, a swap: while the helper moves the folders, a RunOnce value named `name` runs `script` (written here) at
|
|
6
|
+
* the next logon, which puts back what a shutdown or a killed helper left half moved and starts the app. The helper
|
|
7
|
+
* removes the value once the moves are done or undone.
|
|
8
|
+
*/
|
|
9
|
+
recover?: {
|
|
10
|
+
script: string;
|
|
11
|
+
name: string;
|
|
12
|
+
};
|
|
4
13
|
}
|
|
14
|
+
/**
|
|
15
|
+
* The recovery `recover.script` holds: when `exe` is missing, every move whose source is gone and whose target is
|
|
16
|
+
* there goes back, newest first, and the app starts. An app that is there was never half moved: it is left alone.
|
|
17
|
+
*/
|
|
18
|
+
export declare function recoveryScript(exe: string, moves: [string, string][]): string;
|
|
19
|
+
/**
|
|
20
|
+
* The RunOnce command for a recovery script, or null past the 260 characters Windows runs from there. The script is
|
|
21
|
+
* read as text: an execution policy that refuses .ps1 files does not apply to it, as it does not to -EncodedCommand.
|
|
22
|
+
*/
|
|
23
|
+
export declare function recoveryCommand(script: string): string | null;
|
|
5
24
|
/**
|
|
6
25
|
* Once this process has exited: renames `moves` in order and starts `exe` with this environment. macOS and
|
|
7
26
|
* Linux rename a running app's folder right away, so only the start waits; Windows refuses that, so a helper
|
|
8
27
|
* does the renames too (and on a failed rename puts back what it moved, so there is always an app to start).
|
|
9
28
|
* Only the first call of a process starts a helper; later ones answer the same promise.
|
|
10
29
|
*/
|
|
11
|
-
export declare function relaunchAfterExit(exe: string, moves?: [string, string][], { delayMs }?: RelaunchOptions): Promise<void>;
|
|
30
|
+
export declare function relaunchAfterExit(exe: string, moves?: [string, string][], { delayMs, recover }?: RelaunchOptions): Promise<void>;
|
|
12
31
|
/** Relaunches after the browser process ended, in a row: each one's app ran less than RELAUNCH_HEALTHY. */
|
|
13
32
|
export declare const RELAUNCH_LIMIT = 10;
|
|
14
33
|
export declare const RELAUNCH_HEALTHY = 60000;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { DesktopServerState, DesktopServerStatus } from "./plugin.ts";
|
|
1
2
|
export interface ServerManifest {
|
|
2
3
|
/** The file in resources/server that starts the server. */
|
|
3
4
|
entry: string;
|
|
@@ -14,6 +15,8 @@ export declare const MAX_FAILURES = 5;
|
|
|
14
15
|
export declare const BOOT_RESTART_DELAY = 250;
|
|
15
16
|
/** A run that stayed up this long was healthy: its crash starts the count again. */
|
|
16
17
|
export declare const HEALTHY_RUN = 60000;
|
|
18
|
+
/** server-output.log moves to server-output.log.1 past this size. */
|
|
19
|
+
export declare const OUTPUT_LOG_LIMIT: number;
|
|
17
20
|
export interface ServerProcess {
|
|
18
21
|
readonly exited: Promise<number | null>;
|
|
19
22
|
send(message: unknown): void;
|
|
@@ -41,6 +44,8 @@ export interface DesktopServerOptions {
|
|
|
41
44
|
freePort?(preferred?: number): Promise<number>;
|
|
42
45
|
/** The server gave up: MAX_FAILURES crashes in a row, or it could not start at all. */
|
|
43
46
|
onGiveUp?(message: string): void;
|
|
47
|
+
/** Every change of `state`. */
|
|
48
|
+
onState?(state: DesktopServerState): void;
|
|
44
49
|
readyTimeout?: number;
|
|
45
50
|
stopGrace?: number;
|
|
46
51
|
restartDelay?(failures: number, everReady: boolean): number;
|
|
@@ -57,10 +62,23 @@ export interface DesktopServer {
|
|
|
57
62
|
}>;
|
|
58
63
|
/** Settles once: true at the first ready, false when the server gave up or stopped before that. */
|
|
59
64
|
readonly ready: Promise<boolean>;
|
|
65
|
+
/** Where the server is now; `ready` only tells the first answer. */
|
|
66
|
+
readonly state: DesktopServerState;
|
|
60
67
|
stop(): Promise<void>;
|
|
61
68
|
}
|
|
69
|
+
/** What plugins see of the server (ctx.server) from their setup on: the plugin host starts it only after that. */
|
|
70
|
+
export declare function createServerStatus(): {
|
|
71
|
+
status: DesktopServerStatus;
|
|
72
|
+
settle: (value: boolean) => void;
|
|
73
|
+
setState: (next: DesktopServerState) => void;
|
|
74
|
+
};
|
|
62
75
|
export declare function readServerManifest(resources: string): ServerManifest | null;
|
|
63
76
|
export declare function serverArgv(execPath: string, resources: string, entry: string): string[];
|
|
77
|
+
/**
|
|
78
|
+
* `key`'s value in `env`. Windows names a variable in any case (`Path`), and the plugin host's copy of process.env
|
|
79
|
+
* matches a name only in the case it has, where the process's own env ignores case.
|
|
80
|
+
*/
|
|
81
|
+
export declare function envValue(env: Record<string, string | undefined>, key: string, platform?: NodeJS.Platform): string | undefined;
|
|
64
82
|
/** `dir` first on a PATH, once: a relaunched app inherits the PATH its predecessor already extended. */
|
|
65
83
|
export declare function pathWithFirst(dir: string, path: string | undefined, platform?: NodeJS.Platform): string;
|
|
66
84
|
export declare function serverEnv(manifest: ServerManifest, { port, secret, dataDir, env, binDir, platform, }: {
|
|
@@ -62,6 +62,13 @@ export interface AppEvents {
|
|
|
62
62
|
progress: number;
|
|
63
63
|
swipeEdge: "left" | "right";
|
|
64
64
|
};
|
|
65
|
+
/**
|
|
66
|
+
* Desktop, an app that carries a server (desktop.server): where it is, at every change and when the first page
|
|
67
|
+
* starts listening. "gaveUp" means it is not started again until the app is.
|
|
68
|
+
*/
|
|
69
|
+
serverState: {
|
|
70
|
+
state: "starting" | "up" | "restarting" | "gaveUp" | "stopped";
|
|
71
|
+
};
|
|
65
72
|
}
|
|
66
73
|
export declare const app: import("../../../packages/core/src/plugin.ts").Plugin<AppApi, AppEvents>;
|
|
67
74
|
/** Calls `handler` for every deep link while mounted. */
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type DesktopPlugin } from "../../../packages/desktop/src/plugin.ts";
|
|
2
|
+
import { type RelaunchOptions } from "../../../packages/desktop/src/relaunch.ts";
|
|
2
3
|
import type { UpdatesApi, UpdatesEvents } from "./index.ts";
|
|
3
4
|
export interface UpdatesConfig {
|
|
4
5
|
app: string;
|
|
@@ -16,6 +17,10 @@ export interface UpdatesConfig {
|
|
|
16
17
|
* launcher's own wait (READY_TIMEOUT, 8 s) only decides when the window opens.
|
|
17
18
|
*/
|
|
18
19
|
export declare const SERVER_BOOT_ALLOWANCE = 120000;
|
|
20
|
+
/** A server whose init throws answered ready first: its release is confirmed only once it stayed up this long. */
|
|
21
|
+
export declare const SERVER_SETTLE = 5000;
|
|
22
|
+
/** Tries of a release whose server did not come up, or whose Windows swap did not happen, before it is failed. */
|
|
23
|
+
export declare const MAX_STRIKES = 3;
|
|
19
24
|
export interface DesktopUpdatesOptions {
|
|
20
25
|
platform?: NodeJS.Platform;
|
|
21
26
|
/** The installed app; the running one by default. */
|
|
@@ -24,8 +29,9 @@ export interface DesktopUpdatesOptions {
|
|
|
24
29
|
config?: UpdatesConfig | null;
|
|
25
30
|
/** The executable's file name inside an app; the running one's by default. */
|
|
26
31
|
exeName?: string;
|
|
27
|
-
relaunch?(exe: string, moves: [string, string][]): Promise<void>;
|
|
32
|
+
relaunch?(exe: string, moves: [string, string][], recover?: RelaunchOptions["recover"]): Promise<void>;
|
|
28
33
|
serverBootAllowance?: number;
|
|
34
|
+
serverSettle?: number;
|
|
29
35
|
}
|
|
30
36
|
export declare function createDesktopUpdates(options?: DesktopUpdatesOptions): DesktopPlugin<UpdatesApi, UpdatesEvents>;
|
|
31
37
|
declare const _default: DesktopPlugin<UpdatesApi, UpdatesEvents>;
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Over-the-air updates. On iOS and Android they replace the web bundle (UP-2); the native code
|
|
3
|
-
* stays, so a bundle only runs in a binary with the same native API (UP-3, `nativeApi`).
|
|
4
|
-
*
|
|
3
|
+
* stays, so a bundle only runs in a binary with the same native API (UP-3, `nativeApi`). On macOS,
|
|
4
|
+
* Windows and Linux they replace the whole app (UP-1). Releases are made with
|
|
5
|
+
* `akan-native update publish <platform>` and signed with the key of `akan-native update keygen`.
|
|
5
6
|
*
|
|
6
7
|
* A new bundle first runs on trial: the app calls notifyReady() once it works. A trial that does
|
|
7
8
|
* not confirm within `updates.readyTimeout` (akan-native.config.ts, default 10 s), or that crashes and is
|
|
8
|
-
* launched again, is rolled back to the previous bundle.
|
|
9
|
+
* launched again, is rolled back to the previous bundle. On a desktop app that carries a server, the
|
|
10
|
+
* trial also waits for that server to answer and stay up.
|
|
9
11
|
*/
|
|
10
12
|
export interface UpdateState {
|
|
11
13
|
/** The downloaded bundle running now; null = the one inside the app. */
|
|
@@ -24,7 +26,10 @@ export interface UpdateState {
|
|
|
24
26
|
nativeApi: string | null;
|
|
25
27
|
}
|
|
26
28
|
export interface UpdateCheck {
|
|
27
|
-
/**
|
|
29
|
+
/**
|
|
30
|
+
* A newer bundle for this binary exists (not rolled back before). Desktop: never the release on trial, nor one whose
|
|
31
|
+
* server presence differs from this app's.
|
|
32
|
+
*/
|
|
28
33
|
available: boolean;
|
|
29
34
|
bundle: string | null;
|
|
30
35
|
/** The app version it was published with. */
|
|
@@ -45,11 +50,18 @@ export interface UpdatesApi {
|
|
|
45
50
|
download(): Promise<{
|
|
46
51
|
bundle: string;
|
|
47
52
|
}>;
|
|
48
|
-
/**
|
|
53
|
+
/**
|
|
54
|
+
* Phones: reloads the page into the downloaded bundle now (on trial). Desktop: replaces the app with the download and
|
|
55
|
+
* relaunches it (on trial); rejects NOT_ALLOWED while the running release is still on trial. Rejects NOT_FOUND
|
|
56
|
+
* without a download.
|
|
57
|
+
*/
|
|
49
58
|
apply(): Promise<void>;
|
|
50
59
|
/** The running bundle works: keep it. Call it once the app has started; harmless otherwise. */
|
|
51
60
|
notifyReady(): Promise<void>;
|
|
52
|
-
/**
|
|
61
|
+
/**
|
|
62
|
+
* Phones: back to the app's own bundle from the next launch on. Desktop: drops the download and forgets the refused
|
|
63
|
+
* releases; the running app stays, and a trial keeps its record.
|
|
64
|
+
*/
|
|
53
65
|
reset(): Promise<void>;
|
|
54
66
|
}
|
|
55
67
|
export interface UpdatesEvents {
|
|
@@ -25,7 +25,7 @@ import {
|
|
|
25
25
|
type WindowsBuild,
|
|
26
26
|
} from "./lib/prepare.ts";
|
|
27
27
|
import { ConfigError, type Project, projectFromConfig } from "./lib/project.ts";
|
|
28
|
-
import { assertSigningKey, publishRelease } from "./lib/publish.ts";
|
|
28
|
+
import { assertServerOfChannel, assertSigningKey, publishRelease } from "./lib/publish.ts";
|
|
29
29
|
import {
|
|
30
30
|
assertChannel,
|
|
31
31
|
generateUpdateKey,
|
|
@@ -53,7 +53,7 @@ export type {
|
|
|
53
53
|
};
|
|
54
54
|
|
|
55
55
|
/** semver of this API. A caller checks the major before it relies on anything here. */
|
|
56
|
-
export const API_VERSION = "0.
|
|
56
|
+
export const API_VERSION = "0.9.0";
|
|
57
57
|
|
|
58
58
|
export type AkanNativeErrorCode =
|
|
59
59
|
| "CONFIG_INVALID"
|
|
@@ -255,6 +255,35 @@ export interface PublishResult {
|
|
|
255
255
|
size: number;
|
|
256
256
|
}
|
|
257
257
|
|
|
258
|
+
export interface PublishCheckOptions extends Pick<TaskOptions, "appDir" | "config"> {
|
|
259
|
+
platform: Exclude<TargetPlatform, "web">;
|
|
260
|
+
channel?: string;
|
|
261
|
+
out?: string;
|
|
262
|
+
/** Whether the release will carry a server; the config's `desktop.server` by default. */
|
|
263
|
+
server?: boolean;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* What publishUpdate() refuses before it builds, without building (CONFIG_INVALID): no updates settings, a channel no
|
|
268
|
+
* app could be configured for, no signing key on this machine or another app's, and a desktop release whose server
|
|
269
|
+
* presence is not the channel's previous release's. For a caller with a long build of its own before publishUpdate.
|
|
270
|
+
*/
|
|
271
|
+
export function checkPublishUpdate(options: PublishCheckOptions): { channel: string; out: string } {
|
|
272
|
+
if (!options.config.updates)
|
|
273
|
+
throw new AkanNativeError("CONFIG_INVALID", "the config has no updates: { url, publicKey } to publish for");
|
|
274
|
+
const channel = options.channel ?? options.config.updates.channel ?? "production";
|
|
275
|
+
const out = resolve(options.out ?? resolve(options.appDir, ".akan", "native", "updates"));
|
|
276
|
+
try {
|
|
277
|
+
if (options.channel !== undefined) assertChannel(options.channel);
|
|
278
|
+
assertSigningKey(options.config);
|
|
279
|
+
const server = options.server ?? !!options.config.desktop?.server;
|
|
280
|
+
assertServerOfChannel({ desktop: server ? { server } : {} }, options.platform, out, channel);
|
|
281
|
+
} catch (error) {
|
|
282
|
+
throw toAkanNativeError(error, "CONFIG_INVALID");
|
|
283
|
+
}
|
|
284
|
+
return { channel, out };
|
|
285
|
+
}
|
|
286
|
+
|
|
258
287
|
/**
|
|
259
288
|
* A signed release for the updates plugin (UP-1, UP-2): a release build, then `<channel>.json`, its signature and
|
|
260
289
|
* its files under `out` (default <appDir>/.akan/native/updates), signed with the key updateKeygen() made.
|
|
@@ -263,17 +292,8 @@ export function publishUpdate(
|
|
|
263
292
|
options: TaskOptions & { platform: Exclude<TargetPlatform, "web">; channel?: string; out?: string },
|
|
264
293
|
): Promise<PublishResult> {
|
|
265
294
|
return task(options, async (warnings) => {
|
|
266
|
-
|
|
267
|
-
throw new AkanNativeError("CONFIG_INVALID", "the config has no updates: { url, publicKey } to publish for");
|
|
268
|
-
try {
|
|
269
|
-
if (options.channel !== undefined) assertChannel(options.channel);
|
|
270
|
-
assertSigningKey(options.config);
|
|
271
|
-
} catch (error) {
|
|
272
|
-
throw toAkanNativeError(error, "CONFIG_INVALID");
|
|
273
|
-
}
|
|
295
|
+
const { channel, out } = checkPublishUpdate(options);
|
|
274
296
|
const { ctx, artifact, result } = await buildIn(options, "release", options.mode ?? "production", warnings);
|
|
275
|
-
const channel = options.channel ?? ctx.project.config.updates?.channel ?? "production";
|
|
276
|
-
const out = resolve(options.out ?? resolve(options.appDir, ".akan", "native", "updates"));
|
|
277
297
|
const { dir, manifest } = await publishRelease(ctx, options.platform, artifact, out, channel).catch((error) => {
|
|
278
298
|
throw toAkanNativeError(error, "CONFIG_INVALID");
|
|
279
299
|
});
|
|
@@ -5,7 +5,7 @@ import { join, relative, resolve, sep } from "node:path";
|
|
|
5
5
|
import { checkFlags, type ParsedArgs, parseArgs, stringFlag } from "../lib/args.ts";
|
|
6
6
|
import { bold, CliError, dim, log } from "../lib/log.ts";
|
|
7
7
|
import { findAppDir, loadProject } from "../lib/project.ts";
|
|
8
|
-
import { assertSigningKey, publishRelease } from "../lib/publish.ts";
|
|
8
|
+
import { assertServerOfChannel, assertSigningKey, publishRelease } from "../lib/publish.ts";
|
|
9
9
|
import { assertChannel, generateUpdateKey, updateKeyPath } from "../lib/updates.ts";
|
|
10
10
|
import { BOOLEAN_FLAGS, buildFromArgs } from "./build.ts";
|
|
11
11
|
|
|
@@ -39,7 +39,14 @@ async function publish(args: ParsedArgs): Promise<number> {
|
|
|
39
39
|
const platform = desktop ?? (arg as "ios" | "android");
|
|
40
40
|
const requested = stringFlag(args, "channel");
|
|
41
41
|
if (requested !== undefined) assertChannel(requested);
|
|
42
|
-
|
|
42
|
+
const project = await loadProject(findAppDir(stringFlag(args, "app") ?? process.cwd()));
|
|
43
|
+
assertSigningKey(project.config);
|
|
44
|
+
assertServerOfChannel(
|
|
45
|
+
project.config,
|
|
46
|
+
platform,
|
|
47
|
+
outDir(args, project.appDir),
|
|
48
|
+
requested ?? project.config.updates?.channel ?? "production",
|
|
49
|
+
);
|
|
43
50
|
const { ctx, artifact } = await buildFromArgs(
|
|
44
51
|
{ positional: [platform], flags: args.flags },
|
|
45
52
|
{ mode: "production", profile: "release" },
|