Skip to content

Commit

Permalink
Clean up console output when not connected to a TTY (#702)
Browse files Browse the repository at this point in the history
* Use * rather than \u25A0 for progress bars for compatibility with unicode-unaware terminals.

* Take color control away from @colors/colors and give it to Chalk so that it respects our color disable behavior.

* Refactor unpackers to use plain callbacks rather than EventEmitter.

* Change Download and HashVerify events to make it easier to write different frontends for them.

* Add new display backend that uses basic console printing rather than trying to show progress bars.

* Add a download update message every 10 seconds.

* Fix tryReadUTF8.

* Fix regressions in "has TTY" output.
  • Loading branch information
BillyONeal authored Sep 14, 2022
1 parent 3fa3fb5 commit 788ea09
Show file tree
Hide file tree
Showing 23 changed files with 460 additions and 582 deletions.
119 changes: 54 additions & 65 deletions ce/ce/archivers/ZipUnpacker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,83 +2,72 @@
// Licensed under the MIT License.

import { ProgressTrackingStream } from '../fs/streams';
import { InstallEvents } from '../interfaces/events';
import { UnpackEvents } from '../interfaces/events';
import { Session } from '../session';
import { PercentageScaler } from '../util/percentage-scaler';
import { Queue } from '../util/promise';
import { Uri } from '../util/uri';
import { UnpackOptions } from './options';
import { pipeline, Unpacker } from './unpacker';
import { implementUnpackOptions, pipeline } from './unpacker';
import { ZipEntry, ZipFile } from './unzip';

export class ZipUnpacker extends Unpacker {
constructor(private readonly session: Session) {
super();
}

async unpackFile(file: ZipEntry, archiveUri: Uri, outputUri: Uri, options: UnpackOptions) {
const extractPath = Unpacker.implementOutputOptions(file.name, options);
if (extractPath) {
const destination = outputUri.join(extractPath);
const fileEntry = {
archiveUri,
destination,
path: file.name,
extractPath
};
async function unpackFile(session: Session, file: ZipEntry, archiveUri: Uri, outputUri: Uri, events: Partial<UnpackEvents>, options: UnpackOptions) {
const extractPath = implementUnpackOptions(file.name, options);
if (extractPath) {
const destination = outputUri.join(extractPath);
const fileEntry = {
archiveUri,
destination,
path: file.name,
extractPath
};

this.fileProgress(fileEntry, 0);
this.session.channels.debug(`unpacking ZIP file ${archiveUri}/${file.name} => ${destination}`);
await destination.parent.createDirectory();
const readStream = await file.read();
const mode = ((file.attr >> 16) & 0xfff);
events.unpackFileProgress?.(fileEntry, 0);
session.channels.debug(`unpacking ZIP file ${archiveUri}/${file.name} => ${destination}`);
await destination.parent.createDirectory();
const readStream = await file.read();
const mode = ((file.attr >> 16) & 0xfff);

const writeStream = await destination.writeStream({ mtime: file.time, mode: mode ? mode : undefined });
const progressStream = new ProgressTrackingStream(0, file.size);
progressStream.on('progress', (filePercentage) => this.fileProgress(fileEntry, filePercentage));
await pipeline(readStream, progressStream, writeStream);
this.fileProgress(fileEntry, 100);
this.unpacked(fileEntry);
}
const writeStream = await destination.writeStream({ mtime: file.time, mode: mode ? mode : undefined });
const progressStream = new ProgressTrackingStream(0, file.size);
progressStream.on('progress', (filePercentage) => events.unpackFileProgress?.(fileEntry, filePercentage));
await pipeline(readStream, progressStream, writeStream);
events.unpackFileProgress?.(fileEntry, 100);
events.unpackFileComplete?.(fileEntry);
}
}

async unpack(archiveUri: Uri, outputUri: Uri, events: Partial<InstallEvents>, options: UnpackOptions): Promise<void> {
this.subscribe(events);
try {
this.session.channels.debug(`unpacking ZIP ${archiveUri} => ${outputUri}`);

const openedFile = await archiveUri.openFile();
const zipFile = await ZipFile.read(openedFile);
if (options.strip === -1) {
// when strip == -1, strip off all common folders off the front of the file names
options.strip = 0;
const folders = [...zipFile.folders.keys()].sort((a, b) => a.length - b.length);
const files = [...zipFile.files.keys()];
for (const folder of folders) {
if (files.all((filename) => filename.startsWith(folder))) {
options.strip = folder.split('/').length - 1;
continue;
}
break;
}
}

const archiveProgress = new PercentageScaler(0, zipFile.files.size);
this.progress(0);
const q = new Queue();
let count = 0;
for (const file of zipFile.files.values()) {

void q.enqueue(async () => {
await this.unpackFile(file, archiveUri, outputUri, options);
this.progress(archiveProgress.scalePosition(count++));
});
export async function unpackZip(session: Session, archiveUri: Uri, outputUri: Uri, events: Partial<UnpackEvents>, options: UnpackOptions): Promise<void> {
session.channels.debug(`unpacking ZIP ${archiveUri} => ${outputUri}`);
events.unpackArchiveStart?.(archiveUri, outputUri);
const openedFile = await archiveUri.openFile();
const zipFile = await ZipFile.read(openedFile);
if (options.strip === -1) {
// when strip == -1, strip off all common folders off the front of the file names
options.strip = 0;
const folders = [...zipFile.folders.keys()].sort((a, b) => a.length - b.length);
const files = [...zipFile.files.keys()];
for (const folder of folders) {
if (files.all((filename) => filename.startsWith(folder))) {
options.strip = folder.split('/').length - 1;
continue;
}
await q.done;
await zipFile.close();
this.progress(100);
} finally {
this.unsubscribe(events);
break;
}
}

const archiveProgress = new PercentageScaler(0, zipFile.files.size);
events.unpackArchiveProgress?.(archiveUri, 0);
const q = new Queue();
let count = 0;
for (const file of zipFile.files.values()) {
void q.enqueue(async () => {
await unpackFile(session, file, archiveUri, outputUri, events, options);
events.unpackArchiveProgress?.(archiveUri, archiveProgress.scalePosition(count++));
});
}

await q.done;
await zipFile.close();
events.unpackArchiveProgress?.(archiveUri, 100);
}
16 changes: 8 additions & 8 deletions ce/ce/archivers/git.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

import { InstallEvents } from '../interfaces/events';
import { UnpackEvents } from '../interfaces/events';
import { Session } from '../session';
import { Credentials } from '../util/credentials';
import { execute } from '../util/exec-cmd';
Expand Down Expand Up @@ -34,7 +34,7 @@ export class Git {
* @returns Boolean representing whether the execution was completed without error, this is not necessarily
* a guarantee that the clone did what we expected.
*/
async clone(repo: Uri, events: Partial<InstallEvents>, options: { recursive?: boolean, depth?: number } = {}) {
async clone(repo: Uri, events: Partial<UnpackEvents>, options: { recursive?: boolean, depth?: number } = {}) {
const remote = await isFilePath(repo) ? repo.fsPath : repo.toString();

const result = await execute(this.#toolPath, [
Expand All @@ -61,7 +61,7 @@ export class Git {
* @returns Boolean representing whether the execution was completed without error, this is not necessarily
* a guarantee that the fetch did what we expected.
*/
async fetch(remoteName: string, events: Partial<InstallEvents>, options: { commit?: string, depth?: number } = {}) {
async fetch(remoteName: string, events: Partial<UnpackEvents>, options: { commit?: string, depth?: number } = {}) {
const result = await execute(this.#toolPath, [
'-C',
this.#targetFolder.fsPath,
Expand All @@ -85,7 +85,7 @@ export class Git {
* @returns Boolean representing whether the execution was completed without error, this is not necessarily
* a guarantee that the checkout did what we expected.
*/
async checkout(events: Partial<InstallEvents>, options: { commit?: string } = {}) {
async checkout(events: Partial<UnpackEvents>, options: { commit?: string } = {}) {
const result = await execute(this.#toolPath, [
'-C',
this.#targetFolder.fsPath,
Expand All @@ -108,7 +108,7 @@ export class Git {
* @returns Boolean representing whether the execution was completed without error, this is not necessarily
* a guarantee that the reset did what we expected.
*/
async reset(events: Partial<InstallEvents>, options: { commit?: string, recurse?: boolean, hard?: boolean } = {}) {
async reset(events: Partial<UnpackEvents>, options: { commit?: string, recurse?: boolean, hard?: boolean } = {}) {
const result = await execute(this.#toolPath, [
'-C',
this.#targetFolder.fsPath,
Expand Down Expand Up @@ -175,7 +175,7 @@ export class Git {
* @param options Options to control how the submodule update is called.
* @returns true if the update was successful, false otherwise.
*/
async updateSubmodules(events: Partial<InstallEvents>, options: { init?: boolean, recursive?: boolean, depth?: number } = {}) {
async updateSubmodules(events: Partial<UnpackEvents>, options: { init?: boolean, recursive?: boolean, depth?: number } = {}) {
const result = await execute(this.#toolPath, [
'-C',
this.#targetFolder.fsPath,
Expand Down Expand Up @@ -216,13 +216,13 @@ export class Git {
return result.code === 0;
}
}
function chunkToHeartbeat(events: Partial<InstallEvents>): (chunk: any) => void {
function chunkToHeartbeat(events: Partial<UnpackEvents>): (chunk: any) => void {
return (chunk: any) => {
const regex = /\s([0-9]*?)%/;
chunk.toString().split(/^/gim).map((x: string) => x.trim()).filter((each: any) => each).forEach((line: string) => {
const match_array = line.match(regex);
if (match_array !== null) {
events.heartbeat?.(line.trim());
events.unpackArchiveHeartbeat?.(line.trim());
}
});
};
Expand Down
Loading

0 comments on commit 788ea09

Please sign in to comment.