import * as cp from 'child_process';
import { EventEmitter } from 'events';
import * as iconv from 'iconv-lite-umd';
import { CancellationToken } from 'vscode';
import { dispose, IDisposable, toDisposable } from './disposableUtils';
import { onceEvent } from './eventUtils';
export interface IPijulOptions {
path: string
version: string
}
/**
* The Pijul class holds information about the Pijul installation
* in use by the extension which it uses for executing commands with
* the Pijul CLI. All interactions with the CLI come through this class.
*/
export class Pijul {
readonly path: string;
readonly version: string;
private readonly _onOutput = new EventEmitter();
get onOutput (): EventEmitter { return this._onOutput; }
constructor (options: IPijulOptions) {
this.path = options.path;
this.version = options.version;
}
/**
* Opens an existing Pijul repository at the given location
* @param repository The path to the root of the repository
* @param dotPijul The path to the repository's .pijul directory
*/
open (repository: string, dotPijul: string): Repository {
return new Repository(this, repository, dotPijul);
}
/**
* Runs the command to initialize a new pijul repository
* @param repository The location where the new repository will be created
*/
async init (repository: string): Promise<void> {
await this.exec(repository, ['init']);
}
/**
* Executes a new Pijul command in the given working directory.
* @param cwd The current working directory in which the command will be run
* @param args The arguments that will be passed to the command
* @param options The options for the spawned child process
*/
async exec (cwd: string, args: string[], options: SpawnOptions = {}): Promise<IExecutionResult<string>> {
options = Object.assign({ cwd }, options);
return await this._exec(args, options);
}
/**
* Executes a new pijul command with the given arguments and decodes the execution result from a buffer into a string
* @param args The arguments that will be passed to the command
* @param options The options for the spawned child process
*/
private async _exec (args: string[], options: SpawnOptions = {}): Promise<IExecutionResult<string>> {
const child = this.spawn(args, options);
if (options.onSpawn) {
options.onSpawn(child);
}
if (options.input) {
child.stdin!.end(options.input, 'utf8');
}
const bufferResult = await exec(child, options.cancellationToken);
if (options.log !== false && bufferResult.stderr.length > 0) {
this.log(`${bufferResult.stderr}\n`);
}
let encoding = options.encoding ?? 'utf8';
encoding = iconv.encodingExists(encoding) ? encoding : 'utf8';
const result: IExecutionResult<string> = {
exitCode: bufferResult.exitCode,
stdout: iconv.decode(bufferResult.stdout, encoding),
stderr: bufferResult.stderr
};
if (bufferResult.exitCode) {
return await Promise.reject<IExecutionResult<string>>(new Error('Error executing pijul command'));
}
return result;
}
/**
* Spawn a Pijul process using the given arguments to utilize the Pijul CLI
* @param args The arguments that will be passed to the child process
* @param options The options that will be used to spawn the new process
*/
private spawn (args: string[], options: SpawnOptions = {}): cp.ChildProcess {
if (!this.path) {
throw new Error('Pijul could not be found in the system.');
}
if (!options.stdio && !options.input) {
options.stdio = ['ignore', null, null]; // Unless provided, ignore stdin and leave default streams for stdout and stderr
}
if (!options.log) {
this.log(`> pijul ${args.join(' ')}\n`);
}
return cp.spawn(this.path, args, options);
}
/**
* Emit a log message as an event
* @param message The message to be emitted
*/
private log (message: string): void {
this._onOutput.emit('log', message);
}
}
/**
* The repository class represents a single Pijul repository.
* It holds a reference to the Pijul installation which it uses
* for interacting with the Pijul CLI. The Repository class exposes
* functions for all of the Pijul CLI commands which require an
* existing repository to run.
*/
export class Repository {
constructor (
private readonly _pijul: Pijul,
private readonly repositoryRoot: string,
readonly dotPijul: string
) {}
get pijul (): Pijul {
return this._pijul;
}
get root (): string {
return this.repositoryRoot;
}
}
/**
* Handles the execution of a child process and the capture of its results, cancelling it if necessary
* @param child The child process being executed
* @param cancellationToken A cancellation token that can be used to cancel the child process
*/
async function exec (child: cp.ChildProcess, cancellationToken?: CancellationToken): Promise<IExecutionResult<Buffer>> {
if (!child.stdout || !child.stderr) {
throw new Error('Failed to get stdout or stderr from git process.');
}
if (cancellationToken?.isCancellationRequested) {
throw new Error('Cancelled');
}
const disposables: IDisposable[] = [];
// Create handles for stdout and stderr events
const once = (ee: NodeJS.EventEmitter, name: string, fn: (...args: any[]) => void): void => {
ee.once(name, fn);
disposables.push(toDisposable(() => ee.removeListener(name, fn)));
};
const on = (ee: NodeJS.EventEmitter, name: string, fn: (...args: any[]) => void): void => {
ee.on(name, fn);
disposables.push(toDisposable(() => ee.removeListener(name, fn)));
};
// Create a promise representing all the different results of the child process
let result = Promise.all<any>([
new Promise<number>((resolve, reject) => {
once(child, 'error', (err) => (reject(err)));
once(child, 'exit', resolve);
}),
new Promise<Buffer>(resolve => {
const buffers: Buffer[] = [];
on(child.stdout!, 'data', (b: Buffer) => buffers.push(b));
once(child.stdout!, 'close', () => resolve(Buffer.concat(buffers)));
}),
new Promise<string>(resolve => {
const buffers: Buffer[] = [];
on(child.stderr!, 'data', (b: Buffer) => buffers.push(b));
once(child.stderr!, 'close', () => resolve(Buffer.concat(buffers).toString('utf8')));
})
]) as Promise<[number, Buffer, string]>;
if (cancellationToken) {
const cancellationPromise = new Promise<[number, Buffer, string]>((resolve, reject) => {
onceEvent(cancellationToken.onCancellationRequested)(() => {
try {
child.kill();
} catch (err) {
// Do nothing
}
reject(new Error('Cancelled'));
});
});
result = Promise.race([result, cancellationPromise]);
}
try {
const [exitCode, stdout, stderr] = await result;
return { exitCode, stdout, stderr };
} finally {
dispose(disposables);
}
}
/**
* Interface for representing the results of the execution of a child process
*/
export interface IExecutionResult<T extends string | Buffer> {
exitCode: number
stdout: T
stderr: string
}
/**
* Interface for representing the options for spawning a child process
*/
export interface SpawnOptions extends cp.SpawnOptions {
input?: string
encoding?: string
log?: boolean
cancellationToken?: CancellationToken
onSpawn?: (childProcess: cp.ChildProcess) => void
}