Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

SingleFile bundles: Ensure extraction mappings are closed on Windows. #2272

Merged
1 commit merged into from
Feb 14, 2020

Conversation

swaroop-sridhar
Copy link
Contributor

When running a single-file app, the AppHost mmap()s itself in order to read its contents and extract the embedded contents.

The Apphost must always map its contents in order to read the headers, but doesn't always extract the contents, because previously extracted files are re-used when available.

In the case where apphost doesn't extract, currently, the file mapping isn't immediately closed on Windows. This prevents the app from being renamed while running -- an idiom used while updating the app in-place.

This change fixes the problem by closing the open file-map.

Fixes #1260

@@ -0,0 +1,16 @@
using System;
Copy link
Member

Choose a reason for hiding this comment

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

License header?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

There's no License header for any of the test assets in https://github.com/dotnet/runtime/tree/master/src/installer/test/Assets/TestProjects

So, I kept the same pattern.

Copy link
Member

Choose a reason for hiding this comment

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

Can you please fix that in a followup PR? Or at least create an issue for it.

Copy link
Contributor Author

@swaroop-sridhar swaroop-sridhar Feb 14, 2020

Choose a reason for hiding this comment

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

OK I'll fix it for all the projects in a subsequent PR.

@@ -109,7 +109,6 @@ void* pal::map_file_readonly(const pal::string_t& path, size_t &length)
if (map == NULL)
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
if (map == NULL)
if (address == NULL)

@@ -96,11 +96,11 @@ void* pal::map_file_readonly(const pal::string_t& path, size_t &length)
length = (size_t)fileSize.QuadPart;

HANDLE map = CreateFileMappingW(file, NULL, PAGE_READONLY, 0, 0, NULL);
CloseHandle(file);

