mirror of
https://github.com/balena-io/balena-cli.git
synced 2024-12-21 06:33:28 +00:00
d15b54cf40
* Add missing fast-boot `stop()` call on CLI exit to avoid 1s timeout. * Move `.fast-boot.json` to `~/.balena/cli-module-cache.json` to address scenarios where the CLI is installed to a read-only folder: - pkg's internal 'snapshot' filesystem (standalone zip package) - Root-owned folder without write permission to regular users, like `/usr[/local]/lib/balena-cli` (the case of caxa-based installers or the GUI installer for macOS). Change-type: patch
120 lines
3.7 KiB
TypeScript
120 lines
3.7 KiB
TypeScript
/**
|
|
* @license
|
|
* Copyright 2021 Balena Ltd.
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
|
|
/**
|
|
* This module sets up the `fast-boot2` module, including testing whether
|
|
* we have permissions over the cache file before even attempting to load
|
|
* fast boot.
|
|
* DON'T IMPORT BALENA-CLI MODULES HERE, as this module is loaded directly
|
|
* from `bin/balena`, before the CLI's entrypoint in `lib/app.ts`.
|
|
*/
|
|
|
|
import * as fs from 'fs';
|
|
import * as os from 'os';
|
|
import * as path from 'path';
|
|
|
|
// `@types/node` does not know about `options: { bigint?: boolean }`
|
|
type statT = (
|
|
fPath: string,
|
|
options: { bigint?: boolean },
|
|
) => fs.Stats | Promise<fs.Stats>;
|
|
|
|
// async stat does not work with pkg's internal `/snapshot` filesystem
|
|
const stat: statT = process.pkg ? fs.statSync : fs.promises.stat;
|
|
|
|
let fastBootStarted = false;
|
|
|
|
export async function start() {
|
|
if (fastBootStarted) {
|
|
return;
|
|
}
|
|
try {
|
|
await $start();
|
|
fastBootStarted = true;
|
|
} catch (e) {
|
|
if (process.env.DEBUG) {
|
|
console.error(`\
|
|
[debug] Unable to start 'fast-boot2':
|
|
[debug] ${(e.message || '').split('\n').join('\n[debug] ')}
|
|
[debug] The CLI should still work, but it will run a bit slower.`);
|
|
}
|
|
}
|
|
}
|
|
|
|
export function stop() {
|
|
if (fastBootStarted) {
|
|
require('fast-boot2').stop();
|
|
}
|
|
fastBootStarted = false;
|
|
}
|
|
|
|
async function $start() {
|
|
const dotBalena = process.platform === 'win32' ? '_balena' : '.balena';
|
|
// TODO: take into account `~/.balenarc.yml` or `./balenarc.yml`,
|
|
// without hurting performance at this early loading stage.
|
|
const dataDir = path.normalize(
|
|
process.env.BALENARC_DATA_DIRECTORY || path.join(os.homedir(), dotBalena),
|
|
);
|
|
// Consider that the CLI may be installed to a folder owned by root
|
|
// such as `/usr[/local]/lib/balena-cli`, while being executed by
|
|
// a regular user account.
|
|
const cacheFile = path.join(dataDir, 'cli-module-cache.json');
|
|
const root = path.join(__dirname, '..');
|
|
const [, pJson, pStat, nStat] = await Promise.all([
|
|
ensureCanWrite(dataDir, cacheFile),
|
|
import('../package.json'),
|
|
stat(path.join(root, 'package.json'), { bigint: true }),
|
|
stat(path.join(root, 'npm-shrinkwrap.json'), { bigint: true }),
|
|
]);
|
|
// Include timestamps to account for dev-time changes to node_modules
|
|
const cacheKiller = `${pJson.version}-${pStat.mtimeMs}-${nStat.mtimeMs}`;
|
|
require('fast-boot2').start({
|
|
cacheFile,
|
|
cacheKiller,
|
|
cacheScope: root,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Check that `file` has write permission. If so, return straight away.
|
|
* Throw an error if:
|
|
* - `file` exists but does have write permissions.
|
|
* - `file` does not exist and `dir` exists, but `dir` does not have
|
|
* write permissions.
|
|
* - `file` does not exist and `dir` does not exist, and an attempt
|
|
* to create `dir` failed.
|
|
*/
|
|
async function ensureCanWrite(dir: string, file: string) {
|
|
const { access, mkdir } = fs.promises;
|
|
try {
|
|
try {
|
|
await access(file, fs.constants.W_OK);
|
|
return;
|
|
} catch (e) {
|
|
// OK if file does not exist
|
|
if (e.code !== 'ENOENT') {
|
|
throw e;
|
|
}
|
|
}
|
|
// file does not exist; ensure that the directory is writable
|
|
await mkdir(dir, { recursive: true, mode: 0o755 });
|
|
await access(dir, fs.constants.W_OK);
|
|
} catch (e) {
|
|
throw new Error(`Unable to write file "${file}":\n${e.message}`);
|
|
}
|
|
}
|