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

Implement "Debug Locally" code lens for .NET Core. #552

Merged
merged 8 commits into from
May 28, 2019
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 67 additions & 1 deletion src/lambda/local/debugConfiguration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
}

Expand All @@ -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',
Copy link
Contributor

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

Copy link
Contributor Author

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...

pipeTransport: {
pipeProgram: 'sh',
pipeArgs,
debuggerPath: DOTNET_CORE_DEBUGGER_PATH,
pipeCwd: codeUri
},
windows: {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it necessary to have two separate pipeTransports? You've already determined the OS above, should that define what pipeTransport is added to this JSON?

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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 launch.json (see the SAM CLI docs) and this configuration.

pipeTransport: {
pipeProgram: 'powershell',
pipeArgs,
debuggerPath: DOTNET_CORE_DEBUGGER_PATH,
pipeCwd: codeUri
}
},
sourceFileMap: {
['/var/task']: codeUri
}
}
}
119 changes: 119 additions & 0 deletions src/shared/clients/dockerClient.ts
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(', ')}]`)
}
}
2 changes: 2 additions & 0 deletions src/shared/codelens/codeLensUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ interface MakeConfigureCodeLensParams {
language: Language
}

export const DRIVE_LETTER_REGEX = /^\w\:/

export async function makeCodeLenses({ document, token, handlers, language }: {
document: vscode.TextDocument,
token: vscode.CancellationToken,
Expand Down
Loading