Skip to content

Commit b1812f9

Browse files
authored
Merge pull request #95 from UiPath/fix/reload-factory-after-login
fix: reload runtime factory after login
2 parents c0c3624 + 7a76bba commit b1812f9

4 files changed

Lines changed: 109 additions & 43 deletions

File tree

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-dev"
3-
version = "0.0.68"
3+
version = "0.0.69"
44
description = "UiPath Developer Console"
55
readme = { file = "README.md", content-type = "text/markdown" }
66
requires-python = ">=3.11"

src/uipath/dev/server/app.py

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
from __future__ import annotations
44

5+
import asyncio
56
import logging
67
import os
78
from pathlib import Path
@@ -160,12 +161,37 @@ async def _config():
160161
from uipath.dev.server.ws.handler import router as ws_router
161162

162163
if auth_enabled:
163-
from uipath.dev.server.auth import restore_session
164+
from uipath.dev.server.auth import get_auth_state, restore_session
164165
from uipath.dev.server.routes.auth import router as auth_router
165166

166167
app.include_router(auth_router, prefix="/api")
167168
restore_session()
168169

170+
# Reload the runtime factory when authentication completes so the
171+
# newly-written credentials are picked up by subsequent runs.
172+
def _on_authenticated() -> None:
173+
async def _safe_reload() -> None:
174+
# Wait for active runs to finish before reloading
175+
while any(
176+
r.status in ("pending", "running")
177+
for r in server.run_service.runs.values()
178+
):
179+
await asyncio.sleep(1)
180+
await server.reload_factory()
181+
182+
def _on_reload_done(t: asyncio.Task[None]) -> None:
183+
try:
184+
t.result()
185+
except asyncio.CancelledError:
186+
pass
187+
except Exception:
188+
logger.exception("Factory reload after login failed")
189+
190+
task = asyncio.create_task(_safe_reload())
191+
task.add_done_callback(_on_reload_done)
192+
193+
get_auth_state()._on_authenticated = _on_authenticated
194+
169195
app.include_router(entrypoints_router, prefix="/api")
170196
app.include_router(runs_router, prefix="/api")
171197
app.include_router(graph_router, prefix="/api")

src/uipath/dev/server/auth.py

Lines changed: 80 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import socketserver
1919
import threading
2020
import time
21+
from collections.abc import Callable
2122
from dataclasses import dataclass, field
2223
from pathlib import Path
2324
from typing import Any
@@ -109,6 +110,8 @@ class AuthState:
109110
_last_tenant: str | None = None
110111
_last_org: dict[str, str] = field(default_factory=dict)
111112
_last_environment: str | None = None
113+
# Callback invoked after successful authentication
114+
_on_authenticated: Callable[[], None] | None = None
112115
# internal
113116
_code_verifier: str | None = None
114117
_state: str | None = None
@@ -128,9 +131,30 @@ def get_auth_state() -> AuthState:
128131

129132

130133
def reset_auth_state() -> None:
131-
"""Reset the auth state to its initial (unauthenticated) values."""
132-
global _auth
133-
_auth = AuthState()
134+
"""Reset the auth state to its initial (unauthenticated) values.
135+
136+
Preserves registered callbacks (e.g. ``_on_authenticated``).
137+
"""
138+
_auth.status = "unauthenticated"
139+
_auth.environment = "cloud"
140+
_auth.token_data = {}
141+
_auth.tenants = []
142+
_auth.organization = {}
143+
_auth.uipath_url = None
144+
_auth._last_tenant = None
145+
_auth._last_org = {}
146+
_auth._last_environment = None
147+
_auth._code_verifier = None
148+
_auth._state = None
149+
_auth._port = None
150+
_auth._callback_server = None
151+
if _auth._wait_task and not _auth._wait_task.done():
152+
_auth._wait_task.cancel()
153+
_auth._wait_task = None
154+
if _auth._token_event:
155+
_auth._token_event.set()
156+
_auth._token_event = None
157+
_auth._loop = None
134158

135159

136160
# ---------------------------------------------------------------------------
@@ -990,6 +1014,40 @@ def select_tenant(tenant_name: str) -> dict[str, Any]:
9901014
return {"status": "authenticated", "uipath_url": auth.uipath_url}
9911015

9921016

