1 // Copyright 2016 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4
5 #include "chrome/installer/util/delete_old_versions.h"
6
7 #include <map>
8 #include <memory>
9 #include <set>
10 #include <vector>
11
12 #include "base/file_version_info.h"
13 #include "base/files/file.h"
14 #include "base/files/file_enumerator.h"
15 #include "base/files/file_path.h"
16 #include "base/files/file_util.h"
17 #include "base/logging.h"
18 #include "base/stl_util.h"
19 #include "base/version.h"
20 #include "chrome/installer/util/util_constants.h"
21
22 namespace installer {
23
24 namespace {
25
26 using PathVector = std::vector<base::FilePath>;
27 using DirectorySet = std::set<base::FilePath>;
28 using ExecutableMap = std::map<base::FilePath, PathVector>;
29
30 // Returns the name of the version directory for executable |exe_path|.
GetExecutableVersionDirName(const base::FilePath & exe_path)31 base::FilePath GetExecutableVersionDirName(const base::FilePath& exe_path) {
32 std::unique_ptr<FileVersionInfo> file_version_info(
33 FileVersionInfo::CreateFileVersionInfo(exe_path));
34 if (!file_version_info.get())
35 return base::FilePath();
36 return base::FilePath(file_version_info->file_version());
37 }
38
39 // Returns the names of the old version directories found in |install_dir|. The
40 // directories named after the version of chrome.exe or new_chrome.exe are
41 // excluded.
GetOldVersionDirectories(const base::FilePath & install_dir)42 DirectorySet GetOldVersionDirectories(const base::FilePath& install_dir) {
43 const base::FilePath new_chrome_exe_version_dir_name =
44 GetExecutableVersionDirName(install_dir.Append(kChromeNewExe));
45 const base::FilePath chrome_exe_version_dir_name =
46 GetExecutableVersionDirName(install_dir.Append(kChromeExe));
47
48 DirectorySet directories;
49 base::FileEnumerator enum_directories(install_dir, false,
50 base::FileEnumerator::DIRECTORIES);
51 for (base::FilePath directory_path = enum_directories.Next();
52 !directory_path.empty(); directory_path = enum_directories.Next()) {
53 const base::FilePath directory_name = directory_path.BaseName();
54 const base::Version version(directory_name.AsUTF8Unsafe());
55 const size_t kNumChromeVersionComponents = 4;
56 if (version.IsValid() &&
57 version.components().size() == kNumChromeVersionComponents &&
58 directory_name != new_chrome_exe_version_dir_name &&
59 directory_name != chrome_exe_version_dir_name) {
60 directories.insert(directory_name);
61 }
62 }
63 return directories;
64 }
65
66 // Returns a map where the keys are version directory names and values are paths
67 // of old_chrome*.exe executables found in |install_dir|.
GetOldExecutables(const base::FilePath & install_dir)68 ExecutableMap GetOldExecutables(const base::FilePath& install_dir) {
69 ExecutableMap executables;
70 base::FileEnumerator enum_executables(install_dir, false,
71 base::FileEnumerator::FILES,
72 FILE_PATH_LITERAL("old_chrome*.exe"));
73 for (base::FilePath exe_path = enum_executables.Next(); !exe_path.empty();
74 exe_path = enum_executables.Next()) {
75 executables[GetExecutableVersionDirName(exe_path)].push_back(exe_path);
76 }
77 return executables;
78 }
79
80 // Deletes directories that are in |directories| and don't have a matching
81 // executable in |executables|. Returns false if any such directories could not
82 // be deleted.
DeleteDirectoriesWithoutMatchingExecutable(const DirectorySet & directories,const ExecutableMap & executables,const base::FilePath & install_dir)83 bool DeleteDirectoriesWithoutMatchingExecutable(
84 const DirectorySet& directories,
85 const ExecutableMap& executables,
86 const base::FilePath& install_dir) {
87 bool success = true;
88 for (const base::FilePath& directory_name : directories) {
89 // Delete the directory if it doesn't have a matching executable.
90 if (!base::Contains(executables, directory_name)) {
91 const base::FilePath directory_path = install_dir.Append(directory_name);
92 LOG(WARNING) << "Attempting to delete stray directory "
93 << directory_path.value();
94 if (!base::DeletePathRecursively(directory_path)) {
95 PLOG(ERROR) << "Failed to delete stray directory "
96 << directory_path.value();
97 success = false;
98 }
99 }
100 }
101 return success;
102 }
103
104 // Deletes executables that are in |executables| and don't have a matching
105 // directory in |directories|. Returns false if any such files could not be
106 // deleted.
DeleteExecutablesWithoutMatchingDirectory(const DirectorySet & directories,const ExecutableMap & executables)107 bool DeleteExecutablesWithoutMatchingDirectory(
108 const DirectorySet& directories,
109 const ExecutableMap& executables) {
110 bool success = true;
111 for (const auto& version_and_executables : executables) {
112 const auto& version_dir_name = version_and_executables.first;
113 const auto& executables_for_version = version_and_executables.second;
114
115 // Don't delete the executables if they have a matching directory.
116 if (base::Contains(directories, version_dir_name))
117 continue;
118
119 // Delete executables for version |version_dir_name|.
120 for (const auto& executable_path : executables_for_version) {
121 const base::FilePath executable_name = executable_path.BaseName();
122 LOG(WARNING) << "Attempting to delete stray executable "
123 << executable_path.value();
124 if (!base::DeleteFile(executable_path)) {
125 PLOG(ERROR) << "Failed to delete stray executable "
126 << executable_path.value();
127 success = false;
128 }
129 }
130 }
131 return success;
132 }
133
134 // Opens |path| with options that prevent the file from being read or written
135 // via another handle. As long as the returned object is alive, it is guaranteed
136 // that |path| isn't in use. It can however be deleted.
GetFileLock(const base::FilePath & path)137 base::File GetFileLock(const base::FilePath& path) {
138 return base::File(path, base::File::FLAG_OPEN | base::File::FLAG_READ |
139 base::File::FLAG_EXCLUSIVE_READ |
140 base::File::FLAG_EXCLUSIVE_WRITE |
141 base::File::FLAG_SHARE_DELETE);
142 }
143
144 // Deletes |version_directory| and all executables in |version_executables| if
145 // no .exe or .dll file for the version is in use. Returns false if any file
146 // or directory for the version could not be deleted.
DeleteVersion(const base::FilePath & version_directory,const PathVector & version_executables)147 bool DeleteVersion(const base::FilePath& version_directory,
148 const PathVector& version_executables) {
149 std::vector<base::File> locks;
150 PathVector locked_file_paths;
151
152 // Lock .exe/.dll files in |version_directory|.
153 base::FileEnumerator enum_version_directory(version_directory, true,
154 base::FileEnumerator::FILES);
155 for (base::FilePath path = enum_version_directory.Next(); !path.empty();
156 path = enum_version_directory.Next()) {
157 if (!path.MatchesExtension(FILE_PATH_LITERAL(".exe")) &&
158 !path.MatchesExtension(FILE_PATH_LITERAL(".dll"))) {
159 continue;
160 }
161 locks.push_back(GetFileLock(path));
162 if (!locks.back().IsValid()) {
163 LOG(WARNING) << "Failed to delete old version "
164 << version_directory.value() << " because " << path.value()
165 << " is in use.";
166 return false;
167 }
168 locked_file_paths.push_back(path);
169 }
170
171 // Lock executables in |version_executables|.
172 for (const base::FilePath& executable_path : version_executables) {
173 locks.push_back(GetFileLock(executable_path));
174 if (!locks.back().IsValid()) {
175 LOG(WARNING) << "Failed to delete old version "
176 << version_directory.value() << " because "
177 << executable_path.value() << " is in use.";
178 return false;
179 }
180 locked_file_paths.push_back(executable_path);
181 }
182
183 bool success = true;
184
185 // Delete locked files. The files won't actually be deleted until the locks
186 // are released.
187 for (const base::FilePath& locked_file_path : locked_file_paths) {
188 if (!base::DeleteFile(locked_file_path)) {
189 PLOG(ERROR) << "Failed to delete locked file "
190 << locked_file_path.value();
191 success = false;
192 }
193 }
194
195 // Release the locks, causing the locked files to actually be deleted. The
196 // version directory can't be deleted before this is done.
197 locks.clear();
198
199 // Delete the version directory.
200 if (!base::DeletePathRecursively(version_directory)) {
201 PLOG(ERROR) << "Failed to delete version directory "
202 << version_directory.value();
203 success = false;
204 }
205
206 return success;
207 }
208
209 // For each executable in |executables| that has a matching directory in
210 // |directories|, tries to delete the executable and the matching directory. No
211 // deletion occurs for a given version if a .exe or .dll file for that version
212 // is in use. Returns false if any directory/executables pair could not be
213 // deleted.
DeleteMatchingExecutablesAndDirectories(const DirectorySet & directories,const ExecutableMap & executables,const base::FilePath & install_dir)214 bool DeleteMatchingExecutablesAndDirectories(
215 const DirectorySet& directories,
216 const ExecutableMap& executables,
217 const base::FilePath& install_dir) {
218 bool success = true;
219 for (const auto& directory_name : directories) {
220 // Don't delete the version unless the directory has at least one matching
221 // executable.
222 auto version_executables_it = executables.find(directory_name);
223 if (version_executables_it == executables.end())
224 continue;
225
226 // Try to delete all files for the version.
227 success &= DeleteVersion(install_dir.Append(directory_name),
228 version_executables_it->second);
229 }
230 return success;
231 }
232
233 } // namespace
234
DeleteOldVersions(const base::FilePath & install_dir)235 bool DeleteOldVersions(const base::FilePath& install_dir) {
236 const DirectorySet old_directories = GetOldVersionDirectories(install_dir);
237 const ExecutableMap old_executables = GetOldExecutables(install_dir);
238
239 bool success = true;
240 success &= DeleteDirectoriesWithoutMatchingExecutable(
241 old_directories, old_executables, install_dir);
242 success &= DeleteExecutablesWithoutMatchingDirectory(old_directories,
243 old_executables);
244 success &= DeleteMatchingExecutablesAndDirectories(
245 old_directories, old_executables, install_dir);
246
247 return success;
248 }
249
250 } // namespace installer
251