未验证 提交 9a2d1d20 编写于 作者: M Matt Bierner 提交者: GitHub

Add log uploader command line util (#41318)

* Add log uploader command line option

Adds a new --upload-logs command line flag that allows users to upload log files from their current session to a secure endpoint

* Use TPromise

* Better argv description

* Fix var spell

* Use request service
上级 5c8c8341
......@@ -28,7 +28,7 @@ export interface IRequestOptions {
password?: string;
headers?: any;
timeout?: number;
data?: any;
data?: string | Stream;
agent?: Agent;
followRedirects?: number;
strictSSL?: boolean;
......@@ -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);
......@@ -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'],
......@@ -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();
......
......@@ -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';
......@@ -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>;
}
......@@ -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;
......@@ -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 {
......@@ -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> {
......@@ -179,6 +189,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[] = [];
......
/*---------------------------------------------------------------------------------------------
* 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);
}
}
\ No newline at end of file
......@@ -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);
......@@ -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();
......@@ -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),
......@@ -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)
......
......@@ -55,6 +55,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');
......
......@@ -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',
......@@ -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 {
......
......@@ -69,6 +69,7 @@ export interface IProductConfiguration {
'linux-x64': string;
'darwin': string;
};
logUploaderUrl: string;
}
export interface ISurveyData {
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册