langchain-ai / langgraph-example

224 stars 198 forks source link

Unable to Execute langgraph up on Windows: NotImplementedError with asyncio Signal #20

Open c0pper opened 5 months ago

c0pper commented 5 months ago

Issue Summary:

Attempting to run langgraph up on Windows results in the following error:

Traceback (most recent call last):
  File "c:\workspace\python\delfi\.venv\Lib\site-packages\langgraph_cli\exec.py", line 64, in subp_exec
    loop.add_signal_handler(signal.SIGINT, signal_handler)
  File "C:\Users\smarotta\AppData\Local\Programs\Python\Python311\Lib\asyncio\events.py", line 574, in add_signal_handler
    raise NotImplementedError
NotImplementedError

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "<frozen runpy>", line 198, in _run_module_as_main
  File "<frozen runpy>", line 88, in _run_code
  File "c:\workspace\python\delfi\.venv\Scripts\langgraph.exe\__main__.py", line 7, in <module>
  File "c:\workspace\python\delfi\.venv\Lib\site-packages\click\core.py", line 1157, in __call__
    return self.main(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "c:\workspace\python\delfi\.venv\Lib\site-packages\click\core.py", line 1078, in main
    rv = self.invoke(ctx)
         ^^^^^^^^^^^^^^^^
  File "c:\workspace\python\delfi\.venv\Lib\site-packages\click\core.py", line 1688, in invoke
    return _process_result(sub_ctx.command.invoke(sub_ctx))
                           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "c:\workspace\python\delfi\.venv\Lib\site-packages\click\core.py", line 1434, in invoke
    return ctx.invoke(self.callback, **ctx.params)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "c:\workspace\python\delfi\.venv\Lib\site-packages\click\core.py", line 783, in invoke
    return __callback(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "c:\workspace\python\delfi\.venv\Lib\site-packages\langgraph_cli\cli.py", line 183, in up
    capabilities = langgraph_cli.docker.check_capabilities(runner)
                   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "c:\workspace\python\delfi\.venv\Lib\site-packages\langgraph_cli\docker.py", line 83, in check_capabilities
    stdout, _ = runner.run(subp_exec("docker", "info", "-f", "json", collect=True))
                ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\smarotta\AppData\Local\Programs\Python\Python311\Lib\asyncio\runners.py", line 118, in run
    return self._loop.run_until_complete(task)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\smarotta\AppData\Local\Programs\Python\Python311\Lib\asyncio\base_events.py", line 650, in run_until_complete
    return future.result()
           ^^^^^^^^^^^^^^^
  File "c:\workspace\python\delfi\.venv\Lib\site-packages\langgraph_cli\exec.py", line 103, in subp_exec
    os.killpg(os.getpgid(proc.pid), signal.SIGINT)
    ^^^^^^^^^
AttributeError: module 'os' has no attribute 'killpg'. Did you mean: 'kill'?
Exception ignored in: <function BaseSubprocessTransport.__del__ at 0x000001B4EE7E6E80>
Traceback (most recent call last):
  File "C:\Users\smarotta\AppData\Local\Programs\Python\Python311\Lib\asyncio\base_subprocess.py", line 126, in __del__
  File "C:\Users\smarotta\AppData\Local\Programs\Python\Python311\Lib\asyncio\base_subprocess.py", line 104, in close
  File "C:\Users\smarotta\AppData\Local\Programs\Python\Python311\Lib\asyncio\proactor_events.py", line 108, in close
  File "C:\Users\smarotta\AppData\Local\Programs\Python\Python311\Lib\asyncio\base_events.py", line 758, in call_soon
  File "C:\Users\smarotta\AppData\Local\Programs\Python\Python311\Lib\asyncio\base_events.py", line 519, in _check_closed
RuntimeError: Event loop is closed

Steps to Reproduce:

Ensure Docker Desktop is installed and running on Windows.
Activate Python environment (venv or virtualenv).
Install langgraph-cli using pip install langgraph-cli.
Run langgraph up command.

Expected Behavior:

The langgraph up command should start successfully and execute without errors, leveraging Docker functionality on Windows.

Actual Behavior:

Encountered a NotImplementedError related to add_signal_handler and subsequent errors related to os.killpg and Event loop is closed.

Environment Details:

Operating System: Windows 11
Python Version: 3.11

Additional Context:

I have Docker Desktop installed and running on my Windows machine. This issue prevents me from using langgraph-cli effectively for my development workflows.

SouthCentralCode commented 5 months ago

Was having the same issue...

so created a very hacky workaround (using a cursor.so) to get it working on windows...

I changed my local \site-packages\langgraph_cli\exec.py

to the below code and it has worked to create the docker image and containers.

ive only ran it once, so be warned...

import asyncio
import os
import sys
from contextlib import contextmanager
from typing import Callable, Optional, cast

import click.exceptions

@contextmanager  
def Runner():
    if hasattr(asyncio, "Runner"):
        with asyncio.Runner() as runner:
            yield runner
    else:

        class _Runner:
            def __enter__(self):
                return self

            def __exit__(self, *args):
                pass

            def run(self, coro):
                asyncio.run(coro)

        yield _Runner()

async def subp_exec(
    cmd: str,
    *args: str,
    input: Optional[str] = None,
    wait: Optional[float] = None,
    verbose: bool = False,
    collect: bool = False,
    on_stdout: Optional[Callable[[str], Optional[bool]]] = None,
) -> tuple[Optional[str], Optional[str]]:
    if verbose:
        cmd_str = f"+ {cmd} {' '.join(map(str, args))}"
        if input:
            print(cmd_str, " <\n", "\n".join(filter(None, input.splitlines())), sep="")
        else:
            print(cmd_str)
    if wait:
        await asyncio.sleep(wait)

    try:
        proc = await asyncio.create_subprocess_exec(
            cmd,
            *args,
            stdin=asyncio.subprocess.PIPE if input else None,
            stdout=asyncio.subprocess.PIPE,
            stderr=asyncio.subprocess.PIPE,
        )

        def signal_handler():
            # make sure process exists, then terminate it
            if proc.returncode is None:
                proc.terminate()

        loop = asyncio.get_event_loop()
        signal_handlers_installed = False
        if os.name != 'nt':  # Skip signal handling on Windows
            try:
                loop.add_signal_handler(signal.SIGINT, signal_handler)
                loop.add_signal_handler(signal.SIGTERM, signal_handler)
                signal_handlers_installed = True
            except NotImplementedError:
                pass

        empty_fut: asyncio.Future = asyncio.Future()
        empty_fut.set_result(None)
        stdout, stderr, _ = await asyncio.gather(
            monitor_stream(
                cast(asyncio.StreamReader, proc.stdout),
                collect=True,
                display=verbose,
                on_line=on_stdout,
            ),
            monitor_stream(
                cast(asyncio.StreamReader, proc.stderr),
                collect=True,
                display=verbose,
            ),
            proc._feed_stdin(input.encode()) if input else empty_fut,  # type: ignore[attr-defined]
        )
        returncode = await proc.wait()
        if (
            returncode is not None
            and returncode != 0  # success
            and returncode != 130  # user interrupt
        ):
            sys.stdout.write(stdout.decode() if stdout else "")
            sys.stderr.write(stderr.decode() if stderr else "")
            raise click.exceptions.Exit(returncode)
        if collect:
            return (
                stdout.decode() if stdout else None,
                stderr.decode() if stderr else None,
            )
        else:
            return None, None
    finally:
        try:
            if proc.returncode is None:
                try:
                    os.kill(proc.pid, signal.SIGINT)
                except (ProcessLookupError, KeyboardInterrupt):
                    pass

            if signal_handlers_installed:
                loop.remove_signal_handler(signal.SIGINT)
                loop.remove_signal_handler(signal.SIGTERM)
        except UnboundLocalError:
            pass

async def monitor_stream(
    stream: asyncio.StreamReader,
    collect: bool = False,
    display: bool = False,
    on_line: Optional[Callable[[str], Optional[bool]]] = None,
) -> Optional[bytearray]:
    if collect:
        ba = bytearray()

    def handle(line: bytes):
        nonlocal on_line
        nonlocal display

        if collect:
            ba.extend(line)
        if display:
            sys.stdout.write(line.decode())
        if on_line:
            if on_line(line.decode()):
                on_line = None
                display = True

    async for line in stream:
        await asyncio.to_thread(handle, line)
    if collect:
        return ba
    else:
        return None
c0pper commented 5 months ago

That did the trick, thanks a lot!