mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-10-14 02:09:51 +00:00
nodejs: download prebuilt add-on at runtime
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env node
|
||||
import {parseArgs} from "util"
|
||||
import type {Backend} from "./core"
|
||||
import {resolveLibsDir} from "./libs"
|
||||
import {resolveAddonPath, resolveLibsDir} from "./libs"
|
||||
|
||||
const USAGE = "usage: simplex-chat install [--backend sqlite|postgres]"
|
||||
const EXIT_INSTALL_FAILED = 1
|
||||
@@ -27,8 +27,9 @@ export async function main(argv: string[]): Promise<number> {
|
||||
return EXIT_USAGE
|
||||
}
|
||||
try {
|
||||
const dir = await resolveLibsDir(backend)
|
||||
const [dir, addon] = await Promise.all([resolveLibsDir(backend), resolveAddonPath()])
|
||||
console.log(`libsimplex installed at: ${dir}`)
|
||||
console.log(`Node.js add-on installed at: ${addon}`)
|
||||
return 0
|
||||
} catch (e) {
|
||||
console.error(`install failed: ${(e as Error).message}`)
|
||||
|
||||
@@ -15,7 +15,8 @@ export function loadLibrary(backend: Backend): Promise<void> {
|
||||
if (loading.backend === backend) return loading.promise
|
||||
return Promise.reject(new Error(`libsimplex already loaded with backend=${loading.backend}; cannot switch to ${backend} in the same process`))
|
||||
}
|
||||
const promise = libs.resolveLibsDir(backend).then(dir => simplex.load(libs.libPath(dir)))
|
||||
const promise = Promise.all([libs.resolveLibsDir(backend), libs.resolveAddonPath()])
|
||||
.then(([dir, addon]) => simplex.load(addon, libs.libPath(dir)))
|
||||
loading = {backend, promise}
|
||||
// a failed download or load can be retried
|
||||
promise.catch(() => { loading = undefined })
|
||||
|
||||
@@ -45,9 +45,21 @@ export function cacheRoot(platform: string, env: NodeJS.ProcessEnv, home: string
|
||||
return path.join(env.XDG_CACHE_HOME || path.join(home, ".cache"), "simplex-chat")
|
||||
}
|
||||
|
||||
function releaseUrl(asset: string): string {
|
||||
return `https://github.com/${GITHUB_REPO}/releases/download/v${LIBS_VERSION}/${asset}`
|
||||
}
|
||||
|
||||
export function libsUrl(backend: Backend, tag: string): string {
|
||||
const suffix = backend === "postgres" ? "-postgres" : ""
|
||||
return `https://github.com/${GITHUB_REPO}/releases/download/v${LIBS_VERSION}/simplex-chat-libs-${tag}${suffix}.zip`
|
||||
return releaseUrl(`simplex-chat-libs-${tag}${suffix}.zip`)
|
||||
}
|
||||
|
||||
function addonName(tag: string): string {
|
||||
return `simplex-chat-nodejs-${tag}.node`
|
||||
}
|
||||
|
||||
export function addonUrl(tag: string): string {
|
||||
return releaseUrl(addonName(tag))
|
||||
}
|
||||
|
||||
export function libPath(dir: string): string {
|
||||
@@ -75,34 +87,70 @@ export async function resolveLibsDir(
|
||||
return target
|
||||
}
|
||||
|
||||
export async function installLibs(url: string, target: string, lib: string, timeoutMs: number = REQUEST_TIMEOUT_MS): Promise<void> {
|
||||
const parent = path.dirname(target)
|
||||
await fs.promises.mkdir(parent, {recursive: true})
|
||||
const tmp = await fs.promises.mkdtemp(path.join(parent, ".download-"))
|
||||
try {
|
||||
export async function resolveAddonPath(
|
||||
env: NodeJS.ProcessEnv = process.env,
|
||||
platform: string = process.platform,
|
||||
arch: string = process.arch
|
||||
): Promise<string> {
|
||||
if (env.SIMPLEX_ADDON_PATH) {
|
||||
const file = path.resolve(env.SIMPLEX_ADDON_PATH)
|
||||
if (!fs.existsSync(file)) throw new Error(`SIMPLEX_ADDON_PATH does not exist: ${file}`)
|
||||
return file
|
||||
}
|
||||
const tag = platformTag(platform, arch)
|
||||
const target = path.resolve(cacheRoot(platform, env), `v${LIBS_VERSION}`, addonName(tag))
|
||||
if (!fs.existsSync(target)) await installAddon(addonUrl(tag), target)
|
||||
return target
|
||||
}
|
||||
|
||||
export function installLibs(url: string, target: string, lib: string, timeoutMs: number = REQUEST_TIMEOUT_MS): Promise<void> {
|
||||
return inTempDir(target, async tmp => {
|
||||
console.error(`Downloading libsimplex from ${url} ...`)
|
||||
const zipPath = path.join(tmp, "libs.zip")
|
||||
await download(url, zipPath, timeoutMs)
|
||||
await extract(zipPath, {dir: tmp})
|
||||
const extracted = path.join(tmp, "libs")
|
||||
if (!fs.existsSync(path.join(extracted, lib))) throw new Error(`libs/${lib} missing from ${url}`)
|
||||
try {
|
||||
await fs.promises.rename(extracted, target)
|
||||
} catch (e) {
|
||||
// Another process installed the same version first; its files are identical.
|
||||
// Windows reports renaming onto an existing directory as EPERM.
|
||||
const code = (e as NodeJS.ErrnoException).code
|
||||
const lost = (code === "EEXIST" || code === "ENOTEMPTY" || code === "EPERM") && fs.existsSync(target)
|
||||
if (!lost) throw e
|
||||
if (!fs.existsSync(path.join(target, lib))) {
|
||||
throw new Error(`another process partially populated ${target} but libsimplex is missing; remove the directory manually and retry`)
|
||||
}
|
||||
}
|
||||
await renameInto(extracted, target, path.join(target, lib))
|
||||
})
|
||||
}
|
||||
|
||||
export function installAddon(url: string, target: string, timeoutMs: number = REQUEST_TIMEOUT_MS): Promise<void> {
|
||||
return inTempDir(target, async tmp => {
|
||||
console.error(`Downloading the Node.js add-on from ${url} ...`)
|
||||
const file = path.join(tmp, path.basename(target))
|
||||
await download(url, file, timeoutMs)
|
||||
await renameInto(file, target, target)
|
||||
})
|
||||
}
|
||||
|
||||
// The temp dir is next to the target, so the final rename stays on one filesystem.
|
||||
async function inTempDir(target: string, install: (tmp: string) => Promise<void>): Promise<void> {
|
||||
const parent = path.dirname(target)
|
||||
await fs.promises.mkdir(parent, {recursive: true})
|
||||
const tmp = await fs.promises.mkdtemp(path.join(parent, ".download-"))
|
||||
try {
|
||||
await install(tmp)
|
||||
} finally {
|
||||
await fs.promises.rm(tmp, {recursive: true, force: true})
|
||||
}
|
||||
}
|
||||
|
||||
async function renameInto(from: string, target: string, installed: string): Promise<void> {
|
||||
try {
|
||||
await fs.promises.rename(from, target)
|
||||
} catch (e) {
|
||||
// Another process installed the same version first; its files are identical.
|
||||
// Windows reports renaming onto an existing directory or a loaded file as EPERM.
|
||||
const code = (e as NodeJS.ErrnoException).code
|
||||
const lost = (code === "EEXIST" || code === "ENOTEMPTY" || code === "EPERM") && fs.existsSync(target)
|
||||
if (!lost) throw e
|
||||
if (!fs.existsSync(installed)) {
|
||||
throw new Error(`another process partially populated ${target} but ${path.basename(installed)} is missing; remove it manually and retry`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function download(url: string, dest: string, timeoutMs: number, redirects = 0): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const get = url.startsWith("https:") ? https.get : http.get
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
// These functions are defined in CPP add-on ../cpp/simplex.cc
|
||||
|
||||
export function load(libPath: string): void
|
||||
export function load(addonPath: string, libPath: string): void
|
||||
export function chat_migrate_init(dbPath: string, dbKey: string, confirm: string): Promise<[bigint, string]>
|
||||
export function chat_migrate_init_queue(dbPath: string, dbKey: string, confirm: string, queueSize: number): Promise<[bigint, string]>
|
||||
export function chat_close_store(ctrl: bigint): Promise<string>
|
||||
|
||||
@@ -1 +1,22 @@
|
||||
module.exports = require("../build/Release/simplex")
|
||||
// The add-on is downloaded from the libsimplex release, so it is loaded at runtime by core.loadLibrary.
|
||||
const NOT_LOADED = "libsimplex is not loaded, call core.loadLibrary(backend) first"
|
||||
const BINDINGS = ["chat_migrate_init", "chat_migrate_init_queue", "chat_close_store", "chat_send_cmd", "chat_recv_msg_wait",
|
||||
"chat_write_file", "chat_read_file", "chat_encrypt_file", "chat_decrypt_file"]
|
||||
|
||||
let addon
|
||||
|
||||
exports.load = (addonPath, libPath) => {
|
||||
if (!addon) {
|
||||
const addonModule = {exports: {}}
|
||||
process.dlopen(addonModule, addonPath)
|
||||
addon = addonModule.exports
|
||||
}
|
||||
addon.load(libPath)
|
||||
}
|
||||
|
||||
for (const name of BINDINGS) {
|
||||
exports[name] = (...args) => {
|
||||
if (!addon) throw new Error(NOT_LOADED)
|
||||
return addon[name](...args)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user