Skip to content

fix: make stdio redirection scoped and exception-safe - #243

Open
Ayoubbelguellaoui wants to merge 9 commits into
openecos-projects:mainfrom
Ayoubbelguellaoui:main
Open

fix: make stdio redirection scoped and exception-safe#243
Ayoubbelguellaoui wants to merge 9 commits into
openecos-projects:mainfrom
Ayoubbelguellaoui:main

Conversation

@Ayoubbelguellaoui

Copy link
Copy Markdown
Contributor

What Changed

  • Replaced process-global stdout/stderr redirection in chipcompiler/utility/log.py with a thread-safe, context-managed _StdioRedirect implementation that safely saves and restores file descriptors even upon exceptions.

Updated chipcompiler/engine/flow.py, chipcompiler/engine/rerun.py, and agent/engine.py to ensure step-level log redirects close cleanly.

Updated chipcompiler/runtime/operations.py to treat event publisher exceptions as non-fatal and wrapped active operation tracking in finally blocks to guarantee cleanup.

Added and updated tests in test/cli/rendering/test_progress.py, test/runtime/test_events.py, and test/runtime/test_operations.py to cover stdio restoration, thread isolation under concurrency, and resilient operation tracking.

Scope

Select the areas touched by this PR:

  • CLI - command behavior, Typer command surface, output formats, or workspace commands.
  • Flow/runtime - workspace lifecycle, EngineFlow, step execution, logs, metrics, or artifacts.
  • EDA integration - Yosys, ECC-Tools, DreamPlace, KLayout, PDKs, or native/runtime wrappers.
  • Build/package - Nix, PyInstaller, wheels, uv.lock, or release artifacts.
  • CI/release - GitHub Actions, version checks, changelog, or release automation.
  • Tests/docs only

Runtime And Packaging Impact

  • No runtime or packaging impact
  • CLI output or machine-readable contract changed
  • Workspace layout, flow state, or artifact paths changed
  • Native toolchain or wrapper behavior changed
  • ecc-tools or ecc-dreamplace dependency changed
  • PyInstaller, Nix, or release artifact changed

Notes:

  • Fixed critical bugs where process-global stdio redirection corrupts concurrent flow logs, and where event publisher failures leave active operations permanently uncleaned.

Validation

List the commands you ran. Mark checks that are not applicable as N/A.

  • uv run pytest test/
  • uv run ruff check chipcompiler test
  • uv run ruff format --check chipcompiler test
  • PyInstaller smoke: ecc --help, ecc --version, ecc version --json
  • Nix smoke: nix run .#cli -- --help
  • Manual flow smoke:
  • Other: - uv run pytest test/ — 1460 passed, 8 skipped, 4 xfailed, 2 environment-related failures.

Skipped checks and reason:

Checklist

  • I kept the change scoped to ECC.
  • I updated docs or user-facing CLI text where behavior changed.
  • I included lockfile or version metadata updates when dependencies changed.
  • I documented any submodule updates and why they are needed.
  • I did not include local caches, virtual environments, or generated build outputs.
  • I explained skipped validation and remaining risk.

@Ayoubbelguellaoui Ayoubbelguellaoui changed the title stdio redirection + operation cleanup fix: make stdio redirection scoped and exception-safe Aug 25, 2026
@Emin017

Emin017 commented Aug 25, 2026

Copy link
Copy Markdown
Member

Thank you for your contribution! However, does this PR have any overlapping parts with #236?

@Ayoubbelguellaoui

Copy link
Copy Markdown
Contributor Author

There is some overlap at the step-execution/runtime level, particularly around EngineFlow and failure cleanup, but the two PRs address different issues.

PR #236 focuses on containing tool failures, persisting Incomplete, interrupted-operation recovery, and terminal failure state propagation.

This PR focuses specifically on making process-global stdout/stderr redirection scoped and exception-safe, isolating concurrent redirects, and guaranteeing runtime operation cleanup when event publishing fails.

The overlap is therefore limited to the surrounding execution/cleanup paths; the implementations address separate failure modes.

@Emin017

Emin017 commented Aug 25, 2026

Copy link
Copy Markdown
Member

