Skip to content

Instantly share code, notes, and snippets.

@mitkonikov
Created January 18, 2025 08:59
Show Gist options
  • Save mitkonikov/d44fb33cd419ed54908102942cda3979 to your computer and use it in GitHub Desktop.
Save mitkonikov/d44fb33cd419ed54908102942cda3979 to your computer and use it in GitHub Desktop.
This Gist was just an experiment in trying to reproduce an issue that happens when VSCode sends commands to an IPython interactive shell, but from C++ process.
//
// This Gist was just an experiment in trying to reproduce an issue that happens
// when VSCode sends commands to an IPython interactive shell. The issue is that
// the IPython shell doesn't execute simple commands on new line in some special cases.
//
// This program assumes that you have the following python script in the same directory
// ```python
// from IPython.terminal.embed import InteractiveShellEmbed
//
// def main():
// shell = InteractiveShellEmbed()
// shell() # Start interactive shell
//
// if __name__ == "__main__":
// main()
// ```
//
// I'm compiling this through the Visual Studio Developer Command Prompt
// by just running `cl ipython_test.cpp` and then running the executable.
//
// > I'm using Windows pipes and libs because the issue was reported on Windows.
//
#include <windows.h>
#include <csignal>
#include <iostream>
#include <string>
#include <vector>
// Global process info
PROCESS_INFORMATION pi = {};
HANDLE hChildStdoutRead = nullptr, hChildStdoutWrite = nullptr;
HANDLE hChildStdinRead = nullptr, hChildStdinWrite = nullptr;
// This is just for safely terminating the Python process
// If you don't terminate the Python process, it will keep running
// even after the C++ process has exited.
void signalHandler(int signal) {
if (signal == SIGINT) {
std::cout << std::endl;
std::cout << "Terminating Python process..." << std::endl;
// Debug check if pi.hProcess and pi.hThread are valid
std::cout << pi.hProcess << std::endl;
std::cout << pi.hThread << std::endl;
// Terminate the Python process
if (pi.hProcess) {
TerminateProcess(pi.hProcess, 0);
CloseHandle(pi.hProcess);
CloseHandle(pi.hThread);
}
// Close pipe handles
if (hChildStdoutRead) CloseHandle(hChildStdoutRead);
if (hChildStdinWrite) CloseHandle(hChildStdinWrite);
exit(0);
}
}
void writeToPipe(HANDLE pipe, const std::string& command) {
DWORD bytesWritten;
if (!WriteFile(pipe, command.c_str(), command.size(), &bytesWritten, nullptr)) {
std::cerr << "Failed to write to pipe. Error: " << GetLastError() << std::endl;
}
}
std::string readFromPipe(HANDLE pipe) {
std::string result;
char buffer[4096];
DWORD bytesRead;
while (true) {
if (!ReadFile(pipe, buffer, sizeof(buffer) - 1, &bytesRead, nullptr) || bytesRead == 0) {
break;
}
buffer[bytesRead] = '\0'; // Null-terminate the string
result += buffer;
}
return result;
}
int main() {
std::signal(SIGINT, signalHandler);
SECURITY_ATTRIBUTES sa = { sizeof(SECURITY_ATTRIBUTES), nullptr, TRUE };
if (!CreatePipe(&hChildStdoutRead, &hChildStdoutWrite, &sa, 0)) {
std::cerr << "Failed to create stdout pipe. Error: " << GetLastError() << std::endl;
return 1;
}
if (!CreatePipe(&hChildStdinRead, &hChildStdinWrite, &sa, 0)) {
std::cerr << "Failed to create stdin pipe. Error: " << GetLastError() << std::endl;
return 1;
}
// Set up the startup info
STARTUPINFO si = {};
si.cb = sizeof(STARTUPINFO);
si.hStdOutput = hChildStdoutWrite;
si.hStdError = hChildStdoutWrite;
si.hStdInput = hChildStdinRead;
si.dwFlags |= STARTF_USESTDHANDLES;
// Command to run the IPython interactive shell
char* command = "python ./ipython_test.py";
// Create the Python process
if (!CreateProcess(nullptr, command, nullptr, nullptr, TRUE, 0, nullptr, nullptr, &si, &pi)) {
std::cerr << "Failed to create process. Error: " << GetLastError() << std::endl;
return 1;
}
std::cout << "Command: " << command << std::endl;
std::cout << "STDOUT handle: " << hChildStdoutWrite << std::endl;
std::cout << "STDIN handle: " << hChildStdinRead << std::endl;
// Close unused pipes
CloseHandle(hChildStdoutWrite);
CloseHandle(hChildStdinRead);
// Send commands to the Python process
std::vector<std::string> commands = {
"from time import sleep\r\n",
"sleep(6)\r\n",
"print('Hello from C++!')\r\n",
"print(''.join([str(i) for i in range(10000)]))\r\n",
"def hello_world():\r\n",
" print('Hello from function')\r\n",
"hello_world()\r\n",
};
for (int i = 0; i < 200; i++) {
commands.push_back("print('Hello from C++!')\r\n");
}
commands.push_back("exit()\r\n");
for (const auto& cmd : commands) {
writeToPipe(hChildStdinWrite, cmd);
}
std::cout << "Written." << std::endl;
// Read and print the output from the Python process
// If you don't want to see the python process output, you can comment this line
std::cout << "Python Output:\n" << readFromPipe(hChildStdoutRead) << std::endl;
// Wait for the Python process to exit.
// You could even call signalHandler(SIGINT) to kill the process
// without waiting for it to finish. But because readFromPipe waits
// until it reads the whole output, when we get to this point, the
// python process is already dead.
WaitForSingleObject(pi.hProcess, INFINITE);
// Clean up
CloseHandle(hChildStdoutRead);
CloseHandle(hChildStdinWrite);
CloseHandle(pi.hProcess);
CloseHandle(pi.hThread);
return 0;
}
@mitkonikov
Copy link
Author

I may have found something, but it might be some other issue with buffering. I'm not sure yet.

Here's the thing, if you change this line to send 400 commands (spam it), the IPython terminal's pipe never dies, which would mean that Python child process never ended (which implies that the exit() command is never executed). This could be the reproduction of our issue, but it could be something else. More research is needed.

Technically, here we wait until the stdout pipe of the python process is finished, which means without calling exit() this will wait infinitely. I have to see how exactly VSCode handles this, but in VSCode, we don't even read the output of the child process. If you want to read the output of the child process, you will need to have overlapped pipes which are async. Since we don't use this in VSCode, this part could be skipped entirely. This is more of a confirmation that all commands have been executed.

I will now test it with the python original code that was send to the terminal and report the results back.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment