balena-cli/lib/fast-boot.ts

113 lines
3.4 KiB
TypeScript
Raw Permalink Normal View History

/**
* @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';
const stat = 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}`);
}
}