OLD | NEW |
---|---|
1 // Copyright 2014 The Chromium Authors. All rights reserved. | 1 // Copyright 2014 The Chromium Authors. All rights reserved. |
2 // Use of this source code is governed by a BSD-style license that can be | 2 // Use of this source code is governed by a BSD-style license that can be |
3 // found in the LICENSE file. | 3 // found in the LICENSE file. |
4 | 4 |
5 #include "chrome/install_static/install_util.h" | 5 #include "chrome/install_static/install_util.h" |
6 | 6 |
7 #include <assert.h> | |
8 #include <stdlib.h> | |
9 #include <string.h> | |
7 #include <windows.h> | 10 #include <windows.h> |
grt (UTC plus 2)
2016/11/21 10:29:54
nit: i think we generally include windows.h first
scottmg
2016/11/21 19:34:00
Done.
| |
8 #include <assert.h> | |
9 #include <string.h> | |
10 | 11 |
11 #include <algorithm> | 12 #include <algorithm> |
12 #include <memory> | 13 #include <memory> |
13 #include <sstream> | 14 #include <sstream> |
14 | 15 |
15 #include "chrome/install_static/install_details.h" | 16 #include "chrome/install_static/install_details.h" |
16 #include "chrome/install_static/install_modes.h" | 17 #include "chrome/install_static/install_modes.h" |
18 #include "chrome/install_static/policy_path_parser.h" | |
17 #include "chrome_elf/nt_registry/nt_registry.h" | 19 #include "chrome_elf/nt_registry/nt_registry.h" |
18 | 20 |
19 namespace install_static { | 21 namespace install_static { |
20 | 22 |
21 ProcessType g_process_type = ProcessType::UNINITIALIZED; | 23 ProcessType g_process_type = ProcessType::UNINITIALIZED; |
22 | 24 |
23 const wchar_t kRegValueChromeStatsSample[] = L"UsageStatsInSample"; | 25 const wchar_t kRegValueChromeStatsSample[] = L"UsageStatsInSample"; |
24 | 26 |
25 // TODO(ananta) | 27 // TODO(ananta) |
26 // http://crbug.com/604923 | 28 // http://crbug.com/604923 |
27 // The constants defined in this file are also defined in chrome/installer and | 29 // The constants defined in this file are also defined in chrome/installer and |
28 // other places. we need to unify them. | 30 // other places. we need to unify them. |
29 const wchar_t kHeadless[] = L"CHROME_HEADLESS"; | 31 const wchar_t kHeadless[] = L"CHROME_HEADLESS"; |
30 const wchar_t kShowRestart[] = L"CHROME_CRASHED"; | 32 const wchar_t kShowRestart[] = L"CHROME_CRASHED"; |
31 const wchar_t kRestartInfo[] = L"CHROME_RESTART"; | 33 const wchar_t kRestartInfo[] = L"CHROME_RESTART"; |
32 const wchar_t kRtlLocale[] = L"RIGHT_TO_LEFT"; | 34 const wchar_t kRtlLocale[] = L"RIGHT_TO_LEFT"; |
33 | 35 |
34 const char kGpuProcess[] = "gpu-process"; | 36 const wchar_t kCrashpadHandler[] = L"crashpad-handler"; |
35 const char kPpapiPluginProcess[] = "ppapi"; | 37 const wchar_t kProcessType[] = L"type"; |
36 const char kRendererProcess[] = "renderer"; | 38 const wchar_t kUserDataDirSwitch[] = L"user-data-dir"; |
37 const char kUtilityProcess[] = "utility"; | 39 const wchar_t kUtilityProcess[] = L"utility"; |
38 const char kProcessType[] = "type"; | 40 |
39 const char kCrashpadHandler[] = "crashpad-handler"; | 41 #if defined(GOOGLE_CHROME_BUILD) |
42 const wchar_t kRegistryChromePolicyKey[] = | |
grt (UTC plus 2)
2016/11/21 10:29:54
please follow the strategy in ReportingIsEnforcedB
scottmg
2016/11/21 19:34:01
Done.
| |
43 L"SOFTWARE\\Policies\\Google\\Chrome"; | |
44 #else | |
45 const wchar_t kRegistryChromePolicyKey[] = | |
46 L"SOFTWARE\\Policies\\Chromium"; | |
47 #endif | |
48 const wchar_t kUserDataDirRegKey[] = L"UserDataDir"; | |
40 | 49 |
41 namespace { | 50 namespace { |
42 | 51 |
43 // TODO(ananta) | 52 // TODO(ananta) |
44 // http://crbug.com/604923 | 53 // http://crbug.com/604923 |
45 // The constants defined in this file are also defined in chrome/installer and | 54 // The constants defined in this file are also defined in chrome/installer and |
46 // other places. we need to unify them. | 55 // other places. we need to unify them. |
47 // Chrome channel display names. | 56 // Chrome channel display names. |
48 constexpr wchar_t kChromeChannelDev[] = L"dev"; | 57 constexpr wchar_t kChromeChannelDev[] = L"dev"; |
49 constexpr wchar_t kChromeChannelBeta[] = L"beta"; | 58 constexpr wchar_t kChromeChannelBeta[] = L"beta"; |
(...skipping 94 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
144 uint32_t size = 0; | 153 uint32_t size = 0; |
145 BOOL r = ::VerQueryValueW(version_resource, sub_block, &value, &size); | 154 BOOL r = ::VerQueryValueW(version_resource, sub_block, &value, &size); |
146 if (r && value) { | 155 if (r && value) { |
147 value_str->assign(static_cast<wchar_t*>(value)); | 156 value_str->assign(static_cast<wchar_t*>(value)); |
148 return true; | 157 return true; |
149 } | 158 } |
150 } | 159 } |
151 return false; | 160 return false; |
152 } | 161 } |
153 | 162 |
163 std::wstring MakeAbsoluteFilePath(const std::wstring& input) { | |
164 wchar_t file_path[MAX_PATH]; | |
165 if (!_wfullpath(file_path, input.c_str(), _countof(file_path))) | |
166 return std::wstring(); | |
167 return file_path; | |
168 } | |
169 | |
170 bool DirectoryExists(const std::wstring& path) { | |
171 DWORD file_attributes = ::GetFileAttributes(path.c_str()); | |
172 if (file_attributes == INVALID_FILE_ATTRIBUTES) | |
173 return false; | |
174 return (file_attributes & FILE_ATTRIBUTE_DIRECTORY) != 0; | |
175 } | |
176 | |
154 bool RecursiveDirectoryCreate(const std::wstring& full_path) { | 177 bool RecursiveDirectoryCreate(const std::wstring& full_path) { |
155 // If the path exists, we've succeeded if it's a directory, failed otherwise. | 178 // If the path exists, we've succeeded if it's a directory, failed otherwise. |
156 const wchar_t* full_path_str = full_path.c_str(); | 179 const wchar_t* full_path_str = full_path.c_str(); |
157 DWORD file_attributes = ::GetFileAttributes(full_path_str); | 180 DWORD file_attributes = ::GetFileAttributes(full_path_str); |
158 if (file_attributes != INVALID_FILE_ATTRIBUTES) { | 181 if (file_attributes != INVALID_FILE_ATTRIBUTES) { |
159 if ((file_attributes & FILE_ATTRIBUTE_DIRECTORY) != 0) { | 182 if ((file_attributes & FILE_ATTRIBUTE_DIRECTORY) != 0) { |
160 Trace(L"%hs( %ls directory exists )\n", __func__, full_path_str); | 183 Trace(L"%hs( %ls directory exists )\n", __func__, full_path_str); |
161 return true; | 184 return true; |
162 } | 185 } |
163 Trace(L"%hs( %ls directory conflicts with an existing file. )\n", __func__, | 186 Trace(L"%hs( %ls directory conflicts with an existing file. )\n", __func__, |
(...skipping 10 matching lines...) Expand all Loading... | |
174 std::size_t pos = full_path.find_last_of(L"/\\"); | 197 std::size_t pos = full_path.find_last_of(L"/\\"); |
175 if (pos != std::wstring::npos) { | 198 if (pos != std::wstring::npos) { |
176 parent_path = full_path.substr(0, pos); | 199 parent_path = full_path.substr(0, pos); |
177 if (!RecursiveDirectoryCreate(parent_path)) { | 200 if (!RecursiveDirectoryCreate(parent_path)) { |
178 Trace(L"Failed to create one of the parent directories"); | 201 Trace(L"Failed to create one of the parent directories"); |
179 return false; | 202 return false; |
180 } | 203 } |
181 } | 204 } |
182 if (!::CreateDirectory(full_path_str, nullptr)) { | 205 if (!::CreateDirectory(full_path_str, nullptr)) { |
183 DWORD error_code = ::GetLastError(); | 206 DWORD error_code = ::GetLastError(); |
184 if (error_code == ERROR_ALREADY_EXISTS) { | 207 if (error_code == ERROR_ALREADY_EXISTS && DirectoryExists(full_path_str)) { |
185 DWORD file_attributes = ::GetFileAttributes(full_path_str); | 208 // This error code ERROR_ALREADY_EXISTS doesn't indicate whether we |
186 if ((file_attributes != INVALID_FILE_ATTRIBUTES) && | 209 // were racing with someone creating the same directory, or a file |
187 ((file_attributes & FILE_ATTRIBUTE_DIRECTORY) != 0)) { | 210 // with the same path. If the directory exists, we lost the |
188 // This error code ERROR_ALREADY_EXISTS doesn't indicate whether we | 211 // race to create the same directory. |
189 // were racing with someone creating the same directory, or a file | 212 return true; |
190 // with the same path. If the directory exists, we lost the | 213 } else { |
191 // race to create the same directory. | 214 Trace(L"Failed to create directory %ls, last error is %d\n", |
192 return true; | 215 full_path_str, error_code); |
193 } else { | 216 return false; |
194 Trace(L"Failed to create directory %ls, last error is %d\n", | |
195 full_path_str, error_code); | |
196 return false; | |
197 } | |
198 } | 217 } |
199 } | 218 } |
200 return true; | 219 return true; |
201 } | 220 } |
202 | 221 |
203 // Appends "[kCompanyPathName\]kProductPathName[install_suffix]" to |path|, | 222 // Appends "[kCompanyPathName\]kProductPathName[install_suffix]" to |path|, |
204 // returning a reference to |path|. | 223 // returning a reference to |path|. |
205 std::wstring& AppendChromeInstallSubDirectory(std::wstring* path, | 224 std::wstring& AppendChromeInstallSubDirectory(std::wstring* path, |
206 bool include_suffix) { | 225 bool include_suffix) { |
207 if (*kCompanyPathName) { | 226 if (*kCompanyPathName) { |
(...skipping 144 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
352 // rules in the update configs. ChannelInfo::GetChannelName painstakingly | 371 // rules in the update configs. ChannelInfo::GetChannelName painstakingly |
353 // strips off known modifiers (e.g., "-multi-full") to see if the empty string | 372 // strips off known modifiers (e.g., "-multi-full") to see if the empty string |
354 // remains, returning channel "unknown" if not. This differs here in that some | 373 // remains, returning channel "unknown" if not. This differs here in that some |
355 // clients will tag crashes as "stable" rather than "unknown" via this | 374 // clients will tag crashes as "stable" rather than "unknown" via this |
356 // codepath, but it is an accurate reflection of which update channel the | 375 // codepath, but it is an accurate reflection of which update channel the |
357 // client is on according to the server-side rules. | 376 // client is on according to the server-side rules. |
358 | 377 |
359 return channel_name; | 378 return channel_name; |
360 } | 379 } |
361 | 380 |
381 // Checks if the key exists in the given hive and expands any string variables. | |
382 bool LoadUserDataDirPolicyFromRegistry(nt::ROOT_KEY hive, | |
383 const wchar_t* key_name, | |
384 std::wstring* dir) { | |
385 std::wstring value; | |
386 bool result = false; | |
387 HANDLE key; | |
388 if (nt::OpenRegKey(hive, kRegistryChromePolicyKey, KEY_READ, &key, nullptr)) { | |
grt (UTC plus 2)
2016/11/21 10:29:54
nit:
if (nt::QueryRegValueSZ(hive, nt::NONE, pol
scottmg
2016/11/21 19:34:01
Done.
| |
389 if (nt::QueryRegValueSZ(key, key_name, &value)) { | |
390 *dir = install_static::ExpandPathVariables(value); | |
grt (UTC plus 2)
2016/11/21 10:29:54
nit: omit "install_static::"
scottmg
2016/11/21 19:34:00
Done.
| |
391 result = true; | |
392 } | |
393 nt::CloseRegKey(key); | |
394 } | |
395 return result; | |
396 } | |
397 | |
398 void CheckUserDataDirPolicy(std::wstring* user_data_dir) { | |
grt (UTC plus 2)
2016/11/21 10:29:54
please document this. the caller assumes that |use
scottmg
2016/11/21 19:34:00
Done.
| |
399 assert(user_data_dir); | |
400 // Policy from the HKLM hive has precedence over HKCU. | |
grt (UTC plus 2)
2016/11/21 10:29:54
i don't think it would be a bad thing for this to
scottmg
2016/11/21 19:34:01
Done.
| |
401 if (!LoadUserDataDirPolicyFromRegistry(nt::HKLM, kUserDataDirRegKey, | |
402 user_data_dir)) { | |
403 LoadUserDataDirPolicyFromRegistry(nt::HKCU, kUserDataDirRegKey, | |
404 user_data_dir); | |
405 } | |
406 } | |
407 | |
362 } // namespace | 408 } // namespace |
363 | 409 |
364 bool IsSystemInstall() { | 410 bool IsSystemInstall() { |
365 return InstallDetails::Get().system_level(); | 411 return InstallDetails::Get().system_level(); |
366 } | 412 } |
367 | 413 |
368 bool IsMultiInstall() { | 414 bool IsMultiInstall() { |
369 return InstallDetails::Get().multi_install(); | 415 return InstallDetails::Get().multi_install(); |
370 } | 416 } |
371 | 417 |
(...skipping 120 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
492 | 538 |
493 result->swap(user_data_dir); | 539 result->swap(user_data_dir); |
494 if ((*result)[result->length() - 1] != L'\\') | 540 if ((*result)[result->length() - 1] != L'\\') |
495 result->push_back(L'\\'); | 541 result->push_back(L'\\'); |
496 AppendChromeInstallSubDirectory(result, true /* include_suffix */); | 542 AppendChromeInstallSubDirectory(result, true /* include_suffix */); |
497 result->push_back(L'\\'); | 543 result->push_back(L'\\'); |
498 result->append(kUserDataDirname); | 544 result->append(kUserDataDirname); |
499 return true; | 545 return true; |
500 } | 546 } |
501 | 547 |
502 bool GetDefaultCrashDumpLocation(std::wstring* crash_dir) { | 548 bool GetUserDataDirectory(const std::wstring& user_data_dir_from_command_line, |
grt (UTC plus 2)
2016/11/21 10:29:54
if i'm not mistaken, the only codepath for GetUser
scottmg
2016/11/21 19:34:00
Done. It's bit ugly to have to pass the partial In
| |
503 // In order to be able to start crash handling very early, we do not rely on | 549 std::wstring* result, |
504 // chrome's PathService entries (for DIR_CRASH_DUMPS) being available on | 550 std::wstring* invalid_supplied_directory) { |
505 // Windows. See https://crbug.com/564398. | 551 std::wstring user_data_dir = user_data_dir_from_command_line; |
506 if (!GetDefaultUserDataDirectory(crash_dir)) | 552 |
553 CheckUserDataDirPolicy(&user_data_dir); | |
554 | |
555 // On Windows, trailing separators leave Chrome in a bad state. See | |
556 // crbug.com/464616. | |
557 while (!user_data_dir.empty() && | |
558 (user_data_dir.back() == '\\' || user_data_dir.back() == '/')) { | |
559 user_data_dir.pop_back(); | |
560 } | |
561 | |
562 bool got_valid_directory = | |
563 !user_data_dir.empty() && RecursiveDirectoryCreate(user_data_dir); | |
564 if (!got_valid_directory) { | |
565 *invalid_supplied_directory = user_data_dir; | |
566 got_valid_directory = GetDefaultUserDataDirectory(&user_data_dir); | |
567 } | |
568 | |
569 // The Chrome implementation CHECKs() here in the browser process. We | |
570 // don't as this function is used to initialize crash reporting, so | |
571 // we would get no report of this failure. | |
572 assert(got_valid_directory); | |
573 if (!got_valid_directory) | |
507 return false; | 574 return false; |
508 | 575 |
509 // We have to make sure the user data dir exists on first run. See | 576 *result = MakeAbsoluteFilePath(user_data_dir); |
510 // http://crbug.com/591504. | 577 return true; |
511 if (!RecursiveDirectoryCreate(*crash_dir)) | 578 } |
512 return false; | 579 |
580 bool GetUserDataDirectoryUsingProcessCommandLine( | |
581 std::wstring* result, | |
582 std::wstring* invalid_supplied_directory) { | |
583 return GetUserDataDirectory( | |
584 GetSwitchValueFromCommandLine(::GetCommandLine(), | |
585 install_static::kUserDataDirSwitch), | |
grt (UTC plus 2)
2016/11/21 10:29:54
nit: omit "install_static::"
scottmg
2016/11/21 19:34:01
Done.
| |
586 result, invalid_supplied_directory); | |
587 } | |
588 | |
589 bool GetCrashDumpLocation(std::wstring* crash_dir) { | |
590 // In order to be able to start crash handling very early and in chrome_elf, | |
591 // we cannot rely on chrome's PathService entries (for DIR_CRASH_DUMPS) being | |
592 // available on Windows. See https://crbug.com/564398. | |
593 *crash_dir = install_static::InstallDetails::Get().user_data_dir(); | |
grt (UTC plus 2)
2016/11/21 10:29:54
nit: omit "install_static::"
scottmg
2016/11/21 19:34:00
Done.
| |
513 crash_dir->append(L"\\Crashpad"); | 594 crash_dir->append(L"\\Crashpad"); |
514 return true; | 595 return true; |
515 } | 596 } |
516 | 597 |
517 std::string GetEnvironmentString(const std::string& variable_name) { | 598 std::string GetEnvironmentString(const std::string& variable_name) { |
518 return UTF16ToUTF8( | 599 return UTF16ToUTF8( |
519 GetEnvironmentString16(UTF8ToUTF16(variable_name).c_str())); | 600 GetEnvironmentString16(UTF8ToUTF16(variable_name).c_str())); |
520 } | 601 } |
521 | 602 |
522 std::wstring GetEnvironmentString16(const wchar_t* variable_name) { | 603 std::wstring GetEnvironmentString16(const wchar_t* variable_name) { |
(...skipping 121 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
644 bool trim_spaces) { | 725 bool trim_spaces) { |
645 return TokenizeStringT<std::string>(str, delimiter, trim_spaces); | 726 return TokenizeStringT<std::string>(str, delimiter, trim_spaces); |
646 } | 727 } |
647 | 728 |
648 std::vector<std::wstring> TokenizeString16(const std::wstring& str, | 729 std::vector<std::wstring> TokenizeString16(const std::wstring& str, |
649 wchar_t delimiter, | 730 wchar_t delimiter, |
650 bool trim_spaces) { | 731 bool trim_spaces) { |
651 return TokenizeStringT<std::wstring>(str, delimiter, trim_spaces); | 732 return TokenizeStringT<std::wstring>(str, delimiter, trim_spaces); |
652 } | 733 } |
653 | 734 |
654 std::string GetSwitchValueFromCommandLine(const std::string& command_line, | 735 std::wstring GetSwitchValueFromCommandLine(const std::wstring& command_line, |
655 const std::string& switch_name) { | 736 const std::wstring& switch_name) { |
656 assert(!command_line.empty()); | 737 assert(!command_line.empty()); |
657 assert(!switch_name.empty()); | 738 assert(!switch_name.empty()); |
658 | 739 |
659 std::string command_line_copy = command_line; | 740 std::wstring command_line_copy = command_line; |
660 // Remove leading and trailing spaces. | 741 // Remove leading and trailing spaces. |
661 TrimT<std::string>(&command_line_copy); | 742 TrimT<std::wstring>(&command_line_copy); |
662 | 743 |
663 // Find the switch in the command line. If we don't find the switch, return | 744 // Find the switch in the command line. If we don't find the switch, return |
664 // an empty string. | 745 // an empty string. |
665 std::string switch_token = "--"; | 746 std::wstring switch_token = L"--"; |
666 switch_token += switch_name; | 747 switch_token += switch_name; |
667 switch_token += "="; | 748 switch_token += L"="; |
668 size_t switch_offset = command_line_copy.find(switch_token); | 749 size_t switch_offset = command_line_copy.find(switch_token); |
669 if (switch_offset == std::string::npos) | 750 if (switch_offset == std::string::npos) |
670 return std::string(); | 751 return std::wstring(); |
671 | 752 |
672 // The format is "--<switch name>=blah". Look for a space after the | 753 // The format is "--<switch name>=blah". Look for a space after the |
673 // "--<switch name>=" string. If we don't find a space assume that the switch | 754 // "--<switch name>=" string. If we don't find a space assume that the switch |
674 // value ends at the end of the command line. | 755 // value ends at the end of the command line. |
675 size_t switch_value_start_offset = switch_offset + switch_token.length(); | 756 size_t switch_value_start_offset = switch_offset + switch_token.length(); |
676 if (std::string(kWhiteSpaces).find( | 757 if (std::wstring(kWhiteSpaces16).find( |
677 command_line_copy[switch_value_start_offset]) != std::string::npos) { | 758 command_line_copy[switch_value_start_offset]) != std::wstring::npos) { |
678 switch_value_start_offset = command_line_copy.find_first_not_of( | 759 switch_value_start_offset = command_line_copy.find_first_not_of( |
679 GetWhiteSpacesForType<std::string>(), switch_value_start_offset); | 760 GetWhiteSpacesForType<std::wstring>(), switch_value_start_offset); |
680 if (switch_value_start_offset == std::string::npos) | 761 if (switch_value_start_offset == std::wstring::npos) |
681 return std::string(); | 762 return std::wstring(); |
682 } | 763 } |
683 size_t switch_value_end_offset = | 764 size_t switch_value_end_offset = |
684 command_line_copy.find_first_of(GetWhiteSpacesForType<std::string>(), | 765 command_line_copy.find_first_of(GetWhiteSpacesForType<std::wstring>(), |
685 switch_value_start_offset); | 766 switch_value_start_offset); |
686 if (switch_value_end_offset == std::string::npos) | 767 if (switch_value_end_offset == std::wstring::npos) |
687 switch_value_end_offset = command_line_copy.length(); | 768 switch_value_end_offset = command_line_copy.length(); |
688 | 769 |
689 std::string switch_value = command_line_copy.substr( | 770 std::wstring switch_value = command_line_copy.substr( |
690 switch_value_start_offset, | 771 switch_value_start_offset, |
691 switch_value_end_offset - (switch_offset + switch_token.length())); | 772 switch_value_end_offset - (switch_offset + switch_token.length())); |
692 TrimT<std::string>(&switch_value); | 773 TrimT<std::wstring>(&switch_value); |
693 return switch_value; | 774 return switch_value; |
694 } | 775 } |
695 | 776 |
696 // This function takes these inputs rather than accessing the module's | 777 // This function takes these inputs rather than accessing the module's |
697 // InstallDetails instance since it is used to bootstrap InstallDetails. | 778 // InstallDetails instance since it is used to bootstrap InstallDetails. |
698 std::wstring DetermineChannel(const InstallConstants& mode, | 779 std::wstring DetermineChannel(const InstallConstants& mode, |
699 bool system_level, | 780 bool system_level, |
700 bool multi_install) { | 781 bool multi_install) { |
701 if (!kUseGoogleUpdateIntegration) | 782 if (!kUseGoogleUpdateIntegration) |
702 return std::wstring(); | 783 return std::wstring(); |
703 | 784 |
704 switch (mode.channel_strategy) { | 785 switch (mode.channel_strategy) { |
705 case ChannelStrategy::UNSUPPORTED: | 786 case ChannelStrategy::UNSUPPORTED: |
706 assert(false); | 787 assert(false); |
707 break; | 788 break; |
708 case ChannelStrategy::ADDITIONAL_PARAMETERS: | 789 case ChannelStrategy::ADDITIONAL_PARAMETERS: |
709 return ChannelFromAdditionalParameters(mode, system_level, multi_install); | 790 return ChannelFromAdditionalParameters(mode, system_level, multi_install); |
710 case ChannelStrategy::FIXED: | 791 case ChannelStrategy::FIXED: |
711 return mode.default_channel_name; | 792 return mode.default_channel_name; |
712 } | 793 } |
713 | 794 |
714 return std::wstring(); | 795 return std::wstring(); |
715 } | 796 } |
716 | 797 |
717 } // namespace install_static | 798 } // namespace install_static |
OLD | NEW |