diff --git a/util/mac/service_management.cc b/util/mac/service_management.cc new file mode 100644 index 00000000..512a6059 --- /dev/null +++ b/util/mac/service_management.cc @@ -0,0 +1,139 @@ +// 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/service_management.h" + +#include +#include +#include + +#include "base/mac/scoped_launch_data.h" +#include "util/mac/launchd.h" + +namespace { + +launch_data_t LaunchDataDictionaryForJob(const std::string& label) { + base::mac::ScopedLaunchData request( + launch_data_alloc(LAUNCH_DATA_DICTIONARY)); + launch_data_dict_insert( + request, launch_data_new_string(label.c_str()), LAUNCH_KEY_GETJOB); + + base::mac::ScopedLaunchData response(launch_msg(request)); + if (launch_data_get_type(response) != LAUNCH_DATA_DICTIONARY) { + return NULL; + } + + return response.release(); +} + +} // namespace + +namespace crashpad { + +bool ServiceManagementSubmitJob(CFDictionaryRef job_cf) { + base::mac::ScopedLaunchData job_launch(CFPropertyToLaunchData(job_cf)); + if (!job_launch.get()) { + return false; + } + + base::mac::ScopedLaunchData jobs(launch_data_alloc(LAUNCH_DATA_ARRAY)); + launch_data_array_set_index(jobs, job_launch.release(), 0); + + base::mac::ScopedLaunchData request( + launch_data_alloc(LAUNCH_DATA_DICTIONARY)); + launch_data_dict_insert(request, jobs.release(), LAUNCH_KEY_SUBMITJOB); + + base::mac::ScopedLaunchData response(launch_msg(request)); + + if (launch_data_get_type(response) != LAUNCH_DATA_ARRAY) { + return false; + } + + if (launch_data_array_get_count(response) != 1) { + return false; + } + + launch_data_t response_element = launch_data_array_get_index(response, 0); + if (launch_data_get_type(response_element) != LAUNCH_DATA_ERRNO) { + return false; + } + + int err = launch_data_get_errno(response_element); + if (err != 0) { + return false; + } + + return true; +} + +bool ServiceManagementRemoveJob(const std::string& label, bool wait) { + base::mac::ScopedLaunchData request( + launch_data_alloc(LAUNCH_DATA_DICTIONARY)); + launch_data_dict_insert( + request, launch_data_new_string(label.c_str()), LAUNCH_KEY_REMOVEJOB); + + base::mac::ScopedLaunchData response(launch_msg(request)); + if (launch_data_get_type(response) != LAUNCH_DATA_ERRNO) { + return false; + } + + int err = launch_data_get_errno(response); + if (err == EINPROGRESS) { + if (wait) { + // TODO(mark): Use a kqueue to wait for the process to exit. To avoid a + // race, the kqueue would need to be set up prior to asking launchd to + // remove the job. Even so, the job’s PID may change between the time it’s + // obtained and the time the kqueue is set up, so this is nontrivial. + do { + timespec sleep_time; + sleep_time.tv_sec = 0; + sleep_time.tv_nsec = 1E5; // 100 microseconds + nanosleep(&sleep_time, NULL); + } while (ServiceManagementIsJobLoaded(label)); + } + + return true; + } + + if (err != 0) { + return false; + } + + return true; +} + +bool ServiceManagementIsJobLoaded(const std::string& label) { + base::mac::ScopedLaunchData dictionary(LaunchDataDictionaryForJob(label)); + if (!dictionary) { + return false; + } + + return true; +} + +pid_t ServiceManagementIsJobRunning(const std::string& label) { + base::mac::ScopedLaunchData dictionary(LaunchDataDictionaryForJob(label)); + if (!dictionary) { + return 0; + } + + launch_data_t pid = launch_data_dict_lookup(dictionary, LAUNCH_JOBKEY_PID); + if (launch_data_get_type(pid) != LAUNCH_DATA_INTEGER) { + return 0; + } + + return launch_data_get_integer(pid); +} + +} // namespace crashpad diff --git a/util/mac/service_management.h b/util/mac/service_management.h new file mode 100644 index 00000000..5a9275db --- /dev/null +++ b/util/mac/service_management.h @@ -0,0 +1,81 @@ +// 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_SERVICE_MANAGEMENT_H_ +#define CRASHPAD_UTIL_MAC_SERVICE_MANAGEMENT_H_ + +#include +#include + +#include + +namespace crashpad { + +//! \brief Submits a job to the user launchd domain as in `SMJobSubmit()`. +//! +//! \param[in] job_cf A dictionary describing a job. +//! +//! \return `true` if the job was submitted successfully, otherwise `false`. +//! +//! \note This function is provided because `SMJobSubmit()` does not exist prior +//! to Mac OS X 10.6, and behaves flakily on Mac OS X 10.10 DP5 14A314h in +//! that it hangs 25% of the time (radar 17365104). +bool ServiceManagementSubmitJob(CFDictionaryRef job_cf); + +//! \brief Removes a job from the user launchd domain as in `SMJobRemove()`. +//! +//! \param[in] label The label for the job to remove. +//! \param[in] wait `true` if this function should block, waiting for the job to +//! be removed. `false` if the job may be removed asynchronously. +//! +//! \return `true` if the job was removed successfully or if an asynchronous +//! attempt to remove the job was started successfully, otherwise `false`. +//! +//! \note This function is provided because `SMJobRemove()` does not exist prior +//! to Mac OS X 10.6, and when \a wait is true, blocks for far too long on +//! Mac OS X 10.10 DP5 14A314h (radar 17365104; `_block_until_job_exits()` +//! contains a one-second sleep() call). +bool ServiceManagementRemoveJob(const std::string& label, bool wait); + +//! \brief Determines whether a specified job is loaded in the user launchd +//! domain. +//! +//! \param[in] label The label for the job to look up. +//! +//! \return `true` if the job is loaded, otherwise `false`. +//! +//! \note A loaded job is not necessarily presently running, nor has it +//! necessarily ever run in the past. +//! \note This function is provided because `SMJobCopyDictionary()` does not +//! exist prior to Mac OS X 10.6, and on Mac OS X 10.10 DP5 14A314h, it +//! fails to return a job dictionary immediately after a job is loaded +//! (radar 17365104). +bool ServiceManagementIsJobLoaded(const std::string& label); + +//! \brief Determines whether a specified job is running in the user launchd +//! domain. +//! +//! \param[in] label The label for the job to look up. +//! +//! \return The job’s process ID if running, otherwise `0`. +//! +//! \note This function is provided because `SMJobCopyDictionary()` does not +//! exist prior to Mac OS X 10.6, and on Mac OS X 10.10 DP5 14A314h, it +//! fails to return a job dictionary immediately after a job is loaded +//! (radar 17365104). +pid_t ServiceManagementIsJobRunning(const std::string& label); + +} // namespace crashpad + +#endif // CRASHPAD_UTIL_MAC_SERVICE_MANAGEMENT diff --git a/util/mac/service_management_test.mm b/util/mac/service_management_test.mm new file mode 100644 index 00000000..49476219 --- /dev/null +++ b/util/mac/service_management_test.mm @@ -0,0 +1,167 @@ +// 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/service_management.h" + +#import +#include +#include + +#include +#include + +#include "base/mac/foundation_util.h" +#include "base/mac/scoped_cftyperef.h" +#include "base/strings/stringprintf.h" +#include "base/strings/sys_string_conversions.h" +#include "base/rand_util.h" +#include "gtest/gtest.h" +#include "util/posix/process_util.h" +#include "util/stdlib/objc.h" + +namespace { + +using namespace crashpad; + +// Ensures that the process with the specified PID is running, identifying it by +// requiring that its argv[argc - 1] compare equal to last_arg. +void ExpectProcessIsRunning(pid_t pid, std::string& last_arg) { + // The process may not have called exec yet, so loop with a small delay while + // looking for the cookie. + int outer_tries = 10; + std::vector job_argv; + while (outer_tries--) { + // If the process is in the middle of calling exec, ProcessArgumentsForPID + // may fail. Loop with a small retry delay while waiting for the expected + // successful call. + int inner_tries = 10; + bool success; + do { + success = ProcessArgumentsForPID(pid, &job_argv); + if (success) { + break; + } + if (inner_tries > 0) { + timespec sleep_time; + sleep_time.tv_sec = 0; + sleep_time.tv_nsec = 1E5; // 100 microseconds + nanosleep(&sleep_time, NULL); + } + } while (inner_tries--); + ASSERT_TRUE(success); + + ASSERT_TRUE(ProcessArgumentsForPID(pid, &job_argv)); + ASSERT_FALSE(job_argv.empty()); + if (job_argv.back() == last_arg) { + break; + } + + if (outer_tries > 0) { + timespec sleep_time; + sleep_time.tv_sec = 0; + sleep_time.tv_nsec = 1E6; // 1 millisecond + nanosleep(&sleep_time, NULL); + } + } + + ASSERT_FALSE(job_argv.empty()); + EXPECT_EQ(last_arg, job_argv.back()); +} + +// Ensures that the process with the specified PID is not running. Because the +// PID may be reused for another process, a process is only treated as running +// if its argv[argc - 1] compares equal to last_arg. +void ExpectProcessIsNotRunning(pid_t pid, std::string& last_arg) { + // The process may not have exited yet, so loop with a small delay while + // checking that it has exited. + int tries = 10; + std::vector job_argv; + while (tries--) { + if (!ProcessArgumentsForPID(pid, &job_argv)) { + // The PID was not found. + return; + } + + // The PID was found. It may have been recycled for another process. Make + // sure that the cookie isn’t found. + ASSERT_FALSE(job_argv.empty()); + if (job_argv.back() != last_arg) { + break; + } + + if (tries > 0) { + timespec sleep_time; + sleep_time.tv_sec = 0; + sleep_time.tv_nsec = 1E6; // 1 millisecond + nanosleep(&sleep_time, NULL); + } + } + + ASSERT_FALSE(job_argv.empty()); + EXPECT_NE(last_arg, job_argv.back()); +} + +TEST(ServiceManagement, SubmitRemoveJob) { + @autoreleasepool { + std::string cookie; + for (int index = 0; index < 16; ++index) { + cookie.append(1, base::RandInt('A', 'Z')); + } + + std::string shell_script = + base::StringPrintf("sleep 10; echo %s", cookie.c_str()); + NSString* shell_script_ns = base::SysUTF8ToNSString(shell_script); + + const char kJobLabel[] = "com.googlecode.crashpad.test.service_management"; + NSDictionary* job_dictionary_ns = @{ + @LAUNCH_JOBKEY_LABEL : @"com.googlecode.crashpad.test.service_management", + @LAUNCH_JOBKEY_RUNATLOAD : @YES, + @LAUNCH_JOBKEY_PROGRAMARGUMENTS : + @[ @"/bin/sh", @"-c", shell_script_ns, ], + }; + CFDictionaryRef job_dictionary_cf = + base::mac::NSToCFCast(job_dictionary_ns); + + // The job may be left over from a failed previous run. + if (ServiceManagementIsJobLoaded(kJobLabel)) { + EXPECT_TRUE(ServiceManagementRemoveJob(kJobLabel, true)); + } + + EXPECT_FALSE(ServiceManagementIsJobLoaded(kJobLabel)); + ASSERT_FALSE(ServiceManagementIsJobRunning(kJobLabel)); + + // Submit the job. + ASSERT_TRUE(ServiceManagementSubmitJob(job_dictionary_cf)); + EXPECT_TRUE(ServiceManagementIsJobLoaded(kJobLabel)); + + // launchd started the job because RunAtLoad is true. + pid_t job_pid = ServiceManagementIsJobRunning(kJobLabel); + ASSERT_GT(job_pid, 0); + + ExpectProcessIsRunning(job_pid, shell_script); + + // Remove the job. + ASSERT_TRUE(ServiceManagementRemoveJob(kJobLabel, true)); + EXPECT_FALSE(ServiceManagementIsJobLoaded(kJobLabel)); + EXPECT_EQ(0, ServiceManagementIsJobRunning(kJobLabel)); + + // Now that the job is unloaded, a subsequent attempt to unload it should be + // an error. + EXPECT_FALSE(ServiceManagementRemoveJob(kJobLabel, false)); + + ExpectProcessIsNotRunning(job_pid, shell_script); + } +} + +} // namespace diff --git a/util/util.gyp b/util/util.gyp index 9cb2eeb3..4563f0da 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/service_management.cc', + 'mac/service_management.h', 'mach/task_memory.cc', 'mach/task_memory.h', 'misc/initialization_state.h', @@ -87,6 +89,7 @@ 'sources': [ 'file/string_file_writer_test.cc', 'mac/launchd_test.mm', + 'mac/service_management_test.mm', 'mach/task_memory_test.cc', 'misc/initialization_state_dcheck_test.cc', 'misc/initialization_state_test.cc',