Skip to content

Commit 0f0462c

Browse files
[vscode] Improve runInTerminal and support linux
Depends on D93874. runInTerminal was using --wait-for, but it was some problems because it uses process polling looking for a single instance of the debuggee: - it gets to know of the target late, which renders breakpoints in the main function almost impossible - polling might fail if there are already other processes with the same name - polling might also fail on some linux machine, as it's implemented with the ps command, and the ps command's args and output are not standard everywhere As a better way to implement this so that it works well on Darwin and Linux, I'm using now the following process: - lldb-vscode notices the runInTerminal, so it spawns lldb-vscode with a special flag --launch-target <target>. This flags tells lldb-vscode to wait to be attached and then it execs the target program. I'm using lldb-vscode itself to do this, because it makes finding the launcher program easier. Also no CMAKE INSTALL scripts are needed. - Besides this, the debugger creates a temporary FIFO file where the launcher program will write its pid to. That way the debugger will be sure of which program to attach. - Once attach happend, the debugger creates a second temporary file to notify the launcher program that it has been attached, so that it can then exec. I'm using this instead of using a signal or a similar mechanism because I don't want the launcher program to wait indefinitely to be attached in case the debugger crashed. That would pollute the process list with a lot of hanging processes. Instead, I'm setting a 20 seconds timeout (that's an overkill) and the launcher program seeks in intervals the second tepmorary file. Some notes: - I preferred not to use sockets because it requires a lot of code and I only need a pid. It would also require a lot of code when windows support is implemented. - I didn't add Windows support, as I don't have a windows machine, but adding support for it should be easy, as the FIFO file can be implemented with a named pipe, which is standard on Windows and works pretty much the same way. The existing test which didn't pass on Linux, now passes. Differential Revision: https://reviews.llvm.org/D93951
1 parent 71af5a1 commit 0f0462c

File tree

13 files changed

+779
-67
lines changed

13 files changed

+779
-67
lines changed

lldb/packages/Python/lldbsuite/test/tools/lldb-vscode/lldbvscode_testcase.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -282,7 +282,8 @@ def launch(self, program=None, args=None, cwd=None, env=None,
282282
trace=False, initCommands=None, preRunCommands=None,
283283
stopCommands=None, exitCommands=None, terminateCommands=None,
284284
sourcePath=None, debuggerRoot=None, launchCommands=None,
285-
sourceMap=None, disconnectAutomatically=True, runInTerminal=False):
285+
sourceMap=None, disconnectAutomatically=True, runInTerminal=False,
286+
expectFailure=False):
286287
'''Sending launch request to vscode
287288
'''
288289

@@ -317,14 +318,20 @@ def cleanup():
317318
debuggerRoot=debuggerRoot,
318319
launchCommands=launchCommands,
319320
sourceMap=sourceMap,
320-
runInTerminal=runInTerminal)
321+
runInTerminal=runInTerminal,
322+
expectFailure=expectFailure)
323+
324+
if expectFailure:
325+
return response
326+
321327
if not (response and response['success']):
322328
self.assertTrue(response['success'],
323329
'launch failed (%s)' % (response['message']))
324330
# We need to trigger a request_configurationDone after we've successfully
325331
# attached a runInTerminal process to finish initialization.
326332
if runInTerminal:
327333
self.vscode.request_configurationDone()
334+
return response
328335

329336

