skip to Main Content

I have a Docker image which contains JRE, some Java web application and jmxterm. The latter is used for running some ad-hoc administrative tasks. The image is used on the CentOS 7 server with Docker 1.13 (which is pretty old but is the latest version which is supplied via the distro’s repository) to run the web application itself.

All works well, but after updating jmxterm from 1.0.0 to the latest version (1.0.2), I get the following warning when entering the running container and starting jmxterm:

WARNING: Unable to create a system terminal, creating a dumb terminal (enable debug logging for more information)

After this, jmxterm does not react to arrow keys (when trying to navigate through the command history), nor does it provide autocompletion.

Some quick investigation shows that the problem may be reproduced in the clean environment with CentOS 7. Say, this is how I could bootstrap the system and the container with all stuff I need:

$ vagrant init centos/7
$ vagrant up
$ vagrant ssh
[vagrant@localhost ~]$ sudo yum install docker
[vagrant@localhost ~]$ sudo systemctl start docker
[vagrant@localhost ~]$ sudo docker run -it --entrypoint bash openjdk:11
root@0c4c614de0ee:/# wget https://github.com/jiaqi/jmxterm/releases/download/v1.0.2/jmxterm-1.0.2-uber.jar

And this is how I enter the container and run jmxterm:

[vagrant@localhost ~]$ sudo docker exec -it 0c4c614de0ee sh
root@0c4c614de0ee:/# java -jar jmxterm-1.0.2-uber.jar
WARNING: Unable to create a system terminal, creating a dumb terminal (enable debug logging for more information)
root@0c4c614de0ee:/# bea<TAB>
<Nothing happens, but autocompletion had to appear>

Few observations:

  • the problem does not appear with older jmxterm no matter which image do I use;
  • the problem arises with new jmxterm no matter which image do I use;
  • the problem is not reproducible on my laptop (which has newer kernel and Docker);
  • the problem is not reproducible if I use latest Docker (from the external repo) on the CentOS 7 server instead of CentOS 7’s native version 1.13.

What happens, and why the error is reproducible only in specific environments? Is there any workaround for this?

2

Answers


  1. Chosen as BEST ANSWER

    TLDR: running new jmxterm versions as java -jar jmxterm-1.0.2-uber.jar < /dev/tty is a quick, dirty and hacky workaround for having the autocompletion and other stuff work inside the interactive container session.


    A quick check shows that jmxterm tries to determine the terminal device used by the process — probably to obtain the terminal capabilities later — by running the tty utility:

    root@0c4c614de0ee:/# strace -f -e 'trace=execve,wait4' java -jar jmxterm-1.0.2-uber.jar
    execve("/opt/java/openjdk/bin/java", ["java", "-jar", "jmxterm-1.0.2-uber.jar"], 0x7ffed3a53210 /* 36 vars */) = 0
    ...
    [pid   432] execve("/usr/bin/tty", ["tty"], 0x7fff8ea39608 /* 36 vars */) = 0
    [pid   433] wait4(432, [{WIFEXITED(s) && WEXITSTATUS(s) == 1}], 0, NULL) = 432
    WARNING: Unable to create a system terminal, creating a dumb terminal (enable debug logging for more information)
    

    The utility fails with the status of 1, which is likely the reason for the error message. Why?

    root@0c4c614de0ee:/# strace -y tty
    ...
    readlink("/proc/self/fd/0", "/dev/pts/3", 4095) = 10
    stat("/dev/pts/3", 0x7ffe966f2160)      = -1 ENOENT (No such file or directory)
    ...
    write(1</dev/pts/3>, "not a ttyn", 10not a tty
    ) = 10
    

    The utility says "not a tty" while we definitely have one. A quick check shows that... There is really no PTY device in the container though the standard streams of the shell are connected to one!

    root@0c4c614de0ee:/# ls -l /proc/self/fd
    total 0
    lrwx------. 1 root root 64 Jun  3 21:26 0 -> /dev/pts/3
    lrwx------. 1 root root 64 Jun  3 21:26 1 -> /dev/pts/3
    lrwx------. 1 root root 64 Jun  3 21:26 2 -> /dev/pts/3
    lr-x------. 1 root root 64 Jun  3 21:26 3 -> /proc/61/fd
    
    root@0c4c614de0ee:/# ls -l /dev/pts
    total 0
    crw-rw-rw-. 1 root root 5, 2 Jun  3 21:26 ptmx
    

    What if we check the same with latest Docker?

    root@c0ebd608f79a:/# ls -l /proc/self/fd
    total 0
    lrwx------ 1 root root 64 Jun  3 21:45 0 -> /dev/pts/1
    lrwx------ 1 root root 64 Jun  3 21:45 1 -> /dev/pts/1
    lrwx------ 1 root root 64 Jun  3 21:45 2 -> /dev/pts/1
    lr-x------ 1 root root 64 Jun  3 21:45 3 -> /proc/16/fd
    
    root@c0ebd608f79a:/# ls -l /dev/pts
    total 0
    crw--w---- 1 root tty  136, 0 Jun  3 21:44 0
    crw--w---- 1 root tty  136, 1 Jun  3 21:45 1
    crw-rw-rw- 1 root root   5, 2 Jun  3 21:45 ptmx
    

    Bingo! Now we have our PTYs where they should be, so jmxterm works well with latest Docker.

    It seems pretty weird that with older Docker the processes are connected to some PTYs while there are no devices for them in /dev/pts, but tracing the Docker process explains why this happens. Older Docker allocates the PTY for the container before setting other things up (including entering the new mount namespace and mounting devpts into it or just entering the mount namespace in case of docker exec -it):

    [vagrant@localhost ~]$ sudo strace -p $(pidof docker-containerd-current) -f -e trace='execve,mount,unshare,openat,ioctl'
    ...
    [pid  3885] openat(AT_FDCWD, "/dev/ptmx", O_RDWR|O_NOCTTY|O_CLOEXEC) = 9
    [pid  3885] ioctl(9, TIOCGPTN, [1])     = 0
    [pid  3885] ioctl(9, TIOCSPTLCK, [0])   = 0
    ...
    [pid  3898] unshare(CLONE_NEWNS|CLONE_NEWUTS|CLONE_NEWIPC|CLONE_NEWNET|CLONE_NEWPID) = 0
    ...
    [pid  3899] mount("devpts", "/var/lib/docker/overlay2/3af250a9f118d637bfba5701f5b0dfc09ed154c6f9d0240ae12523bf252e350c/merged/dev/pts", "devpts", MS_NOSUID|MS_NOEXEC, "newinstance,ptmxmode=0666,mode=0"...) = 0
    ...
    [pid  3899] execve("/bin/bash", ["bash"], 0xc4201626c0 /* 7 vars */ <unfinished ...>
    

    Note the newinstance mount option which ensures that the devpts mount owns its PTYs exclusively and does not share them with other mounts. This leads to the interesting effect: the PTY device for the container stays on the host and belongs to the host's devpts mount, while the containerized process still has access to it, as it obtained the already-open file descriptors just in the beginning of its life!

    The latest Docker first mounts devpts for the container and then allocates the PTY, so the PTY belongs to container's devpts mount and is visible inside the container's filesystem:

    $ sudo strace -p $(pidof containerd) -f -e trace='execve,mount,unshare,openat,ioctl'
    ...
    [pid 14043] unshare(CLONE_NEWNS|CLONE_NEWUTS|CLONE_NEWIPC|CLONE_NEWPID|CLONE_NEWNET) = 0
    ...
    [pid 14044] mount("devpts", "/var/lib/docker/overlay2/b743cf16ab954b9a4b4005bca0aeaa019c4836c7d58d6073044e5b48446c3d62/merged/dev/pts", "devpts", 
    MS_NOSUID|MS_NOEXEC, "newinstance,ptmxmode=0666,mode=0"...) = 0
    ...
    [pid 14044] openat(AT_FDCWD, "/dev/ptmx", O_RDWR|O_NOCTTY|O_CLOEXEC) = 7
    [pid 14044] ioctl(7, TIOCGPTN, [0])     = 0
    [pid 14044] ioctl(7, TIOCSPTLCK, [0])   = 0
    ...
    [pid 14044] execve("/bin/bash", ["/bin/bash"], 0xc000203530 /* 4 vars */ <unfinished ...>
    

    Well, the problem is caused by inappropriate Docker behavior, but how comes that older jmxterm worked well in the same environment? Let's check (note, that Java 8 image is used here, as older jmxterm does not play well with Java 11):

    root@504a7757e310:/# wget https://github.com/jiaqi/jmxterm/releases/download/v1.0.0/jmxterm-1.0.0-uber.jar
    root@504a7757e310:/# strace -f -e 'trace=execve,wait4' java -jar jmxterm-1.0.0-uber.jar
    execve("/usr/local/openjdk-8/bin/java", ["java", "-jar", "jmxterm-1.0.0-uber.jar"], 0x7fffdcaebdd0 /* 10 vars */) = 0
    ...
    [pid   310] execve("/bin/sh", ["sh", "-c", "stty -a < /dev/tty"], 0x7fff1f2a1cc8 /* 10 vars */) = 0
    

    So, older jmxterm just uses /dev/tty instead of asking tty for the device name, and this works, as this device is present inside the container:

    root@504a7757e310:/# ls -l /dev/tty
    crw-rw-rw-. 1 root root 5, 0 Jun  3 21:36 /dev/tty
    

    The huge difference between these versions of jmxterm is that newer tool version uses higher major version of jline, which is the library responsible for interaction with the terminal (akin to the readline in the C world). The difference between major jline versions leads to the difference in jmxterm's behavior, and current versions just rely on tty.

    This observation leads us to the quick and dirty workaround which does not require neither updating Docker nor patching the jline/jmxterm tandem: we may just attach jmxterm's stdin to /dev/tty forcibly and thus make jline use this device (which is now referenced by /proc/self/fd/0) instead of the /dev/pts entry (which, formally, is not always correct, but is still enough for ad-hoc use):

    root@0c4c614de0ee:/# java -jar jmxterm-1.0.2-uber.jar < /dev/tty
    Welcome to JMX terminal. Type "help" for available commands.
    $>bea<TAB>
    bean    beans
    

    Now we have the autocompletion, history and other cool things we need to have.


  2. If you are trying to run an interactive application (that needs tty) inside a docker container or a pod in kubernetes, then the following should work.

    For docker-compose use:

    image: image-name:2.0
    container_name: container-name
    restart: always
    
    stdin_open: true
    tty: true
    

    For kubernetes use:

    spec:
          containers:
          - name: web
            image: web:latest
    
            tty: true
            stdin: true
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search