提交 06072ef7 编写于 作者: D Dirk Baeumer

Cannot define multiple commands in tasks.json #981

上级 4c58d0dd
......@@ -71,6 +71,52 @@ export namespace ShowOutput {
}
}
export interface CommandOptions {
/**
* The current working directory of the executed program or shell.
* If omitted VSCode's current workspace root is used.
*/
cwd?: string;
/**
* The environment of the executed program or shell. If omitted
* the parent process' environment is used.
*/
env?: { [key: string]: string; };
}
export interface CommandConfiguration {
/**
* The command to execute
*/
name?: string;
/**
* Whether the command is a shell command or not
*/
isShellCommand?: boolean;
/**
* Additional command options.
*/
options?: CommandOptions;
/**
* Command arguments.
*/
args?: string[];
/**
* The task selector if needed.
*/
taskSelector?: string;
/**
* Controls whether the executed command is printed to the output windows as well.
*/
echo?: boolean;
}
/**
* A task description
*/
......@@ -86,6 +132,11 @@ export interface TaskDescription {
*/
name: string;
/**
* The command configuration
*/
command: CommandConfiguration;
/**
* Suppresses the task name when calling the task using the task runner.
*/
......@@ -113,56 +164,25 @@ export interface TaskDescription {
*/
showOutput: ShowOutput;
/**
* Controls whether the executed command is printed to the output windows as well.
*/
echoCommand?: boolean;
/**
* The problem watchers to use for this task
*/
problemMatchers?: ProblemMatcher[];
}
export interface CommandOptions {
/**
* The current working directory of the executed program or shell.
* If omitted VSCode's current workspace root is used.
*/
cwd?: string;
/**
* The environment of the executed program or shell. If omitted
* the parent process' environment is used.
*/
env?: { [key: string]: string; };
}
/**
* Describs the settings of a task runner
*/
export interface BaseTaskRunnerConfiguration {
/**
* The command to execute
*/
command?: string;
export interface TaskRunnerConfiguration {
/**
* Whether the task is a shell command or not
* The inferred build tasks
*/
isShellCommand?: boolean;
/**
* Additional command options
*/
options?: CommandOptions;
buildTasks: string[];
/**
* General args
* The inferred test tasks;
*/
args?: string[];
testTasks: string[];
/**
* The configured tasks
......@@ -170,17 +190,6 @@ export interface BaseTaskRunnerConfiguration {
tasks?: { [id: string]: TaskDescription; };
}
/**
* Describs the settings of a task runner
*/
export interface TaskRunnerConfiguration extends BaseTaskRunnerConfiguration {
/**
* The command to execute. Not optional.
*/
command: string;
}
export interface ITaskSummary {
/**
* Exit code of the process.
......
......@@ -669,6 +669,10 @@ class TaskService extends EventEmitter implements ITaskService {
lifecycleService.onWillShutdown(event => event.veto(this.beforeShutdown()));
}
public log(value: string): void {
this.outputChannel.append(value + '\n');
}
private disposeTaskSystemListeners(): void {
this.taskSystemListeners = dispose(this.taskSystemListeners);
}
......@@ -744,11 +748,15 @@ class TaskService extends EventEmitter implements ITaskService {
throw new TaskError(Severity.Info, nls.localize('TaskSystem.noConfiguration', 'No task runner configured.'), TaskErrors.NotConfigured);
}
let result: ITaskSystem = null;
let parseResult = FileConfig.parse(<FileConfig.ExternalTaskRunnerConfiguration>config, this);
if (parseResult.validationStatus.isFatal()) {
throw new TaskError(Severity.Error, nls.localize('TaskSystem.fatalError', 'The provided task configuration has validation errors. See tasks output log for details.'), TaskErrors.ConfigValidationError);
}
if (this.isRunnerConfig(config)) {
result = new ProcessRunnerSystem(<FileConfig.ExternalTaskRunnerConfiguration>config, this.markerService, this.modelService, this.telemetryService, this.outputService, this.configurationResolverService, TaskService.OutputChannelId, clearOutput);
result = new ProcessRunnerSystem(parseResult.configuration, this.markerService, this.modelService, this.telemetryService, this.outputService, this.configurationResolverService, TaskService.OutputChannelId, clearOutput);
} else if (this.isTerminalConfig(config)) {
result = new TerminalTaskSystem(
<FileConfig.ExternalTaskRunnerConfiguration>config,
parseResult.configuration,
this.terminalService, this.outputService, this.markerService,
this.modelService, this.configurationResolverService, this.telemetryService,
TaskService.OutputChannelId
......@@ -1041,6 +1049,26 @@ let schema: IJSONSchema =
'type': 'string',
'enum': ['always', 'silent', 'never']
},
'options': {
'type': 'object',
'description': nls.localize('JsonSchema.options', 'Additional command options'),
'properties': {
'cwd': {
'type': 'string',
'description': nls.localize('JsonSchema.options.cwd', 'The current working directory of the executed program or script. If omitted Code\'s current workspace root is used.')
},
'env': {
'type': 'object',
'additionalProperties': {
'type': 'string'
},
'description': nls.localize('JsonSchema.options.env', 'The environment of the executed program or shell. If omitted the parent process\' environment is used.')
}
},
'additionalProperties': {
'type': ['string', 'array', 'object']
}
},
'patternType': {
'anyOf': [
{
......@@ -1256,24 +1284,7 @@ let schema: IJSONSchema =
}
},
'options': {
'type': 'object',
'description': nls.localize('JsonSchema.options', 'Additional command options'),
'properties': {
'cwd': {
'type': 'string',
'description': nls.localize('JsonSchema.options.cwd', 'The current working directory of the executed program or script. If omitted Code\'s current workspace root is used.')
},
'env': {
'type': 'object',
'additionalProperties': {
'type': 'string'
},
'description': nls.localize('JsonSchema.options.env', 'The environment of the executed program or shell. If omitted the parent process\' environment is used.')
}
},
'additionalProperties': {
'type': ['string', 'array', 'object']
}
'$ref': '#/definitions/options'
},
'showOutput': {
'$ref': '#/definitions/showOutputType',
......@@ -1326,13 +1337,25 @@ let schema: IJSONSchema =
'type': 'string',
'description': nls.localize('JsonSchema.tasks.taskName', "The task's name")
},
'command': {
'type': 'string',
'description': nls.localize('JsonSchema.command', 'The command to be executed. Can be an external program or a shell command.')
},
'isShellCommand': {
'type': 'boolean',
'default': true,
'description': nls.localize('JsonSchema.shell', 'Specifies whether the command is a shell command or an external program. Defaults to false if omitted.')
},
'args': {
'type': 'array',
'description': nls.localize('JsonSchema.tasks.args', 'Additional arguments passed to the command when this task is invoked.'),
'description': nls.localize('JsonSchema.tasks.args', 'Arguments passed to the command when this task is invoked.'),
'items': {
'type': 'string'
}
},
'options': {
'$ref': '#/definitions/options'
},
'suppressTaskName': {
'type': 'boolean',
'description': nls.localize('JsonSchema.tasks.suppressTaskName', 'Controls whether the task name is added as an argument to the command. If omitted the globally defined value is used.'),
......
......@@ -22,7 +22,6 @@ import { TerminateResponse } from 'vs/base/common/processes';
import * as TPath from 'vs/base/common/paths';
import { IMarkerService } from 'vs/platform/markers/common/markers';
import { ValidationStatus } from 'vs/base/common/parsers';
import { IModelService } from 'vs/editor/common/services/modelService';
import { ProblemMatcher } from 'vs/platform/markers/common/problemMatcher';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
......@@ -33,7 +32,6 @@ import { TerminalConfigHelper } from 'vs/workbench/parts/terminal/electron-brows
import { IOutputService, IOutputChannel } from 'vs/workbench/parts/output/common/output';
import { StartStopProblemCollector, WatchingProblemCollector, ProblemCollectorEvents } from 'vs/workbench/parts/tasks/common/problemCollectors';
import { ITaskSystem, ITaskSummary, ITaskExecuteResult, TaskExecuteKind, TaskError, TaskErrors, TaskRunnerConfiguration, TaskDescription, ShowOutput, TelemetryEvent, Triggers, TaskSystemEvents, TaskEvent, TaskType, CommandOptions } from 'vs/workbench/parts/tasks/common/taskSystem';
import * as FileConfig from '../node/processRunnerConfiguration';
interface TerminalData {
terminal: ITerminalInstance;
......@@ -87,15 +85,10 @@ export class TerminalTaskSystem extends EventEmitter implements ITaskSystem {
public static TelemetryEventName: string = 'taskService';
private validationStatus: ValidationStatus;
private buildTaskIdentifier: string;
private testTaskIdentifier: string;
private configuration: TaskRunnerConfiguration;
private outputChannel: IOutputChannel;
private activeTasks: IStringDictionary<TerminalData>;
constructor(private fileConfig: FileConfig.ExternalTaskRunnerConfiguration, private terminalService: ITerminalService, private outputService: IOutputService,
constructor(private configuration: TaskRunnerConfiguration, private terminalService: ITerminalService, private outputService: IOutputService,
private markerService: IMarkerService, private modelService: IModelService, private configurationResolverService: IConfigurationResolverService,
private telemetryService: ITelemetryService, outputChannelId: string) {
super();
......@@ -103,23 +96,13 @@ export class TerminalTaskSystem extends EventEmitter implements ITaskSystem {
this.outputChannel = this.outputService.getChannel(outputChannelId);
this.clearOutput();
this.activeTasks = Object.create(null);
let parseResult = FileConfig.parse(fileConfig, this);
this.validationStatus = parseResult.validationStatus;
this.configuration = parseResult.configuration;
this.buildTaskIdentifier = parseResult.defaultBuildTaskIdentifier;
this.testTaskIdentifier = parseResult.defaultTestTaskIdentifier;
if (!this.validationStatus.isOK()) {
this.showOutput();
}
}
public log(value: string): void {
this.outputChannel.append(value + '\n');
}
private showOutput(): void {
protected showOutput(): void {
this.outputChannel.show(true);
}
......@@ -128,10 +111,10 @@ export class TerminalTaskSystem extends EventEmitter implements ITaskSystem {
}
public build(): ITaskExecuteResult {
if (!this.buildTaskIdentifier) {
if (this.configuration.buildTasks.length === 0) {
throw new TaskError(Severity.Info, nls.localize('TerminalTaskSystem.noBuildTask', 'No build task defined in tasks.json'), TaskErrors.NoBuildTask);
}
return this.run(this.buildTaskIdentifier, Triggers.shortcut);
return this.run(this.configuration.buildTasks[0], Triggers.shortcut);
}
public rebuild(): ITaskExecuteResult {
......@@ -143,10 +126,10 @@ export class TerminalTaskSystem extends EventEmitter implements ITaskSystem {
}
public runTest(): ITaskExecuteResult {
if (!this.testTaskIdentifier) {
if (this.configuration.testTasks.length === 0) {
throw new TaskError(Severity.Info, nls.localize('TerminalTaskSystem.noTestTask', 'No test task defined in tasks.json'), TaskErrors.NoTestTask);
}
return this.run(this.testTaskIdentifier, Triggers.shortcut);
return this.run(this.configuration.testTasks[0], Triggers.shortcut);
}
public run(taskIdentifier: string, trigger: string = Triggers.command): ITaskExecuteResult {
......@@ -257,6 +240,7 @@ export class TerminalTaskSystem extends EventEmitter implements ITaskSystem {
});
});
terminal.onExit((exitCode) => {
delete this.activeTasks[task.id];
watchingProblemMatcher.dispose();
toUnbind = dispose(toUnbind);
toUnbind = null;
......@@ -283,12 +267,11 @@ export class TerminalTaskSystem extends EventEmitter implements ITaskSystem {
});
});
terminal.onExit((exitCode) => {
delete this.activeTasks[task.id];
startStopProblemMatcher.processLine(decoder.end());
startStopProblemMatcher.done();
startStopProblemMatcher.dispose();
this.emit(TaskSystemEvents.Inactive, event);
delete this.activeTasks[task.id];
terminal.reuseTerminal();
resolve({ exitCode });
});
this.terminalService.setActiveInstance(terminal);
......@@ -307,11 +290,11 @@ export class TerminalTaskSystem extends EventEmitter implements ITaskSystem {
}
private createTerminal(task: TaskDescription): ITerminalInstance {
let options = this.resolveOptions(this.configuration.options);
let options = this.resolveOptions(task.command.options);
let { command, args } = this.resolveCommandAndArgs(task);
let terminalName = nls.localize('TerminalTaskSystem.terminalName', 'Task - {0}', task.name);
let waitOnExit = task.showOutput !== ShowOutput.Never || !task.isBackground;
if (this.configuration.isShellCommand) {
if (task.command.isShellCommand) {
if (Platform.isWindows && ((options.cwd && TPath.isUNC(options.cwd)) || (!options.cwd && TPath.isUNC(process.cwd())))) {
throw new TaskError(Severity.Error, nls.localize('TerminalTaskSystem', 'Can\'t execute a shell command on an UNC drive.'), TaskErrors.UnknownError);
}
......@@ -334,7 +317,12 @@ export class TerminalTaskSystem extends EventEmitter implements ITaskSystem {
let toAdd: string[] = [];
let commandLine: string;
if (Platform.isWindows) {
toAdd.push('/d', '/c');
let basename = path.basename(shellConfig.executable).toLowerCase();
if (basename === 'powershell.exe') {
toAdd.push('-Command');
} else {
toAdd.push('/d', '/c');
}
let quotedCommand: boolean = false;
let quotedArg: boolean = false;
let quoted = this.ensureDoubleQuotes(command);
......@@ -382,7 +370,7 @@ export class TerminalTaskSystem extends EventEmitter implements ITaskSystem {
let cwd = options && options.cwd ? options.cwd : process.cwd();
// On Windows executed process must be described absolute. Since we allowed command without an
// absolute path (e.g. "command": "node") we need to find the executable in the CWD or PATH.
let executable = Platform.isWindows && !this.configuration.isShellCommand ? this.findExecutable(command, cwd) : command;
let executable = Platform.isWindows && !task.command.isShellCommand ? this.findExecutable(command, cwd) : command;
const shellLaunchConfig: IShellLaunchConfig = {
name: terminalName,
executable: executable,
......@@ -394,11 +382,12 @@ export class TerminalTaskSystem extends EventEmitter implements ITaskSystem {
}
private resolveCommandAndArgs(task: TaskDescription): { command: string, args: string[] } {
let args: string[] = this.configuration.args ? this.configuration.args.slice() : [];
// First we need to use the command args:
let args: string[] = task.command.args ? task.command.args.slice() : [];
// We need to first pass the task name
if (!task.suppressTaskName) {
if (this.fileConfig.taskSelector) {
args.push(this.fileConfig.taskSelector + task.name);
if (task.command.taskSelector) {
args.push(task.command.taskSelector + task.name);
} else {
args.push(task.name);
}
......@@ -408,7 +397,7 @@ export class TerminalTaskSystem extends EventEmitter implements ITaskSystem {
args = args.concat(task.args);
}
args = this.resolveVariables(args);
let command: string = this.resolveVariable(this.configuration.command);
let command: string = this.resolveVariable(task.command.name);
return { command, args };
}
......
......@@ -21,14 +21,16 @@ import { IOutputService, IOutputChannel } from 'vs/workbench/parts/output/common
import { IConfigurationResolverService } from 'vs/workbench/services/configurationResolver/common/configurationResolver';
import { IMarkerService } from 'vs/platform/markers/common/markers';
import { ValidationStatus } from 'vs/base/common/parsers';
import { IModelService } from 'vs/editor/common/services/modelService';
import { ProblemMatcher } from 'vs/platform/markers/common/problemMatcher';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { StartStopProblemCollector, WatchingProblemCollector, ProblemCollectorEvents } from 'vs/workbench/parts/tasks/common/problemCollectors';
import { ITaskSystem, ITaskSummary, ITaskExecuteResult, TaskExecuteKind, TaskError, TaskErrors, TaskRunnerConfiguration, TaskDescription, CommandOptions, ShowOutput, TelemetryEvent, Triggers, TaskSystemEvents, TaskEvent, TaskType } from 'vs/workbench/parts/tasks/common/taskSystem';
import * as FileConfig from './processRunnerConfiguration';
import {
ITaskSystem, ITaskSummary, ITaskExecuteResult, TaskExecuteKind, TaskError, TaskErrors, TaskRunnerConfiguration,
TaskDescription, CommandOptions, ShowOutput, TelemetryEvent, Triggers, TaskSystemEvents, TaskEvent, TaskType,
CommandConfiguration
} from 'vs/workbench/parts/tasks/common/taskSystem';
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
......@@ -36,16 +38,12 @@ export class ProcessRunnerSystem extends EventEmitter implements ITaskSystem {
public static TelemetryEventName: string = 'taskService';
private fileConfig: FileConfig.ExternalTaskRunnerConfiguration;
private markerService: IMarkerService;
private modelService: IModelService;
private outputService: IOutputService;
private telemetryService: ITelemetryService;
private configurationResolverService: IConfigurationResolverService;
private validationStatus: ValidationStatus;
private defaultBuildTaskIdentifier: string;
private defaultTestTaskIdentifier: string;
private configuration: TaskRunnerConfiguration;
private outputChannel: IOutputChannel;
......@@ -54,18 +52,16 @@ export class ProcessRunnerSystem extends EventEmitter implements ITaskSystem {
private activeTaskIdentifier: string;
private activeTaskPromise: TPromise<ITaskSummary>;
constructor(fileConfig: FileConfig.ExternalTaskRunnerConfiguration, markerService: IMarkerService, modelService: IModelService, telemetryService: ITelemetryService,
constructor(configuration: TaskRunnerConfiguration, markerService: IMarkerService, modelService: IModelService, telemetryService: ITelemetryService,
outputService: IOutputService, configurationResolverService: IConfigurationResolverService, outputChannelId: string, clearOutput: boolean = true) {
super();
this.fileConfig = fileConfig;
this.configuration = configuration;
this.markerService = markerService;
this.modelService = modelService;
this.outputService = outputService;
this.telemetryService = telemetryService;
this.configurationResolverService = configurationResolverService;
this.defaultBuildTaskIdentifier = null;
this.defaultTestTaskIdentifier = null;
this.childProcess = null;
this.activeTaskIdentifier = null;
this.activeTaskPromise = null;
......@@ -75,27 +71,19 @@ export class ProcessRunnerSystem extends EventEmitter implements ITaskSystem {
this.clearOutput();
}
this.errorsShown = false;
let parseResult = FileConfig.parse(fileConfig, this);
this.validationStatus = parseResult.validationStatus;
this.configuration = parseResult.configuration;
this.defaultBuildTaskIdentifier = parseResult.defaultBuildTaskIdentifier;
this.defaultTestTaskIdentifier = parseResult.defaultTestTaskIdentifier;
if (!this.validationStatus.isOK()) {
this.showOutput();
}
}
public build(): ITaskExecuteResult {
let buildTaskIdentifier = this.configuration.buildTasks.length > 0 ? this.configuration.buildTasks[0] : undefined;
if (this.activeTaskIdentifier) {
let task = this.configuration.tasks[this.activeTaskIdentifier];
return { kind: TaskExecuteKind.Active, active: { same: this.activeTaskIdentifier === this.defaultBuildTaskIdentifier, background: task.isBackground }, promise: this.activeTaskPromise };
return { kind: TaskExecuteKind.Active, active: { same: this.activeTaskIdentifier === buildTaskIdentifier, background: task.isBackground }, promise: this.activeTaskPromise };
}
if (!this.defaultBuildTaskIdentifier) {
if (!buildTaskIdentifier) {
throw new TaskError(Severity.Info, nls.localize('TaskRunnerSystem.noBuildTask', 'No task is marked as a build task in the tasks.json. Mark a task with \'isBuildCommand\'.'), TaskErrors.NoBuildTask);
}
return this.executeTask(this.defaultBuildTaskIdentifier, Triggers.shortcut);
return this.executeTask(buildTaskIdentifier, Triggers.shortcut);
}
public rebuild(): ITaskExecuteResult {
......@@ -107,14 +95,15 @@ export class ProcessRunnerSystem extends EventEmitter implements ITaskSystem {
}
public runTest(): ITaskExecuteResult {
let testTaskIdentifier = this.configuration.testTasks.length > 0 ? this.configuration.testTasks[0] : undefined;
if (this.activeTaskIdentifier) {
let task = this.configuration.tasks[this.activeTaskIdentifier];
return { kind: TaskExecuteKind.Active, active: { same: this.activeTaskIdentifier === this.defaultTestTaskIdentifier, background: task.isBackground }, promise: this.activeTaskPromise };
return { kind: TaskExecuteKind.Active, active: { same: this.activeTaskIdentifier === testTaskIdentifier, background: task.isBackground }, promise: this.activeTaskPromise };
}
if (!this.defaultTestTaskIdentifier) {
if (!testTaskIdentifier) {
throw new TaskError(Severity.Info, nls.localize('TaskRunnerSystem.noTestTask', 'No test task configured.'), TaskErrors.NoTestTask);
}
return this.executeTask(this.defaultTestTaskIdentifier, Triggers.shortcut);
return this.executeTask(testTaskIdentifier, Triggers.shortcut);
}
public run(taskIdentifier: string): ITaskExecuteResult {
......@@ -164,9 +153,6 @@ export class ProcessRunnerSystem extends EventEmitter implements ITaskSystem {
}
private executeTask(taskIdentifier: string, trigger: string = Triggers.command): ITaskExecuteResult {
if (this.validationStatus.isFatal()) {
throw new TaskError(Severity.Error, nls.localize('TaskRunnerSystem.fatalError', 'The provided task configuration has validation errors. See tasks output log for details.'), TaskErrors.ConfigValidationError);
}
let task = this.configuration.tasks[taskIdentifier];
if (!task) {
throw new TaskError(Severity.Info, nls.localize('TaskRunnerSystem.norebuild', 'No task to execute found.'), TaskErrors.TaskNotFound);
......@@ -205,19 +191,19 @@ export class ProcessRunnerSystem extends EventEmitter implements ITaskSystem {
private doExecuteTask(task: TaskDescription, telemetryEvent: TelemetryEvent): ITaskExecuteResult {
let taskSummary: ITaskSummary = {};
let configuration = this.configuration;
if (!this.validationStatus.isOK() && !this.errorsShown) {
let commandConfig: CommandConfiguration = task.command;
if (!this.errorsShown) {
this.showOutput();
this.errorsShown = true;
} else {
this.clearOutput();
}
let args: string[] = this.configuration.args ? this.configuration.args.slice() : [];
let args: string[] = commandConfig.args ? commandConfig.args.slice() : [];
// We need to first pass the task name
if (!task.suppressTaskName) {
if (this.fileConfig.taskSelector) {
args.push(this.fileConfig.taskSelector + task.name);
if (commandConfig.taskSelector) {
args.push(commandConfig.taskSelector + task.name);
} else {
args.push(task.name);
}
......@@ -227,15 +213,15 @@ export class ProcessRunnerSystem extends EventEmitter implements ITaskSystem {
args = args.concat(task.args);
}
args = this.resolveVariables(args);
let command: string = this.resolveVariable(configuration.command);
this.childProcess = new LineProcess(command, args, configuration.isShellCommand, this.resolveOptions(configuration.options));
let command: string = this.resolveVariable(commandConfig.name);
this.childProcess = new LineProcess(command, args, commandConfig.isShellCommand, this.resolveOptions(commandConfig.options));
telemetryEvent.command = this.childProcess.getSanitizedCommand();
// we have no problem matchers defined. So show the output log
if (task.showOutput === ShowOutput.Always || (task.showOutput === ShowOutput.Silent && task.problemMatchers.length === 0)) {
this.showOutput();
}
if (task.echoCommand) {
if (commandConfig.echo) {
let prompt: string = Platform.isWindows ? '>' : '$';
this.log(`running command${prompt} ${command} ${args.join(' ')}`);
}
......@@ -339,8 +325,8 @@ export class ProcessRunnerSystem extends EventEmitter implements ITaskSystem {
private handleError(task: TaskDescription, error: ErrorData): Promise {
let makeVisible = false;
if (error.error && !error.terminated) {
let args: string = this.configuration.args ? this.configuration.args.join(' ') : '';
this.log(nls.localize('TaskRunnerSystem.childProcessError', 'Failed to launch external program {0} {1}.', this.configuration.command, args));
let args: string = task.command.args ? task.command.args.join(' ') : '';
this.log(nls.localize('TaskRunnerSystem.childProcessError', 'Failed to launch external program {0} {1}.', task.command.name, args));
this.outputChannel.append(error.error.message);
makeVisible = true;
}
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册