Skip to content

Fix PermissionError on Windows when building durable functions emulator image#8807

Merged
roger-zhangg merged 9 commits intodevelopfrom
dar-temp-windows
Mar 17, 2026
Merged

Fix PermissionError on Windows when building durable functions emulator image#8807
roger-zhangg merged 9 commits intodevelopfrom
dar-temp-windows

Conversation

@roger-zhangg
Copy link
Copy Markdown
Member

@roger-zhangg roger-zhangg commented Mar 16, 2026

Problem

Durable function integration tests (test_tier1_callback, test_tier1_execution, test_tier1_durable_invoke) fail on Windows (WSL2) runners with three distinct issues:

  1. PermissionError: [Errno 13] Permission denied: 'D:\\a\\_temp\\...Dockerfile'
  2. invalid volume specification: 'D:\a\aws-sam-cli\...'
  3. 'charmap' codec can't encode character '\u2705' and \r\n vs \n assertion mismatches
    All tests pass on Linux.

Failed integ test: https://github.com/aws/aws-sam-cli/actions/runs/23079727144/job/67046559248

Root Causes

1. NamedTemporaryFile file locking (PermissionError)

In _build_emulator_image(), NamedTemporaryFile with default delete=True holds an exclusive lock on the file while the with block is active. When tarfile.add() tries to read the same file, Windows enforces mandatory file locking and raises PermissionError. Linux doesn't enforce mandatory locks, so it works there.

2. Windows path in Docker volume mount

_get_emulator_data_dir() returns a Windows-style path (D:\a\aws-sam-cli\...), which is passed directly as a Docker volume mount key. Docker inside WSL2 cannot parse Windows paths. SAM CLI already has to_posix_path() for this exact conversion (D:\a\.../d/a/...), but it wasn't being used here.

3. Emoji encoding + CRLF line endings

CLI output uses Unicode emoji characters (✅❌💓🛑⚠️🔄) that Windows' default charmap codec cannot encode, causing click.echo() to crash when running the test in github windows runner. Additionally, Windows outputs \r\n line endings in stderr, but tests compare against \n.

Fix

NamedTemporaryFile

Use delete=False, close the file before tarfile.add() reads it, and manually clean up with os.unlink() in a finally block.

Volume mount path

Apply to_posix_path() to the emulator data directory before using it as a Docker volume key. This is a no-op on Linux/macOS (os.name != 'nt'), and on Windows it produces the /d/... format that both Docker Desktop and WSL2 Docker understand.

Emoji + CRLF

Set utf-8 as default encoding when running the integration tests.. Add \r\n\n normalization in integration test stderr comparisons.

Testing

@roger-zhangg roger-zhangg requested a review from a team as a code owner March 16, 2026 21:17
Use to_posix_path() to convert the emulator data directory path
from Windows-style (D:\a\...) to POSIX-style (/d/a/...) before
passing it as a Docker volume mount key. Docker inside WSL2 cannot
parse Windows paths in volume specifications.
…st volume paths

- Remove emoji characters from CLI output in durable_formatters.py and
  durable_callback_handler.py. Windows charmap codec cannot encode Unicode
  emojis, causing 'charmap codec can't encode character' errors.
- Normalize CRLF to LF in integration test stderr comparisons for
  test_execution.py and test_callback.py (Windows outputs \r\n).
- Update unit test volume path assertion to use to_posix_path() matching
  the source code change.
- Update all unit tests referencing the removed emoji strings.
Use encoding='utf-8' in start_command_with_streaming Popen call.
On Windows, text=True defaults to cp1252, which silently kills the
output reader thread if any non-cp1252 bytes appear, causing
wait_for_callback_id to time out.
Restore all emoji characters in durable CLI output. Instead of removing
them, set PYTHONUTF8=1 in the integration-tests.yml workflow env to
force Python to use UTF-8 on Windows. This makes click.echo handle
emoji correctly without changing the user-facing output.

volumes = {
emulator_data_dir: {"bind": "/tmp/.durable-executions-local", "mode": "rw"},
to_posix_path(emulator_data_dir): {"bind": "/tmp/.durable-executions-local", "mode": "rw"},
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Not an issue, but something I found and I leave it here in case it helps someone else.

This to_posix_path is done inside our Container class

# Make sure all mounts are of posix path style.
kwargs["volumes"] = {to_posix_path(host_dir): mount for host_dir, mount in kwargs["volumes"].items()}
so it gets applied to any class that extends from it or it uses it.. But this method is using self._docker_client.containers.create directly, without passing through the Container implementation, so it needs to do this again.

LOG.info("Sending first callback: %s", " ".join(succeed_command))
result = run_command(succeed_command)
self.assertEqual(result.process.returncode, 0)
stdout_str = result.stdout.decode("utf-8") if isinstance(result.stdout, bytes) else result.stdout
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Nit: we should have a util function for this check and conversion. Not a blocker but for clarity.

@roger-zhangg roger-zhangg added this pull request to the merge queue Mar 17, 2026
Merged via the queue into develop with commit 8f94713 Mar 17, 2026
45 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants