Skip to content

Commit ce23ff9

Browse files
Single file: Guard against partial cleanup of extracted files (#32649)
This change fixes #3778 This change is mainly targeted to be servicing fix for .net core 3.1. When executing single-file apps, if a pre-existing extraction exists, the contents of the extraction directory are now verified. If files are missing, they are recovered. **Extraction Algorithm** `ExtractionDir` = `$DOTNET_BUNDLE_EXTRACT_BASE_DIR/<app>/<bundle-id>` `WorkingDir` = `$DOTNET_BUNDLE_EXTRACT_BASE_DIR/<app>/<process-id>` If `ExtractionDir` does not exist, then Create `WorkingDir`, and extract bundle contents within it Attempt to rename `WorkingDir` as `ExtractionDir` If the rename succeeds, continue execution of the app If not, another process completed extraction; Verify and reuse this extraction (as in step 2). If `ExtractionDir` exists, then verify that it contains all files listed in the manifest. If all contents are available, Remove `WorkingDir` Continue execution of the app, reusing the contents. If certain files are missing within `ExtractionDir`, then For each missing file, do the following individually Extract the files within `WorkingDir` Create sub-paths within `ExtractionDir` if necessary Rename the file from `WorkingDir/path/<file>` to `ExtractionDir/path/<file>` unless `ExtractionDir/path/<file>` exists (extracted there by another process in the meantime) Remove `WorkingDir` Continue execution of the app. All of the renames above are done with appropriate retries to circumvent interference from anti-virus apps. **Startup time impact** * Console HelloWorld execution time: * Framework dependent app: Windows/Linux No measurable difference * Self-contained app: Windows: ~10ms additional * Self-contained app: Linux: No measurable difference * Greenshot Startup: * Self-contained Windows: No noticiable/measurable difference * NugetPackageExplorer Startup: * Self-contained Windows: No noticiable/measurable difference
1 parent 1961f80 commit ce23ff9

File tree

6 files changed

+280
-108
lines changed

6 files changed

+280
-108
lines changed

src/installer/corehost/cli/apphost/bundle/dir_utils.cpp

+52-3
Original file line numberDiff line numberDiff line change
@@ -56,17 +56,23 @@ void dir_utils_t::remove_directory_tree(const pal::string_t& path)
5656

5757
for (const pal::string_t &dir : dirs)
5858
{
59-
remove_directory_tree(dir);
59+
pal::string_t dir_path = path;
60+
append_path(&dir_path, dir.c_str());
61+
62+
remove_directory_tree(dir_path);
6063
}
6164

6265
std::vector<pal::string_t> files;
6366
pal::readdir(path, &files);
6467

6568
for (const pal::string_t &file : files)
6669
{
67-
if (!pal::remove(file.c_str()))
70+
pal::string_t file_path = path;
71+
append_path(&file_path, file.c_str());
72+
73+
if (!pal::remove(file_path.c_str()))
6874
{
69-
trace::warning(_X("Failed to remove temporary file [%s]."), file.c_str());
75+
trace::warning(_X("Failed to remove temporary file [%s]."), file_path.c_str());
7076
}
7177
}
7278

@@ -91,3 +97,46 @@ void dir_utils_t::fixup_path_separator(pal::string_t& path)
9197
}
9298
}
9399
}
100+
101+
// Retry the rename operation with some wait in between the attempts.
102+
// This is an attempt to workaround for possible file locking caused by AV software.
103+
104+
bool dir_utils_t::rename_with_retries(pal::string_t& old_name, pal::string_t& new_name, bool& dir_exists)
105+
{
106+
for (int retry_count=0; retry_count < 500; retry_count++)
107+
{
108+
if (pal::rename(old_name.c_str(), new_name.c_str()) == 0)
109+
{
110+
return true;
111+
}
112+
bool should_retry = errno == EACCES;
113+
114+
if (pal::directory_exists(new_name))
115+
{
116+
// Check directory_exists() on each run, because a concurrent process may have
117+
// created the new_name directory.
118+
//
119+
// The rename() operation above fails with errono == EACCESS if
120+
// * Directory new_name already exists, or
121+
// * Paths are invalid paths, or
122+
// * Due to locking/permission problems.
123+
// Therefore, we need to perform the directory_exists() check again.
124+
125+
dir_exists = true;
126+
return false;
127+
}
128+
129+
if (should_retry)
130+
{
131+
trace::info(_X("Retrying Rename [%s] to [%s] due to EACCES error"), old_name.c_str(), new_name.c_str());
132+
pal::sleep(100);
133+
continue;
134+
}
135+
else
136+
{
137+
return false;
138+
}
139+
}
140+
141+
return false;
142+
}

src/installer/corehost/cli/apphost/bundle/dir_utils.h

+1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ namespace bundle
1717
static void remove_directory_tree(const pal::string_t &path);
1818
static void create_directory_tree(const pal::string_t &path);
1919
static void fixup_path_separator(pal::string_t& path);
20+
static bool rename_with_retries(pal::string_t& old_name, pal::string_t& new_name, bool &new_dir_exists);
2021
};
2122
}
2223

src/installer/corehost/cli/apphost/bundle/extractor.cpp

+152-85
Original file line numberDiff line numberDiff line change
@@ -10,53 +10,63 @@
1010

1111
using namespace bundle;
1212

13-
// Compute the final extraction location as:
14-
// m_extraction_dir = $DOTNET_BUNDLE_EXTRACT_BASE_DIR/<app>/<id>/...
15-
//
16-
// If DOTNET_BUNDLE_EXTRACT_BASE_DIR is not set in the environment, the
17-
// base directory defaults to $TMPDIR/.net
18-
void extractor_t::determine_extraction_dir()
13+
pal::string_t& extractor_t::extraction_dir()
1914
{
20-
if (!pal::getenv(_X("DOTNET_BUNDLE_EXTRACT_BASE_DIR"), &m_extraction_dir))
15+
if (m_extraction_dir.empty())
2116
{
22-
if (!pal::get_default_bundle_extraction_base_dir(m_extraction_dir))
17+
// Compute the final extraction location as:
18+
// m_extraction_dir = $DOTNET_BUNDLE_EXTRACT_BASE_DIR/<app>/<id>/...
19+
//
20+
// If DOTNET_BUNDLE_EXTRACT_BASE_DIR is not set in the environment,
21+
// a default is choosen within the temporary directory.
22+
23+
if (!pal::getenv(_X("DOTNET_BUNDLE_EXTRACT_BASE_DIR"), &m_extraction_dir))
2324
{
24-
trace::error(_X("Failure processing application bundle."));
25-
trace::error(_X("Failed to determine location for extracting embedded files."));
26-
trace::error(_X("DOTNET_BUNDLE_EXTRACT_BASE_DIR is not set, and a read-write temp-directory couldn't be created."));
27-
throw StatusCode::BundleExtractionFailure;
25+
if (!pal::get_default_bundle_extraction_base_dir(m_extraction_dir))
26+
{
27+
trace::error(_X("Failure processing application bundle."));
28+
trace::error(_X("Failed to determine location for extracting embedded files."));
29+
trace::error(_X("DOTNET_BUNDLE_EXTRACT_BASE_DIR is not set, and a read-write temp-directory couldn't be created."));
30+
throw StatusCode::BundleExtractionFailure;
31+
}
2832
}
29-
}
3033

31-
pal::string_t host_name = strip_executable_ext(get_filename(m_bundle_path));
32-
append_path(&m_extraction_dir, host_name.c_str());
33-
append_path(&m_extraction_dir, m_bundle_id.c_str());
34+
pal::string_t host_name = strip_executable_ext(get_filename(m_bundle_path));
35+
append_path(&m_extraction_dir, host_name.c_str());
36+
append_path(&m_extraction_dir, m_bundle_id.c_str());
37+
38+
trace::info(_X("Files embedded within the bundled will be extracted to [%s] directory."), m_extraction_dir.c_str());
39+
}
3440

35-
trace::info(_X("Files embedded within the bundled will be extracted to [%s] directory."), m_extraction_dir.c_str());
41+
return m_extraction_dir;
3642
}
3743

38-
// Compute the working extraction location for this process, before the
39-
// extracted files are committed to the final location
40-
// m_working_extraction_dir = $DOTNET_BUNDLE_EXTRACT_BASE_DIR/<app>/<proc-id-hex>
41-
void extractor_t::determine_working_extraction_dir()
44+
pal::string_t& extractor_t::working_extraction_dir()
4245
{
43-
m_working_extraction_dir = get_directory(extraction_dir());
44-
pal::char_t pid[32];
45-
pal::snwprintf(pid, 32, _X("%x"), pal::get_pid());
46-
append_path(&m_working_extraction_dir, pid);
46+
if (m_working_extraction_dir.empty())
47+
{
48+
// Compute the working extraction location for this process,
49+
// before the extracted files are committed to the final location
50+
// working_extraction_dir = $DOTNET_BUNDLE_EXTRACT_BASE_DIR/<app>/<proc-id-hex>
4751

48-
dir_utils_t::create_directory_tree(m_working_extraction_dir);
52+
m_working_extraction_dir = get_directory(extraction_dir());
53+
pal::char_t pid[32];
54+
pal::snwprintf(pid, 32, _X("%x"), pal::get_pid());
55+
append_path(&m_working_extraction_dir, pid);
56+
57+
trace::info(_X("Temporary directory used to extract bundled files is [%s]."), m_working_extraction_dir.c_str());
58+
}
4959

50-
trace::info(_X("Temporary directory used to extract bundled files is [%s]."), m_working_extraction_dir.c_str());
60+
return m_working_extraction_dir;
5161
}
5262

