microsoft / node-pty

Fork pseudoterminals in Node.JS
Other
1.48k stars 243 forks source link

Every integrated terminal opens one more fd pointing to /dev/ptmx or /dev/pts/ptmx #710

Open hxhue opened 2 months ago

hxhue commented 2 months ago

Does this issue occur when all extensions are disabled?: Yes

Version: 1.92.2 (user setup)
Commit: fee1edb8d6d72a0ddff41e5f71a671c23ed924b9
Date: 2024-08-14T17:29:30.058Z
Electron: 30.1.2
ElectronBuildId: 9870757
Chromium: 124.0.6367.243
Node.js: 20.14.0
V8: 12.4.254.20-electron.0
OS: Windows_NT x64 10.0.22631

Every new integrated terminal opens /dev/ptmx instead of reusing the old one or opening the new one after closing the old one. This bug occurs both in my WSL and remote Ubuntu server.

Steps to Reproduce when VS Code is connected to WSL (Debian):

  1. Compile the following C++ code (assume the executable is a.out). It's a test program trying to allocate as many fds as possible. It also reports suspicious occupied fds.
#include <cassert>
#include <cerrno>
#include <cstdio>
#include <cstdlib>
#include <fcntl.h>
#include <sys/resource.h>
#include <unistd.h>
#include <unordered_set>
#include <vector>

int main() {
  // check rlimit
  int ret;
  struct rlimit rlimit_res;
  if ((ret = getrlimit(RLIMIT_NOFILE, &rlimit_res)) == -1) {
    perror("getrlimit");
    exit(EXIT_FAILURE);
  }
  printf("soft limit of fd count: %lu\n", (unsigned long)rlimit_res.rlim_cur);
  printf("hard limit of fd count: %lu\n", (unsigned long)rlimit_res.rlim_max);

  // try to open as many files as we can
  std::vector<int> opened_files{0, 1, 2};
  while (true) {
    int fd = open("/dev/zero", O_RDONLY);
    if (fd == -1) {
      if (errno == EMFILE) {
        break;
      } else {
        perror("open");
        exit(EXIT_FAILURE);
      }
    }
    opened_files.push_back(fd);
  }
  // check if sorted
  for (size_t i = 1; i < opened_files.size(); ++i) {
    assert(opened_files[i - 1] < opened_files[i]);
  }
  // find missing fds
  std::vector<int> missing_fds;
  {
    int maxfd = opened_files.back();
    std::unordered_set<int> st(opened_files.begin(), opened_files.end());
    for (int i = 0; i < maxfd; ++i) {
      if (!st.count(i)) {
        printf("missing fd %d\n", i);
        missing_fds.push_back(i);
      }
    }
  }
  printf("fd count: %zd\n", opened_files.size());
  if (!missing_fds.empty()) {
    printf("You can run the following commands:\n");
    printf("======================\n");
    long pid = getpid();
    bool first = true;
    for (int fd : missing_fds) {
      if (first) {
        printf("file /proc/%ld/fd/%d", pid, fd);
        first = false;
      } else {
        printf(" \\\n     /proc/%ld/fd/%d", pid, fd);
      }
    }
    printf("\n");
    printf("======================\n");
    printf("<Press enter to exit>\n");
    getchar();
  }
}
  1. Open 3 terminals.
  2. Run a.out in the 1st terminal. The result may be:
soft limit of fd count: 1048576
hard limit of fd count: 1048576
missing fd 19
missing fd 20
missing fd 21
missing fd 22
fd count: 1048572
You can run the following commands:
======================
file /proc/30552/fd/19 \
     /proc/30552/fd/20 \
     /proc/30552/fd/21 \
     /proc/30552/fd/22
======================
<Press enter to exit>
  1. Keep the program running and copy-paste the command from output (the file command and its arguments) to the 2nd terminal. The results may be:
/proc/30552/fd/19: symbolic link to /dev/urandom
/proc/30552/fd/20: symbolic link to /home/USER/.vscode-server/data/logs/20240901T131205/ptyhost.log
/proc/30552/fd/21: symbolic link to /home/USER/.vscode-server/data/logs/20240901T131205/remoteagent.log
/proc/30552/fd/22: symbolic link to /home/USER/.vscode-server/data/logs/20240901T131205/network.log
  1. Now the process in the first terminal can be terminated, but the terminal should be kept open.
  2. Try running the program in the second terminal and paste the output in another terminal, the results may be (there is one more fd pointing to /dev/ptmx):
/proc/30971/fd/19: symbolic link to /dev/urandom
/proc/30971/fd/20: symbolic link to /home/USER/.vscode-server/data/logs/20240901T131205/ptyhost.log
/proc/30971/fd/21: symbolic link to /home/USER/.vscode-server/data/logs/20240901T131205/remoteagent.log
/proc/30971/fd/22: symbolic link to /home/USER/.vscode-server/data/logs/20240901T131205/network.log
/proc/30971/fd/23: symbolic link to /dev/ptmx
  1. Now run the program in the third terminal and paste the output in another terminal, the results may be (one more fd pointing to /dev/ptmx, again):
/proc/31198/fd/19: symbolic link to /dev/urandom
/proc/31198/fd/20: symbolic link to /home/USER/.vscode-server/data/logs/20240901T131205/ptyhost.log
/proc/31198/fd/21: symbolic link to /home/USER/.vscode-server/data/logs/20240901T131205/remoteagent.log
/proc/31198/fd/22: symbolic link to /home/USER/.vscode-server/data/logs/20240901T131205/network.log
/proc/31198/fd/23: symbolic link to /dev/ptmx
/proc/31198/fd/24: symbolic link to /dev/ptmx