if (map == NULL)
{
trace::warning(_X("Failed to map file. CreateFileMappingW(%s) failed with error %d"), path.c_str(), GetLastError());
Copy link
Member

Choose a reason for hiding this comment

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

CloseHandle above may alter GetLastError. If that happens, the last error in the log will be bogus.


if (map == NULL)
{
trace::warning(_X("Failed to map file. CreateFileMappingW(%s) failed with error %d"), path.c_str(), GetLastError());
CloseHandle(file);
return nullptr;
}

Copy link
Member

Choose a reason for hiding this comment

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

Do you also need to close map handle before the function returns?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

There is no API to close a File Mapping. The UnMapViewOfFile documentation says:
"Although an application may close the file handle used to create a file mapping object, the system holds the corresponding file open until the last view of the file is unmapped. Files for which the last view has not yet been unmapped are held open with no sharing restrictions."

CreateFileMapping documentation says:
"Mapped views of a file mapping object maintain internal references to the object, and a file mapping object does not close until all references to it are released. Therefore, to fully close a file mapping object, an application must unmap all mapped views of the file mapping object by calling UnmapViewOfFile and close the file mapping object handle by calling CloseHandle. These functions can be called in any order."

Therefore, I think map handle cannot be closed; the sharing violation was because of the FileHandle.

Copy link
Member

@jkotas jkotas Jan 28, 2020

Choose a reason for hiding this comment

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

The documentation you have quoted says that you should close the map handle using CloseHandle.

You should close it even if it does not cause any obvious problem like sharing violation. It is good practice to avoid leaking resources.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks @jkotas. I misunderstood the text to mean that the mapping handle is automatically closed after all the references are unmapped. I've fixed it now.

Comment on lines 109 to 120
if (address == NULL)
{
trace::warning(_X("Failed to map file. MapViewOfFile(%s) failed with error %d"), path.c_str(), GetLastError());
CloseHandle(file);
CloseHandle(map);
return nullptr;
}

// The file-handle (file) and mapping object handle (map) can be safely closed
// once the file is mapped. The OS keeps the file open if there is an open mapping into the file.

CloseHandle(file);
CloseHandle(map);

return address;
Copy link
Contributor

Choose a reason for hiding this comment

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

Nitpick, but this could be written as:

if (address == nullptr)
{
    trace::warning(...);
}

// ...
CloseHandle(file);
CloseHandle(map);

return address;

(Slightly shorter code.)

@@ -291,6 +292,11 @@ private void ValidateRequiredDirectories(RepoDirectoriesProvider repoDirectories
publishArgs.Add(outputDirectory);
}

if (singleFile)
{
publishArgs.Add($"/p:PublishSingleFile=true");
Copy link
Contributor

@lpereira lpereira Feb 7, 2020

Choose a reason for hiding this comment

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

Nitpick: do we need string interpolation here? (Nevermind. This is just for testing, so it's OK.)

@lpereira
Copy link
Contributor

lpereira commented Feb 7, 2020

LGTM sans a nit.

@swaroop-sridhar
Copy link
Contributor Author

Thanks @lpereira ; I've made the changes you suggested.
I also made another small change to print mmap() failures as trace::error() instead of trace::warning() in the host trace.

@swaroop-sridhar
Copy link
Contributor Author

@jkotas @lpereira do you have any further concerns/suggestions for this PR?
Thanks.

// once the file is mapped. The OS keeps the file open if there is an open mapping into the file.

CloseHandle(file);
CloseHandle(map);
Copy link
Member

Choose a reason for hiding this comment

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

Nit: It would make more sense to swap the order - we first open the file and then create the mapping, so closing them should be done in reverse order. Thus close the mapping first and then close the file. (I know it doesn't matter as the OS will do the right thing in both cases anyway).

@@ -0,0 +1,16 @@
using System;
Copy link
Member

Choose a reason for hiding this comment

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

Can you please fix that in a followup PR? Or at least create an issue for it.

When running a single-file app, the AppHost mmap()s itself in order
to read its contents and extract the embedded contents.

The Apphost must always map its contents in order to read the headers,
but doesn't always extract the contents, because previously extracted
files are re-used when available.

In the case where apphost doesn't extract, the file mapping wasn't
immediately closed on Windows. This prevents the app from being renamed
while running -- an idiom used while updating the app in-place.
@ghost
Copy link

ghost commented Feb 14, 2020

Hello @swaroop-sridhar!

Because this pull request has the auto-merge label, I will be glad to assist with helping to merge this pull request once all check-in policies pass.

p.s. you can customize the way I help with merging this pull request, such as holding this pull request until a specific person approves. Simply @mention me (@msftbot) and give me an instruction to get started! Learn more here.

@swaroop-sridhar
Copy link
Contributor Author

@msftbot Squash and merge once testing succeeds.

@@ -22,6 +22,11 @@ public static string GetAppPath(TestProjectFixture fixture)
return Path.Combine(GetPublishPath(fixture), GetAppName(fixture));
}

public static string GetPublishedSingleFilePath(TestProjectFixture fixture)
{
return GetHostPath(fixture);
Copy link
Member

Choose a reason for hiding this comment

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

Do we really need a separate function for this if all it is calling is GetHostPath?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I do think it is useful to have the wrapper call. In the single-file tests, we generate single-file apps in two ways:

  • Through the SDK (dotnet publish /p:PublishSingleFile=true)
  • Through invoking HostModel functions directly.

In the former case GetHostPath() will be the pre-bundled app, whereas in the later case, GetHostPath() will just be the host. So, I think it is easier to refer to them by different names.
Helps documenting the intent, and in searching for the use with different semantics.

Copy link
Member

Choose a reason for hiding this comment

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

Ahh, okay. I didn't realize the different use cases and just assumed the paths in BundleHelper were for the pre-bundled app.

@ghost ghost merged commit b95e523 into dotnet:master Feb 14, 2020
swaroop-sridhar added a commit to swaroop-sridhar/core-setup that referenced this pull request Feb 28, 2020
dotnet/runtime#1260

A single-file app cannot be renamed while running -- an idiom used while updating the app in-place.

When running a single-file app, the AppHost reads itself in order to extract the embedded contents.
The Apphost must always read its contents in order to read the headers, but doesn't always extract the contents, because previously extracted files are re-used when available.

In the case where apphost doesn't extract, currently, the file stream isn't immediately closed on Windows.
This prevents the app from being renamed while running.

This change fixes the problem by closing the open stream in all cases.

Very Low

dotnet/runtime#2272
swaroop-sridhar added a commit to swaroop-sridhar/core-setup that referenced this pull request Feb 28, 2020
dotnet/runtime#1260

A single-file app cannot be renamed while running -- an idiom used while updating the app in-place.

When running a single-file app, the AppHost reads itself in order to extract the embedded contents.
The Apphost must always read its contents in order to read the headers, but doesn't always extract the contents, because previously extracted files are re-used when available.

In the case where apphost doesn't extract, currently, the file stream isn't immediately closed on Windows.
This prevents the app from being renamed while running.

This change fixes the problem by closing the open stream in all cases.

Very Low

dotnet/runtime#2272
Anipik pushed a commit to dotnet/core-setup that referenced this pull request Mar 25, 2020
dotnet/runtime#1260

A single-file app cannot be renamed while running -- an idiom used while updating the app in-place.

When running a single-file app, the AppHost reads itself in order to extract the embedded contents.
The Apphost must always read its contents in order to read the headers, but doesn't always extract the contents, because previously extracted files are re-used when available.

In the case where apphost doesn't extract, currently, the file stream isn't immediately closed on Windows.
This prevents the app from being renamed while running.

This change fixes the problem by closing the open stream in all cases.

Very Low

dotnet/runtime#2272
@ghost ghost locked as resolved and limited conversation to collaborators Dec 11, 2020
This pull request was closed.
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Weird binary file lock with PublishSingleFile=true
6 participants