diff --git a/docs/app/commands/install.md b/docs/app/commands/install.md index f0510b63..45592739 100755 --- a/docs/app/commands/install.md +++ b/docs/app/commands/install.md @@ -42,6 +42,7 @@ _For parallel mode, you will be asked to provide your password only once_ ## Options - `--no-parallel` - Disable parallel mode for applications installation. +- `-p --parallel-count ` - Set the number of parallel application installations. The default is based on the number of CPU cores. ## Profiles diff --git a/src/commands/install/config/apps-groups/mac.ts b/src/commands/install/config/apps-groups/mac.ts index e63b6bea..e63328f4 100755 --- a/src/commands/install/config/apps-groups/mac.ts +++ b/src/commands/install/config/apps-groups/mac.ts @@ -6,6 +6,7 @@ export const MACOS: Readonly = [ description: 'Enable Touch ID for sudo (password needed)', group: 'MacOS', default: true, + first: true, commands: () => [ 'sudo -v', 'sudo cp -f /etc/pam.d/sudo_local.template /etc/pam.d/sudo_local', diff --git a/src/commands/install/install.command.ts b/src/commands/install/install.command.ts index 7c30a4a0..28cdfd27 100755 --- a/src/commands/install/install.command.ts +++ b/src/commands/install/install.command.ts @@ -1,9 +1,9 @@ import { Listr, ListrTask } from 'listr2' import { Command, CommandRunner, Option } from 'nest-commander' import { cpus } from 'node:os' -import { arch as ARCH } from 'node:process' +import { arch as ARCH, exit } from 'node:process' import ora from 'ora' -import { BREW_NON_ERRORS } from '@common/constants' +import { BREW_INSTALL_RETRIES, BREW_NON_ERRORS } from '@common/constants' import { execPromise } from '@common/utils' import type { IAppSetup } from '@models/app-setup.model' import { ITag, TAGS_DEPS } from '@models/tag.model' @@ -41,8 +41,24 @@ export class InstallCommand extends CommandRunner { return true } + @Option({ + name: 'parallelCount', + flags: '-p --parallel-count ', + defaultValue: cpus().length / 2 + 1, + description: 'Amount of parallel processes for installation', + }) + private parallelCount(count: string): number { + const parsed = Number(count) + if (isNaN(parsed)) { + this.logger.error('Parallel count should be a number') + exit(1) + } + + return parsed + } + async run(inputs: string[], options: IInstallCommandOptions): Promise { - const { noParallel } = options + const { noParallel, parallelCount } = options await this.checkUpdateService.checkForUpdates() try { @@ -55,24 +71,30 @@ export class InstallCommand extends CommandRunner { const toInstall = await MULTI_SELECT_APPS_PROMPT(uniqueTags) - const order = this.resolveDeps(toInstall).sort((a, b) => { - if (a.last) { - return 1 - } + const resolvedDeps = this.resolveDeps(toInstall) - if (b.last) { - return -1 - } + this.logger.debug(`Installing apps, resolvedDeps: ${resolvedDeps.map((app) => app.name).join(', ')}`) + this.logger.debug(`Current arch ${ARCH}`) - return 0 - }) + const firstApps = resolvedDeps.filter((app) => app.first) + const lastApps = resolvedDeps.filter((app) => app.last) + const restApps = resolvedDeps.filter((app) => !app.first && !app.last) - this.logger.debug(`Installing apps: ${order.map((app) => app.name).join(', ')}`) + if (noParallel) { + this.logger.debug('No parallel installation!') - this.logger.debug(`Current arch ${ARCH}`) + this.logger.debug(`Installing apps, firstApps: ${firstApps.map((app) => app.name).join(', ')}`) + for (const app of firstApps) { + await this.installApp(app) + } - if (noParallel) { - for (const app of order) { + this.logger.debug(`Installing apps, restApps: ${restApps.map((app) => app.name).join(', ')}`) + for (const app of restApps) { + await this.installApp(app) + } + + this.logger.debug(`Installing apps, lastApps: ${lastApps.map((app) => app.name).join(', ')}`) + for (const app of lastApps) { await this.installApp(app) } @@ -82,24 +104,29 @@ export class InstallCommand extends CommandRunner { /** * Parallel installation */ - this.logger.log('Parallel installation is experimental!', 'yellow') - this.logger.log('Enter sudo password in order to have parallel installation', 'red-background') + this.logger.log( + '\n\n---------- Enter sudo password in order to have parallel installation ----------\n\n', + 'red-background', + ) await execPromise(`sudo -v`) - const cpusAmount = cpus().length - const parallelProcessAmount = cpusAmount / 2 + 1 + this.logger.debug(`Installing apps, firstApps: ${firstApps.map((app) => app.name).join(', ')}`) + for (const app of firstApps) { + await this.installApp(app) + } - const orderNoLast = order.filter((app) => !app.last) - const lastApps = order.filter((app) => app.last) + this.logger.debug(`Installing apps, restApps: ${restApps.map((app) => app.name).join(', ')}`) - const tasksChunks = this.generateParallelTasks(orderNoLast, parallelProcessAmount) + const tasksChunks = this.generateParallelTasks(restApps, parallelCount) for (const tasks of tasksChunks) { - const spinners = new Listr(tasks, TASKS_CONFIG(parallelProcessAmount)) + const spinners = new Listr(tasks, TASKS_CONFIG(parallelCount)) await spinners.run() } + this.logger.debug(`Installing apps, lastApps: ${lastApps.map((app) => app.name).join(', ')}`) + for (const app of lastApps) { await this.installApp(app) } @@ -243,6 +270,7 @@ export class InstallCommand extends CommandRunner { return { title: `Installing ${name}`, + retry: BREW_INSTALL_RETRIES, task: async (ctx, task) => { try { await this.installAppV2(app) @@ -307,12 +335,21 @@ export class InstallCommand extends CommandRunner { try { try { const parsedCommands = commands(ARCH) + let forceStop = false + for (const command of parsedCommands) { + if (forceStop) { + break + } + await execPromise(command).catch((err) => { this.logger.debug( `Error installApp app: ${name}, command failed: ${command}, error: ${err.stack}`, ) - throw err + + this.processInstallError(err) + + forceStop = true }) } } catch (error) { @@ -323,12 +360,21 @@ export class InstallCommand extends CommandRunner { this.logger.debug(`Installing ${name} with fallback commands`) const parsedFallbackCommands = fallbackCommands(ARCH) + let forceStop = false + for (const command of parsedFallbackCommands) { + if (forceStop) { + break + } + await execPromise(command).catch((err) => { this.logger.debug( `Error installApp app: ${name}, fallback command failed: ${command}, error: ${err.stack}`, ) - throw err + + this.processInstallError(err) + + forceStop = true }) } } @@ -355,12 +401,21 @@ export class InstallCommand extends CommandRunner { try { try { const parsedCommands = commands(ARCH) + let forceStop = false + for (const command of parsedCommands) { + if (forceStop) { + break + } + await execPromise(command).catch((err) => { this.logger.debug( `Error installApp app: ${name}, command failed: ${command}, error: ${err.stack}`, ) - throw err + + this.processInstallError(err) + + forceStop = true }) } } catch (error) { @@ -371,12 +426,21 @@ export class InstallCommand extends CommandRunner { this.logger.debug(`Installing ${name} with fallback commands`) const parsedFallbackCommands = fallbackCommands(ARCH) + let forceStop = false + for (const command of parsedFallbackCommands) { + if (forceStop) { + break + } + await execPromise(command).catch((err) => { this.logger.debug( `Error installApp app: ${name}, fallback command failed: ${command}, error: ${err.stack}`, ) - throw err + + this.processInstallError(err) + + forceStop = true }) } } @@ -386,17 +450,23 @@ export class InstallCommand extends CommandRunner { const successMsg = `Installed ${name}` this.logger.debug(successMsg) } catch (error) { - const { message } = error - /** - * Do not throw error if app already installed - */ - const isOk = BREW_NON_ERRORS.some((nonErrorMsg) => message?.includes(nonErrorMsg)) - if (isOk) { - return - } - this.logger.debug(`Error installApp2 app: ${name}, error: ${error.message}`) throw error } } + + /** + * Checks if an error should be treated as a non-error + * @returns true - if error should be treated as a non-error + * @throws - If error should be an error + */ + private processInstallError(error: Error): void { + const { message } = error + const isOk = BREW_NON_ERRORS.some((nonErrorMsg) => message?.includes(nonErrorMsg)) + if (isOk) { + return + } + + throw error + } } diff --git a/src/commands/install/models/install-command.options.ts b/src/commands/install/models/install-command.options.ts index b8e62e86..36db4b12 100644 --- a/src/commands/install/models/install-command.options.ts +++ b/src/commands/install/models/install-command.options.ts @@ -4,4 +4,10 @@ export interface IInstallCommandOptions { * @default false */ noParallel: boolean + + /** + * Number of parallel install commands to run + * @default - (number of CPU cores) / 2 + 1 + */ + parallelCount: number } diff --git a/src/common/constants.ts b/src/common/constants.ts index 973d8757..3827c9c7 100755 --- a/src/common/constants.ts +++ b/src/common/constants.ts @@ -12,3 +12,5 @@ export const BREW_NON_ERRORS: string[] = [ `Error: It seems there is already an App at`, `the latest version is already installed`, ] + +export const BREW_INSTALL_RETRIES = 3 diff --git a/src/models/app-setup.model.ts b/src/models/app-setup.model.ts index 2a12b936..fbc4810c 100755 --- a/src/models/app-setup.model.ts +++ b/src/models/app-setup.model.ts @@ -35,6 +35,12 @@ export interface IAppSetup { */ paid?: boolean + /** + * @default false + * @description If true, the app will be installed first. + */ + first?: boolean + /** * @default false * @description If true, the app will be installed last. Cannot be a dependency! diff --git a/src/services/logger.service.ts b/src/services/logger.service.ts index 2aee04ca..67be0697 100755 --- a/src/services/logger.service.ts +++ b/src/services/logger.service.ts @@ -20,7 +20,7 @@ export class LoggerService { } public error(message: string, trace?) { const generatedMessage = this.generateMessage(message) - console.error(`\x1b[31m${message}\x1b[0m`) + console.error(this.coloredMessage(message, 'red')) appendFile(this.logPath, `ERROR | ${generatedMessage}\n`, { mode: 0o770 }) } public warn(message: string) {