This PR focuses specifically on making process-global stdout/stderr redirection scoped and exception-safe, isolating concurrent redirects, and guaranteeing runtime operation cleanup when event publishing fails.

The overlap is therefore limited to the surrounding execution/cleanup paths; the implementations address separate failure modes.

Got it, I just merged #236. Could you rebase and resolve the conflicts?

@Emin017

Emin017 commented Aug 26, 2026

Copy link
Copy Markdown
Member

Hi @Ayoubbelguellaoui , I’d like to ask why this PR was closed. Is there anything we can help with?

@Ayoubbelguellaoui

Copy link
Copy Markdown
Contributor Author

just for making sur that is work good , thanks i will reopen it

@Emin017

Emin017 commented Aug 26, 2026

Copy link
Copy Markdown
Member

@Ayoubbelguellaoui Is this PR ready to be merged?

@Ayoubbelguellaoui

Copy link
Copy Markdown
Contributor Author

yes it's ready

@Emin017 Emin017 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 - Close the process-level redirect created by init_api_runtime_log

At this line, _StdioRedirect.__enter__() saves dup(1)/dup(2), but the instance is immediately discarded. Since _restore() is only reached through close()/__exit__(), every call leaks two file descriptors and there is no handle to restore or replace the previous redirect. Repeated initialization on the same process grows the fd count (8 -> 10 -> 12 in a local check). Please retain a process-owned handle and close the previous redirect before reinitializing, and add a repeated-init cleanup test.

Comment thread chipcompiler/utility/log.py Outdated
Comment thread chipcompiler/utility/log.py Outdated
Comment thread agent/engine.py Outdated
Comment thread test/test_engine_flow.py Outdated

@Emin017 Emin017 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The previous step-log failure-summary issue is fixed, but the new capture error path introduces a blocking execution regression. Please address the inline finding before merging.

Comment thread chipcompiler/utility/log.py Outdated
@Ayoubbelguellaoui

Copy link
Copy Markdown
Contributor Author

fixed !

@Emin017 Emin017 added the bug Something isn't working label Sep 3, 2026
@Emin017 Emin017 added this to the 0.1.0-alpha.12 milestone Sep 3, 2026

@Emin017 Emin017 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The previous exception-escape regression is addressed, but the latest fixes still leave two blocking execution/ownership regressions. Please address the inline findings before merging.

except (Exception, SystemExit) as exc:
step_error = record_tool_failure(workspace.logger, step_tag, exc)
with capture_stdio_to_file(log_file) as capture_ok:
if not capture_ok:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Do not skip tool execution when log capture fails

When capture_ok is false, this branch sets step_error and bypasses run_step() entirely. AgentEngineFlow now does the same. A step-log path is an observability output, not a prerequisite for running the EDA tool; the base revision logged the redirection failure and still invoked the tool. With workspace_step.log.file pointing to a directory and the tool/check mocks returning success, I changed the new regression tests to require those mocks to be called: both the Agent and standard EngineFlow cases fail because neither tool function runs. The added tests currently only expect Incomplete and never assert that the mocks were invoked, so they encode this regression as the desired behavior.

Please keep tool execution independent of whether capture setup succeeds (logging the capture failure through the remaining usable channel), then determine the step state from the real tool result and outputs. Add call assertions to both unusable-log-path tests so a logging failure cannot silently cancel implementation work.

"operation.completed",
{"result": result},
)
self._cleanup_workspace_active(operation_id)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Make the terminal transition and active-owner cleanup atomic, with a finally fallback

This cleanup runs only after the lock that records succeeded has been released. A polling client can therefore observe the terminal state and immediately call start(), but still receive RuntimeOperationConflict because _active_by_workspace points to the completed operation. I reproduced this deterministically by blocking _cleanup_workspace_active() after the terminal state was written: operation_status() returned succeeded, while the next operation for the same workspace was rejected.

Moving cleanup out of the outer finally also loses the previous guarantee for uncaught BaseExceptions. A runner that raises SystemExit now leaves the workspace permanently present in shutdown_barrier() and blocks all later operations; the same test passes on the base revision. Please remove the active mapping in the same critical section that publishes the terminal state, and retain guaranteed cleanup in finally for exits that do not produce a normal terminal event. Add regression tests for both the terminal-state/start race and a runner SystemExit.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants