Skip to content

Commit 1cca682

Browse files
feat(cli): serve uipath server over uipath-ipc only when --ipc-pipe is given alone [ROBO-6057]
1 parent c253dcf commit 1cca682

4 files changed

Lines changed: 78 additions & 20 deletions

File tree

‎packages/uipath/pyproject.toml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "uipath"
3-
version = "2.14.31"
3+
version = "2.14.32"
44
description = "Python SDK and CLI for UiPath Platform, enabling programmatic interaction with automation services, process management, and deployment tools."
55
readme = { file = "README.md", content-type = "text/markdown" }
66
requires-python = ">=3.11"

‎packages/uipath/src/uipath/_cli/cli_server.py‎

Lines changed: 24 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -297,8 +297,8 @@ async def start_tcp_server(host: str, port: int) -> None:
297297

298298

299299
# The uipath-ipc transport (contract, DTOs, service, ``start_ipc_server``) lives
300-
# in ``cli_server_ipc`` and is served alongside HTTP when ``--ipc-pipe`` is given.
301-
# Older servers served HTTP only; the .NET Handler copes.
300+
# in ``cli_server_ipc``. ``--ipc-pipe`` alone serves it alone; with any HTTP option it
301+
# is served alongside HTTP. Older servers served HTTP only; the .NET Handler copes.
302302

303303

