-
Notifications
You must be signed in to change notification settings - Fork 438
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
Implement "Debug Locally" code lens for .NET Core. #552
Changes from all commits
11a58b6
a674976
e2ae962
1ab0726
436ad33
63e3b98
d0c048a
a7fb3f6
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -5,10 +5,14 @@ | |
|
||
'use strict' | ||
|
||
import * as os from 'os' | ||
import * as vscode from 'vscode' | ||
import { DRIVE_LETTER_REGEX } from '../../shared/codelens/codeLensUtils' | ||
|
||
const DOTNET_CORE_DEBUGGER_PATH = '/tmp/lambci_debug_files/vsdbg' | ||
|
||
export interface DebugConfiguration extends vscode.DebugConfiguration { | ||
readonly type: 'node' | 'python' | ||
readonly type: 'node' | 'python' | 'coreclr' | ||
readonly request: 'attach' | ||
} | ||
|
||
|
@@ -33,3 +37,65 @@ export interface PythonDebugConfiguration extends DebugConfiguration { | |
} | ||
] | ||
} | ||
|
||
export interface DotNetCoreDebugConfiguration extends DebugConfiguration { | ||
type: 'coreclr' | ||
processId: string | ||
pipeTransport: PipeTransport | ||
windows: { | ||
pipeTransport: PipeTransport | ||
} | ||
sourceFileMap: { | ||
[key: string]: string | ||
} | ||
} | ||
|
||
export interface PipeTransport { | ||
pipeProgram: 'sh' | 'powershell' | ||
pipeArgs: string[] | ||
debuggerPath: typeof DOTNET_CORE_DEBUGGER_PATH, | ||
pipeCwd: string | ||
} | ||
|
||
export interface MakeCoreCLRDebugConfigurationArguments { | ||
port: number, | ||
codeUri: string | ||
} | ||
|
||
export function makeCoreCLRDebugConfiguration( | ||
{ codeUri, port }: MakeCoreCLRDebugConfigurationArguments | ||
): DotNetCoreDebugConfiguration { | ||
const pipeArgs = [ | ||
'-c', | ||
`docker exec -i $(docker ps -q -f publish=${port}) \${debuggerCommand}` | ||
] | ||
|
||
if (os.platform() === 'win32') { | ||
// Coerce drive letter to uppercase. While Windows is case-insensitive, sourceFileMap is case-sensitive. | ||
codeUri = codeUri.replace(DRIVE_LETTER_REGEX, match => match.toUpperCase()) | ||
} | ||
|
||
return { | ||
name: 'SamLocalDebug', | ||
type: 'coreclr', | ||
request: 'attach', | ||
processId: '1', | ||
pipeTransport: { | ||
pipeProgram: 'sh', | ||
pipeArgs, | ||
debuggerPath: DOTNET_CORE_DEBUGGER_PATH, | ||
pipeCwd: codeUri | ||
}, | ||
windows: { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is it necessary to have two separate There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is isn't necessary since this configuration isn't saved anywhere (i.e. will only run on the current platform). But I wanted to minimize differences between the configuration that would appear in |
||
pipeTransport: { | ||
pipeProgram: 'powershell', | ||
pipeArgs, | ||
debuggerPath: DOTNET_CORE_DEBUGGER_PATH, | ||
pipeCwd: codeUri | ||
} | ||
}, | ||
sourceFileMap: { | ||
['/var/task']: codeUri | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,119 @@ | ||
/*! | ||
* Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. | ||
* SPDX-License-Identifier: Apache-2.0 | ||
*/ | ||
|
||
'use strict' | ||
|
||
import * as child_process from 'child_process' | ||
import * as crossSpawn from 'cross-spawn' | ||
|
||
export interface DockerClient { | ||
invoke(args: DockerInvokeArguments): Promise<void> | ||
} | ||
|
||
export interface DockerInvokeArguments { | ||
command: 'run' | ||
image: string | ||
removeOnExit?: boolean | ||
mount?: { | ||
type: 'bind', | ||
source: string, | ||
destination: string | ||
} | ||
entryPoint?: { | ||
command: string | ||
args: string[] | ||
} | ||
} | ||
|
||
export interface Closeable { | ||
onClose(callback: (code: number, signal: string, args?: string[]) => void): void | ||
} | ||
|
||
class CloseableChildProcess implements Closeable { | ||
public constructor( | ||
private readonly process: child_process.ChildProcess, | ||
private readonly args?: string[] | ||
) { } | ||
|
||
public onClose(callback: (code: number, signal: string, args?: string[]) => void): void { | ||
this.process.once('close', (_code, _signal) => callback(_code, _signal, this.args)) | ||
} | ||
} | ||
|
||
export interface DockerInvokeContext { | ||
spawn( | ||
command: string, | ||
args?: string[], | ||
options?: child_process.SpawnOptions | ||
): Closeable | ||
} | ||
|
||
// TODO: Replace with a library such as https://www.npmjs.com/package/node-docker-api. | ||
class DefaultDockerInvokeContext implements DockerInvokeContext { | ||
public spawn(command: string, args?: string[], options?: child_process.SpawnOptions): Closeable { | ||
const process = crossSpawn('docker', args, { windowsVerbatimArguments: true }) | ||
|
||
return new CloseableChildProcess(process, args) | ||
} | ||
} | ||
|
||
export class DefaultDockerClient implements DockerClient { | ||
|
||
public constructor(private readonly context: DockerInvokeContext = new DefaultDockerInvokeContext()) { } | ||
|
||
public async invoke({ | ||
command, | ||
image, | ||
removeOnExit, | ||
mount, | ||
entryPoint | ||
}: DockerInvokeArguments): Promise<void> { | ||
const args: string[] = [ command ] | ||
|
||
if (removeOnExit) { | ||
args.push('--rm') | ||
} | ||
|
||
if (mount) { | ||
args.push('--mount', `type=${mount.type},src=${mount.source},dst=${mount.destination}`) | ||
} | ||
|
||
if (entryPoint) { | ||
args.push('--entrypoint', entryPoint.command) | ||
} | ||
|
||
args.push(image) | ||
|
||
if (entryPoint) { | ||
args.push(...entryPoint.args) | ||
} | ||
|
||
const process = this.context.spawn( | ||
'docker', | ||
args, | ||
{ windowsVerbatimArguments: true } | ||
) | ||
|
||
await new Promise<void>((resolve, reject) => { | ||
process.onClose((code, signal) => { | ||
if (code === 0) { | ||
resolve() | ||
} else { | ||
reject(new DockerError(code, signal, args)) | ||
} | ||
}) | ||
}) | ||
} | ||
} | ||
|
||
export class DockerError extends Error { | ||
public constructor( | ||
public readonly code: number, | ||
public readonly signal: string, | ||
args: string[] | ||
) { | ||
super(`Could not invoke docker. Code: ${code}, Signal: ${signal}, Arguments: [${args.join(', ')}]`) | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is processId necessary? This seems like a magic value
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is taken directly from the SAM CLI docs. Checking with SAM CLI folks to determine if it is necessary...