330337
def build_and_launch(self, program, args=None, cwd=None, env=None,
@@ -340,7 +347,7 @@ def build_and_launch(self, program, args=None, cwd=None, env=None,
340347
self.build_and_create_debug_adaptor()
341348
self.assertTrue(os.path.exists(program), 'executable must exist')
342349

343-
self.launch(program, args, cwd, env, stopOnEntry, disableASLR,
350+
return self.launch(program, args, cwd, env, stopOnEntry, disableASLR,
344351
disableSTDIO, shellExpandArguments, trace,
345352
initCommands, preRunCommands, stopCommands, exitCommands,
346353
terminateCommands, sourcePath, debuggerRoot, runInTerminal=runInTerminal)

lldb/packages/Python/lldbsuite/test/tools/lldb-vscode/vscode.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -612,7 +612,7 @@ def request_launch(self, program, args=None, cwd=None, env=None,
612612
stopCommands=None, exitCommands=None,
613613
terminateCommands=None ,sourcePath=None,
614614
debuggerRoot=None, launchCommands=None, sourceMap=None,
615-
runInTerminal=False):
615+
runInTerminal=False, expectFailure=False):
616616
args_dict = {
617617
'program': program
618618
}
@@ -660,9 +660,10 @@ def request_launch(self, program, args=None, cwd=None, env=None,
660660
}
661661
response = self.send_recv(command_dict)
662662

663-
# Wait for a 'process' and 'initialized' event in any order
664-
self.wait_for_event(filter=['process', 'initialized'])
665-
self.wait_for_event(filter=['process', 'initialized'])
663+
if not expectFailure:
664+
# Wait for a 'process' and 'initialized' event in any order
665+
self.wait_for_event(filter=['process', 'initialized'])
666+
self.wait_for_event(filter=['process', 'initialized'])
666667
return response
667668

668669
def request_next(self, threadId):

lldb/test/API/tools/lldb-vscode/runInTerminal/TestVSCode_runInTerminal.py

Lines changed: 111 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,22 +11,47 @@
1111
import lldbvscode_testcase
1212
import time
1313
import os
14+
import subprocess
15+
import shutil
16+
import json
17+
from threading import Thread
1418

1519

1620
class TestVSCode_runInTerminal(lldbvscode_testcase.VSCodeTestCaseBase):
1721

1822
mydir = TestBase.compute_mydir(__file__)
1923

20-
@skipUnlessDarwin
24+
def readPidMessage(self, fifo_file):
25+
with open(fifo_file, "r") as file:
26+
self.assertIn("pid", file.readline())
27+
28+
def sendDidAttachMessage(self, fifo_file):
29+
with open(fifo_file, "w") as file:
30+
file.write(json.dumps({"kind": "didAttach"}) + "\n")
31+
32+
def readErrorMessage(self, fifo_file):
33+
with open(fifo_file, "r") as file:
34+
return file.readline()
35+
36+
@skipIfWindows
2137
@skipIfRemote
2238
def test_runInTerminal(self):
2339
'''
2440
Tests the "runInTerminal" reverse request. It makes sure that the IDE can
2541
launch the inferior with the correct environment variables and arguments.
2642
'''
43+
if "debug" in str(os.environ["LLDBVSCODE_EXEC"]).lower():
44+
# We skip this test for debug builds because it takes too long parsing lldb's own
45+
# debug info. Release builds are fine.
46+
# Checking this environment variable seems to be a decent proxy for a quick
47+
# detection
48+
return
2749
program = self.getBuildArtifact("a.out")
2850
source = 'main.c'
29-
self.build_and_launch(program, stopOnEntry=True, runInTerminal=True, args=["foobar"], env=["FOO=bar"])
51+
self.build_and_launch(
52+
program, stopOnEntry=True, runInTerminal=True, args=["foobar"],
53+
env=["FOO=bar"])
54+
3055
breakpoint_line = line_number(source, '// breakpoint')
3156

3257
self.set_source_breakpoints(source, [breakpoint_line])
@@ -46,3 +71,87 @@ def test_runInTerminal(self):
4671
# We verify we were able to set the environment
4772
env = self.vscode.request_evaluate('foo')['body']['result']
4873
self.assertIn('bar', env)
74+
75+
@skipIfWindows
76+
@skipIfRemote
77+
def test_runInTerminalInvalidTarget(self):
78+
self.build_and_create_debug_adaptor()
79+
response = self.launch(
80+
"INVALIDPROGRAM", stopOnEntry=True, runInTerminal=True, args=["foobar"], env=["FOO=bar"], expectFailure=True)
81+
self.assertFalse(response['success'])
82+
self.assertIn("Could not create a target for a program 'INVALIDPROGRAM': unable to find executable",
83+
response['message'])
84+
85+
@skipIfWindows
86+
@skipIfRemote
87+
def test_missingArgInRunInTerminalLauncher(self):
88+
proc = subprocess.run([self.lldbVSCodeExec, "--launch-target", "INVALIDPROGRAM"],
89+
capture_output=True, universal_newlines=True)
90+
self.assertTrue(proc.returncode != 0)
91+
self.assertIn('"--launch-target" requires "--comm-file" to be specified', proc.stderr)
92+
93+
@skipIfWindows
94+
@skipIfRemote
95+
def test_FakeAttachedRunInTerminalLauncherWithInvalidProgram(self):
96+
comm_file = os.path.join(self.getBuildDir(), "comm-file")
97+
os.mkfifo(comm_file)
98+
99+
proc = subprocess.Popen(
100+
[self.lldbVSCodeExec, "--comm-file", comm_file, "--launch-target", "INVALIDPROGRAM"],
101+
universal_newlines=True, stderr=subprocess.PIPE)
102+
103+
self.readPidMessage(comm_file)
104+
self.sendDidAttachMessage(comm_file)
105+
self.assertIn("No such file or directory", self.readErrorMessage(comm_file))
106+
107+
_, stderr = proc.communicate()
108+
self.assertIn("No such file or directory", stderr)
109+
110+
@skipIfWindows
111+
@skipIfRemote
112+
def test_FakeAttachedRunInTerminalLauncherWithValidProgram(self):
113+
comm_file = os.path.join(self.getBuildDir(), "comm-file")
114+
os.mkfifo(comm_file)
115+
116+
proc = subprocess.Popen(
117+
[self.lldbVSCodeExec, "--comm-file", comm_file, "--launch-target", "echo", "foo"],
118+
universal_newlines=True, stdout=subprocess.PIPE)
119+
120+
self.readPidMessage(comm_file)
121+
self.sendDidAttachMessage(comm_file)
122+
123+
stdout, _ = proc.communicate()
124+
self.assertIn("foo", stdout)
125+
126+
@skipIfWindows
127+
@skipIfRemote
128+
def test_FakeAttachedRunInTerminalLauncherAndCheckEnvironment(self):
129+
comm_file = os.path.join(self.getBuildDir(), "comm-file")
130+
os.mkfifo(comm_file)
131+
132+
proc = subprocess.Popen(
133+
[self.lldbVSCodeExec, "--comm-file", comm_file, "--launch-target", "env"],
134+
universal_newlines=True, stdout=subprocess.PIPE,
135+
env={**os.environ, "FOO": "BAR"})
136+
137+
self.readPidMessage(comm_file)
138+
self.sendDidAttachMessage(comm_file)
139+
140+
stdout, _ = proc.communicate()
141+
self.assertIn("FOO=BAR", stdout)
142+
143+
@skipIfWindows
144+
@skipIfRemote
145+
def test_NonAttachedRunInTerminalLauncher(self):
146+
comm_file = os.path.join(self.getBuildDir(), "comm-file")
147+
os.mkfifo(comm_file)
148+
149+
proc = subprocess.Popen(
150+
[self.lldbVSCodeExec, "--comm-file", comm_file, "--launch-target", "echo", "foo"],
151+
universal_newlines=True, stderr=subprocess.PIPE,
152+
env={**os.environ, "LLDB_VSCODE_RIT_TIMEOUT_IN_MS": "1000"})
153+
154+
self.readPidMessage(comm_file)
155+
156+
_, stderr = proc.communicate()
157+
self.assertIn("Timed out trying to get messages from the debug adaptor", stderr)

lldb/tools/lldb-vscode/CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,12 @@ add_lldb_tool(lldb-vscode
2727
lldb-vscode.cpp
2828
BreakpointBase.cpp
2929
ExceptionBreakpoint.cpp
30+
FifoFiles.cpp
3031
FunctionBreakpoint.cpp
3132
IOStream.cpp
3233
JSONUtils.cpp
3334
LLDBUtils.cpp
35+
RunInTerminal.cpp
3436
SourceBreakpoint.cpp
3537
VSCode.cpp
3638

lldb/tools/lldb-vscode/FifoFiles.cpp

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
//===-- FifoFiles.cpp -------------------------------------------*- C++ -*-===//
2+
//
3+
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
4+
// See https://llvm.org/LICENSE.txt for license information.
5+
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
6+
//
7+
//===----------------------------------------------------------------------===//
8+
9+
#if !defined(WIN32)
10+
#include <sys/stat.h>
11+
#include <sys/types.h>
12+
#include <unistd.h>
13+
#endif
14+
15+
#include <chrono>
16+
#include <fstream>
17+
#include <future>
18+
#include <thread>
19+
20+
#include "llvm/Support/FileSystem.h"
21+
22+
#include "lldb/lldb-defines.h"
23+
24+
#include "FifoFiles.h"
25+
26+
using namespace llvm;
27+
28+
namespace lldb_vscode {
29+
30+
FifoFile::FifoFile(StringRef path) : m_path(path) {}
31+
32+
FifoFile::~FifoFile() {
33+
#if !defined(WIN32)
34+
unlink(m_path.c_str());
35+
#endif
36+
};
37+
38+
Expected<std::shared_ptr<FifoFile>> CreateFifoFile(StringRef path) {
39+
#if defined(WIN32)
40+
return createStringError(inconvertibleErrorCode(), "Unimplemented");
41+
#else
42+
if (int err = mkfifo(path.data(), 0600))
43+
return createStringError(std::error_code(err, std::generic_category()),
44+
"Couldn't create fifo file: %s", path.data());
45+
return std::make_shared<FifoFile>(path);
46+
#endif
47+
}
48+
49+
FifoFileIO::FifoFileIO(StringRef fifo_file, StringRef other_endpoint_name)
50+
: m_fifo_file(fifo_file), m_other_endpoint_name(other_endpoint_name) {}
51+
52+
Expected<json::Value> FifoFileIO::ReadJSON(std::chrono::milliseconds timeout) {
53+
// We use a pointer for this future, because otherwise its normal destructor
54+
// would wait for the getline to end, rendering the timeout useless.
55+
Optional<std::string> line;
56+
std::future<void> *future =
57+
new std::future<void>(std::async(std::launch::async, [&]() {
58+
std::ifstream reader(m_fifo_file, std::ifstream::in);
59+
std::string buffer;
60+
std::getline(reader, buffer);
61+
if (!buffer.empty())
62+
line = buffer;
63+
}));
64+
if (future->wait_for(timeout) == std::future_status::timeout ||
65+
!line.hasValue())
66+
return createStringError(inconvertibleErrorCode(),
67+
"Timed out trying to get messages from the " +
68+
m_other_endpoint_name);
69+
delete future;
70+
return json::parse(*line);
71+
}
72+
73+
Error FifoFileIO::SendJSON(const json::Value &json,
74+
std::chrono::milliseconds timeout) {
75+
bool done = false;
76+
std::future<void> *future =
77+
new std::future<void>(std::async(std::launch::async, [&]() {
78+
std::ofstream writer(m_fifo_file, std::ofstream::out);
79+
writer << JSONToString(json) << std::endl;
80+
done = true;
81+
}));
82+
if (future->wait_for(timeout) == std::future_status::timeout || !done) {
83+
return createStringError(inconvertibleErrorCode(),
84+
"Timed out trying to send messages to the " +
85+
m_other_endpoint_name);
86+
}
87+
delete future;
88+
return Error::success();
89+
}
90+
91+
} // namespace lldb_vscode

lldb/tools/lldb-vscode/FifoFiles.h

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
//===-- FifoFiles.h ---------------------------------------------*- C++ -*-===//
2+
//
3+
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
4+
// See https://llvm.org/LICENSE.txt for license information.
5+
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
6+
//
7+
//===----------------------------------------------------------------------===//
8+
9+
#ifndef LLDB_TOOLS_LLDB_VSCODE_FIFOFILES_H
10+
#define LLDB_TOOLS_LLDB_VSCODE_FIFOFILES_H
11+
12+
#include "llvm/Support/Error.h"
13+
14+
#include "JSONUtils.h"
15+
16+
namespace lldb_vscode {
17+
18+
/// Struct that controls the life of a fifo file in the filesystem.
19+
///
20+
/// The file is destroyed when the destructor is invoked.
21+
struct FifoFile {
22+
FifoFile(llvm::StringRef path);
23+
24+
~FifoFile();
25+
26+
std::string m_path;
27+
};
28+
29+
/// Create a fifo file in the filesystem.
30+
///
31+
/// \param[in] path
32+
/// The path for the fifo file.
33+
///
34+
/// \return
35+
/// A \a std::shared_ptr<FifoFile> if the file could be created, or an
36+
/// \a llvm::Error in case of failures.
37+
llvm::Expected<std::shared_ptr<FifoFile>> CreateFifoFile(llvm::StringRef path);
38+
39+
class FifoFileIO {
40+
public:
41+
/// \param[in] fifo_file
42+
/// The path to an input fifo file that exists in the file system.
43+
///
44+
/// \param[in] other_endpoint_name
45+
/// A human readable name for the other endpoint that will communicate
46+
/// using this file. This is used for error messages.
47+
FifoFileIO(llvm::StringRef fifo_file, llvm::StringRef other_endpoint_name);
48+
49+
/// Read the next JSON object from the underlying input fifo file.
50+
///
51+
/// The JSON object is expected to be a single line delimited with \a
52+
/// std::endl.
53+
///
54+
/// \return
55+
/// An \a llvm::Error object indicating the success or failure of this
56+
/// operation. Failures arise if the timeout is hit, the next line of text
57+
/// from the fifo file is not a valid JSON object, or is it impossible to
58+
/// read from the file.
59+
llvm::Expected<llvm::json::Value> ReadJSON(std::chrono::milliseconds timeout);
60+
61+
/// Serialize a JSON object and write it to the underlying output fifo file.
62+
///
63+
/// \param[in] json
64+
/// The JSON object to send. It will be printed as a single line delimited
65+
/// with \a std::endl.
66+
///
67+
/// \param[in] timeout
68+
/// A timeout for how long we should until for the data to be consumed.
69+
///
70+
/// \return
71+
/// An \a llvm::Error object indicating whether the data was consumed by
72+
/// a reader or not.
73+
llvm::Error SendJSON(
74+
const llvm::json::Value &json,
75+
std::chrono::milliseconds timeout = std::chrono::milliseconds(20000));
76+
77+
private:
78+
std::string m_fifo_file;
79+
std::string m_other_endpoint_name;
80+
};
81+
82+
} // namespace lldb_vscode
83+
84+
#endif // LLDB_TOOLS_LLDB_VSCODE_FIFOFILES_H

0 commit comments

Comments
 (0)