Skip to content

fix: dispose source streams in writable archive async add entry#1310

Closed
puk06 wants to merge 2 commits into
adamhathcock:masterfrom
puk06:fix/writable-archive-async-dispose
Closed

fix: dispose source streams in writable archive async add entry#1310
puk06 wants to merge 2 commits into
adamhathcock:masterfrom
puk06:fix/writable-archive-async-dispose

Conversation

@puk06

@puk06 puk06 commented Apr 29, 2026

Copy link
Copy Markdown
Contributor

This pull request introduces an important resource management improvement to the archive disposal process and adds a new test to ensure source files are properly released after creating a tar archive asynchronously.

Resource management improvements:

  • Added an override for DisposeAsync in AbstractWritableArchive.Async.cs to ensure that all entries in newEntries, removedEntries, and modifiedEntries are properly closed when the archive is disposed asynchronously. This helps prevent resource leaks and file locks.

Testing enhancements:

  • Added a new test TarAsync_AddEntryAndReleaseSourceFile in TarArchiveAsyncTests.cs to verify that after creating and disposing a tar archive asynchronously, the source file is no longer locked and can be deleted, ensuring correct resource release.

Co-authored-by: Copilot <copilot@github.com>
Copilot AI review requested due to automatic review settings April 29, 2026 04:33

Copilot AI left a comment

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.

Pull request overview

Improves async resource cleanup for writable archives so streams opened for newly added/removed/modified entries are released when an archive is disposed via await using, and adds an async tar test intended to catch file-handle leaks after AddEntryAsync(filePath).

Changes:

  • Add DisposeAsync override in AbstractWritableArchive.Async.cs to close entries tracked in newEntries, removedEntries, and modifiedEntries.
  • Add TarAsync_AddEntryAndReleaseSourceFile test to verify the source file can be deleted after async archive creation/disposal.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 4 comments.

File Description
src/SharpCompress/Archives/AbstractWritableArchive.Async.cs Ensures async disposal closes writable-entry sources tracked outside the base archive entry collection.
tests/SharpCompress.Test/Tar/TarArchiveAsyncTests.cs Adds a regression test aimed at detecting leaked file handles after async tar creation.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +210 to +218
try
{
File.Delete(archivePath);
File.Delete(sourcePath);
}
catch (IOException ex)
{
Assert.True(false, "Source file is still locked after disposing archive: " + ex.Message);
}

Copilot AI Apr 29, 2026

Copy link

Choose a reason for hiding this comment

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

Catching IOException and then calling Assert.True(false, ...) hides the original stack location of the failure and the message is ambiguous because both archivePath and sourcePath are deleted in the same try block. It’s clearer to let the delete/open failure throw naturally, or split the operations and assert with a message that identifies which file was locked.

Suggested change
try
{
File.Delete(archivePath);
File.Delete(sourcePath);
}
catch (IOException ex)
{
Assert.True(false, "Source file is still locked after disposing archive: " + ex.Message);
}
File.Delete(archivePath);
File.Delete(sourcePath);

Copilot uses AI. Check for mistakes.
new TarWriterOptions(CompressionType.None) { LeaveStreamOpen = false }
);
}

Copilot AI Apr 29, 2026

Copy link

Choose a reason for hiding this comment

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

This test may be flaky under LEGACY_DOTNET (net48) given the existing pattern in this file of adding a post-await using delay because async disposal can lag behind releasing file handles. Consider adding the same #if LEGACY_DOTNET delay after disposing the archive, before attempting to delete the source file.

Suggested change
#if LEGACY_DOTNET
await Task.Delay(100);
#endif

