// Copyright 2014 The Crashpad Authors
//
// 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 <Foundation/Foundation.h>
#include <launch.h>
#include <string>
#include <vector>
#include "base/apple/bridging.h"
#include "base/apple/scoped_cftyperef.h"
#include "base/strings/stringprintf.h"
#include "base/strings/sys_string_conversions.h"
#include "gtest/gtest.h"
#include "util/misc/clock.h"
#include "util/misc/random_string.h"
#include "util/posix/process_info.h"
#include "util/stdlib/objc.h"
namespace crashpad {
namespace test {
namespace {
// 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) {
ProcessInfo process_info;
ASSERT_TRUE(process_info.InitializeWithPid(pid));
// 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<std::string> job_argv;
while (outer_tries--) {
// If the process is in the middle of calling exec, process_info.Arguments()
// may fail. Loop with a small retry delay while waiting for the expected
// successful call.
int inner_tries = 10;
bool success;
do {
success = process_info.Arguments(&job_argv);
if (success) {
break;
}
if (inner_tries > 0) {
SleepNanoseconds(1E6); // 1 millisecond
}
} while (inner_tries--);
ASSERT_TRUE(success);
ASSERT_FALSE(job_argv.empty());
if (job_argv.back() == last_arg) {
break;
}
if (outer_tries > 0) {
SleepNanoseconds(1E6); // 1 millisecond
}
}
ASSERT_FALSE(job_argv.empty());
EXPECT_EQ(job_argv.back(), last_arg);
}
// 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<std::string> job_argv;
while (tries--) {
ProcessInfo process_info;
if (!process_info.InitializeWithPid(pid) ||
!process_info.Arguments(&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) {
SleepNanoseconds(1E6); // 1 millisecond
}
}
ASSERT_FALSE(job_argv.empty());
EXPECT_NE(job_argv.back(), last_arg);
}
TEST(ServiceManagement, SubmitRemoveJob) {
@autoreleasepool {
const std::string cookie = RandomString();
std::string shell_script =
base::StringPrintf("sleep 10; echo %s", cookie.c_str());
NSString* shell_script_ns = base::SysUTF8ToNSString(shell_script);
static constexpr char kJobLabel[] =
"org.chromium.crashpad.test.service_management";
NSDictionary* job_dictionary_ns = @{
@LAUNCH_JOBKEY_LABEL : @"org.chromium.crashpad.test.service_management",
@LAUNCH_JOBKEY_RUNATLOAD : @YES,
@LAUNCH_JOBKEY_PROGRAMARGUMENTS :
@[ @"/bin/sh", @"-c", shell_script_ns, ],
};
CFDictionaryRef job_dictionary_cf =
base::apple::NSToCFPtrCast(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(ServiceManagementIsJobRunning(kJobLabel), 0);
// 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
} // namespace test
} // namespace crashpad