diff --git a/Extension/package.json b/Extension/package.json index 6903c47085..99993d5b1b 100644 --- a/Extension/package.json +++ b/Extension/package.json @@ -2333,7 +2333,7 @@ "markdownDescription": "%c_cpp.configuration.simplifyStructuredComments.markdownDescription%", "scope": "application" }, - "C_Cpp.doxygen.generateOnType":{ + "C_Cpp.doxygen.generateOnType": { "type": "boolean", "default": true, "description": "%c_cpp.configuration.doxygen.generateOnType.description%", @@ -2341,13 +2341,13 @@ }, "C_Cpp.doxygen.generatedStyle": { "type": "string", - "enum":[ + "enum": [ "///", "/**", "/*!", "//!" ], - "default":"///", + "default": "///", "description": "%c_cpp.configuration.doxygen.generatedStyle.description%", "scope": "resource" }, @@ -2897,7 +2897,7 @@ "icon": "$(debug-configure)" }, { - "command": "C_Cpp.GenerateDoxygenComment", + "command": "C_Cpp.GenerateDoxygenComment", "title": "%c_cpp.command.GenerateDoxygenComment.title%", "category": "C/C++" } @@ -3355,7 +3355,7 @@ "anyOf": [ { "type": "object", - "description": "%c_cpp.debuggers.deploySteps.scp.description%", + "description": "%c_cpp.debuggers.deploySteps.copyFile.description%", "default": {}, "required": [ "type", @@ -3366,10 +3366,11 @@ "properties": { "type": { "type": "string", - "description": "%c_cpp.debuggers.deploySteps.scp.description%", + "description": "%c_cpp.debuggers.deploySteps.copyFile.description%", "default": "", "enum": [ - "scp" + "scp", + "rsync" ] }, "files": { @@ -3384,7 +3385,7 @@ } } ], - "description": "%c_cpp.debuggers.deploySteps.scp.files.description%", + "description": "%c_cpp.debuggers.deploySteps.copyFile.files.description%", "default": "" }, "host": { @@ -3512,19 +3513,57 @@ }, "targetDir": { "type": "string", - "description": "%c_cpp.debuggers.deploySteps.scp.targetDir.description%", + "description": "%c_cpp.debuggers.deploySteps.copyFile.targetDir.description%", "default": "" }, - "scpPath": { - "type": "string", - "description": "%c_cpp.debuggers.deploySteps.scp.scpPath.description%", - "default": "" + "recursive": { + "type": "boolean", + "description": "%c_cpp.debuggers.deploySteps.copyFile.recursive.description%", + "default": "true" }, "debug": { "type": "boolean", "description": "%c_cpp.debuggers.deploySteps.debug%" } - } + }, + "allOf": [ + { + "if": { + "properties": { + "type": { + "const": "scp" + } + } + }, + "then": { + "properties": { + "scpPath": { + "type": "string", + "description": "%c_cpp.debuggers.deploySteps.copyFile.scpPath.description%", + "default": "" + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "rsync" + } + } + }, + "then": { + "properties": { + "rsyncPath": { + "type": "string", + "description": "%c_cpp.debuggers.deploySteps.copyFile.rsyncPath.description%", + "default": "" + } + } + } + } + ] }, { "type": "object", @@ -4034,7 +4073,7 @@ "anyOf": [ { "type": "object", - "description": "%c_cpp.debuggers.deploySteps.scp.description%", + "description": "%c_cpp.debuggers.deploySteps.copyFile.description%", "default": {}, "required": [ "type", @@ -4045,10 +4084,11 @@ "properties": { "type": { "type": "string", - "description": "%c_cpp.debuggers.deploySteps.scp.description%", + "description": "%c_cpp.debuggers.deploySteps.copyFile.description%", "default": "", "enum": [ - "scp" + "scp", + "rsync" ] }, "files": { @@ -4063,7 +4103,7 @@ } } ], - "description": "%c_cpp.debuggers.deploySteps.scp.files.description%", + "description": "%c_cpp.debuggers.deploySteps.copyFile.files.description%", "default": "" }, "host": { @@ -4191,19 +4231,57 @@ }, "targetDir": { "type": "string", - "description": "%c_cpp.debuggers.deploySteps.scp.targetDir.description%", + "description": "%c_cpp.debuggers.deploySteps.copyFile.targetDir.description%", "default": "" }, - "scpPath": { - "type": "string", - "description": "%c_cpp.debuggers.deploySteps.scp.scpPath.description%", - "default": "" + "recursive": { + "type": "boolean", + "description": "%c_cpp.debuggers.deploySteps.copyFile.recursive.description%", + "default": "true" }, "debug": { "type": "boolean", "description": "%c_cpp.debuggers.deploySteps.debug%" } - } + }, + "allOf": [ + { + "if": { + "properties": { + "type": { + "const": "scp" + } + } + }, + "then": { + "properties": { + "scpPath": { + "type": "string", + "description": "%c_cpp.debuggers.deploySteps.copyFile.scpPath.description%", + "default": "" + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "rsync" + } + } + }, + "then": { + "properties": { + "rsyncPath": { + "type": "string", + "description": "%c_cpp.debuggers.deploySteps.copyFile.rsyncPath.description%", + "default": "" + } + } + } + } + ] }, { "type": "object", diff --git a/Extension/package.nls.json b/Extension/package.nls.json index 65fca2d92d..06fb48615c 100644 --- a/Extension/package.nls.json +++ b/Extension/package.nls.json @@ -321,10 +321,12 @@ "c_cpp.debuggers.host.localForward.localSocket.description": "Local socket", "c_cpp.debuggers.host.localForward.remoteSocket.description": "Remote socket", "c_cpp.debuggers.deploySteps.description": "Steps needed to deploy the application. Order matters.", - "c_cpp.debuggers.deploySteps.scp.description": "Copy files using SCP.", - "c_cpp.debuggers.deploySteps.scp.files.description": "Files to be copied via SCP. Supports path pattern.", - "c_cpp.debuggers.deploySteps.scp.targetDir.description": "Target directory.", - "c_cpp.debuggers.deploySteps.scp.scpPath.description": "Optional full path to SCP. Assumes SCP is on PATH if not specified", + "c_cpp.debuggers.deploySteps.copyFile.description": "Copy files using SCP or rsync.", + "c_cpp.debuggers.deploySteps.copyFile.files.description": "Files to be copied. Supports path pattern.", + "c_cpp.debuggers.deploySteps.copyFile.targetDir.description": "Target directory.", + "c_cpp.debuggers.deploySteps.copyFile.recursive.description": "If true, copies folders recursively.", + "c_cpp.debuggers.deploySteps.copyFile.scpPath.description": "Optional full path to SCP. Assumes SCP is on PATH if not specified", + "c_cpp.debuggers.deploySteps.copyFile.rsyncPath.description": "Optional full path to rsync. Assumes rsync is on PATH if not specified", "c_cpp.debuggers.deploySteps.debug": "If true, skip when starting without debugging. If false, skip when starting debugging. If undefined, never skip.", "c_cpp.debuggers.deploySteps.ssh.description": "SSH command step.", "c_cpp.debuggers.deploySteps.ssh.command.description": "Command to be executed via SSH. The command after '-c' in SSH command.", diff --git a/Extension/src/Debugger/configurationProvider.ts b/Extension/src/Debugger/configurationProvider.ts index bffa461215..8917b5de4d 100644 --- a/Extension/src/Debugger/configurationProvider.ts +++ b/Extension/src/Debugger/configurationProvider.ts @@ -24,7 +24,7 @@ import { Environment, ParsedEnvironmentFile } from './ParsedEnvironmentFile'; import { CppSettings, OtherSettings } from '../LanguageServer/settings'; import { configPrefix } from '../LanguageServer/extension'; import { expandAllStrings, ExpansionOptions, ExpansionVars } from '../expand'; -import { scp, ssh } from '../SSH/commands'; +import { rsync, scp, ssh } from '../SSH/commands'; import * as glob from 'glob'; import { promisify } from 'util'; import { AttachItemsProvider, AttachPicker, RemoteAttachPicker } from './attachToProcess'; @@ -35,6 +35,7 @@ const localize: nls.LocalizeFunc = nls.loadMessageBundle(); enum StepType { scp = 'scp', + rsync = 'rsync', ssh = 'ssh', shell = 'shell', remoteShell = 'remoteShell', @@ -1011,7 +1012,8 @@ export class DebugConfigurationProvider implements vscode.DebugConfigurationProv // Skip steps that doesn't match current launch mode. Explicit true/false check, since a step is always run when debug is undefined. return true; } - switch (step.type) { + const stepType: StepType = step.type; + switch (stepType) { case StepType.command: { // VS Code commands are the same regardless of which extension invokes them, so just invoke them here. if (step.args && !Array.isArray(step.args)) { @@ -1021,9 +1023,11 @@ export class DebugConfigurationProvider implements vscode.DebugConfigurationProv const returnCode: unknown = await vscode.commands.executeCommand(step.command, ...step.args); return !returnCode; } - case StepType.scp: { + case StepType.scp: + case StepType.rsync: { + const isScp: boolean = stepType === StepType.scp; if (!step.files || !step.targetDir || !step.host) { - logger.getOutputChannelLogger().showErrorMessage(localize('missing.properties.scp', '"host", "files", and "targetDir" are required in scp steps.')); + logger.getOutputChannelLogger().showErrorMessage(localize('missing.properties.copyFile', '"host", "files", and "targetDir" are required in {0} steps.', isScp ? 'SCP' : 'rsync')); return false; } const host: util.ISshHostInfo = { hostName: step.host.hostName, user: step.host.user, port: step.host.port }; @@ -1036,10 +1040,17 @@ export class DebugConfigurationProvider implements vscode.DebugConfigurationProv files = files.concat((await globAsync(fileGlob)).map(file => vscode.Uri.file(file))); } } else { - logger.getOutputChannelLogger().showErrorMessage(localize('incorrect.files.type.scp', '"files" must be a string or an array of strings in scp steps.')); + logger.getOutputChannelLogger().showErrorMessage(localize('incorrect.files.type.copyFile', '"files" must be a string or an array of strings in {0} steps.', isScp ? 'SCP' : 'rsync')); return false; } - const scpResult: util.ProcessReturnType = await scp(files, host, step.targetDir, config.scpPath, jumpHosts, cancellationToken); + + let scpResult: util.ProcessReturnType; + if (isScp) { + scpResult = await scp(files, host, step.targetDir, config.scpPath, config.recursive, jumpHosts, cancellationToken); + } else { + scpResult = await rsync(files, host, step.targetDir, config.scpPath, config.recursive, jumpHosts, cancellationToken); + } + if (!scpResult.succeeded || cancellationToken?.isCancellationRequested) { return false; } diff --git a/Extension/src/SSH/commands.ts b/Extension/src/SSH/commands.ts index 08378704e1..1d11aae167 100644 --- a/Extension/src/SSH/commands.ts +++ b/Extension/src/SSH/commands.ts @@ -12,8 +12,11 @@ import { runSshTerminalCommandWithLogin } from './sshCommandRunner'; nls.config({ messageFormat: nls.MessageFormat.bundle, bundleFormat: nls.BundleFormat.standalone })(); const localize: nls.LocalizeFunc = nls.loadMessageBundle(); -export async function scp(files: vscode.Uri[], host: ISshHostInfo, targetDir: string, scpPath?: string, jumpHosts?: ISshHostInfo[], cancellationToken?: vscode.CancellationToken): Promise { +export async function scp(files: vscode.Uri[], host: ISshHostInfo, targetDir: string, recursive: boolean = true, scpPath?: string, jumpHosts?: ISshHostInfo[], cancellationToken?: vscode.CancellationToken): Promise { const args: string[] = []; + if (recursive) { + args.push('-r'); + } if (jumpHosts && jumpHosts.length > 0) { args.push('-J', jumpHosts.map(getFullHostAddress).join(',')); } @@ -26,6 +29,30 @@ export async function scp(files: vscode.Uri[], host: ISshHostInfo, targetDir: st return runSshTerminalCommandWithLogin(host, { systemInteractor: defaultSystemInteractor, nickname: 'scp', command: `"${scpPath || 'scp'}" ${args.join(' ')}`, token: cancellationToken }); } +// Recursive is less important in rsync thank in SCP. SCP under recursive mode always follows symlinks, and potentially causes problems. +// In rsync, there are options to avoid this issue (-l, -K). To mitigate confusion, we still provide a recursive option here like in SCP. +export async function rsync(files: vscode.Uri[], host: ISshHostInfo, targetDir: string, recursive: boolean = true, rsyncPath?: string, jumpHosts?: ISshHostInfo[], cancellationToken?: vscode.CancellationToken): Promise { + // --links, -l When symlinks are encountered, recreate the symlink on the destination. + // --keep-dirlinks, -K Treat symlinked dir on receiver as dir. + // --perms, -p Keep permissions. + // --verbose, -v Verbose. + // --compress, -z Compress file data during the transfer. + const args: string[] = ['-lKpvz']; + if (recursive) { + args.push('-r'); + } + if (jumpHosts && jumpHosts.length > 0) { + args.push('-e', `ssh -J ${jumpHosts.map(getFullHostAddress).join(',')}`); + } + if (host.port) { + // upper case P + args.push(`--port=${host.port}`); + } + args.push(files.map(uri => `"${uri.fsPath}"`).join(' '), `${getFullHostAddressNoPort(host)}:${targetDir}`); + + return runSshTerminalCommandWithLogin(host, { systemInteractor: defaultSystemInteractor, nickname: 'rsync', command: `"${rsyncPath || 'rsync'}" ${args.join(' ')}`, token: cancellationToken }); +} + export function ssh(host: ISshHostInfo, command: string, sshPath?: string, jumpHosts?: ISshHostInfo[], localForwards?: ISshLocalForwardInfo[], continueOn?: string, cancellationToken?: vscode.CancellationToken): Promise { const args: string[] = []; if (jumpHosts && jumpHosts.length > 0) { diff --git a/Extension/tools/OptionsSchema.json b/Extension/tools/OptionsSchema.json index 204d397a5f..3e94a44cbe 100644 --- a/Extension/tools/OptionsSchema.json +++ b/Extension/tools/OptionsSchema.json @@ -1,6 +1,6 @@ { "_comment": "See README.md for information about this file", - "$schema": "http://json-schema.org/draft-04/schema#", + "$schema": "http://json-schema.org/draft-07/schema#", "title": "VS Code launch/attach options", "description": "A json schema for the VS Code attach and launch options", "type": "object", @@ -367,7 +367,7 @@ "anyOf": [ { "type": "object", - "description": "%c_cpp.debuggers.deploySteps.scp.description%", + "description": "%c_cpp.debuggers.deploySteps.copyFile.description%", "default": {}, "required": [ "type", @@ -378,10 +378,11 @@ "properties": { "type": { "type": "string", - "description": "%c_cpp.debuggers.deploySteps.scp.description%", + "description": "%c_cpp.debuggers.deploySteps.copyFile.description%", "default": "", "enum": [ - "scp" + "scp", + "rsync" ] }, "files": { @@ -396,7 +397,7 @@ } } ], - "description": "%c_cpp.debuggers.deploySteps.scp.files.description%", + "description": "%c_cpp.debuggers.deploySteps.copyFile.files.description%", "default": "" }, "host": { @@ -404,19 +405,49 @@ }, "targetDir": { "type": "string", - "description": "%c_cpp.debuggers.deploySteps.scp.targetDir.description%", + "description": "%c_cpp.debuggers.deploySteps.copyFile.targetDir.description%", "default": "" }, - "scpPath": { - "type": "string", - "description": "%c_cpp.debuggers.deploySteps.scp.scpPath.description%", - "default": "" + "recursive": { + "type": "boolean", + "description": "%c_cpp.debuggers.deploySteps.copyFile.recursive.description%", + "default": "true" }, "debug": { "type": "boolean", "description": "%c_cpp.debuggers.deploySteps.debug%" } - } + }, + "allOf": [ + { + "if": { + "properties": { "type": { "const": "scp" } } + }, + "then": { + "properties": { + "scpPath": { + "type": "string", + "description": "%c_cpp.debuggers.deploySteps.copyFile.scpPath.description%", + "default": "" + } + } + } + }, + { + "if": { + "properties": { "type": { "const": "rsync" } } + }, + "then": { + "properties": { + "rsyncPath": { + "type": "string", + "description": "%c_cpp.debuggers.deploySteps.copyFile.rsyncPath.description%", + "default": "" + } + } + } + } + ] }, { "type": "object",