304304
# --------------------------------------------------------------------------- #
@@ -324,8 +324,9 @@ async def start_tcp_server(host: str, port: int) -> None:
324324
"--ipc-pipe",
325325
type=str,
326326
default=None,
327-
help="Named pipe for the uipath-ipc channel. IPC is served only when this is "
328-
"given; omit it for HTTP-only.",
327+
help="Named pipe for the uipath-ipc channel. Given alone, only uipath-ipc is "
328+
"served: no HTTP listener and no ready ACK. Given with any HTTP option, HTTP is "
329+
"served as well.",
329330
)
330331
@click.option(
331332
"--port",
@@ -346,7 +347,7 @@ def server(
346347
port: int | None,
347348
tcp: bool,
348349
) -> None:
349-
"""Serve run/debug/eval over HTTP, plus uipath-ipc when --ipc-pipe is given."""
350+
"""Serve run/debug/eval over HTTP, uipath-ipc (--ipc-pipe alone), or both."""
350351
preload_modules()
351352
_run_server(client_socket, server_socket, ipc_pipe, port, tcp)
352353

@@ -357,15 +358,18 @@ async def _serve(
357358
ipc_pipe: str | None,
358359
port: int,
359360
use_tcp: bool,
361+
serve_http: bool = True,
360362
) -> None:
361-
"""Run the HTTP channel, plus the uipath-ipc channel when a pipe name is given."""
363+
"""Run the HTTP channel unless told not to, plus uipath-ipc when a pipe is named."""
362364
_state.init()
363365

364366
tasks: list[Any] = []
365-
if use_tcp:
366-
tasks.append(start_tcp_server("127.0.0.1", port))
367-
else:
368-
tasks.append(start_unix_server(ack_socket_path, server_socket))
367+
if serve_http:
368+
tasks.append(
369+
start_tcp_server("127.0.0.1", port)
370+
if use_tcp
371+
else start_unix_server(ack_socket_path, server_socket)
372+
)
369373

370374
# IPC is opt-in and independent of the HTTP socket: it is served only when an
371375
# explicit pipe name is given, which both sides agree on out of band (the .NET
@@ -384,12 +388,21 @@ def _run_server(
384388
tcp: bool,
385389
) -> None:
386390
"""Drive ``_serve`` on the right event loop for the platform."""
391+
# Any HTTP option means the caller expects the HTTP channel (and, on Unix, its ACK).
392+
serve_http = not ipc_pipe or any(
393+
(client_socket is not None, server_socket is not None, port is not None, tcp)
394+
)
387395
use_tcp = IS_WINDOWS or tcp
388396
ack_socket_path = (
389397
client_socket or os.environ.get(SOCKET_ENV_VAR) or DEFAULT_SOCKET_PATH
390398
)
391399
coro = _serve(
392-
ack_socket_path, server_socket, ipc_pipe, port or DEFAULT_PORT, use_tcp
400+
ack_socket_path,
401+
server_socket,
402+
ipc_pipe,
403+
port or DEFAULT_PORT,
404+
use_tcp,
405+
serve_http,
393406
)
394407
try:
395408
if sys.platform == "win32":

‎packages/uipath/tests/cli/test_server_transport.py‎

Lines changed: 52 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1-
"""`uipath server` serves BOTH transports concurrently — never either/or.
1+
"""`uipath server` serves HTTP, plus uipath-ipc when a pipe is named.
22
3-
The HTTP channel (aiohttp over a Unix socket, or TCP on Windows / ``--tcp``) is
4-
ALWAYS started. The uipath-ipc named-pipe channel is opt-in and independent of the
5-
HTTP socket: it is started alongside HTTP only when ``--ipc-pipe`` names a pipe,
6-
served verbatim on that name (both sides agree on it out of band). The HTTP channel
7-
is never torn down.
3+
The uipath-ipc named-pipe channel is opt-in: it is started when ``--ipc-pipe`` names
4+
a pipe, served verbatim on that name (both sides agree on it out of band). The HTTP
5+
channel (aiohttp over a Unix socket, or TCP on Windows / ``--tcp``) is started
6+
unless ``--ipc-pipe`` is given without any HTTP option (``--client-socket``,
7+
``--server-socket``, ``--port``, ``--tcp``).
88
99
These tests stub the three channel runners (so ``_serve``'s ``asyncio.gather``
1010
returns at once instead of serving forever) and assert which channels ``_serve``
@@ -15,6 +15,7 @@
1515
import asyncio
1616
from typing import Any
1717

18+
import pytest
1819
from click.testing import CliRunner
1920

2021
import uipath._cli._telemetry as _telemetry
@@ -97,6 +98,14 @@ def test_serve_rides_ipc_alongside_tcp(monkeypatch):
9798
assert calls["ipc"] == "agent.pipe" # IPC rides next to TCP too, not only UDS
9899

99100

101+
def test_serve_without_http_runs_ipc_only(monkeypatch):
102+
calls = _stub_channels(monkeypatch)
103+
asyncio.run(
104+
cli_server._serve("/tmp/ack.sock", None, "agent.pipe", 8765, True, False)
105+
)
106+
assert calls == {"ipc": "agent.pipe"}
107+
108+
100109
def test_serve_skips_ipc_without_ipc_pipe(monkeypatch):
101110
"""No ``--ipc-pipe`` ⇒ HTTP only, regardless of the HTTP socket."""
102111
calls = _stub_channels(monkeypatch)
@@ -118,13 +127,16 @@ def _capture_serve(monkeypatch) -> dict[str, Any]:
118127
Linux) without actually serving anything."""
119128
seen: dict[str, Any] = {}
120129

121-
async def _rec_serve(ack_socket_path, server_socket, ipc_pipe, port, use_tcp):
130+
async def _rec_serve(
131+
ack_socket_path, server_socket, ipc_pipe, port, use_tcp, serve_http=True
132+
):
122133
seen.update(
123134
ack=ack_socket_path,
124135
server_socket=server_socket,
125136
ipc_pipe=ipc_pipe,
126137
port=port,
127138
use_tcp=use_tcp,
139+
serve_http=serve_http,
128140
)
129141

130142
monkeypatch.setattr(cli_server, "_serve", _rec_serve)
@@ -157,6 +169,39 @@ def test_run_server_falls_back_to_default_ack(monkeypatch):
157169
assert seen["ack"] == cli_server.DEFAULT_SOCKET_PATH
158170

159171

172+
def test_run_server_bare_ipc_pipe_serves_ipc_only(monkeypatch):
173+
seen = _capture_serve(monkeypatch)
174+
cli_server._run_server(None, None, "agent.pipe", None, False)
175+
assert seen["serve_http"] is False
176+
assert seen["ipc_pipe"] == "agent.pipe"
177+
178+
179+
@pytest.mark.parametrize(
180+
"client_socket, server_socket, port, tcp",
181+
[
182+
("/tmp/ack.sock", None, None, False),
183+
(None, "/tmp/s.sock", None, False),
184+
(None, None, 9000, False),
185+
(None, None, None, True),
186+
("/tmp/ack.sock", "/tmp/s.sock", None, False),
187+
],
188+
)
189+
def test_run_server_any_http_option_keeps_http(
190+
monkeypatch, client_socket, server_socket, port, tcp
191+
):
192+
seen = _capture_serve(monkeypatch)
193+
cli_server._run_server(client_socket, server_socket, "agent.pipe", port, tcp)
194+
assert seen["serve_http"] is True
195+
assert seen["ipc_pipe"] == "agent.pipe"
196+
197+
198+
@pytest.mark.parametrize("ipc_pipe", [None, ""])
199+
def test_run_server_without_ipc_pipe_serves_http(monkeypatch, ipc_pipe):
200+
seen = _capture_serve(monkeypatch)
201+
cli_server._run_server(None, None, ipc_pipe, None, False)
202+
assert seen["serve_http"] is True
203+
204+
160205
def test_run_server_tcp_flag_forces_tcp(monkeypatch):
161206
seen = _capture_serve(monkeypatch)
162207
cli_server._run_server("/tmp/a.sock", "/tmp/s.sock", None, None, True)

‎packages/uipath/uv.lock‎

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)