Skip to content
This repository was archived by the owner on Dec 8, 2020. It is now read-only.

Commit 20e56e7

Browse files
authored
Show Rustup invocation in Output Channel (#250)
1 parent 0b50fbb commit 20e56e7

File tree

2 files changed

+159
-21
lines changed

2 files changed

+159
-21
lines changed

src/OutputChannelProcess.ts

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
import { window } from 'vscode';
2+
import { spawn, ChildProcess, SpawnOptions } from 'child_process';
3+
import { Readable } from 'stream';
4+
import { OutputChannel } from 'vscode';
5+
6+
export interface Success {
7+
success: true;
8+
code: number;
9+
stdout: string;
10+
stderr: string;
11+
}
12+
13+
export interface Error {
14+
success: false;
15+
}
16+
17+
export interface Options {
18+
/**
19+
* The flag indicating whether data from stdout should be captured. By default, the data is
20+
* not captured. If the data is captured, then it will be given when the process ends
21+
*/
22+
captureStdout?: boolean;
23+
24+
/**
25+
* The flag indicating whether data from stderr should be captured. By default, the data is
26+
* not captured. If the data is captured, then it will be given when the process ends
27+
*/
28+
captureStderr?: boolean;
29+
}
30+
31+
export async function create(spawnCommand: string, spawnArgs: string[] | undefined,
32+
spawnOptions: SpawnOptions | undefined, outputChannelName: string): Promise<Success | Error> {
33+
if (spawnOptions === undefined) {
34+
spawnOptions = {};
35+
}
36+
spawnOptions.stdio = 'pipe';
37+
const spawnedProcess = spawn(spawnCommand, spawnArgs, spawnOptions);
38+
const outputChannel = window.createOutputChannel(outputChannelName);
39+
outputChannel.show();
40+
const result = await process(spawnedProcess, outputChannel);
41+
if (result.success && result.code === 0) {
42+
outputChannel.hide();
43+
outputChannel.dispose();
44+
}
45+
return result;
46+
}
47+
48+
/**
49+
* Writes data from the process to the output channel. The function also can accept options
50+
* @param process The process to write data from. The process should be creates with
51+
* options.stdio = "pipe"
52+
* @param outputChannel The output channel to write data to
53+
* @return The result of processing the process
54+
*/
55+
export function process(process: ChildProcess, outputChannel: OutputChannel, options?: Options
56+
): Promise<Success | Error> {
57+
const stdout = '';
58+
const captureStdout = getOption(options, o => o.captureStdout, false);
59+
subscribeToDataEvent(process.stdout, outputChannel, captureStdout, stdout);
60+
const stderr = '';
61+
const captureStderr = getOption(options, o => o.captureStderr, false);
62+
subscribeToDataEvent(process.stderr, outputChannel, captureStderr, stderr);
63+
return new Promise<Success | Error>(resolve => {
64+
const processProcessEnding = (code: number) => {
65+
resolve({
66+
success: true,
67+
code,
68+
stdout,
69+
stderr
70+
});
71+
};
72+
// If some error happens, then the "error" and "close" events happen.
73+
// If the process ends, then the "exit" and "close" events happen.
74+
// It is known that the order of events is not determined.
75+
let processExited = false;
76+
let processClosed = false;
77+
process.on('error', (error: any) => {
78+
outputChannel.appendLine(`error: error=${error}`);
79+
resolve({ success: false });
80+
});
81+
process.on('close', (code, signal) => {
82+
outputChannel.appendLine(`\nclose: code=${code}, signal=${signal}`);
83+
processClosed = true;
84+
if (processExited) {
85+
processProcessEnding(code);
86+
}
87+
});
88+
process.on('exit', (code, signal) => {
89+
outputChannel.appendLine(`\nexit: code=${code}, signal=${signal}`);
90+
processExited = true;
91+
if (processClosed) {
92+
processProcessEnding(code);
93+
}
94+
});
95+
});
96+
}
97+
98+
function getOption(options: Options | undefined, getOption: (options: Options) => boolean | undefined,
99+
defaultValue: boolean): boolean {
100+
if (options === undefined) {
101+
return defaultValue;
102+
}
103+
const option = getOption(options);
104+
if (option === undefined) {
105+
return defaultValue;
106+
}
107+
return option;
108+
}
109+
110+
function subscribeToDataEvent(readable: Readable, outputChannel: OutputChannel, saveData: boolean, dataStorage: string): void {
111+
readable.on('data', chunk => {
112+
const chunkAsString = typeof chunk === 'string' ? chunk : chunk.toString();
113+
outputChannel.append(chunkAsString);
114+
if (saveData) {
115+
dataStorage += chunkAsString;
116+
}
117+
});
118+
}

src/components/configuration/Rustup.ts

Lines changed: 41 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
import { join } from 'path';
22

3-
// import { EOL } from 'os';
4-
53
import { OutputtingProcess } from '../../OutputtingProcess';
64

75
import { FileSystem } from '../file_system/FileSystem';
86

97
import ChildLogger from '../logging/child_logger';
108

9+
import * as OutputChannelProcess from '../../OutputChannelProcess';
10+
1111
namespace Constants {
1212
export const DEFAULT_TOOLCHAIN_SUFFIX = '(default)';
1313
}
@@ -112,14 +112,15 @@ export class Rustup {
112112
* @return true if no error occurred and the toolchain has been installed otherwise false
113113
*/
114114
public async installToolchain(toolchain: string): Promise<boolean> {
115-
const logger = this.logger.createChildLogger(`installToolchain: toolchain=${toolchain}`);
116-
const output = await Rustup.invoke(['toolchain', 'install', toolchain], logger);
117-
if (output) {
118-
logger.debug(`output=${output}`);
119-
} else {
115+
const logger = this.logger.createChildLogger(`installToolchain(toolchain=${toolchain}): `);
116+
const args = ['toolchain', 'install', toolchain];
117+
const outputChannelName = `Rustup: Installing ${toolchain} toolchain`;
118+
const output = await Rustup.invokeWithOutputChannel(args, logger, outputChannelName);
119+
if (output === undefined) {
120120
logger.error(`output=${output}`);
121121
return false;
122122
}
123+
logger.debug(`output=${output}`);
123124
await this.updateToolchains();
124125
if (this.toolchains.length === 0) {
125126
logger.error('this.toolchains.length === 0');
@@ -320,7 +321,8 @@ export class Rustup {
320321
* @return The output of the invocation if the invocation exited successfully otherwise undefined
321322
*/
322323
private static async invokeGettingSysrootPath(toolchain: string, logger: ChildLogger): Promise<string | undefined> {
323-
const output: string | undefined = await this.invokeRun(toolchain, ['rustc', '--print', 'sysroot'], logger);
324+
const args = ['run', toolchain, 'rustc', '--print', 'sysroot'];
325+
const output: string | undefined = await this.invoke(args, logger);
324326
if (!output) {
325327
return undefined;
326328
}
@@ -337,16 +339,6 @@ export class Rustup {
337339
return output.trim().split('\n');
338340
}
339341

340-
/**
341-
* Invokes `rustup run...` with the specified toolchain and arguments, checks if it exited successfully and returns its output
342-
* @param toolchain The toolchain to invoke rustup with
343-
* @param args The arguments to invoke rustup with
344-
* @param logger The logger to log messages
345-
*/
346-
private static async invokeRun(toolchain: string, args: string[], logger: ChildLogger): Promise<string | undefined> {
347-
return await this.invoke(['run', toolchain, ...args], logger);
348-
}
349-
350342
/**
351343
* Invokes Rustup with specified arguments, checks if it exited successfully and returns its output
352344
* @param args Arguments to invoke Rustup with
@@ -368,6 +360,34 @@ export class Rustup {
368360
return result.stdoutData;
369361
}
370362

363+
/**
364+
* Invokes rustup with the specified arguments, creates an output channel with the specified
365+
* name, writes output of the invocation and returns the output
366+
* @param args The arguments which to invoke rustup with
367+
* @param logger The logger to log messages
368+
* @param outputChannelName The name which to create an output channel with
369+
*/
370+
private static async invokeWithOutputChannel(args: string[], logger: ChildLogger,
371+
outputChannelName: string): Promise<string | undefined> {
372+
const functionLogger = logger.createChildLogger(`invokeWithOutputChannel(args=${JSON.stringify(args)}, outputChannelName=${outputChannelName}): `);
373+
const result = await OutputChannelProcess.create(this.getRustupExecutable(), args, undefined, outputChannelName);
374+
if (!result.success) {
375+
functionLogger.error('failed to start');
376+
return undefined;
377+
}
378+
if (result.code !== 0) {
379+
functionLogger.error(`exited with not zero; code=${result.code}`);
380+
functionLogger.error('Beginning of stdout');
381+
functionLogger.error(result.stdout);
382+
functionLogger.error('Ending of stdout');
383+
functionLogger.error('Beginning of stderr');
384+
functionLogger.error(result.stderr);
385+
functionLogger.error('Ending of stderr');
386+
return undefined;
387+
}
388+
return result.stdout;
389+
}
390+
371391
/**
372392
* Constructs a new instance of the class.
373393
* The constructor is private because creating a new instance should be done via the method `create`
@@ -415,10 +435,10 @@ export class Rustup {
415435
return true;
416436
}
417437
const args = ['component', 'add', componentName, '--toolchain', 'nightly'];
418-
const stdoutData: string | undefined = await Rustup.invoke(args, logger);
419-
// Some error occurred. It is already logged in the method invokeRustup.
420-
// So we just need to notify a caller that the installation failed
438+
const stdoutData = await Rustup.invokeWithOutputChannel(args, logger, `Rustup: Installing ${componentName}`);
421439
if (stdoutData === undefined) {
440+
// Some error occurred. It is already logged
441+
// So we just need to notify a caller that the installation failed
422442
return false;
423443
}
424444
await this.updateComponents();

0 commit comments

Comments
 (0)