Skip to content

Commit a80df34

Browse files
committed
Fix issue #35: Add smart-threading to prevent overlapping credential prompts
- Add smart-threading configuration option (default: true) - Process HTTPS packages serially to avoid prompt conflicts - Then process SSH/local packages in parallel for speed - Applied to both checkout() and update() methods - CHANGES.md: Document new smart-threading feature
1 parent e9db894 commit a80df34

File tree

4 files changed

+117
-3
lines changed

4 files changed

+117
-3
lines changed

CHANGES.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22

33
## 4.1.2 (unreleased)
44

5+
- Fix #35: Add `smart-threading` configuration option to prevent overlapping credential prompts when using HTTPS URLs. When enabled (default), HTTPS packages are processed serially first to ensure clean credential prompts, then other packages are processed in parallel for speed. Can be disabled with `smart-threading = false` if you have credential helpers configured.
6+
[jensens]
7+
58
- Fix #34: The `offline` configuration setting and `--offline` CLI flag are now properly respected to prevent VCS fetch/update operations. Previously, setting `offline = true` in mx.ini or using the `--offline` CLI flag was ignored, and VCS operations still occurred.
69
[jensens]
710

src/mxdev/config.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,10 @@ def __init__(
4646
else:
4747
settings["threads"] = "4"
4848

49+
# Set default for smart-threading (process HTTPS packages serially to avoid
50+
# overlapping credential prompts)
51+
settings.setdefault("smart-threading", "true")
52+
4953
mode = settings.get("default-install-mode", "direct")
5054
if mode not in ["direct", "skip"]:
5155
raise ValueError("default-install-mode must be one of 'direct' or 'skip'")

src/mxdev/processing.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -191,8 +191,11 @@ def fetch(state: State) -> None:
191191
return
192192

193193
logger.info("# Fetch sources from VCS")
194+
smart_threading = to_bool(state.configuration.settings.get("smart-threading", True))
194195
workingcopies = WorkingCopies(
195-
packages, threads=int(state.configuration.settings["threads"])
196+
packages,
197+
threads=int(state.configuration.settings["threads"]),
198+
smart_threading=smart_threading,
196199
)
197200
# Pass offline setting from configuration instead of hardcoding False
198201
offline = to_bool(state.configuration.settings.get("offline", False))

src/mxdev/vcs/common.py

Lines changed: 106 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -163,12 +163,41 @@ def get_workingcopytypes() -> typing.Dict[str, typing.Type[BaseWorkingCopy]]:
163163

164164

165165
class WorkingCopies:
166-
def __init__(self, sources: typing.Dict[str, typing.Dict], threads=5):
166+
def __init__(
167+
self,
168+
sources: typing.Dict[str, typing.Dict],
169+
threads=5,
170+
smart_threading=True,
171+
):
167172
self.sources = sources
168173
self.threads = threads
174+
self.smart_threading = smart_threading
169175
self.errors = False
170176
self.workingcopytypes = get_workingcopytypes()
171177

178+
def _separate_https_packages(
179+
self, packages: typing.List[str]
180+
) -> typing.Tuple[typing.List[str], typing.List[str]]:
181+
"""Separate HTTPS packages from others for smart threading.
182+
183+
Returns (https_packages, other_packages)
184+
"""
185+
https_packages = []
186+
other_packages = []
187+
188+
for name in packages:
189+
if name not in self.sources:
190+
other_packages.append(name)
191+
continue
192+
source = self.sources[name]
193+
url = source.get("url", "")
194+
if url.startswith("https://"):
195+
https_packages.append(name)
196+
else:
197+
other_packages.append(name)
198+
199+
return https_packages, other_packages
200+
172201
def process(self, the_queue: queue.Queue) -> None:
173202
if self.threads < 2:
174203
worker(self, the_queue)
@@ -187,6 +216,43 @@ def process(self, the_queue: queue.Queue) -> None:
187216
sys.exit(1)
188217

189218
def checkout(self, packages: typing.Iterable[str], **kwargs) -> None:
219+
# Smart threading: process HTTPS packages serially to avoid overlapping prompts
220+
packages_list = list(packages)
221+
if self.smart_threading and self.threads > 1:
222+
https_pkgs, other_pkgs = self._separate_https_packages(packages_list)
223+
if https_pkgs and other_pkgs:
224+
logger.info(
225+
"Smart threading: processing %d HTTPS package(s) serially...",
226+
len(https_pkgs),
227+
)
228+
# Save original thread count and process HTTPS packages serially
229+
original_threads = self.threads
230+
self.threads = 1
231+
self._checkout_impl(https_pkgs, **kwargs)
232+
self.threads = original_threads
233+
# Process remaining packages in parallel
234+
logger.info(
235+
"Smart threading: processing %d other package(s) in parallel...",
236+
len(other_pkgs),
237+
)
238+
self._checkout_impl(other_pkgs, **kwargs)
239+
return
240+
elif https_pkgs:
241+
logger.info(
242+
"Smart threading: processing %d HTTPS package(s) serially...",
243+
len(https_pkgs),
244+
)
245+
original_threads = self.threads
246+
self.threads = 1
247+
self._checkout_impl(packages_list, **kwargs)
248+
self.threads = original_threads
249+
return
250+
251+
# Normal processing (smart_threading disabled or threads=1)
252+
self._checkout_impl(packages_list, **kwargs)
253+
254+
def _checkout_impl(self, packages: typing.List[str], **kwargs) -> None:
255+
"""Internal implementation of checkout logic."""
190256
the_queue: queue.Queue = queue.Queue()
191257
if "update" in kwargs and not isinstance(kwargs["update"], bool):
192258
if kwargs["update"].lower() in ("true", "yes", "on", "force"):
@@ -287,12 +353,50 @@ def status(
287353
sys.exit(1)
288354

289355
def update(self, packages: typing.Iterable[str], **kwargs) -> None:
290-
the_queue: queue.Queue = queue.Queue()
291356
# Check for offline mode early - skip all updates if offline
292357
offline = kwargs.get("offline", False)
293358
if offline:
294359
logger.info("Skipped updates (offline mode)")
295360
return
361+
362+
# Smart threading: process HTTPS packages serially to avoid overlapping prompts
363+
packages_list = list(packages)
364+
if self.smart_threading and self.threads > 1:
365+
https_pkgs, other_pkgs = self._separate_https_packages(packages_list)
366+
if https_pkgs and other_pkgs:
367+
logger.info(
368+
"Smart threading: updating %d HTTPS package(s) serially...",
369+
len(https_pkgs),
370+
)
371+
# Save original thread count and process HTTPS packages serially
372+
original_threads = self.threads
373+
self.threads = 1
374+
self._update_impl(https_pkgs, **kwargs)
375+
self.threads = original_threads
376+
# Process remaining packages in parallel
377+
logger.info(
378+
"Smart threading: updating %d other package(s) in parallel...",
379+
len(other_pkgs),
380+
)
381+
self._update_impl(other_pkgs, **kwargs)
382+
return
383+
elif https_pkgs:
384+
logger.info(
385+
"Smart threading: updating %d HTTPS package(s) serially...",
386+
len(https_pkgs),
387+
)
388+
original_threads = self.threads
389+
self.threads = 1
390+
self._update_impl(packages_list, **kwargs)
391+
self.threads = original_threads
392+
return
393+
394+
# Normal processing (smart_threading disabled or threads=1)
395+
self._update_impl(packages_list, **kwargs)
396+
397+
def _update_impl(self, packages: typing.List[str], **kwargs) -> None:
398+
"""Internal implementation of update logic."""
399+
the_queue: queue.Queue = queue.Queue()
296400
for name in packages:
297401
kw = kwargs.copy()
298402
if name not in self.sources:

0 commit comments

Comments
 (0)