Conclusion: The first terminal has fds pointing to /dev/urandom and some log files. Every new terminal beginning with the second one opens one more fd pointing to /dev/ptmx and the subprocesses inherit the fds from the terminal.

Other observations: The occupied fds are fixed once the terminal is open. After closing some terminals, the occupied fds in the previously opened terminals are not released, but the fds are released for new terminals.

The results are reproducible on a remote Ubuntu server, except that there is no fd pointing to /dev/urandom, and every new terminal opens /dev/pts/ptmx instead of /dev/ptmx.

I've seen a similar issue: https://github.com/microsoft/vscode/issues/182212, but it doesn't mention WSL or remote servers.

hxhue commented 2 months ago

I find there is an easier way to reproduce this. Just type the following command in each terminal (open a few terminals before doing this):

file /proc/$$/fd/* | grep -v 'No such file'

Every terminal shows a different count of symbolic link to /dev/ptmx entry.

The first terminal (1 fd -> /dev/urandom):

/proc/29722/fd/0:   symbolic link to /dev/pts/4
/proc/29722/fd/1:   symbolic link to /dev/pts/4
/proc/29722/fd/19:  symbolic link to /dev/urandom
/proc/29722/fd/2:   symbolic link to /dev/pts/4
/proc/29722/fd/20:  symbolic link to /home/USER/.vscode-server/data/logs/20240902T140816/ptyhost.log
/proc/29722/fd/21:  symbolic link to /home/USER/.vscode-server/data/logs/20240902T140816/network.log
/proc/29722/fd/24:  symbolic link to /home/USER/.vscode-server/data/logs/20240902T140816/remoteagent.log
/proc/29722/fd/255: symbolic link to /dev/pts/4

The second terminal (1 fd -> /dev/urandom and 1 fd -> /dev/ptmx):

/proc/29831/fd/0:   symbolic link to /dev/pts/5
/proc/29831/fd/1:   symbolic link to /dev/pts/5
/proc/29831/fd/19:  symbolic link to /dev/urandom
/proc/29831/fd/2:   symbolic link to /dev/pts/5
/proc/29831/fd/20:  symbolic link to /home/USER/.vscode-server/data/logs/20240902T140816/ptyhost.log
/proc/29831/fd/21:  symbolic link to /home/USER/.vscode-server/data/logs/20240902T140816/network.log
/proc/29831/fd/22:  symbolic link to /dev/ptmx
/proc/29831/fd/24:  symbolic link to /home/USER/.vscode-server/data/logs/20240902T140816/remoteagent.log
/proc/29831/fd/255: symbolic link to /dev/pts/5

The third terminal (1 fd -> /dev/urandom and 2 fds -> /dev/ptmx):

/proc/29893/fd/0:   symbolic link to /dev/pts/6
/proc/29893/fd/1:   symbolic link to /dev/pts/6
/proc/29893/fd/19:  symbolic link to /dev/urandom
/proc/29893/fd/2:   symbolic link to /dev/pts/6
/proc/29893/fd/20:  symbolic link to /home/USER/.vscode-server/data/logs/20240902T140816/ptyhost.log
/proc/29893/fd/21:  symbolic link to /home/USER/.vscode-server/data/logs/20240902T140816/network.log
/proc/29893/fd/22:  symbolic link to /dev/ptmx
/proc/29893/fd/24:  symbolic link to /home/USER/.vscode-server/data/logs/20240902T140816/remoteagent.log
/proc/29893/fd/25:  symbolic link to /dev/ptmx
/proc/29893/fd/255: symbolic link to /dev/pts/6

I also did some research after posting the issue and now understand that /dev/ptmx is a pseudo-terminal master device and not needed by shells (which need slave devices). Maybe there is a missing FD_CLOEXEC flag during the fork-exec of the shells?

meganrogge commented 2 months ago

AFAIK, that symbolic link creation, symbolic link to /dev/ptmx, is not coming from VS Code. Can you reproduce in an external terminal?

@Tyriar or @roblourens any thoughts here?

hxhue commented 2 months ago

The symbolic links come from Linux's /proc virtual filesystem (only Linux has it). /proc/<pid>/ is a folder describing the process whose pid is <pid>. /proc/<pid>/fd/ folder contains all open fds shown as symbolic links to actual files on the filesystem.

This doesn't mean that VS Code creates any symbolic links, but means the shells (and their subprocesses) keep those files open when they're running.

I only reproduced the issue in my WSL and a remote Linux server. I don't have a Linux desktop environment for now, but I will see if I can reproduce it on a virtual machine.

hxhue commented 2 months ago

The integrated terminal on a Linux desktop (inside a virtual machine) has the same problem. Here are some screenshots.

image image image

The issue does not exist in external terminals on a Linux desktop.

There's my .vscode/launch.json, it uses external terminals to launch the given Python file:

{
    // Use IntelliSense to learn about possible attributes.
    // Hover to view descriptions of existing attributes.
    // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
    "version": "0.2.0",
    "configurations": [
        {
            "name": "Python Debugger: Current File",
            "type": "debugpy",
            "request": "launch",
            "program": "${file}",
            "console": "externalTerminal"
        }
    ]
}

The contents of main.py:

import os

assert 0 == os.system("file /proc/$$/fd/* | grep -v 'No such file'")
_ = input('Waiting for anything...')
print('<Exited>')

The result is:

image

hxhue commented 1 month ago

Similar issues: https://github.com/microsoft/node-pty/issues/657 https://github.com/microsoft/vscode/issues/202558