Copilot uses AI. Check for mistakes.
Comment on lines +209 to +217
// Assert: after the archive is disposed the source file should no longer be locked
try
{
File.Delete(archivePath);
File.Delete(sourcePath);
}
catch (IOException ex)
{
Assert.True(false, "Source file is still locked after disposing archive: " + ex.Message);

Copilot AI Apr 29, 2026

Copy link

Choose a reason for hiding this comment

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

This test’s assertion relies on Windows-style file locking (delete fails when a file is still open). On Linux/macOS, deleting an in-use file is allowed, so the test can pass even if the source stream is leaked. Consider wrapping the lock/assert portion in #if WINDOWS (or providing a non-Windows variant with a different assertion) so the test meaningfully validates the intended behavior in CI across OSes.

Suggested change
// Assert: after the archive is disposed the source file should no longer be locked
try
{
File.Delete(archivePath);
File.Delete(sourcePath);
}
catch (IOException ex)
{
Assert.True(false, "Source file is still locked after disposing archive: " + ex.Message);
// Assert: after the archive is disposed the source file should no longer be locked.
// Windows enforces this through delete semantics; Unix-like platforms do not.
if (OperatingSystem.IsWindows())
{
try
{
File.Delete(archivePath);
File.Delete(sourcePath);
}
catch (IOException ex)
{
Assert.True(false, "Source file is still locked after disposing archive: " + ex.Message);
}
}
else
{
if (File.Exists(archivePath))
{
File.Delete(archivePath);
}
if (File.Exists(sourcePath))
{
File.Delete(sourcePath);
}

Copilot uses AI. Check for mistakes.
FileAccess.Write,
FileShare.None,
bufferSize: 1024 * 1024,
FileOptions.SequentialScan

Copilot AI Apr 29, 2026

Copy link

Choose a reason for hiding this comment

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

FileOptions.SequentialScan is primarily a read-optimization hint and is confusing (and potentially ineffective) on a write-only FileStream. Consider removing it or using FileOptions.Asynchronous here if the intent is to optimize async writes.

Suggested change
FileOptions.SequentialScan
FileOptions.Asynchronous

Copilot uses AI. Check for mistakes.
@adamhathcock

Copy link
Copy Markdown
Owner

I've noticed there's a lot of issues around writing and async. I'm working on a fix now

@puk06

puk06 commented Apr 29, 2026

Copy link
Copy Markdown
Contributor Author

Good luck!
Feel free to use this pull request or close it—either is fine!

@adamhathcock

Copy link
Copy Markdown
Owner

Closing in favor #1311

@puk06 puk06 deleted the fix/writable-archive-async-dispose branch May 9, 2026 02:08
@puk06 puk06 restored the fix/writable-archive-async-dispose branch May 28, 2026 06:52
@puk06

puk06 commented May 28, 2026

Copy link
Copy Markdown
Contributor Author

I have a question about this previously closed PR.

Was DisposeAsync actually included in the 0.49.0 release? Looking at the current master branch, it still does not appear to be implemented. This means that even when using the await using pattern as shown below, the resource is never properly released since there is no actual disposal logic:

await using IWritableAsyncArchive<TarWriterOptions> archive = await TarArchive.CreateAsyncArchive();

Could you clarify whether this was intentionally deferred, or if it was perhaps missed in this PR? Thank you.

@adamhathcock

Copy link
Copy Markdown
Owner

There is an attempt to do that. That should be fine. Please open a new issue if you think it's wrong

I believe I test that here:

public async ValueTask Tar_Create_New_Async()

@puk06

puk06 commented May 28, 2026

Copy link
Copy Markdown
Contributor Author

Thank you very much. There's a chance it may have been my misunderstanding, so I'll test it on my end as well.
Thank you for taking the time to reply.

@puk06

puk06 commented May 28, 2026

Copy link
Copy Markdown
Contributor Author

There is an attempt to do that. That should be fine. Please open a new issue if you think it's wrong

I believe I test that here:

public async ValueTask Tar_Create_New_Async()

The existing Tar_Create_New_Async test is not sufficient to reproduce this issue because it only verifies that the archive contents are correct. It does not attempt to delete the output directory immediately after SaveToAsync, so it cannot detect whether the output file is still locked. To reproduce the bug, the test must try to delete the output directory right after SaveToAsync while the writable archive is still in scope.

So I’ve opened a new issue, so I’d appreciate it if you could refer to that one.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants