Panel Running Terminal Subprocess Blocking?

Hi everyone,

I’m having some trouble getting a terminal to work correctly. I’d like to run a subprocess, turn on a activity monitor while running, stream the output to the terminal in real-time, and after success, enable a button (or continue to the next part in the DAG of a pipeline.)

Here’s a minimal example:

import panel as pn

pn.extension("terminal")

button = pn.widgets.Button(name="Click me!", button_type="primary")
activity = pn.widgets.LoadingSpinner(value=False, width=50, height=50)
clicks = pn.widgets.StaticText(value=0, name="Clicks", align="center")
terminal = pn.widgets.Terminal(
    "Welcome to the Panel Terminal!\nI'm based on xterm.js\n\n",
    options={"cursorBlink": True},
    height=300,
    sizing_mode="stretch_width",
)


def on_click(event):
    clicks.value += 1
    terminal.subprocess.run("echo 'Hello, World!'", shell=True, stdout=terminal)


button.on_click(on_click)
activity.link(terminal.subprocess, value="running")

pn.Column(pn.Row(button, activity, clicks), terminal).servable()

Even with this minimal example, I would expect multiple "Hello world"s to appear, and the counter to update all the time, and the spinner to turn on and off. However, this doesn’t seem to be the case, and I need to click several times (4 to 5) to get an update. The spinner doesn’t react at all.

In a more realistic example, a small program is run. It takes 3 minutes and writes a line of text then sleeps for 1 to 5 seconds:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <time.h>

// Lorem Ipsum text broken into sentences
const char* lorem_ipsum[] = {
    "Lorem ipsum dolor sit amet, consectetur adipiscing elit.",
    "Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
    "Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.",
    "Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.",
    "Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.",
    "Curabitur pretium tincidunt lacus.",
    "Nulla gravida orci a odio.",
    "Nullam varius, turpis et commodo pharetra, est eros bibendum elit, nec luctus magna felis sollicitudin mauris.",
    "Integer in mauris eu nibh euismod gravida.",
    "Duis ac tellus et risus vulputate vehicula.",
    "Donec lobortis risus a elit.",
    "Etiam tempor.",
    "Ut ullamcorper, ligula eu tempor congue, eros est euismod turpis, id tincidunt sapien risus a quam.",
    "Maecenas fermentum consequat mi.",
    "Donec fermentum.",
    "Pellentesque malesuada nulla a mi.",
    "Duis sapien sem, aliquet nec, commodo eget, consequat quis, neque.",
    "Aliquam faucibus, elit ut dictum aliquet, felis nisl adipiscing sapien, sed malesuada diam lacus eget erat.",
    "Cras mollis scelerisque nunc.",
    "Nulla et lectus vestibulum urna fringilla ultrices.",
    "Phasellus eu tellus sit amet tortor gravida placerat.",
    "Integer sapien est, iaculis in, pretium quis, viverra ac, nunc.",
    "Praesent eget sem vel leo ultrices bibendum.",
    "Aenean faucibus. Morbi dolor nulla, malesuada eu, pulvinar at, mollis ac, nulla."
};
const int num_sentences = sizeof(lorem_ipsum) / sizeof(lorem_ipsum[0]);

int main() {
    srand(time(NULL));  // Seed the random number generator

    int total_time = 0;
    while (total_time < 180) {  // 3 minutes is 180 seconds
        for (int i = 0; i < num_sentences; i++) {
            printf("%s\n", lorem_ipsum[i]);
            fflush(stdout);

            int sleep_time = (rand() % 5) + 1;  // Random sleep time between 1 and 5 seconds
            sleep(sleep_time);
            total_time += sleep_time;

            if (total_time >= 180) {
                break;
            }
        }
    }

    return 0;
}

Simple compile:

$ gcc -o lorem_ipsum lorem_ipsum.c

And the Python part is basically the same

import panel as pn

pn.extension("terminal")

button = pn.widgets.Button(name="Click me!", button_type="primary")
activity = pn.widgets.LoadingSpinner(value=False, width=50, height=50)
clicks = pn.widgets.StaticText(value=0, name="Clicks", align="center")
terminal = pn.widgets.Terminal(
    "Welcome to the Panel Terminal!\nI'm based on xterm.js\n\n",
    options={"cursorBlink": True},
    height=300,
    sizing_mode="stretch_width",
)


def on_click(event):
    clicks.value += 1
    terminal.subprocess.run("./lorem_ipsum", shell=True, stdout=terminal)


button.on_click(on_click)
activity.link(terminal.subprocess, value="running")

pn.Column(pn.Row(button, activity, clicks), terminal).servable()

Again, the streaming part of output works. Spinner does not, and I need to click several times to repeat.

Amy hints?

Maybe something similar to this
https://panel.holoviz.org/how_to/state/busy.html

At the end of the program, I get:

    kev_list = self._selector.control(None, max_ev, timeout)
OSError: [Errno 9] Bad file descriptor

I think there’s a bug in terminal, but here’s how I would get spinner to work:

import param
import panel as pn

pn.extension("terminal")

button = pn.widgets.Button(name="Click me!", button_type="primary")
activity = pn.widgets.LoadingSpinner(value=False, width=50, height=50)
clicks = pn.widgets.StaticText(value=0, name="Clicks", align="center")
terminal = pn.widgets.Terminal(
    "Welcome to the Panel Terminal!\nI'm based on xterm.js\n\n",
    options={"cursorBlink": True},
    height=300,
    sizing_mode="stretch_width",
    write_to_console=True
)


def on_click(event):
    if terminal.subprocess.running:
        print(terminal.subprocess.running)
        return

    with button.param.update(disabled=True), activity.param.update(value=True):
        terminal.subprocess.run("echo 'Hello, World!' && sleep 1", shell=True)
        clicks.value += 1


button.on_click(on_click)

pn.Column(pn.Row(button, activity, clicks), terminal).show()