From 8fe32b7b9c557766b75e0f6ef9a68b290a7d9704 Mon Sep 17 00:00:00 2001 From: Mark Mentovai Date: Fri, 15 Aug 2014 22:33:14 -0700 Subject: [PATCH] Add mac_util, including MacOSXMinorVersion(), MacOSXVersion(), and MacModelAndBoard(), along with their tests. TEST=util_test MacUtil.* R=rsesek@chromium.org Review URL: https://codereview.chromium.org/473023002 --- util/mac/mac_util.cc | 282 ++++++++++++++++++++++++++++++++++++++ util/mac/mac_util.h | 73 ++++++++++ util/mac/mac_util_test.mm | 149 ++++++++++++++++++++ util/util.gyp | 3 + 4 files changed, 507 insertions(+) create mode 100644 util/mac/mac_util.cc create mode 100644 util/mac/mac_util.h create mode 100644 util/mac/mac_util_test.mm diff --git a/util/mac/mac_util.cc b/util/mac/mac_util.cc new file mode 100644 index 00000000..da6930c7 --- /dev/null +++ b/util/mac/mac_util.cc @@ -0,0 +1,282 @@ +// Copyright 2014 The Crashpad Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "util/mac/mac_util.h" + +#include +#include +#include +#include +#include + +#include "base/logging.h" +#include "base/mac/foundation_util.h" +#include "base/mac/scoped_cftyperef.h" +#include "base/mac/scoped_ioobject.h" +#include "base/strings/string_number_conversions.h" +#include "base/strings/string_piece.h" +#include "base/strings/stringprintf.h" +#include "base/strings/sys_string_conversions.h" + +extern "C" { +// Private CoreFoundation internals. See 10.9.2 CF-855.14/CFPriv.h and +// CF-855.14/CFUtilities.c. These are marked for weak import because they’re +// private and subject to change. + +#define WEAK_IMPORT __attribute__((weak_import)) + +// Don’t call these functions directly, call them through the +// TryCFCopy*VersionDictionary() helpers to account for the possibility that +// they may not be present at runtime. +CFDictionaryRef _CFCopySystemVersionDictionary() WEAK_IMPORT; +CFDictionaryRef _CFCopyServerVersionDictionary() WEAK_IMPORT; + +// Don’t use these constants with CFDictionaryGetValue() directly, use them with +// the TryCFDictionaryGetValue() wrapper to account for the possibility that +// they may not be present at runtime. +extern const CFStringRef _kCFSystemVersionProductNameKey WEAK_IMPORT; +extern const CFStringRef _kCFSystemVersionProductVersionKey WEAK_IMPORT; +extern const CFStringRef _kCFSystemVersionProductVersionExtraKey WEAK_IMPORT; +extern const CFStringRef _kCFSystemVersionBuildVersionKey WEAK_IMPORT; + +#undef WEAK_IMPORT + +} // extern "C" + +namespace { + +// Returns the running system’s Darwin major version. Don’t call this, it’s an +// implementation detail and its result is meant to be cached by +// MacOSXMinorVersion(). +// +// This is very similar to Chromium’s base/mac/mac_util.mm +// DarwinMajorVersionInternal(). +int DarwinMajorVersion() { + // base::OperatingSystemVersionNumbers calls Gestalt(), which is a + // higher-level function than is needed. It might perform unnecessary + // operations. On 10.6, it was observed to be able to spawn threads (see + // http://crbug.com/53200). It might also read files or perform other blocking + // operations. Actually, nobody really knows for sure just what Gestalt() + // might do, or what it might be taught to do in the future. + // + // uname(), on the other hand, is implemented as a simple series of sysctl() + // system calls to obtain the relevant data from the kernel. The data is + // compiled right into the kernel, so no threads or blocking or other funny + // business is necessary. + + utsname uname_info; + int rv = uname(&uname_info); + PCHECK(rv == 0) << "uname"; + + DCHECK_EQ(strcmp(uname_info.sysname, "Darwin"), 0) << "unexpected sysname " + << uname_info.sysname; + + char* dot = strchr(uname_info.release, '.'); + CHECK(dot); + + int darwin_major_version = 0; + CHECK(base::StringToInt( + base::StringPiece(uname_info.release, dot - uname_info.release), + &darwin_major_version)); + + return darwin_major_version; +} + +// Helpers for the weak-imported private CoreFoundation internals. + +CFDictionaryRef TryCFCopySystemVersionDictionary() { + if (_CFCopySystemVersionDictionary) { + return _CFCopySystemVersionDictionary(); + } + return NULL; +} + +CFDictionaryRef TryCFCopyServerVersionDictionary() { + if (_CFCopyServerVersionDictionary) { + return _CFCopyServerVersionDictionary(); + } + return NULL; +} + +const void* TryCFDictionaryGetValue(CFDictionaryRef dictionary, + const void* value) { + if (value) { + return CFDictionaryGetValue(dictionary, value); + } + return NULL; +} + +// Converts |version| to a triplet of version numbers on behalf of +// MacOSXVersion(). Returns true on success. If |version| does not have the +// expected format, returns false. |version| must be in the form "10.9.2" or +// just "10.9". In the latter case, |bugfix| will be set to 0. +bool StringToVersionNumbers(const std::string& version, + int* major, + int* minor, + int* bugfix) { + size_t first_dot = version.find_first_of('.'); + if (first_dot == 0 || first_dot == std::string::npos || + first_dot == version.length() - 1) { + LOG(ERROR) << "version has unexpected format"; + return false; + } + if (!base::StringToInt(base::StringPiece(&version[0], first_dot), major)) { + LOG(ERROR) << "version has unexpected format"; + return false; + } + + size_t second_dot = version.find_first_of('.', first_dot + 1); + if (second_dot == version.length() - 1) { + LOG(ERROR) << "version has unexpected format"; + return false; + } else if (second_dot == std::string::npos) { + second_dot = version.length(); + } + + if (!base::StringToInt(base::StringPiece(&version[first_dot + 1], + second_dot - first_dot - 1), + minor)) { + LOG(ERROR) << "version has unexpected format"; + return false; + } + + if (second_dot == version.length()) { + *bugfix = 0; + } else if (!base::StringToInt( + base::StringPiece(&version[second_dot + 1], + version.length() - second_dot - 1), + bugfix)) { + LOG(ERROR) << "version has unexpected format"; + return false; + } + + return true; +} + +std::string IORegistryEntryDataPropertyAsString(io_registry_entry_t entry, + CFStringRef key) { + base::ScopedCFTypeRef property( + IORegistryEntryCreateCFProperty(entry, key, kCFAllocatorDefault, 0)); + CFDataRef data = base::mac::CFCast(property); + if (data && CFDataGetLength(data) > 0) { + return reinterpret_cast(CFDataGetBytePtr(data)); + } + + return std::string(); +} + +} // namespace + +namespace crashpad { + +int MacOSXMinorVersion() { + // The Darwin major version is always 4 greater than the Mac OS X minor + // version for Darwin versions beginning with 6, corresponding to Mac OS X + // 10.2. + static int mac_os_x_minor_version = DarwinMajorVersion() - 4; + DCHECK(mac_os_x_minor_version >= 2); + return mac_os_x_minor_version; +} + +bool MacOSXVersion(int* major, + int* minor, + int* bugfix, + std::string* build, + bool* server, + std::string* version_string) { + base::ScopedCFTypeRef dictionary( + TryCFCopyServerVersionDictionary()); + if (dictionary) { + *server = true; + } else { + dictionary.reset(TryCFCopySystemVersionDictionary()); + if (!dictionary) { + LOG(ERROR) << "_CFCopySystemVersionDictionary failed"; + return false; + } + *server = false; + } + + bool success = true; + + CFStringRef version_cf = base::mac::CFCast( + TryCFDictionaryGetValue(dictionary, _kCFSystemVersionProductVersionKey)); + std::string version; + if (!version_cf) { + LOG(ERROR) << "version_cf not found"; + success = false; + } else { + version = base::SysCFStringRefToUTF8(version_cf); + success &= StringToVersionNumbers(version, major, minor, bugfix); + } + + CFStringRef build_cf = base::mac::CFCast( + TryCFDictionaryGetValue(dictionary, _kCFSystemVersionBuildVersionKey)); + if (!build_cf) { + LOG(ERROR) << "build_cf not found"; + success = false; + } else { + build->assign(base::SysCFStringRefToUTF8(build_cf)); + } + + CFStringRef product_cf = base::mac::CFCast( + TryCFDictionaryGetValue(dictionary, _kCFSystemVersionProductNameKey)); + std::string product; + if (!product_cf) { + LOG(ERROR) << "product_cf not found"; + success = false; + } else { + product = base::SysCFStringRefToUTF8(product_cf); + } + + // This key is not required, and in fact is normally not present. + CFStringRef extra_cf = base::mac::CFCast(TryCFDictionaryGetValue( + dictionary, _kCFSystemVersionProductVersionExtraKey)); + std::string extra; + if (extra_cf) { + extra = base::SysCFStringRefToUTF8(extra_cf); + } + + if (!product.empty() || !version.empty() || !build->empty()) { + if (!extra.empty()) { + version_string->assign(base::StringPrintf("%s %s %s (%s)", + product.c_str(), + version.c_str(), + extra.c_str(), + build->c_str())); + } else { + version_string->assign(base::StringPrintf( + "%s %s (%s)", product.c_str(), version.c_str(), build->c_str())); + } + } + + return success; +} + +void MacModelAndBoard(std::string* model, std::string* board_id) { + base::mac::ScopedIOObject platform_expert( + IOServiceGetMatchingService(kIOMasterPortDefault, + IOServiceMatching("IOPlatformExpertDevice"))); + if (platform_expert) { + model->assign( + IORegistryEntryDataPropertyAsString(platform_expert, CFSTR("model"))); + board_id->assign(IORegistryEntryDataPropertyAsString(platform_expert, + CFSTR("board-id"))); + } else { + model->clear(); + board_id->clear(); + } +} + +} // namespace crashpad diff --git a/util/mac/mac_util.h b/util/mac/mac_util.h new file mode 100644 index 00000000..c16b14fc --- /dev/null +++ b/util/mac/mac_util.h @@ -0,0 +1,73 @@ +// Copyright 2014 The Crashpad Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef CRASHPAD_UTIL_MAC_MAC_UTIL_H_ +#define CRASHPAD_UTIL_MAC_MAC_UTIL_H_ + +#include + +namespace crashpad { + +//! \brief Returns the version of the running operating system. +//! +//! \return The minor version of the operating system, such as `9` for Mac OS X +//! 10.9.2. +//! +//! \note This is similar to the base::mac::IsOS*() family of functions, but +//! is provided for situations where the caller needs to obtain version +//! information beyond what is provided by Chromium’s base, or for when the +//! caller needs the actual minor version value. +int MacOSXMinorVersion(); + +//! \brief Returns the version of the running operating system. +//! +//! All parameters are required. No parameter may be `NULL`. +//! +//! \param[out] major The major version of the operating system, such as `10` +//! for Mac OS X 10.9.2. +//! \param[out] minor The major version of the operating system, such as `9` for +//! Mac OS X 10.9.2. +//! \param[out] bugfix The bugfix version of the operating system, such as `2` +//! for Mac OS X 10.9.2. +//! \param[out] build The operating system’s build string, such as "13C64" for +//! Mac OS X 10.9.2. +//! \param[out] server `true` for a Mac OS X Server installation, `false` +//! otherwise (for a desktop/laptop, client, or workstation system). +//! \param[out] version_string A string representing the full operating system +//! version, such as `"Mac OS X 10.9.2 (13C64)"`. +//! +//! \return `true` on success, `false` on failure, with an error message logged. +//! A failure is considered to have occurred if any element could not be +//! determined. When this happens, their values will be untouched, but other +//! values that could be determined will still be set properly. +bool MacOSXVersion(int* major, + int* minor, + int* bugfix, + std::string* build, + bool* server, + std::string* version_string); + +//! \brief Returns the model name and board ID of the running system. +//! +//! \param[out] model The system’s model name. A mid-2012 15" MacBook Pro would +//! report “MacBookPro10,1”. +//! \param[out] board_id The system’s board ID. A mid-2012 15" MacBook Pro would +//! report “Mac-C3EC7CD22292981F”. +//! +//! If a value cannot be determined, its string is cleared. +void MacModelAndBoard(std::string* model, std::string* board_id); + +} // namespace crashpad + +#endif // CRASHPAD_UTIL_MAC_MAC_UTIL_H_ diff --git a/util/mac/mac_util_test.mm b/util/mac/mac_util_test.mm new file mode 100644 index 00000000..5c1366fb --- /dev/null +++ b/util/mac/mac_util_test.mm @@ -0,0 +1,149 @@ +// Copyright 2014 The Crashpad Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "util/mac/mac_util.h" + +#import +#include + +#include + +#include "base/mac/scoped_nsobject.h" +#include "base/strings/stringprintf.h" +#include "gtest/gtest.h" + +#ifdef __GLIBCXX__ +// When C++ exceptions are disabled, libstdc++ from GCC 4.2 defines |try| and +// |catch| so as to allow exception-expecting C++ code to build properly when +// language support for exceptions is not present. These macros interfere with +// the use of |@try| and |@catch| in Objective-C files such as this one. +// Undefine these macros here, after everything has been #included, since there +// will be no C++ uses and only Objective-C uses from this point on. +#undef try +#undef catch +#endif + +namespace { + +using namespace crashpad; + +// Runs /usr/bin/sw_vers with a single argument, |argument|, and places the +// command’s standard output into |output| after stripping the trailing newline. +// Fatal gtest assertions report tool failures, which the caller should check +// for with testing::Test::HasFatalFailure(). +void SwVers(NSString* argument, std::string* output) { + @autoreleasepool { + base::scoped_nsobject pipe([[NSPipe alloc] init]); + base::scoped_nsobject task([[NSTask alloc] init]); + [task setStandardOutput:pipe]; + [task setLaunchPath:@"/usr/bin/sw_vers"]; + [task setArguments:@[ argument ]]; + + @try { + [task launch]; + } + @catch (NSException* exception) { + FAIL() << [[exception name] UTF8String] << ": " + << [[exception reason] UTF8String]; + } + + NSData* data = [[pipe fileHandleForReading] readDataToEndOfFile]; + [task waitUntilExit]; + + ASSERT_EQ(NSTaskTerminationReasonExit, [task terminationReason]); + ASSERT_EQ(EXIT_SUCCESS, [task terminationStatus]); + + output->assign(reinterpret_cast([data bytes]), [data length]); + + EXPECT_EQ('\n', output->at(output->size() - 1)); + output->resize(output->size() - 1); + } +} + +TEST(MacUtil, MacOSXVersion) { + int major; + int minor; + int bugfix; + std::string build; + bool server; + std::string version_string; + ASSERT_TRUE( + MacOSXVersion(&major, &minor, &bugfix, &build, &server, &version_string)); + + std::string version; + if (bugfix) { + version = base::StringPrintf("%d.%d.%d", major, minor, bugfix); + } else { + // 10.x.0 releases report their version string as simply 10.x. + version = base::StringPrintf("%d.%d", major, minor); + } + + std::string expected_product_version; + SwVers(@"-productVersion", &expected_product_version); + if (Test::HasFatalFailure()) { + return; + } + + EXPECT_EQ(expected_product_version, version); + + std::string expected_build_version; + SwVers(@"-buildVersion", &expected_build_version); + if (Test::HasFatalFailure()) { + return; + } + + EXPECT_EQ(expected_build_version, build); + + std::string expected_product_name; + SwVers(@"-productName", &expected_product_name); + if (Test::HasFatalFailure()) { + return; + } + + // Look for a space after the product name in the complete version string. + expected_product_name += ' '; + EXPECT_EQ(0u, version_string.find(expected_product_name)); +} + +TEST(MacUtil, MacOSXMinorVersion) { + // Make sure that MacOSXMinorVersion() and MacOSXVersion() agree. The two have + // their own distinct implementations, and the latter was checked against + // sw_vers above. + int major; + int minor; + int bugfix; + std::string build; + bool server; + std::string version_string; + ASSERT_TRUE( + MacOSXVersion(&major, &minor, &bugfix, &build, &server, &version_string)); + + EXPECT_EQ(minor, MacOSXMinorVersion()); +} + +TEST(MacUtil, MacModelAndBoard) { + // There’s not much that can be done to test these, so just make sure they’re + // not empty. The model could be compared against the parsed output of + // “system_profiler SPHardwareDataType”, but the board doesn’t show up + // anywhere other than the I/O Registry, and that’s exactly how + // MacModelAndBoard() gets the data, so it wouldn’t be a very useful test. + std::string model; + std::string board; + MacModelAndBoard(&model, &board); + + EXPECT_FALSE(model.empty()); + EXPECT_FALSE(board.empty()); +} + +} // namespace diff --git a/util/util.gyp b/util/util.gyp index 4563f0da..39dcaca2 100644 --- a/util/util.gyp +++ b/util/util.gyp @@ -34,6 +34,8 @@ 'file/string_file_writer.h', 'mac/launchd.h', 'mac/launchd.mm', + 'mac/mac_util.cc', + 'mac/mac_util.h', 'mac/service_management.cc', 'mac/service_management.h', 'mach/task_memory.cc', @@ -89,6 +91,7 @@ 'sources': [ 'file/string_file_writer_test.cc', 'mac/launchd_test.mm', + 'mac/mac_util_test.mm', 'mac/service_management_test.mm', 'mach/task_memory_test.cc', 'misc/initialization_state_dcheck_test.cc',