holoviz / panel

Panel: The powerful data exploration & web app framework for Python
https://panel.holoviz.org
BSD 3-Clause "New" or "Revised" License
4.46k stars 488 forks source link

Terminal broken. Raises lots of exceptions in browser console. #5908

Closed MarcSkovMadsen closed 3 days ago

MarcSkovMadsen commented 7 months ago

I'm on the current main branch of Panel trying to help Ignozalezsa in https://discourse.holoviz.org/t/terminal-how-to-finish-one-terminal-subprocess-run-before-starting-another/3004/2?u=marc.

I'm trying to show how he can subprocess.run multiple tasks one after the other.

I would expect the below code to work. But as you can see in the video

https://github.com/holoviz/panel/assets/42288570/5be9ed2b-cdfe-4c87-9548-4e76bceaba56

import panel as pn
import param
from time import sleep

class Runner(pn.viewable.Viewer):
    value = param.List(constant=True)

    _terminal = param.ClassSelector(class_=pn.widgets.Terminal, constant=True)

    def __init__(self, **params):
        if not "value" in params:
            params["value"]=[]
        params["_terminal"]=pn.widgets.Terminal(sizing_mode="stretch_width", height=500)

        super().__init__(**params)

        self._terminal.write("Hi. I'm the Panel terminal ❤️ 🐍 😊\n\n")

    @pn.depends("value", "_terminal.subprocess.running", watch=True)
    def _run_next_task(self):
        if not self.value:
            print("no task to run")
            return
        if self._terminal.subprocess.running:
            print("already running")
            return

        task = self.value.pop(0)
        self._terminal.write(f"\nRUNNING: {' '.join(task)}\n")
        self._terminal.subprocess.run(*task)
        self.param.trigger("value")
        print("left", self.value)

    def run(self, *tasks):
        with param.edit_constant(self):
            self.value = [*self.value] + [*tasks]

    def __panel__(self):
        return self._terminal

runner = Runner()

def _add_tasks(_):
    runner.run(
        ["ls"], ["echo", "sleeping"], ["sleep", "2"], ["echo", "finished sleeping!"]
    )

add_tasks = pn.widgets.Button(name="Add Tasks", on_click=_add_tasks)

pn.Column(add_tasks, runner, runner.param.value).servable()
hoxbro commented 7 months ago

Somewhat related is the problem raised here, where the sidebar is not loaded when the terminal is in an accordion.

import panel as pn

pn.extension("terminal")
terminal_accordion = pn.Accordion(("Terminal", pn.widgets.Terminal()))
pn.template.FastListTemplate(main=[terminal_accordion], sidebar=["test"]).servable()
philippjfr commented 3 days ago

Looked at this, the problem here is that the Terminal.subprocess.run method forks the process including the entire server, which means both processes will then send events via the websocket, messing up the protocol. There's no straightforward fix as far as I can see.