Skip to content

Commit

Permalink
Merge pull request #90 from Avivbens/improve-install-command
Browse files Browse the repository at this point in the history
improve install command
  • Loading branch information
Avivbens authored May 4, 2024
2 parents 1192e7f + b1a561e commit 6345036
Show file tree
Hide file tree
Showing 7 changed files with 124 additions and 38 deletions.
1 change: 1 addition & 0 deletions docs/app/commands/install.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <parallelCount>` - Set the number of parallel application installations. The default is based on the number of CPU cores.

## Profiles

Expand Down
1 change: 1 addition & 0 deletions src/commands/install/config/apps-groups/mac.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export const MACOS: Readonly<IAppSetup[]> = [
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',
Expand Down
144 changes: 107 additions & 37 deletions src/commands/install/install.command.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -41,8 +41,24 @@ export class InstallCommand extends CommandRunner {
return true
}

@Option({
name: 'parallelCount',
flags: '-p --parallel-count <parallelCount>',
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<void> {
const { noParallel } = options
const { noParallel, parallelCount } = options
await this.checkUpdateService.checkForUpdates()

try {
Expand All @@ -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)
}

Expand All @@ -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)
}
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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) {
Expand All @@ -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
})
}
}
Expand All @@ -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) {
Expand All @@ -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
})
}
}
Expand All @@ -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
}
}
6 changes: 6 additions & 0 deletions src/commands/install/models/install-command.options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
2 changes: 2 additions & 0 deletions src/common/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
6 changes: 6 additions & 0 deletions src/models/app-setup.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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!
Expand Down
2 changes: 1 addition & 1 deletion src/services/logger.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down

0 comments on commit 6345036

Please sign in to comment.