Skip to content

Commit a6b49d2

Browse files
authored
Fix issues when running without debugging and debugged code terminates (#249)
- Fixes #25, #32, #35, #235, #242 (unable to run without debugging using CTRL+F5) - Fixes #191, #158, #24, #136 (error message displayed when debugged code terminates) - Fixes #157 (debugger crashes when python program does not launch) - Fixes #114, #149, #250 (use vscode infrastructure to launch debugger in integrated and external terminals) - Fixes #239 Remove prompt added to pause terminal upon program completion
1 parent 7372367 commit a6b49d2

File tree

18 files changed

+2121
-2079
lines changed

18 files changed

+2121
-2079
lines changed

package.json

Lines changed: 1572 additions & 1628 deletions
Large diffs are not rendered by default.
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
# Copyright (c) Microsoft Corporation. All rights reserved.
2+
# Licensed under the MIT License.
3+
4+
"""Run a block of code or Python file."""
5+
6+
import sys
7+
import os.path
8+
import traceback
9+
import time
10+
import socket
11+
try:
12+
import visualstudio_py_util as _vspu
13+
except:
14+
traceback.print_exc()
15+
print("""Internal error detected. Please copy the above traceback and report at
16+
https://github.com/Microsoft/vscode-python/issues""")
17+
sys.exit(1)
18+
19+
LAST = _vspu.to_bytes('LAST')
20+
OUTP = _vspu.to_bytes('OUTP')
21+
LOAD = _vspu.to_bytes('LOAD')
22+
23+
def parse_argv():
24+
"""Parses arguments for use with the launcher.
25+
Arguments are:
26+
1. Working directory.
27+
2. VS debugger port to connect to.
28+
3. GUID for the debug session.
29+
4. Debug options (not used).
30+
5. '-m' or '-c' to override the default run-as mode. [optional].
31+
6. Startup script name.
32+
7. Script arguments.
33+
"""
34+
35+
# Change to directory we expected to start from.
36+
os.chdir(sys.argv[1])
37+
38+
port_num = int(sys.argv[2])
39+
debug_id = sys.argv[3]
40+
41+
del sys.argv[:5]
42+
43+
# Set run_as mode appropriately
44+
run_as = 'script'
45+
if sys.argv and sys.argv[0] == '-m':
46+
run_as = 'module'
47+
del sys.argv[0]
48+
elif sys.argv and sys.argv[0] == '-c':
49+
run_as = 'code'
50+
del sys.argv[0]
51+
52+
# Preserve filename before we del sys.
53+
filename = sys.argv[0]
54+
55+
# Fix sys.path to be the script file dir.
56+
sys.path[0] = ''
57+
58+
pid = os.getpid()
59+
60+
return (filename, port_num, debug_id, pid, run_as)
61+
62+
def run(file, port_num, debug_id, pid, run_as='script'):
63+
attach_process(port_num, pid, debug_id)
64+
65+
# Now execute main file.
66+
globals_obj = {'__name__': '__main__'}
67+
68+
try:
69+
if run_as == 'module':
70+
_vspu.exec_module(file, globals_obj)
71+
elif run_as == 'code':
72+
_vspu.exec_code(file, '<string>', globals_obj)
73+
else:
74+
_vspu.exec_file(file, globals_obj)
75+
except:
76+
exc_type, exc_value, exc_tb = sys.exc_info()
77+
handle_exception(exc_type, exc_value, exc_tb)
78+
79+
_vspu.write_bytes(conn, LAST)
80+
# Wait for message to be received by debugger.
81+
time.sleep(0.5)
82+
83+
84+
def attach_process(port_num, pid, debug_id):
85+
global conn
86+
for i in xrange(50):
87+
try:
88+
conn = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
89+
conn.connect(('127.0.0.1', port_num))
90+
# Initial handshake.
91+
_vspu.write_string(conn, debug_id)
92+
_vspu.write_int(conn, 0)
93+
_vspu.write_int(conn, pid)
94+
95+
# Notify debugger that process has launched.
96+
_vspu.write_bytes(conn, LOAD)
97+
_vspu.write_int(conn, 0)
98+
break
99+
except:
100+
time.sleep(50./1000)
101+
else:
102+
raise Exception('failed to attach')
103+
104+
105+
def handle_exception(exc_type, exc_value, exc_tb):
106+
# Specifies list of files not to display in stack trace.
107+
do_not_debug = [__file__, _vspu.__file__]
108+
if sys.version_info >= (3, 3):
109+
do_not_debug.append('<frozen importlib._bootstrap>')
110+
if sys.version_info >= (3, 5):
111+
do_not_debug.append('<frozen importlib._bootstrap_external>')
112+
113+
# Remove debugger frames from the top and bottom of the traceback.
114+
tb = traceback.extract_tb(exc_tb)
115+
for i in [0, -1]:
116+
while tb:
117+
frame_file = path.normcase(tb[i][0])
118+
if not any(is_same_py_file(frame_file, f) for f in do_not_debug):
119+
break
120+
del tb[i]
121+
122+
# Print the traceback.
123+
if tb:
124+
sys.stderr.write('Traceback (most recent call last):')
125+
for out in traceback.format_list(tb):
126+
sys.stderr.write(out)
127+
sys.stderr.flush()
128+
129+
# Print the exception.
130+
for out in traceback.format_exception_only(exc_type, exc_value):
131+
sys.stderr.write(out)
132+
sys.stderr.flush()
133+
134+
135+
def is_same_py_file(file_1, file_2):
136+
"""Compares 2 filenames accounting for .pyc files."""
137+
if file_1.endswith('.pyc') or file_1.endswith('.pyo'):
138+
file_1 = file_1[:-1]
139+
if file_2.endswith('.pyc') or file_2.endswith('.pyo'):
140+
file_2 = file_2[:-1]
141+
142+
return file_1 == file_2
143+
144+
if __name__ == '__main__':
145+
filename, port_num, debug_id, pid, run_as = parse_argv()
146+
run(filename, port_num, debug_id, pid, run_as)

src/client/common/envFileParser.ts

Lines changed: 37 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import * as fs from 'fs';
22
import * as path from 'path';
33

4-
export function parseEnvFile(envFile: string): any {
4+
type EnvVars = Object & { [key: string]: string };
5+
6+
export function parseEnvFile(envFile: string, mergeWithProcessEnvVars: boolean = true): EnvVars {
57
const buffer = fs.readFileSync(envFile, 'utf8');
68
const env = {};
79
buffer.split('\n').forEach(line => {
@@ -14,28 +16,43 @@ export function parseEnvFile(envFile: string): any {
1416
env[r[1]] = value.replace(/(^['"]|['"]$)/g, '');
1517
}
1618
});
17-
return mergeEnvVariables(env);
19+
return mergeWithProcessEnvVars ? mergeEnvVariables(env, process.env) : mergePythonPath(env, process.env.PYTHONPATH);
1820
}
1921

20-
export function mergeEnvVariables(newVariables: { [key: string]: string }, mergeWith: any = process.env): any {
21-
for (let setting in mergeWith) {
22-
if (setting === 'PYTHONPATH') {
23-
let PYTHONPATH: string = newVariables['PYTHONPATH'];
24-
if (typeof PYTHONPATH !== 'string') {
25-
PYTHONPATH = '';
26-
}
27-
if (mergeWith['PYTHONPATH']) {
28-
PYTHONPATH += (PYTHONPATH.length > 0 ? path.delimiter : '') + mergeWith['PYTHONPATH'];
29-
}
30-
if (PYTHONPATH.length > 0) {
31-
newVariables[setting] = PYTHONPATH;
32-
}
33-
continue;
34-
}
35-
if (!newVariables[setting]) {
36-
newVariables[setting] = mergeWith[setting];
22+
/**
23+
* Merge the target environment variables into the source.
24+
* Note: The source variables are modified and returned (i.e. it modifies value passed in).
25+
* @export
26+
* @param {EnvVars} targetEnvVars target environment variables.
27+
* @param {EnvVars} [sourceEnvVars=process.env] source environment variables (defaults to current process variables).
28+
* @returns {EnvVars}
29+
*/
30+
export function mergeEnvVariables(targetEnvVars: EnvVars, sourceEnvVars: EnvVars = process.env): EnvVars {
31+
Object.keys(sourceEnvVars).forEach(setting => {
32+
if (targetEnvVars[setting] === undefined) {
33+
targetEnvVars[setting] = sourceEnvVars[setting];
3734
}
35+
});
36+
return mergePythonPath(targetEnvVars, sourceEnvVars.PYTHONPATH);
37+
}
38+
39+
/**
40+
* Merge the target PYTHONPATH value into the env variables passed.
41+
* Note: The env variables passed in are modified and returned (i.e. it modifies value passed in).
42+
* @export
43+
* @param {EnvVars} env target environment variables.
44+
* @param {string | undefined} [currentPythonPath] PYTHONPATH value.
45+
* @returns {EnvVars}
46+
*/
47+
export function mergePythonPath(env: EnvVars, currentPythonPath: string | undefined): EnvVars {
48+
if (typeof currentPythonPath !== 'string' || currentPythonPath.length === 0) {
49+
return env;
3850
}
3951

40-
return newVariables;
52+
if (typeof env.PYTHONPATH === 'string' && env.PYTHONPATH.length > 0) {
53+
env.PYTHONPATH = env.PYTHONPATH + path.delimiter + currentPythonPath;
54+
} else {
55+
env.PYTHONPATH = currentPythonPath;
56+
}
57+
return env;
4158
}

src/client/debugger/Common/Contracts.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
"use strict";
22
import * as net from "net";
3+
import { ChildProcess } from 'child_process';
34
import { DebugProtocol } from "vscode-debugprotocol";
45
import { OutputEvent } from "vscode-debugadapter";
56

@@ -49,7 +50,6 @@ export interface LaunchRequestArguments extends DebugProtocol.LaunchRequestArgum
4950
stopOnEntry?: boolean;
5051
args: string[];
5152
applicationType?: string;
52-
externalConsole?: boolean;
5353
cwd?: string;
5454
debugOptions?: string[];
5555
env?: Object;
@@ -103,8 +103,9 @@ export enum PythonEvaluationResultFlags {
103103
}
104104

105105
export interface IPythonProcess extends NodeJS.EventEmitter {
106-
Connect(buffer: Buffer, socket: net.Socket, isRemoteProcess: boolean);
106+
Connect(buffer: Buffer, socket: net.Socket, isRemoteProcess: boolean): boolean;
107107
HandleIncomingData(buffer: Buffer);
108+
attach(proc: ChildProcess): void;
108109
Detach();
109110
Kill();
110111
SendStepInto(threadId: number);

src/client/debugger/Common/Utils.ts

Lines changed: 34 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1-
"use strict";
1+
'use strict';
22

3-
import { IPythonProcess, IPythonThread, IPythonModule, IPythonEvaluationResult } from "./Contracts";
4-
import * as path from "path";
5-
import * as fs from 'fs';
63
import * as child_process from 'child_process';
7-
import { mergeEnvVariables, parseEnvFile } from '../../common/envFileParser';
4+
import * as fs from 'fs';
5+
import * as path from 'path';
86
import * as untildify from 'untildify';
7+
import { mergeEnvVariables, mergePythonPath, parseEnvFile } from '../../common/envFileParser';
8+
import { IPythonEvaluationResult, IPythonModule, IPythonProcess, IPythonThread } from './Contracts';
99

1010
export const IS_WINDOWS = /^win/.test(process.platform);
1111
export const PATH_VARIABLE_NAME = IS_WINDOWS ? 'Path' : 'PATH';
@@ -38,7 +38,7 @@ export function validatePathSync(filePath: string): boolean {
3838
return exists;
3939
}
4040

41-
export function CreatePythonThread(id: number, isWorker: boolean, process: IPythonProcess, name: string = ""): IPythonThread {
41+
export function CreatePythonThread(id: number, isWorker: boolean, process: IPythonProcess, name: string = ''): IPythonThread {
4242
return {
4343
IsWorkerThread: isWorker,
4444
Process: process,
@@ -50,15 +50,13 @@ export function CreatePythonThread(id: number, isWorker: boolean, process: IPyth
5050

5151
export function CreatePythonModule(id: number, fileName: string): IPythonModule {
5252
let name = fileName;
53-
if (typeof fileName === "string") {
53+
if (typeof fileName === 'string') {
5454
try {
5555
name = path.basename(fileName);
56-
}
57-
catch (ex) {
58-
}
59-
}
60-
else {
61-
name = "";
56+
// tslint:disable-next-line:no-empty
57+
} catch { }
58+
} else {
59+
name = '';
6260
}
6361

6462
return {
@@ -74,7 +72,7 @@ export function FixupEscapedUnicodeChars(value: string): string {
7472

7573
export function getPythonExecutable(pythonPath: string): string {
7674
pythonPath = untildify(pythonPath);
77-
// If only 'python'
75+
// If only 'python'.
7876
if (pythonPath === 'python' ||
7977
pythonPath.indexOf(path.sep) === -1 ||
8078
path.basename(pythonPath) === path.dirname(pythonPath)) {
@@ -84,21 +82,20 @@ export function getPythonExecutable(pythonPath: string): string {
8482
if (isValidPythonPath(pythonPath)) {
8583
return pythonPath;
8684
}
87-
// Keep python right on top, for backwards compatibility
85+
// Keep python right on top, for backwards compatibility.
8886
const KnownPythonExecutables = ['python', 'python4', 'python3.6', 'python3.5', 'python3', 'python2.7', 'python2'];
8987

9088
for (let executableName of KnownPythonExecutables) {
91-
// Suffix with 'python' for linux and 'osx', and 'python.exe' for 'windows'
89+
// Suffix with 'python' for linux and 'osx', and 'python.exe' for 'windows'.
9290
if (IS_WINDOWS) {
93-
executableName = executableName + '.exe';
91+
executableName = `${executableName}.exe`;
9492
if (isValidPythonPath(path.join(pythonPath, executableName))) {
9593
return path.join(pythonPath, executableName);
9694
}
9795
if (isValidPythonPath(path.join(pythonPath, 'scripts', executableName))) {
9896
return path.join(pythonPath, 'scripts', executableName);
9997
}
100-
}
101-
else {
98+
} else {
10299
if (isValidPythonPath(path.join(pythonPath, executableName))) {
103100
return path.join(pythonPath, executableName);
104101
}
@@ -113,39 +110,36 @@ export function getPythonExecutable(pythonPath: string): string {
113110

114111
function isValidPythonPath(pythonPath): boolean {
115112
try {
116-
let output = child_process.execFileSync(pythonPath, ['-c', 'print(1234)'], { encoding: 'utf8' });
113+
const output = child_process.execFileSync(pythonPath, ['-c', 'print(1234)'], { encoding: 'utf8' });
117114
return output.startsWith('1234');
118-
}
119-
catch (ex) {
115+
} catch {
120116
return false;
121117
}
122118
}
123119

120+
type EnvVars = Object & { [key: string]: string };
124121

125-
export function getCustomEnvVars(envVars: any, envFile: string): any {
126-
let envFileVars = null;
122+
export function getCustomEnvVars(envVars: Object, envFile: string, mergeWithProcessEnvVars: boolean = true): EnvVars {
123+
let envFileVars: EnvVars = null;
127124
if (typeof envFile === 'string' && envFile.length > 0 && fs.existsSync(envFile)) {
128125
try {
129-
envFileVars = parseEnvFile(envFile);
130-
}
131-
catch (ex) {
126+
envFileVars = parseEnvFile(envFile, mergeWithProcessEnvVars);
127+
} catch (ex) {
132128
console.error('Failed to load env file');
133129
console.error(ex);
134130
}
135131
}
136-
let configVars = null;
137-
if (envVars && Object.keys(envVars).length > 0 && envFileVars) {
138-
configVars = mergeEnvVariables(envVars, envFileVars);
139-
}
140-
if (envVars && Object.keys(envVars).length > 0) {
141-
configVars = envVars;
142-
}
143-
if (envFileVars) {
144-
configVars = envFileVars;
132+
if (envFileVars && Object.keys(envFileVars).length > 0) {
133+
if (!envVars || Object.keys(envVars).length === 0) {
134+
return envFileVars;
135+
} else {
136+
envVars = envVars || {};
137+
return mergeEnvVariables(envVars as EnvVars, envFileVars);
138+
}
145139
}
146-
if (configVars && typeof configVars === 'object' && Object.keys(configVars).length > 0) {
147-
return configVars;
140+
if (!envVars || Object.keys(envVars).length === 0) {
141+
return null;
148142
}
149143

150-
return null;
151-
}
144+
return mergePythonPath(envVars as EnvVars, process.env.PYTHONPATH);
145+
}

src/client/debugger/DebugClients/DebugClient.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ export enum DebugType {
1414
}
1515
export abstract class DebugClient extends EventEmitter {
1616
protected debugSession: DebugSession;
17-
constructor(args: any, debugSession: DebugSession) {
17+
constructor(protected args: any, debugSession: DebugSession) {
1818
super();
1919
this.debugSession = debugSession;
2020
}

0 commit comments

Comments
 (0)