-
Notifications
You must be signed in to change notification settings - Fork 4.9k
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
Conversation
@@ -0,0 +1,16 @@ | |||
using System; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
License header?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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()); |
There was a problem hiding this comment.
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; | ||
} | ||
|
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
47743ac
to
dd4e9d3
Compare
c25ae8e
to
8efa026
Compare
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; |
There was a problem hiding this comment.
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"); |
There was a problem hiding this comment.
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.)
LGTM sans a nit. |
8efa026
to
071e047
Compare
Thanks @lpereira ; I've made the changes you suggested. |
1932524
to
75ef751
Compare
// once the file is mapped. The OS keeps the file open if there is an open mapping into the file. | ||
|
||
CloseHandle(file); | ||
CloseHandle(map); |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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.
75ef751
to
4f45b06
Compare
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.
4f45b06
to
3fa15c9
Compare
Hello @swaroop-sridhar! Because this pull request has the 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 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); |
There was a problem hiding this comment.
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
?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
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
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
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
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