1017+
def _update_env_file(env_contents: dict[str, str]) -> None:
1018+
"""Merge *env_contents* into the CWD ``.env`` file.
1019+
1020+
New keys take priority; existing keys not in *env_contents* are preserved.
1021+
Comments and blank lines are kept as-is.
1022+
"""
1023+
env_path = Path.cwd() / ".env"
1024+
lines: list[str] = []
1025+
seen_keys: set[str] = set()
1026+
1027+
if env_path.exists():
1028+
with open(env_path) as f:
1029+
for raw_line in f:
1030+
stripped = raw_line.strip()
1031+
if stripped.startswith("#") or "=" not in stripped:
1032+
# Preserve comments and blank lines
1033+
lines.append(raw_line)
1034+
continue
1035+
key = stripped.split("=", 1)[0]
1036+
if key in env_contents:
1037+
lines.append(f"{key}={env_contents[key]}\n")
1038+
else:
1039+
lines.append(raw_line)
1040+
seen_keys.add(key)
1041+
1042+
# Append new keys that weren't already in the file
1043+
for key, value in env_contents.items():
1044+
if key not in seen_keys:
1045+
lines.append(f"{key}={value}\n")
1046+
1047+
with open(env_path, "w") as f:
1048+
f.writelines(lines)
1049+
1050+
9931051
def _finalize_tenant(auth: AuthState, tenant_name: str) -> None:
9941052
"""Write .env and os.environ with the resolved credentials."""
9951053
org_name = auth.organization.get("name", "")
@@ -1010,45 +1068,27 @@ def _finalize_tenant(auth: AuthState, tenant_name: str) -> None:
10101068
auth._last_org = dict(auth.organization)
10111069
auth._last_environment = auth.environment
10121070

1013-
# Update os.environ
1014-
os.environ["UIPATH_ACCESS_TOKEN"] = access_token
1015-
os.environ["UIPATH_URL"] = uipath_url
1016-
os.environ["UIPATH_TENANT_ID"] = tenant_id
1017-
os.environ["UIPATH_ORGANIZATION_ID"] = org_id
1018-
1019-
# Write/update .env file (preserving comments, blank lines, and ordering)
1020-
env_path = Path.cwd() / ".env"
1021-
lines: list[str] = []
1022-
updated_keys: set[str] = set()
1023-
new_values = {
1024-
"UIPATH_ACCESS_TOKEN": access_token,
1025-
"UIPATH_URL": uipath_url,
1026-
"UIPATH_TENANT_ID": tenant_id,
1027-
"UIPATH_ORGANIZATION_ID": org_id,
1028-
}
1029-
1030-
if env_path.exists():
1031-
with open(env_path) as f:
1032-
for raw_line in f:
1033-
stripped = raw_line.strip()
1034-
if "=" in stripped and not stripped.startswith("#"):
1035-
key = stripped.split("=", 1)[0]
1036-
if key in new_values:
1037-
lines.append(f"{key}={new_values[key]}\n")
1038-
updated_keys.add(key)
1039-
continue
1040-
lines.append(raw_line)
1041-
1042-
# Append any keys that weren't already in the file
1043-
for key, value in new_values.items():
1044-
if key not in updated_keys:
1045-
lines.append(f"{key}={value}\n")
1071+
# Write .env using the same approach as `uipath auth`
1072+
_update_env_file(
1073+
{
1074+
"UIPATH_ACCESS_TOKEN": access_token,
1075+
"UIPATH_URL": uipath_url,
1076+
"UIPATH_TENANT_ID": tenant_id,
1077+
"UIPATH_ORGANIZATION_ID": org_id,
1078+
}
1079+
)
10461080

1047-
with open(env_path, "w") as f:
1048-
f.writelines(lines)
1081+
# Reload .env into os.environ (same as CLI root: cwd + override)
1082+
load_dotenv(
1083+
dotenv_path=os.path.join(os.getcwd(), ".env"),
1084+
override=True,
1085+
)
10491086

1050-
# Reload all .env variables into os.environ
1051-
load_dotenv(override=True)
1087+
if auth._on_authenticated:
1088+
try:
1089+
auth._on_authenticated()
1090+
except Exception:
1091+
logger.exception("Error in post-authentication callback")
10521092

10531093

10541094
def logout() -> None:

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)