Skip to content

Commit 10dee00

Browse files
ayeshurunAlon Yeshurun
andauthored
fix(core): correct context persistence in virtual environments (#61)
Co-authored-by: Alon Yeshurun <alonyeshurun+microsoft@microsoft.com>
1 parent 0914173 commit 10dee00

File tree

3 files changed

+111
-31
lines changed

3 files changed

+111
-31
lines changed
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
kind: fixed
2+
body: Fix context persistence in virtual environment
3+
time: 2025-11-11T05:39:40.62257109Z

src/fabric_cli/core/fab_context.py

Lines changed: 58 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import json
66
import os
77
import platform
8+
import sys
89

910
import psutil
1011

@@ -247,6 +248,43 @@ def _process_exists(self, pid):
247248
# If we can't determine process existence, assume it exists to be safe
248249
return True
249250

251+
def _is_in_venv(self):
252+
"""Check if running inside a virtual environment."""
253+
return sys.prefix != sys.base_prefix
254+
255+
def _get_session_process(self, parent_process: psutil.Process) -> psutil.Process:
256+
"""
257+
Get the session process, handling virtual environments and fallbacks.
258+
259+
If a virtual environment is detected, it traverses one additional level up to
260+
find the true shell process.
261+
262+
Args:
263+
parent_process (psutil.Process): The parent process to check.
264+
265+
Returns:
266+
psutil.Process: The actual session process.
267+
"""
268+
grandparent_process = parent_process.parent()
269+
270+
if not grandparent_process:
271+
fab_logger.log_debug(
272+
"No grandparent process was found. Falling back to parent process for session ID resolution"
273+
)
274+
return parent_process
275+
276+
if self._is_in_venv():
277+
great_grandparent_process = grandparent_process.parent()
278+
279+
if not great_grandparent_process:
280+
fab_logger.log_debug(
281+
"No great-grandparent process found in virtual environment. Falling back to grandparent process for session ID resolution"
282+
)
283+
else:
284+
return great_grandparent_process
285+
286+
return grandparent_process
287+
250288
def _get_context_session_id(self):
251289
"""Get the session ID for the current shell session.
252290
@@ -255,35 +293,34 @@ def _get_context_session_id(self):
255293
process and anchors to the actual shell session, ensuring multiple shell sessions
256294
don't interfere with each other.
257295
296+
If a virtual environment is detected, it traverses one additional level up to
297+
find the true shell process.
298+
258299
Fallback hierarchy:
259-
1. Try to get grandparent process (session/shell)
260-
2. If grandparent fails, fall back to parent process
300+
1. Try to get session process (grandparent or great-grandparent in venv)
301+
2. If that fails, fall back to parent process
261302
3. If parent fails, fall back to current process
262303
"""
304+
parent_process = None
263305
try:
264-
parent_process = psutil.Process().parent()
265-
if parent_process is None:
306+
current_process = psutil.Process()
307+
parent_process = current_process.parent()
308+
if not parent_process:
266309
fab_logger.log_debug(
267-
"No parent process was found. Falling back to the current process for session ID resolution."
310+
"No parent process found. Falling back to current process for session ID."
268311
)
269312
return os.getpid()
270-
except Exception as e:
271-
fab_logger.log_debug(
272-
f"Failed to get parent process: {e}. Falling back to current process for session ID resolution."
273-
)
274-
return os.getpid()
275313

276-
try:
277-
session_process = parent_process.parent()
278-
if session_process is not None:
279-
return session_process.pid
280-
else:
314+
session_process = self._get_session_process(parent_process)
315+
return session_process.pid
316+
except Exception as e:
317+
if parent_process:
281318
fab_logger.log_debug(
282-
"No grandparent process was found. Falling back to parent process for session ID resolution."
319+
f"Failed to get session process: {e}. Falling back to parent process for session ID resolution."
283320
)
284321
return parent_process.pid
285-
except Exception as e:
286-
fab_logger.log_debug(
287-
f"Failed to get grandparent process: {e}. Falling back to parent process for session ID resolution."
288-
)
289-
return parent_process.pid
322+
else:
323+
fab_logger.log_debug(
324+
f"Failed to get parent process: {e}. Falling back to current process for session ID resolution."
325+
)
326+
return os.getpid()

tests/test_core/test_fab_context.py

Lines changed: 50 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -132,19 +132,57 @@ def mock_process():
132132
assert session_id == 1234
133133

134134

135-
def test_get_context_session_id_no_grandparent_process(monkeypatch):
136-
parent_process = MockProcess(pid=1234, parent=None)
137-
current_process = MockProcess(pid=9999, parent=parent_process)
135+
def test_get_context_session_id_in_venv(monkeypatch):
136+
great_grandparent_process = MockProcess(pid=1111, parent=None)
137+
grandparent_process = MockProcess(pid=2222, parent=great_grandparent_process)
138+
parent_process = MockProcess(pid=3333, parent=grandparent_process)
139+
current_process = MockProcess(pid=4444, parent=parent_process)
138140

139141
def mock_process():
140142
return current_process
141143

142144
monkeypatch.setattr("fabric_cli.core.fab_context.psutil.Process", mock_process)
145+
context = Context()
146+
monkeypatch.setattr(context, "_is_in_venv", lambda: True)
147+
session_id = context._get_context_session_id()
148+
149+
assert session_id == 1111
150+
151+
152+
def test_get_context_session_id_in_venv_no_great_grandparent(monkeypatch):
153+
grandparent_process = MockProcess(pid=2222, parent=None)
154+
parent_process = MockProcess(pid=3333, parent=grandparent_process)
155+
current_process = MockProcess(pid=4444, parent=parent_process)
143156

157+
def mock_process():
158+
return current_process
159+
160+
monkeypatch.setattr("fabric_cli.core.fab_context.psutil.Process", mock_process)
144161
context = Context()
162+
monkeypatch.setattr(context, "_is_in_venv", lambda: True)
145163
session_id = context._get_context_session_id()
146164

147-
assert session_id == 1234
165+
# Falls back to grandparent PID
166+
assert session_id == 2222
167+
168+
169+
def test_get_context_session_id_not_in_venv(monkeypatch):
170+
great_grandparent_process = MockProcess(pid=1111, parent=None)
171+
grandparent_process = MockProcess(pid=2222, parent=great_grandparent_process)
172+
parent_process = MockProcess(pid=3333, parent=grandparent_process)
173+
current_process = MockProcess(pid=4444, parent=parent_process)
174+
175+
def mock_process():
176+
return current_process
177+
178+
monkeypatch.setattr("fabric_cli.core.fab_context.psutil.Process", mock_process)
179+
180+
context = Context()
181+
monkeypatch.setattr(context, "_is_in_venv", lambda: False)
182+
session_id = context._get_context_session_id()
183+
184+
# Should return grandparent PID, not great-grandparent
185+
assert session_id == 2222
148186

149187

150188
def test_get_context_session_id_no_parent_process(monkeypatch):
@@ -172,9 +210,9 @@ def mock_log_debug(message):
172210

173211
assert session_id == 9999
174212
assert len(log_calls) == 1
175-
assert "No parent process was found" in log_calls[0]
176213
assert (
177-
"Falling back to the current process for session ID resolution" in log_calls[0]
214+
"No parent process found. Falling back to current process for session ID"
215+
in log_calls[0]
178216
)
179217

180218

@@ -227,11 +265,11 @@ def mock_log_debug(message):
227265

228266
assert session_id == 5678 # Falls back to parent process PID
229267
assert len(log_calls) == 1
230-
assert "Failed to get grandparent process:" in log_calls[0]
268+
assert "Failed to get session process:" in log_calls[0]
231269
assert "Falling back to parent process for session ID resolution" in log_calls[0]
232270

233271

234-
def test_get_context_session_id_grandparent_process_none(monkeypatch):
272+
def test_get_context_session_id_no_grandparent_process(monkeypatch):
235273
parent_process = MockProcess(pid=5678, parent=None)
236274
current_process = MockProcess(pid=9999, parent=parent_process)
237275

@@ -253,8 +291,10 @@ def mock_log_debug(message):
253291

254292
assert session_id == 5678 # Falls back to parent process PID
255293
assert len(log_calls) == 1
256-
assert "No grandparent process was found" in log_calls[0]
257-
assert "Falling back to parent process for session ID resolution" in log_calls[0]
294+
assert (
295+
"No grandparent process was found. Falling back to parent process for session ID resolution"
296+
in log_calls[0]
297+
)
258298

259299

260300
class MockProcess:

0 commit comments

Comments
 (0)