5363
// Create a file to be extracted out on disk, including any intermediate sub-directories.
5464
FILE* extractor_t::create_extraction_file(const pal::string_t& relative_path)
5565
{
56-
pal::string_t file_path = m_working_extraction_dir;
66+
pal::string_t file_path = working_extraction_dir();
5767
append_path(&file_path, relative_path.c_str());
5868

59-
// m_working_extraction_dir is assumed to exist,
69+
// working_extraction_dir is assumed to exist,
6070
// so we only create sub-directories if relative_path contains directories
6171
if (dir_utils_t::has_dirs_in_path(relative_path))
6272
{
@@ -92,29 +102,6 @@ void extractor_t::extract(const file_entry_t &entry, reader_t &reader)
92102
fclose(file);
93103
}
94104

95-
pal::string_t& extractor_t::extraction_dir()
96-
{
97-
if (m_extraction_dir.empty())
98-
{
99-
determine_extraction_dir();
100-
}
101-
102-
return m_extraction_dir;
103-
}
104-
105-
bool extractor_t::can_reuse_extraction()
106-
{
107-
// In this version, the extracted files are assumed to be
108-
// correct by construction.
109-
//
110-
// Files embedded in the bundle are first extracted to m_working_extraction_dir
111-
// Once all files are successfully extracted, the extraction location is
112-
// committed (renamed) to m_extraction_dir. Therefore, the presence of
113-
// m_extraction_dir means that the files are pre-extracted.
114-
115-
return pal::directory_exists(extraction_dir());
116-
}
117-
118105
void extractor_t::begin()
119106
{
120107
// Files are extracted to a specific deterministic location on disk
@@ -126,58 +113,138 @@ void extractor_t::begin()
126113
//
127114
// In order to solve these issues, we implement a extraction as a two-phase approach:
128115
// 1) Files embedded in a bundle are extracted to a process-specific temporary
129-
// extraction location (m_working_extraction_dir)
130-
// 2) Upon successful extraction, m_working_extraction_dir is renamed to the actual
131-
// extraction location (m_extraction_dir)
116+
// extraction location (working_extraction_dir)
117+
// 2) Upon successful extraction, working_extraction_dir is renamed to the actual
118+
// extraction location (extraction_dir)
132119
//
133120
// This effectively creates a file-lock to protect against races and failed extractions.
134121

135-
determine_working_extraction_dir();
122+
123+
dir_utils_t::create_directory_tree(working_extraction_dir());
124+
}
125+
126+
void extractor_t::clean()
127+
{
128+
dir_utils_t::remove_directory_tree(working_extraction_dir());
136129
}
137130

138-
void extractor_t::commit()
131+
void extractor_t::commit_dir()
139132
{
140-
// Commit files to the final extraction directory
133+
// Commit an entire new extraction to the final extraction directory
141134
// Retry the move operation with some wait in between the attempts. This is to workaround for possible file locking
142135
// caused by AV software. Basically the extraction process above writes a bunch of executable files to disk
143136
// and some AV software may decide to scan them on write. If this happens the files will be locked which blocks
144137
// our ablity to move them.
145-
int retry_count = 500;
146-
while (true)
138+
139+
bool extracted_by_concurrent_process = false;
140+
bool extracted_by_current_process =
141+
dir_utils_t::rename_with_retries(working_extraction_dir(), extraction_dir(), extracted_by_concurrent_process);
142+
143+
if (extracted_by_concurrent_process)
147144
{
148-
if (pal::rename(m_working_extraction_dir.c_str(), m_extraction_dir.c_str()) == 0)
149-
break;
145+
// Another process successfully extracted the dependencies
146+
trace::info(_X("Extraction completed by another process, aborting current extraction."));
147+
clean();
148+
}
150149

151-
bool should_retry = errno == EACCES;
152-
if (can_reuse_extraction())
153-
{
154-
// Another process successfully extracted the dependencies
155-
trace::info(_X("Extraction completed by another process, aborting current extraction."));
150+
if (!extracted_by_current_process && !extracted_by_concurrent_process)
151+
{
152+
trace::error(_X("Failure processing application bundle."));
153+
trace::error(_X("Failed to commit extracted files to directory [%s]."), extraction_dir().c_str());
154+
throw StatusCode::BundleExtractionFailure;
155+
}
156156

157-
dir_utils_t::remove_directory_tree(m_working_extraction_dir);
158-
break;
159-
}
157+
trace::info(_X("Completed new extraction."));
158+
}
160159

161-
if (should_retry && (retry_count--) > 0)
162-
{
163-
trace::info(_X("Retrying extraction due to EACCES trying to rename the extraction folder to [%s]."), m_extraction_dir.c_str());
164-
pal::sleep(100);
165-
continue;
166-
}
167-
else
168-
{
169-
trace::error(_X("Failure processing application bundle."));
170-
trace::error(_X("Failed to commit extracted files to directory [%s]."), m_extraction_dir.c_str());
171-
throw StatusCode::BundleExtractionFailure;
172-
}
160+
void extractor_t::commit_file(const pal::string_t& relative_path)
161+
{
162+
// Commit individual files to the final extraction directory.
163+
164+
pal::string_t working_file_path = working_extraction_dir();
165+
append_path(&working_file_path, relative_path.c_str());
166+
167+
pal::string_t final_file_path = extraction_dir();
168+
append_path(&final_file_path, relative_path.c_str());
169+
170+
if (dir_utils_t::has_dirs_in_path(relative_path))
171+
{
172+
dir_utils_t::create_directory_tree(get_directory(final_file_path));
173173
}
174+
175+
bool extracted_by_concurrent_process = false;
176+
bool extracted_by_current_process =
177+
dir_utils_t::rename_with_retries(working_file_path, final_file_path, extracted_by_concurrent_process);
178+
179+
if (extracted_by_concurrent_process)
180+
{
181+
// Another process successfully extracted the dependencies
182+
trace::info(_X("Extraction completed by another process, aborting current extraction."));
183+
}
184+
185+
if (!extracted_by_current_process && !extracted_by_concurrent_process)
186+
{
187+
trace::error(_X("Failure processing application bundle."));
188+
trace::error(_X("Failed to commit extracted files to directory [%s]."), extraction_dir().c_str());
189+
throw StatusCode::BundleExtractionFailure;
190+
}
191+
192+
trace::info(_X("Extraction recovered [%s]"), relative_path.c_str());
174193
}
175194

176-
void extractor_t::extract(const manifest_t& manifest, reader_t& reader)
195+
void extractor_t::extract_new(reader_t& reader)
177196
{
178197
begin();
179-
for (const file_entry_t& entry : manifest.files) {
198+
for (const file_entry_t& entry : m_manifest.files)
199+
{
180200
extract(entry, reader);
181201
}
182-
commit();
202+
commit_dir();
203+
}
204+
205+
// Verify an existing extraction contains all files listed in the bundle manifest.
206+
// If some files are missing, extract them individually.
207+
void extractor_t::verify_recover_extraction(reader_t& reader)
208+
{
209+
pal::string_t& ext_dir = extraction_dir();
210+
bool recovered = false;
211+
212+
for (const file_entry_t& entry : m_manifest.files)
213+
{
214+
pal::string_t file_path = ext_dir;
215+
append_path(&file_path, entry.relative_path().c_str());
216+
217+
if (!pal::file_exists(file_path))
218+
{
219+
if (!recovered)
220+
{
221+
recovered = true;
222+
begin();
223+
}
224+
225+
extract(entry, reader);
226+
commit_file(entry.relative_path());
227+
}
228+
}
229+
230+
if (recovered)
231+
{
232+
clean();
233+
}
234+
}
235+
236+
pal::string_t& extractor_t::extract(reader_t& reader)
237+
{
238+
if (pal::directory_exists(extraction_dir()))
239+
{
240+
trace::info(_X("Reusing existing extraction of application bundle."));
241+
verify_recover_extraction(reader);
242+
}
243+
else
244+
{
245+
trace::info(_X("Starting new extraction of application bundle."));
246+
extract_new(reader);
247+
}
248+
249+
return m_extraction_dir;
183250
}

0 commit comments

Comments
 (0)