Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Userland/Utilities/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,7 @@ set(CMD_SOURCES_CPP
test-pthread.cpp
test-unveil.cpp
test.cpp
timeout.cpp
test_env.cpp
timezone.cpp
top.cpp
Expand Down
144 changes: 144 additions & 0 deletions Userland/Utilities/timeout.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
/*
* Copyright (c) 2025, Jake Knoth <[email protected]>
*
* SPDX-License-Identifier: BSD-2-Clause
*/

#include <AK/ByteString.h>
#include <AK/Format.h>
#include <AK/StringView.h>
#include <AK/Vector.h>
#include <LibCore/ArgsParser.h>
#include <LibCore/System.h>
#include <LibMain/Main.h>
#include <errno.h>
#include <signal.h>
#include <stdio.h>
#include <string.h>
#include <time.h>
#include <unistd.h>

static bool volatile g_interrupted = false;
static void handle_sigint(int)
{
g_interrupted = true;
}

ErrorOr<int> serenity_main(Main::Arguments arguments)
{
double secs;
Vector<StringView> command_and_args;
Core::ArgsParser args_parser;
args_parser.set_stop_on_first_non_option(true);
args_parser.add_positional_argument(secs, "Time limit in seconds", "secs", Core::ArgsParser::Required::Yes);
args_parser.add_positional_argument(command_and_args, "Command and arguments to be run",
"command", Core::ArgsParser::Required::Yes);
args_parser.parse(arguments);
Vector<ByteString> argv_storage;
argv_storage.ensure_capacity(command_and_args.size());
for (auto sv : command_and_args) {
argv_storage.append(sv.to_byte_string());
}
Vector<char*> argv_ptrs;
argv_ptrs.ensure_capacity(argv_storage.size() + 1);
for (auto& bs : argv_storage) {
argv_ptrs.append(const_cast<char*>(bs.characters()));
}
argv_ptrs.append(nullptr);
if (argv_ptrs.size() < 2) {
// error: no command to run
return 125;
}
struct sigaction sa;
memset(&sa, 0, sizeof(struct sigaction));
sa.sa_handler = handle_sigint;
sigaction(SIGINT, &sa, nullptr);
Comment on lines +52 to +55
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's try to use less C-ism.

Suggested change
struct sigaction sa;
memset(&sa, 0, sizeof(struct sigaction));
sa.sa_handler = handle_sigint;
sigaction(SIGINT, &sa, nullptr);
struct sigaction sa{}; // This will zero initialize the struct.
sa.sa_handler = handle_sigint;
TRY(Core::System::sigaction(SIGINT, &sa, nullptr)); // To get error handling.


if (secs < 0) {
perror("negative timeout");
return 125;
}
TRY(Core::System::pledge("stdio proc exec sigaction"));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I haven't used pledge with sigaction but I would expect that it is only required for the syscall. And here you're already done with that so you could drop sigaction from the pledge line. Feel free to dismiss if my assumption is false.

double whole_seconds = static_cast<time_t>(secs);
double fraction = secs - whole_seconds;
timespec requested_timeout {
.tv_sec = static_cast<time_t>(whole_seconds),
.tv_nsec = static_cast<long>(fraction * (double)1000000000),
};
timespec tmp {};
clock_gettime(CLOCK_MONOTONIC, &tmp);
timespec deadline = {
.tv_sec = tmp.tv_sec + requested_timeout.tv_sec,
.tv_nsec = tmp.tv_nsec + requested_timeout.tv_nsec,
};
if (deadline.tv_nsec >= 1'000'000'000) {
deadline.tv_sec++;
deadline.tv_nsec -= 1'000'000'000;
}
Comment on lines +62 to +77
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have some time utilities to help you here.
From AK/Time.h, you can invoke MonotonicTime::now() and then add a Duration that you created from secs (that can probably have a less abbreviated and more descriptive name, e.g. seconds, time_limit).


pid_t child_pid = fork();
if (child_pid < 0) {
perror("fork");
return 125;
}
if (child_pid == 0) {
setpgid(0, 0);
execvp(argv_ptrs[0], argv_ptrs.data());
perror("execvp");
if (errno == ENOENT) {
_exit(127);
}
_exit(126);
Comment on lines +79 to +91
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again, Core::System got your back here. And Core::System::exec will even take a Span<StringView> so you won't have to convert the Vector from ArgsParser.

} else {
int status = 0;
bool timed_out = false;
auto timespec_compare = [](timespec const& a, timespec const& b) {
return (a.tv_sec > b.tv_sec) || (a.tv_sec == b.tv_sec && a.tv_nsec >= b.tv_nsec);
};
setpgid(child_pid, child_pid);
while (true) {
pid_t w = waitpid(child_pid, &status, WNOHANG);
if (w == child_pid) {
break;
}
if (w < 0 && errno != EINTR) {
perror("waitpid");
// try to clean up
kill(-child_pid, SIGKILL);
waitpid(child_pid, &status, 0);
return 125;
}
timespec now {};
clock_gettime(CLOCK_MONOTONIC, &now);
if (timespec_compare(now, deadline)) {
timed_out = true;
kill(-child_pid, SIGTERM);
timespec grace { 1, 0 };
nanosleep(&grace, nullptr);
w = waitpid(child_pid, &status, WNOHANG);
if (w == 0) {
kill(-child_pid, SIGKILL);
waitpid(child_pid, &status, 0);
}
break;
}
if (g_interrupted) {
kill(-child_pid, SIGINT);
waitpid(child_pid, &status, 0);
TRY(Core::System::signal(SIGINT, SIG_DFL));
raise(SIGINT);
__builtin_unreachable();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We use VERIFY_NOT_REACHED(); for this.

}
timespec spin { 0, 25'000'000 };
nanosleep(&spin, nullptr);
}
if (timed_out)
return 124;
if (WIFEXITED(status))
return WEXITSTATUS(status);
if (WIFSIGNALED(status))
return 128 + WTERMSIG(status);
return 125;
}
return 0;
}
Loading