blob: b80c34e8e84cab68de7c843c7eafefbd84c3328c [file] [log] [blame]
// 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 <stdint.h>
#if !defined(__linux__) && !defined(__WINDOWS__)
#include <sys/time.h> // For gettimeofday.
#endif
#ifdef __FreeBSD__
#include <sys/sysctl.h>
#include <sys/types.h>
#endif
#include <algorithm>
#include <chrono>
#include <cstdlib> // For rand.
#include <list>
#include <map>
#include <set>
#include <sstream>
#include <string>
#include <vector>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <stout/duration.hpp>
#include <stout/foreach.hpp>
#include <stout/fs.hpp>
#include <stout/gtest.hpp>
#include <stout/hashset.hpp>
#include <stout/numify.hpp>
#include <stout/os.hpp>
#include <stout/stopwatch.hpp>
#include <stout/strings.hpp>
#include <stout/try.hpp>
#include <stout/uuid.hpp>
#include <stout/os/environment.hpp>
#include <stout/os/int_fd.hpp>
#include <stout/os/kill.hpp>
#include <stout/os/killtree.hpp>
#include <stout/os/realpath.hpp>
#include <stout/os/shell.hpp>
#include <stout/os/stat.hpp>
#include <stout/os/which.hpp>
#include <stout/os/write.hpp>
#if defined(__APPLE__) || defined(__FreeBSD__)
#include <stout/os/sysctl.hpp>
#endif
#include <stout/tests/utils.hpp>
#ifndef __WINDOWS__
using os::Exec;
#endif // __WINDOWS__
using os::Fork;
using os::Process;
using os::ProcessTree;
using os::stat::FollowSymlink;
using std::list;
using std::set;
using std::string;
using std::vector;
class OsTest : public TemporaryDirectoryTest {};
#ifndef __WINDOWS__
TEST_F(OsTest, Environment)
{
// Make sure the environment has some entries with '=' in the value.
os::setenv("SOME_SPECIAL_FLAG", "--flag=foobar");
char** environ = os::raw::environment();
hashmap<string, string> environment = os::environment();
for (size_t index = 0; environ[index] != nullptr; index++) {
string entry(environ[index]);
size_t position = entry.find_first_of('=');
if (position == string::npos) {
continue; // Skip malformed environment entries.
}
const string key = entry.substr(0, position);
const string value = entry.substr(position + 1);
EXPECT_TRUE(environment.contains(key));
EXPECT_EQ(value, environment[key]);
}
}
#endif // __WINDOWS__
TEST_F(OsTest, TrivialEnvironment)
{
// NOTE: Regression test that ensures Windows can get and set an environment
// variable. This is easy to break: Windows maintains two non-compatible ways
// to get and set environment variables: the CRT way (using `environ`,
// `setenv`, and `getenv`), and the Win32 way (using `GetEnvironmentStrings`,
// `SetEnvironmentVariable`, and `GetEnvironmentVariable`). This test makes
// sure that we consistently back the Stout environment variable APIs with
// with one or the other; a mix won't work.
const string key = "test_key1";
const string value = "value";
os::setenv(key, value);
hashmap<string, string> environment = os::environment();
ASSERT_TRUE(environment.count(key) != 0);
ASSERT_EQ(value, environment[key]);
// NOTE: Regression test that ensures we can set an environment variable to
// be an empty string. On Windows, this is only possible with the Win32 APIs:
// the CRT `environ` macro will simply delete the variable if it is passed an
// empty string as a value.
os::setenv(key, "", true);
environment = os::environment();
ASSERT_TRUE(environment.count(key) != 0);
ASSERT_EQ("", environment[key]);
}
TEST_F(OsTest, Argv)
{
vector<string> args = {"arg0", "arg1", "arg2"};
os::raw::Argv _argv(args);
char** argv = _argv;
for (size_t i = 0; i < args.size(); i++) {
EXPECT_EQ(args[i], argv[i]);
}
}
TEST_F(OsTest, System)
{
EXPECT_SOME_EQ(0, os::system("exit 0"));
EXPECT_SOME_EQ(0, os::system(SLEEP_COMMAND(0)));
EXPECT_SOME_NE(0, os::system("exit 1"));
EXPECT_SOME_NE(0, os::system("invalid.command"));
// Note that ::system returns 0 for the following two cases as well.
EXPECT_SOME_EQ(0, os::system(""));
EXPECT_SOME_EQ(0, os::system(" "));
}
// NOTE: `os::cloexec` is a stub on Windows that returns `true`.
#ifdef __WINDOWS__
TEST_F(OsTest, Cloexec)
{
ASSERT_SOME_TRUE(os::isCloexec(int_fd(INVALID_HANDLE_VALUE)));
}
#else
TEST_F(OsTest, Cloexec)
{
Try<int_fd> fd = os::open(
"cloexec",
O_CREAT | O_WRONLY | O_APPEND | O_CLOEXEC,
S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH);
ASSERT_SOME(fd);
EXPECT_SOME_TRUE(os::isCloexec(fd.get()));
ASSERT_SOME(os::unsetCloexec(fd.get()));
EXPECT_SOME_FALSE(os::isCloexec(fd.get()));
close(fd.get());
fd = os::open(
"non-cloexec",
O_CREAT | O_WRONLY | O_APPEND,
S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH);
ASSERT_SOME(fd);
EXPECT_SOME_FALSE(os::isCloexec(fd.get()));
ASSERT_SOME(os::cloexec(fd.get()));
EXPECT_SOME_TRUE(os::isCloexec(fd.get()));
close(fd.get());
}
#endif // __WINDOWS__
// NOTE: `os::isNonblock` is a stub on Windows that returns `true`.
#ifdef __WINDOWS__
TEST_F(OsTest, Nonblock)
{
// `os::isNonblock` is a stub on Windows that returns `true`.
EXPECT_SOME_TRUE(os::isNonblock(int_fd(INVALID_HANDLE_VALUE)));
// `os::nonblock` is a no-op for handles.
EXPECT_SOME(os::nonblock(int_fd(INVALID_HANDLE_VALUE)));
// `os::nonblock` should fail for an invalid socket.
EXPECT_ERROR(os::nonblock(int_fd(INVALID_SOCKET)));
// NOTE: There is no way on Windows to check if the socket is in
// blocking or non-blocking mode, so `os::isNonblock` is a stub. A
// Windows socket always starts in blocking mode, and then can be
// set as non-blocking. All we can check here is that `os::nonblock`
// does not fail on a valid socket.
ASSERT_TRUE(net::wsa_initialize());
Try<int_fd> socket =
net::socket(AF_INET, SOCK_STREAM, IPPROTO_TCP, WSA_FLAG_NO_HANDLE_INHERIT);
ASSERT_SOME(socket);
EXPECT_SOME(os::nonblock(socket.get()));
EXPECT_SOME(os::close(socket.get()));
ASSERT_TRUE(net::wsa_cleanup());
}
#else
TEST_F(OsTest, Nonblock)
{
int pipes[2];
ASSERT_NE(-1, pipe(pipes));
Try<bool> isNonBlock = os::isNonblock(pipes[0]);
EXPECT_SOME_FALSE(isNonBlock);
ASSERT_SOME(os::nonblock(pipes[0]));
isNonBlock = os::isNonblock(pipes[0]);
EXPECT_SOME_TRUE(isNonBlock);
close(pipes[0]);
close(pipes[1]);
EXPECT_ERROR(os::nonblock(pipes[0]));
EXPECT_ERROR(os::nonblock(pipes[1]));
}
#endif // __WINDOWS__
// Tests whether a file's size is reported by os::stat::size as expected.
// Tests all four combinations of following a link or not and of a file
// or a link as argument. Also tests that an error is returned for a
// non-existing file.
TEST_F(OsTest, SYMLINK_Size)
{
const string file = path::join(os::getcwd(), id::UUID::random().toString());
const Bytes size = 1053;
ASSERT_SOME(os::write(file, string(size.bytes(), 'X')));
// The reported file size should be the same whether following links
// or not, given that the input parameter is not a link.
EXPECT_SOME_EQ(size,
os::stat::size(file, FollowSymlink::FOLLOW_SYMLINK));
EXPECT_SOME_EQ(size,
os::stat::size(file, FollowSymlink::DO_NOT_FOLLOW_SYMLINK));
EXPECT_ERROR(os::stat::size("aFileThatDoesNotExist"));
const string link = path::join(os::getcwd(), id::UUID::random().toString());
ASSERT_SOME(fs::symlink(file, link));
// Following links we expect the file's size, not the link's.
EXPECT_SOME_EQ(size, os::stat::size(link, FollowSymlink::FOLLOW_SYMLINK));
#ifdef __WINDOWS__
// On Windows, the reported size of a symlink is zero.
EXPECT_SOME_EQ(
Bytes(0),
os::stat::size(link, FollowSymlink::DO_NOT_FOLLOW_SYMLINK));
#else
// Not following links, we expect the string length of the linked path.
EXPECT_SOME_EQ(
Bytes(file.size()),
os::stat::size(link, FollowSymlink::DO_NOT_FOLLOW_SYMLINK));
#endif // __WINDOWS__
}
TEST_F(OsTest, BootId)
{
Try<string> bootId = os::bootId();
ASSERT_SOME(bootId);
EXPECT_NE("", bootId.get());
#ifdef __linux__
Try<string> read = os::read("/proc/sys/kernel/random/boot_id");
ASSERT_SOME(read);
EXPECT_EQ(bootId.get(), strings::trim(read.get()));
#elif defined(__APPLE__) || defined(__FreeBSD__) || defined(__WINDOWS__)
// For OS X, FreeBSD, and Windows systems, the boot id is the system
// boot time in seconds, so assert it can be numified and is a
// reasonable value.
Try<uint64_t> numified = numify<uint64_t>(bootId.get());
ASSERT_SOME(numified);
EXPECT_GT(Seconds(numified.get()), Seconds(0));
using namespace std::chrono;
EXPECT_LT(
Seconds(numified.get()),
Seconds(duration_cast<seconds>(system_clock::now().time_since_epoch())
.count()));
#endif // APPLE / FreeBSD / WINDOWS
}
TEST_F(OsTest, Sleep)
{
Duration duration = Milliseconds(10);
Stopwatch stopwatch;
stopwatch.start();
ASSERT_SOME(os::sleep(duration));
ASSERT_LE(duration, stopwatch.elapsed());
ASSERT_ERROR(os::sleep(Milliseconds(-10)));
}
#if defined(__APPLE__) || defined(__FreeBSD__)
TEST_F(OsTest, Sysctl)
{
// String test.
Try<os::UTSInfo> uname = os::uname();
ASSERT_SOME(uname);
Try<string> release = os::sysctl(CTL_KERN, KERN_OSRELEASE).string();
EXPECT_SOME_EQ(uname->release, release);
Try<string> type = os::sysctl(CTL_KERN, KERN_OSTYPE).string();
EXPECT_SOME_EQ(uname->sysname, type);
// Integer test.
Try<int> maxproc = os::sysctl(CTL_KERN, KERN_MAXPROC).integer();
ASSERT_SOME(maxproc);
// Table test.
Try<vector<kinfo_proc>> processes =
os::sysctl(CTL_KERN, KERN_PROC, KERN_PROC_ALL).table(maxproc.get());
ASSERT_SOME(processes);
std::set<pid_t> pids;
foreach (const kinfo_proc& process, processes.get()) {
#ifdef __APPLE__
pids.insert(process.kp_proc.p_pid);
#else
pids.insert(process.ki_pid);
#endif // __APPLE__
}
EXPECT_EQ(1u, pids.count(getpid()));
// Timeval test.
Try<timeval> bootTime = os::sysctl(CTL_KERN, KERN_BOOTTIME).time();
ASSERT_SOME(bootTime);
timeval time;
gettimeofday(&time, nullptr);
EXPECT_GT(Seconds(bootTime->tv_sec), Seconds(0));
EXPECT_LT(Seconds(bootTime->tv_sec), Seconds(time.tv_sec));
}
#endif // __APPLE__ || __FreeBSD__
// TODO(hausdorff): Enable when we implement `Fork` and `Exec`. See MESOS-3638.
#ifndef __WINDOWS__
TEST_F(OsTest, Children)
{
Try<set<pid_t>> children = os::children(getpid());
ASSERT_SOME(children);
EXPECT_TRUE(children->empty());
Try<ProcessTree> tree =
Fork(None(), // Child.
Fork(Exec(SLEEP_COMMAND(10))), // Grandchild.
Exec(SLEEP_COMMAND(10)))();
ASSERT_SOME(tree);
ASSERT_EQ(1u, tree->children.size());
pid_t child = tree->process.pid;
pid_t grandchild = tree->children.front().process.pid;
// Ensure the non-recursive children does not include the
// grandchild.
children = os::children(getpid(), false);
ASSERT_SOME(children);
EXPECT_EQ(1u, children->size());
EXPECT_EQ(1u, children->count(child));
children = os::children(getpid());
ASSERT_SOME(children);
// Depending on whether or not the shell has fork/exec'ed in each
// above 'Exec', we could have 2 or 4 children. That is, some shells
// might simply for exec the command above (i.e., 'sleep 10') while
// others might fork/exec the command, keeping around a 'sh -c'
// process as well.
EXPECT_LE(2u, children->size());
EXPECT_GE(4u, children->size());
EXPECT_EQ(1u, children->count(child));
EXPECT_EQ(1u, children->count(grandchild));
// Cleanup by killing the descendant processes.
EXPECT_EQ(0, kill(grandchild, SIGKILL));
EXPECT_EQ(0, kill(child, SIGKILL));
// We have to reap the child for running the tests in repetition.
ASSERT_EQ(child, waitpid(child, nullptr, 0));
}
void dosetsid()
{
if (::setsid() == -1) {
ABORT(string("Failed to setsid: ") + os::strerror(errno));
}
}
TEST_F(OsTest, Killtree)
{
Try<ProcessTree> tree =
Fork(&dosetsid, // Child.
Fork(None(), // Grandchild.
Fork(None(), // Great-grandchild.
Fork(&dosetsid, // Great-great-granchild.
Exec(SLEEP_COMMAND(10))),
Exec(SLEEP_COMMAND(10))),
Exec("exit 0")),
Exec(SLEEP_COMMAND(10)))();
ASSERT_SOME(tree);
// The process tree we instantiate initially looks like this:
//
// -+- child sleep 10
// \-+- grandchild exit 0
// \-+- greatGrandchild sleep 10
// \--- greatGreatGrandchild sleep 10
//
// But becomes two process trees after the grandchild exits:
//
// -+- child sleep 10
// \--- grandchild (exit 0)
//
// -+- greatGrandchild sleep 10
// \--- greatGreatGrandchild sleep 10
// Grab the pids from the instantiated process tree.
ASSERT_EQ(1u, tree->children.size());
ASSERT_EQ(1u, tree->children.front().children.size());
ASSERT_EQ(1u, tree->children.front().children.front().children.size());
pid_t child = tree.get();
pid_t grandchild = tree->children.front();
pid_t greatGrandchild = tree->children.front().children.front();
pid_t greatGreatGrandchild =
tree->children.front().children.front().children.front();
// Now wait for the grandchild to exit splitting the process tree.
Duration elapsed = Duration::zero();
while (true) {
Result<os::Process> process = os::process(grandchild);
ASSERT_FALSE(process.isError());
if (process.isNone() || process->zombie) {
break;
}
if (elapsed > Seconds(10)) {
FAIL() << "Granchild process '" << process->pid << "' "
<< "(" << process->command << ") did not terminate";
}
os::sleep(Milliseconds(5));
elapsed += Milliseconds(5);
}
// Kill the process tree and follow sessions and groups to make sure
// we cross the broken link due to the grandchild.
Try<list<ProcessTree>> trees =
os::killtree(child, SIGKILL, true, true);
ASSERT_SOME(trees);
EXPECT_EQ(2u, trees->size()) << stringify(trees.get());
foreach (const ProcessTree& tree, trees.get()) {
if (tree.process.pid == child) {
// The 'grandchild' _might_ still be in the tree, just zombied,
// unless the 'child' reaps the 'grandchild', which may happen
// if the shell "sticks around" (i.e., some invocations of 'sh
// -c' will 'exec' the command which will likely not do any
// reaping, but in other cases an invocation of 'sh -c' will not
// 'exec' the command, for example when the command is a
// sequence of commands separated by ';').
EXPECT_FALSE(tree.contains(greatGrandchild)) << tree;
EXPECT_FALSE(tree.contains(greatGreatGrandchild)) << tree;
} else if (tree.process.pid == greatGrandchild) {
EXPECT_TRUE(tree.contains(greatGreatGrandchild)) << tree;
} else {
FAIL()
<< "Not expecting a process tree rooted at "
<< tree.process.pid << "\n" << tree;
}
}
// All processes should be reaped since we've killed everything.
// The direct child must be reaped by us below.
elapsed = Duration::zero();
while (true) {
Result<os::Process> _child = os::process(child);
ASSERT_SOME(_child);
if (os::process(greatGreatGrandchild).isNone() &&
os::process(greatGrandchild).isNone() &&
os::process(grandchild).isNone() &&
_child->zombie) {
break;
}
if (elapsed > Seconds(10)) {
FAIL() << "Processes were not reaped after killtree invocation";
}
os::sleep(Milliseconds(5));
elapsed += Milliseconds(5);
}
// Expect the pids to be wiped!
EXPECT_NONE(os::process(greatGreatGrandchild));
EXPECT_NONE(os::process(greatGrandchild));
EXPECT_NONE(os::process(grandchild));
EXPECT_SOME(os::process(child));
EXPECT_TRUE(os::process(child)->zombie);
// We have to reap the child for running the tests in repetition.
ASSERT_EQ(child, waitpid(child, nullptr, 0));
}
TEST_F(OsTest, KilltreeNoRoot)
{
Try<ProcessTree> tree =
Fork(&dosetsid, // Child.
Fork(None(), // Grandchild.
Fork(None(),
Exec(SLEEP_COMMAND(100))),
Exec(SLEEP_COMMAND(100))),
Exec("exit 0"))();
ASSERT_SOME(tree);
// The process tree we instantiate initially looks like this:
//
// -+- child exit 0 [new session and process group leader]
// \-+- grandchild sleep 100
// \-+- great grandchild sleep 100
//
// But becomes the following tree after the child exits:
//
// -+- child (exited 0)
// \-+- grandchild sleep 100
// \-+- great grandchild sleep 100
//
// And gets reparented when we reap the child:
//
// -+- new parent
// \-+- grandchild sleep 100
// \-+- great grandchild sleep 100
// Grab the pids from the instantiated process tree.
ASSERT_EQ(1u, tree->children.size());
ASSERT_EQ(1u, tree->children.front().children.size());
pid_t child = tree.get();
pid_t grandchild = tree->children.front();
pid_t greatGrandchild = tree->children.front().children.front();
// Wait for the child to exit.
Duration elapsed = Duration::zero();
while (true) {
Result<os::Process> process = os::process(child);
ASSERT_FALSE(process.isError());
if (process->zombie) {
break;
}
if (elapsed > Seconds(1)) {
FAIL() << "Child process " << stringify(child) << " did not terminate";
}
os::sleep(Milliseconds(5));
elapsed += Milliseconds(5);
}
// Ensure we reap our child now.
EXPECT_SOME(os::process(child));
EXPECT_TRUE(os::process(child)->zombie);
ASSERT_EQ(child, waitpid(child, nullptr, 0));
// Check the grandchild and great grandchild are still running.
ASSERT_TRUE(os::exists(grandchild));
ASSERT_TRUE(os::exists(greatGrandchild));
// Check the subtree has been reparented: the parent is no longer
// child (the root of the tree), and that the process is not a
// zombie. This is done because some systems run a secondary init
// process for the user (init --user) that does not have pid 1,
// meaning we can't just check that the parent pid == 1.
Result<os::Process> _grandchild = os::process(grandchild);
ASSERT_SOME(_grandchild);
ASSERT_NE(child, _grandchild->parent);
ASSERT_FALSE(_grandchild->zombie);
// Check to see if we're in a jail on FreeBSD in case we've been
// reparented to pid 1
#if __FreeBSD__
if (!isJailed()) {
#endif
// Check that grandchild's parent is also not a zombie.
Result<os::Process> currentParent = os::process(_grandchild->parent);
ASSERT_SOME(currentParent);
ASSERT_FALSE(currentParent->zombie);
#ifdef __FreeBSD__
}
#endif
// Kill the process tree. Even though the root process has exited,
// we specify to follow sessions and groups which should kill the
// grandchild and greatgrandchild.
Try<list<ProcessTree>> trees = os::killtree(child, SIGKILL, true, true);
ASSERT_SOME(trees);
EXPECT_FALSE(trees->empty());
// All processes should be reparented and reaped by init.
elapsed = Duration::zero();
while (true) {
if (os::process(grandchild).isNone() &&
os::process(greatGrandchild).isNone()) {
break;
}
if (elapsed > Seconds(10)) {
FAIL() << "Processes were not reaped after killtree invocation";
}
os::sleep(Milliseconds(5));
elapsed += Milliseconds(5);
}
EXPECT_NONE(os::process(grandchild));
EXPECT_NONE(os::process(greatGrandchild));
}
TEST_F(OsTest, ProcessExists)
{
// Check we exist.
EXPECT_TRUE(os::exists(::getpid()));
// In a FreeBSD jail, pid 1 may not exist.
#if !defined(__FreeBSD__)
// Check init/launchd/systemd exists.
// NOTE: This should return true even if we don't have permission to signal
// the pid.
EXPECT_TRUE(os::exists(1));
#endif
// Check existence of a child process through its lifecycle: running,
// zombied, reaped.
pid_t pid = ::fork();
ASSERT_NE(-1, pid);
if (pid == 0) {
// In child process.
while (true) { sleep(1); }
ABORT("Child should not reach this statement");
}
// In parent.
EXPECT_TRUE(os::exists(pid));
ASSERT_EQ(0, kill(pid, SIGKILL));
// Wait until the process is a zombie.
Duration elapsed = Duration::zero();
while (true) {
Result<os::Process> process = os::process(pid);
ASSERT_SOME(process);
if (process->zombie) {
break;
}
ASSERT_LT(elapsed, Milliseconds(100));
os::sleep(Milliseconds(5));
elapsed += Milliseconds(5);
};
// The process should still 'exist', even if it's a zombie.
EXPECT_TRUE(os::exists(pid));
// Reap the zombie and confirm the process no longer exists.
int status;
EXPECT_EQ(pid, ::waitpid(pid, &status, 0));
EXPECT_WTERMSIG_EQ(SIGKILL, status);
EXPECT_FALSE(os::exists(pid));
}
TEST_F(OsTest, User)
{
Try<string> user_ = os::shell("id -un");
EXPECT_SOME(user_);
Result<string> user = os::user();
ASSERT_SOME_EQ(strings::trim(user_.get()), user);
Try<string> uid_ = os::shell("id -u");
EXPECT_SOME(uid_);
Try<uid_t> uid = numify<uid_t>(strings::trim(uid_.get()));
ASSERT_SOME(uid);
EXPECT_SOME_EQ(uid.get(), os::getuid(user.get()));
Try<string> gid_ = os::shell("id -g");
EXPECT_SOME(gid_);
Try<gid_t> gid = numify<gid_t>(strings::trim(gid_.get()));
ASSERT_SOME(gid);
EXPECT_SOME_EQ(gid.get(), os::getgid(user.get()));
// A random UUID is an invalid username on some platforms. Some
// versions of Linux (e.g., RHEL7) treat invalid usernames
// differently from valid-but-not-found usernames.
EXPECT_NONE(os::getuid(id::UUID::random().toString()));
EXPECT_NONE(os::getgid(id::UUID::random().toString()));
// A username that is valid but that is unlikely to exist.
EXPECT_NONE(os::getuid("zzzvaliduserzzz"));
EXPECT_NONE(os::getgid("zzzvaliduserzzz"));
EXPECT_SOME(os::su(user.get()));
EXPECT_ERROR(os::su(id::UUID::random().toString()));
Try<string> gids_ = os::shell("id -G " + user.get());
EXPECT_SOME(gids_);
Try<vector<string>> tokens =
strings::split(strings::trim(gids_.get(), strings::ANY, "\n"), " ");
ASSERT_SOME(tokens);
std::sort(tokens->begin(), tokens->end());
Try<vector<gid_t>> gids = os::getgrouplist(user.get());
EXPECT_SOME(gids);
vector<string> expected_gids;
foreach (gid_t gid, gids.get()) {
expected_gids.push_back(stringify(gid));
}
std::sort(expected_gids.begin(), expected_gids.end());
EXPECT_EQ(tokens.get(), expected_gids);
EXPECT_SOME(os::setgid(gid.get()));
// 'setgroups' requires root permission.
if (user.get() != "root") {
return;
}
EXPECT_SOME(os::setgroups(gids.get(), uid.get()));
EXPECT_SOME(os::setuid(uid.get()));
}
TEST_F(OsTest, SYMLINK_Chown)
{
Result<uid_t> uid = os::getuid();
ASSERT_SOME(uid);
// 'chown' requires root permission.
if (uid.get() != 0) {
return;
}
// In the following tests, we chown to an artitrary UID. There is
// no special significance to the value 9.
// Set up a simple directory hierarchy.
EXPECT_SOME(os::mkdir("chown/one/two/three"));
EXPECT_SOME(os::touch("chown/one/file"));
EXPECT_SOME(os::touch("chown/one/two/file"));
EXPECT_SOME(os::touch("chown/one/two/three/file"));
// Make a symlink back to the top of the tree so we can verify
// that it isn't followed.
EXPECT_SOME(fs::symlink("../../../../chown", "chown/one/two/three/link"));
// Make a symlink to the middle of the tree and verify that chowning the
// symlink does not chown that subtree.
EXPECT_SOME(fs::symlink("chown/one/two", "two.link"));
EXPECT_SOME(os::chown(9, 9, "two.link", true));
EXPECT_SOME_EQ(9u,
os::stat::uid("two.link", FollowSymlink::DO_NOT_FOLLOW_SYMLINK));
EXPECT_SOME_EQ(0u,
os::stat::uid("chown", FollowSymlink::DO_NOT_FOLLOW_SYMLINK));
EXPECT_SOME_EQ(0u,
os::stat::uid("chown/one/two/three/file",
FollowSymlink::DO_NOT_FOLLOW_SYMLINK));
// Recursively chown the whole tree.
EXPECT_SOME(os::chown(9, 9, "chown", true));
EXPECT_SOME_EQ(9u,
os::stat::uid("chown", FollowSymlink::DO_NOT_FOLLOW_SYMLINK));
EXPECT_SOME_EQ(9u,
os::stat::uid("chown/one/two/three/file",
FollowSymlink::DO_NOT_FOLLOW_SYMLINK));
EXPECT_SOME_EQ(9u,
os::stat::uid("chown/one/two/three/link",
FollowSymlink::DO_NOT_FOLLOW_SYMLINK));
// Chown the subtree with the embedded link back and verify that it
// doesn't follow back to the top of the tree.
EXPECT_SOME(os::chown(0, 0, "chown/one/two/three", true));
EXPECT_SOME_EQ(9u,
os::stat::uid("chown", FollowSymlink::DO_NOT_FOLLOW_SYMLINK));
EXPECT_SOME_EQ(0u,
os::stat::uid("chown/one/two/three",
FollowSymlink::DO_NOT_FOLLOW_SYMLINK));
EXPECT_SOME_EQ(0u,
os::stat::uid("chown/one/two/three/link",
FollowSymlink::DO_NOT_FOLLOW_SYMLINK));
// Verify that non-recursive chown changes the directory and not
// its contents.
EXPECT_SOME(os::chown(0, 0, "chown/one", false));
EXPECT_SOME_EQ(0u,
os::stat::uid("chown/one",
FollowSymlink::DO_NOT_FOLLOW_SYMLINK));
EXPECT_SOME_EQ(9u,
os::stat::uid("chown/one/file",
FollowSymlink::DO_NOT_FOLLOW_SYMLINK));
}
TEST_F(OsTest, ChownNoAccess)
{
Result<uid_t> uid = os::getuid();
Result<gid_t> gid = os::getgid();
ASSERT_SOME(uid);
ASSERT_SOME(gid);
// This test requires that we not be root, since root will
// bypass access checks.
if (uid.get() == 0) {
return;
}
ASSERT_SOME(os::mkdir("one/two"));
// Chown to ourself should be a noop.
EXPECT_SOME(os::chown(uid.get(), gid.get(), "one/two", true));
ASSERT_SOME(os::chmod("one/two", 0));
// Recursive chown should now fail to fully recurse due to
// the lack of permission on "one/two".
EXPECT_ERROR(os::chown(uid.get(), gid.get(), "one", true));
EXPECT_ERROR(os::chown(uid.get(), gid.get(), "one/two", true));
// A non-recursive should succeed when the child is not traversable.
EXPECT_SOME(os::chown(uid.get(), gid.get(), "one", false));
// A non-recursive should succeed on the non-traversable child too.
EXPECT_SOME(os::chown(uid.get(), gid.get(), "one/two", false));
// Restore directory access so the rmdir can work.
ASSERT_SOME(os::chmod("one/two", 0755));
}
#endif // __WINDOWS__
TEST_F(OsTest, TrivialUser)
{
const Result<string> user1 = os::user();
ASSERT_SOME(user1);
ASSERT_NE("", user1.get());
#ifdef __WINDOWS__
const Result<string> user2 = os::user(INT_MAX);
ASSERT_ERROR(user2);
#endif // __WINDOWS__
}
// Test setting/resetting/appending to LD_LIBRARY_PATH environment
// variable (DYLD_LIBRARY_PATH on OS X).
//
// NOTE: This will never be enabled on Windows as there is no equivalent.
#ifndef __WINDOWS__
TEST_F(OsTest, Libraries)
{
const string path1 = "/tmp/path1";
const string path2 = "/tmp/path1";
string ldLibraryPath;
const string originalLibraryPath = os::libraries::paths();
// Test setPaths.
os::libraries::setPaths(path1);
EXPECT_EQ(os::libraries::paths(), path1);
// Test appendPaths.
// 1. With empty LD_LIBRARY_PATH.
// 1a. Set LD_LIBRARY_PATH to an empty string.
os::libraries::setPaths("");
ldLibraryPath = os::libraries::paths();
EXPECT_EQ(ldLibraryPath, "");
// 1b. Now test appendPaths.
os::libraries::appendPaths(path1);
EXPECT_EQ(os::libraries::paths(), path1);
// 2. With non-empty LD_LIBRARY_PATH.
// 2a. Set LD_LIBRARY_PATH to some non-empty value.
os::libraries::setPaths(path2);
ldLibraryPath = os::libraries::paths();
EXPECT_EQ(ldLibraryPath, path2);
// 2b. Now test appendPaths.
os::libraries::appendPaths(path1);
EXPECT_EQ(os::libraries::paths(), path2 + ":" + path1);
// Reset LD_LIBRARY_PATH.
os::libraries::setPaths(originalLibraryPath);
EXPECT_EQ(os::libraries::paths(), originalLibraryPath);
}
#endif // __WINDOWS__
TEST_F(OsTest, Shell)
{
Try<string> result = os::shell("echo %s", "hello world");
#ifdef __WINDOWS__
EXPECT_SOME_EQ("hello world\r\n", result);
#else
EXPECT_SOME_EQ("hello world\n", result);
#endif // __WINDOWS__
result = os::shell("foobar");
EXPECT_ERROR(result);
#ifdef __WINDOWS__
// NOTE: This relies on the strange semantics of Windows' echo,
// where quotes are not removed. We are testing that a quoted
// argument with a space in it remains quoted by `os::shell`.
result = os::shell("echo \"foo bar\"");
EXPECT_SOME_EQ("\"foo bar\"\r\n", result);
// Because the arguments do not have whitespace in them,
// `CommandLineToArgv` removes the surrounding quotes before passing
// them to `echo`.
result = os::shell("echo \"foo\" \"bar\"");
EXPECT_SOME_EQ("foo bar\r\n", result);
#endif // __WINDOWS__
// The `|| true`` necessary so that os::shell() sees a success
// exit code and returns stdout (which we have piped stderr to).
#ifdef __WINDOWS__
result = os::shell("dir foobar889076 2>&1 || exit /b 0");
ASSERT_SOME(result);
EXPECT_TRUE(strings::contains(result.get(), "File Not Found"));
#else
result = os::shell("LC_ALL=C ls /tmp/foobar889076 2>&1 || true");
ASSERT_SOME(result);
EXPECT_TRUE(strings::contains(result.get(), "No such file or directory"));
#endif // __WINDOWS__
// Testing a command that wrote a substantial amount of data.
const string output(2 * os::pagesize(), 'c');
const string outfile = path::join(sandbox.get(), "out.txt");
ASSERT_SOME(os::write(outfile, output));
#ifdef __WINDOWS__
result = os::shell("type %s", outfile.c_str());
#else
result = os::shell("cat %s", outfile.c_str());
#endif // __WINDOWS__
EXPECT_SOME_EQ(output, result);
// Testing a more ambitious command that mutates the filesystem.
const string path = path::join(sandbox.get(), "os_tests.txt");
#ifdef __WINDOWS__
result = os::shell("copy /y nul %s", path.c_str());
ASSERT_SOME(result);
EXPECT_EQ("1 file(s) copied.", strings::trim(result.get()));
#else
result = os::shell("touch %s", path.c_str());
EXPECT_SOME_EQ("", result);
#endif // __WINDOWS__
EXPECT_TRUE(os::exists(path));
// Let's clean up, and ensure this worked too.
#ifdef __WINDOWS__
result = os::shell("del %s", path.c_str());
#else
result = os::shell("rm %s", path.c_str());
#endif // __WINDOWS__
EXPECT_SOME_EQ("", result);
EXPECT_FALSE(os::exists(path));
}
// NOTE: Disabled on Windows because `mknod` does not exist.
#ifndef __WINDOWS__
TEST_F(OsTest, Mknod)
{
#ifdef __FreeBSD__
// If we're in a jail on FreeBSD, we can't use mknod.
if (isJailed()) {
return;
}
#endif
// mknod requires root permission.
Result<string> user = os::user();
ASSERT_SOME(user);
if (user.get() != "root") {
return;
}
const string device = "null";
const string existing = path::join("/dev", device);
ASSERT_TRUE(os::exists(existing));
Try<mode_t> mode = os::stat::mode(existing);
ASSERT_SOME(mode);
Try<dev_t> rdev = os::stat::rdev(existing);
ASSERT_SOME(rdev);
const string another = path::join(os::getcwd(), device);
ASSERT_FALSE(os::exists(another));
EXPECT_SOME(os::mknod(another, mode.get(), rdev.get()));
EXPECT_SOME(os::rm(another));
}
#endif // __WINDOWS__
TEST_F(OsTest, SYMLINK_Realpath)
{
// Create a file.
const Try<string> _testFile = os::mktemp();
ASSERT_SOME(_testFile);
ASSERT_SOME(os::touch(_testFile.get()));
const string& testFile = _testFile.get();
// Create a symlink pointing to a file.
const string testLink = id::UUID::random().toString();
ASSERT_SOME(fs::symlink(testFile, testLink));
// Validate the symlink.
#ifdef __WINDOWS__
Try<int_fd> handle = os::open(testFile, O_RDONLY);
ASSERT_SOME(handle);
FILE_ID_INFO fileInfo;
BOOL result = ::GetFileInformationByHandleEx(
handle.get(), FileIdInfo, &fileInfo, sizeof(fileInfo));
ASSERT_SOME(os::close(handle.get()));
ASSERT_EQ(TRUE, result);
handle = os::open(testLink, O_RDONLY);
ASSERT_SOME(handle);
FILE_ID_INFO linkInfo;
result = ::GetFileInformationByHandleEx(
handle.get(), FileIdInfo, &linkInfo, sizeof(linkInfo));
ASSERT_SOME(os::close(handle.get()));
ASSERT_EQ(TRUE, result);
ASSERT_EQ(fileInfo.VolumeSerialNumber, linkInfo.VolumeSerialNumber);
ASSERT_TRUE(std::equal(
std::begin(fileInfo.FileId.Identifier),
std::end(fileInfo.FileId.Identifier),
std::begin(linkInfo.FileId.Identifier),
std::end(linkInfo.FileId.Identifier)));
#else
const Try<ino_t> fileInode = os::stat::inode(testFile);
ASSERT_SOME(fileInode);
const Try<ino_t> linkInode = os::stat::inode(testLink);
ASSERT_SOME(linkInode);
ASSERT_EQ(fileInode.get(), linkInode.get());
#endif // __WINDOWS__
// Verify that the symlink resolves correctly.
Result<string> resolved = os::realpath(testLink);
ASSERT_SOME(resolved);
EXPECT_TRUE(strings::contains(resolved.get(), testFile));
// Verify that the file itself resolves correctly.
resolved = os::realpath(testFile);
ASSERT_SOME(resolved);
EXPECT_TRUE(strings::contains(resolved.get(), testFile));
// Remove the file and the symlink.
os::rm(testFile);
os::rm(testLink);
}
TEST_F(OsTest, Which)
{
// TODO(jieyu): Test PATH search ordering and file execution bit.
Option<string> which = os::which("ping");
ASSERT_SOME(which);
which = os::which("bar");
EXPECT_NONE(which);
}