Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add log uploader command line util #41318

Merged
merged 5 commits into from
Jan 11, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
11 changes: 8 additions & 3 deletions src/vs/base/node/request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export interface IRequestOptions {
password?: string;
headers?: any;
timeout?: number;
data?: any;
data?: string | Stream;
agent?: Agent;
followRedirects?: number;
strictSSL?: boolean;
Expand Down Expand Up @@ -63,6 +63,7 @@ export function request(options: IRequestOptions): TPromise<IRequestContext> {
: getNodeRequest(options);

return rawRequestPromise.then(rawRequest => {

return new TPromise<IRequestContext>((c, e) => {
const endpoint = parseUrl(options.url);

Expand All @@ -83,7 +84,6 @@ export function request(options: IRequestOptions): TPromise<IRequestContext> {

req = rawRequest(opts, (res: http.ClientResponse) => {
const followRedirects = isNumber(options.followRedirects) ? options.followRedirects : 3;

if (res.statusCode >= 300 && res.statusCode < 400 && followRedirects > 0 && res.headers['location']) {
request(assign({}, options, {
url: res.headers['location'],
Expand All @@ -107,7 +107,12 @@ export function request(options: IRequestOptions): TPromise<IRequestContext> {
}

if (options.data) {
req.write(options.data);
if (typeof options.data === 'string') {
req.write(options.data);
} else {
options.data.pipe(req);
return;
}
}

req.end();
Expand Down
19 changes: 17 additions & 2 deletions src/vs/code/electron-main/launch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { IChannel } from 'vs/base/parts/ipc/common/ipc';
import { ILogService } from 'vs/platform/log/common/log';
import { IURLService } from 'vs/platform/url/common/url';
import { IProcessEnvironment } from 'vs/base/common/platform';
import { ParsedArgs } from 'vs/platform/environment/common/environment';
import { ParsedArgs, IEnvironmentService } from 'vs/platform/environment/common/environment';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { OpenContext } from 'vs/platform/windows/common/windows';
import { IWindowsMainService, ICodeWindow } from 'vs/platform/windows/electron-main/windows';
Expand Down Expand Up @@ -42,12 +42,14 @@ export interface ILaunchService {
start(args: ParsedArgs, userEnv: IProcessEnvironment): TPromise<void>;
getMainProcessId(): TPromise<number>;
getMainProcessInfo(): TPromise<IMainProcessInfo>;
getLogsPath(): TPromise<string>;
}

export interface ILaunchChannel extends IChannel {
call(command: 'start', arg: IStartArguments): TPromise<void>;
call(command: 'get-main-process-id', arg: null): TPromise<any>;
call(command: 'get-main-process-info', arg: null): TPromise<any>;
call(command: 'get-logs-path', arg: null): TPromise<string>;
call(command: string, arg: any): TPromise<any>;
}

Expand All @@ -66,6 +68,9 @@ export class LaunchChannel implements ILaunchChannel {

case 'get-main-process-info':
return this.service.getMainProcessInfo();

case 'get-logs-path':
return this.service.getLogsPath();
}

return undefined;
Expand All @@ -89,6 +94,10 @@ export class LaunchChannelClient implements ILaunchService {
public getMainProcessInfo(): TPromise<IMainProcessInfo> {
return this.channel.call('get-main-process-info', null);
}

public getLogsPath(): TPromise<string> {
return this.channel.call('get-logs-path', null);
}
}

export class LaunchService implements ILaunchService {
Expand All @@ -99,7 +108,8 @@ export class LaunchService implements ILaunchService {
@ILogService private logService: ILogService,
@IWindowsMainService private windowsMainService: IWindowsMainService,
@IURLService private urlService: IURLService,
@IWorkspacesMainService private workspacesMainService: IWorkspacesMainService
@IWorkspacesMainService private workspacesMainService: IWorkspacesMainService,
@IEnvironmentService private readonly environmentService: IEnvironmentService
) { }

public start(args: ParsedArgs, userEnv: IProcessEnvironment): TPromise<void> {
Expand Down Expand Up @@ -180,6 +190,11 @@ export class LaunchService implements ILaunchService {
} as IMainProcessInfo);
}

public getLogsPath(): TPromise<string> {
this.logService.trace('Received request for logs path from other instance.');
return TPromise.as(this.environmentService.logsPath);
}

private getWindowInfo(window: ICodeWindow): IWindowInfo {
const folders: string[] = [];

Expand Down
134 changes: 134 additions & 0 deletions src/vs/code/electron-main/logUploader.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

'use strict';

import * as os from 'os';
import * as cp from 'child_process';
import * as fs from 'fs';
import * as path from 'path';
import * as readline from 'readline';

import { localize } from 'vs/nls';
import { ILaunchChannel } from 'vs/code/electron-main/launch';
import { TPromise } from 'vs/base/common/winjs.base';
import product from 'vs/platform/node/product';
import { IRequestService } from 'vs/platform/request/node/request';
import { IRequestContext } from 'vs/base/node/request';

interface PostResult {
readonly blob_id: string;
}

class Endpoint {
private constructor(
public readonly url: string
) { }

public static getFromProduct(): Endpoint | undefined {
const logUploaderUrl = product.logUploaderUrl;
return logUploaderUrl ? new Endpoint(logUploaderUrl) : undefined;
}
}

export async function uploadLogs(
channel: ILaunchChannel,
requestService: IRequestService
): TPromise<any> {
const endpoint = Endpoint.getFromProduct();
if (!endpoint) {
console.error(localize('invalidEndpoint', 'Invalid log uploader endpoint'));
return;
}

const logsPath = await channel.call('get-logs-path', null);

if (await promptUserToConfirmLogUpload(logsPath)) {
const outZip = await zipLogs(logsPath);
const result = await postLogs(endpoint, outZip, requestService);
console.log(localize('didUploadLogs', 'Uploaded logs ID: {0}', result.blob_id));
} else {
console.log(localize('userDeniedUpload', 'Canceled upload'));
}
}

async function promptUserToConfirmLogUpload(
logsPath: string
): Promise<boolean> {
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});

return new TPromise<boolean>(resolve =>
rl.question(
localize('logUploadPromptHeader', 'Upload session logs to secure endpoint?')
+ '\n\n' + localize('logUploadPromptBody', 'Please review your log files: \'{0}\'', logsPath)
+ '\n\n' + localize('logUploadPromptKey', 'Enter \'y\' to confirm upload...'),
(answer: string) => {
rl.close();
resolve(answer && answer.trim()[0].toLowerCase() === 'y');
}));
}

async function postLogs(
endpoint: Endpoint,
outZip: string,
requestService: IRequestService
): TPromise<PostResult> {
let result: IRequestContext;
try {
result = await requestService.request({
url: endpoint.url,
type: 'POST',
data: fs.createReadStream(outZip),
headers: {
'Content-Type': 'application/zip',
'Content-Length': fs.statSync(outZip).size
}
});
} catch (e) {
console.log(localize('postError', 'Error posting logs: {0}', e));
throw e;
}

try {
return JSON.parse(result.stream.toString());
} catch (e) {
console.log(localize('parseError', 'Error parsing response'));
throw e;
}
}

function zipLogs(
logsPath: string
): TPromise<string> {
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'vscode-log-upload'));
const outZip = path.join(tempDir, 'logs.zip');
return new TPromise<string>((resolve, reject) => {
doZip(logsPath, outZip, (err, stdout, stderr) => {
if (err) {
console.error(localize('zipError', 'Error zipping logs: {0}', err));
reject(err);
} else {
resolve(outZip);
}
});
});
}

function doZip(
logsPath: string,
outZip: string,
callback: (error: Error, stdout: string, stderr: string) => void
) {
switch (os.platform()) {
case 'win32':
return cp.execFile('powershell', ['-Command', `Compress-Archive -Path "${logsPath}" -DestinationPath ${outZip}`], { cwd: logsPath }, callback);

default:
return cp.execFile('zip', ['-r', outZip, '.'], { cwd: logsPath }, callback);
}
}
16 changes: 15 additions & 1 deletion src/vs/code/electron-main/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ class ExpectedError extends Error {
function setupIPC(accessor: ServicesAccessor): TPromise<Server> {
const logService = accessor.get(ILogService);
const environmentService = accessor.get(IEnvironmentService);
const requestService = accessor.get(IRequestService);

function allowSetForegroundWindow(service: LaunchChannelClient): TPromise<void> {
let promise = TPromise.wrap<void>(void 0);
Expand Down Expand Up @@ -133,6 +134,12 @@ function setupIPC(accessor: ServicesAccessor): TPromise<Server> {
throw new ExpectedError('Terminating...');
}

// Log uploader usage info
if (environmentService.args['upload-logs']) {
logService.warn('Warning: The --upload-logs argument can only be used if Code is already running. Please run it again after Code has started.');
throw new ExpectedError('Terminating...');
}

// dock might be hidden at this case due to a retry
if (platform.isMacintosh) {
app.dock.show();
Expand Down Expand Up @@ -170,7 +177,7 @@ function setupIPC(accessor: ServicesAccessor): TPromise<Server> {
// Skip this if we are running with --wait where it is expected that we wait for a while.
// Also skip when gathering diagnostics (--status) which can take a longer time.
let startupWarningDialogHandle: number;
if (!environmentService.wait && !environmentService.status) {
if (!environmentService.wait && !environmentService.status && !environmentService.args['upload-logs']) {
startupWarningDialogHandle = setTimeout(() => {
showStartupWarningDialog(
localize('secondInstanceNoResponse', "Another instance of {0} is running but not responding", product.nameShort),
Expand All @@ -189,6 +196,13 @@ function setupIPC(accessor: ServicesAccessor): TPromise<Server> {
});
}

// Log uploader
if (environmentService.args['upload-logs']) {
return import('vs/code/electron-main/logUploader')
.then(logUploader => logUploader.uploadLogs(channel, requestService))
.then(() => TPromise.wrapError(new ExpectedError()));
}

logService.trace('Sending env to running instance...');

return allowSetForegroundWindow(service)
Expand Down
1 change: 1 addition & 0 deletions src/vs/platform/environment/common/environment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ export interface ParsedArgs {
'skip-add-to-recently-opened'?: boolean;
'file-write'?: boolean;
'file-chmod'?: boolean;
'upload-logs'?: boolean;
}

export const IEnvironmentService = createDecorator<IEnvironmentService>('environmentService');
Expand Down
6 changes: 4 additions & 2 deletions src/vs/platform/environment/node/argv.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,8 @@ const options: minimist.Opts = {
'skip-add-to-recently-opened',
'status',
'file-write',
'file-chmod'
'file-chmod',
'upload-logs'
],
alias: {
add: 'a',
Expand Down Expand Up @@ -162,7 +163,8 @@ const troubleshootingHelp: { [name: string]: string; } = {
'--disable-extensions': localize('disableExtensions', "Disable all installed extensions."),
'--inspect-extensions': localize('inspect-extensions', "Allow debugging and profiling of extensions. Check the developer tools for the connection uri."),
'--inspect-brk-extensions': localize('inspect-brk-extensions', "Allow debugging and profiling of extensions with the extension host being paused after start. Check the developer tools for the connection uri."),
'--disable-gpu': localize('disableGPU', "Disable GPU hardware acceleration.")
'--disable-gpu': localize('disableGPU', "Disable GPU hardware acceleration."),
'--upload-logs': localize('uploadLogs', "Uploads logs from current session to a secure endpoint.")
};

export function formatOptions(options: { [name: string]: string; }, columns: number): string {
Expand Down
1 change: 1 addition & 0 deletions src/vs/platform/node/product.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ export interface IProductConfiguration {
'linux-x64': string;
'darwin': string;
};
logUploaderUrl: string;
}

export interface ISurveyData {
Expand Down