2020-06-29 17:06:19 +03:00
|
|
|
/*
|
|
|
|
* Copyright (c) 2020, Sahan Fernando <sahan.h.fernando@gmail.com>
|
2023-07-13 01:23:34 +03:00
|
|
|
* Copyright (c) 2023, Tim Ledbetter <timledbetter@gmail.com>
|
2020-06-29 17:06:19 +03:00
|
|
|
*
|
2021-04-22 11:24:48 +03:00
|
|
|
* SPDX-License-Identifier: BSD-2-Clause
|
2020-06-29 17:06:19 +03:00
|
|
|
*/
|
|
|
|
|
2021-05-15 13:34:40 +03:00
|
|
|
#include <AK/Assertions.h>
|
2023-12-16 17:19:34 +03:00
|
|
|
#include <AK/ByteString.h>
|
2020-06-29 17:06:19 +03:00
|
|
|
#include <AK/StringBuilder.h>
|
|
|
|
#include <AK/Time.h>
|
|
|
|
#include <AK/Vector.h>
|
|
|
|
#include <LibCore/ArgsParser.h>
|
2021-10-03 02:12:39 +03:00
|
|
|
#include <LibCore/FileWatcher.h>
|
2021-11-24 18:36:07 +03:00
|
|
|
#include <LibCore/System.h>
|
2023-03-21 18:35:30 +03:00
|
|
|
#include <LibFileSystem/FileSystem.h>
|
2021-11-24 18:36:07 +03:00
|
|
|
#include <LibMain/Main.h>
|
2021-05-14 17:32:57 +03:00
|
|
|
#include <errno.h>
|
2020-06-29 17:06:19 +03:00
|
|
|
#include <stdio.h>
|
|
|
|
#include <time.h>
|
|
|
|
#include <unistd.h>
|
|
|
|
|
|
|
|
static int opt_interval = 2;
|
|
|
|
static bool flag_noheader = false;
|
|
|
|
static bool flag_beep_on_fail = false;
|
2022-04-01 20:58:27 +03:00
|
|
|
static int volatile exit_code = 0;
|
2020-06-29 17:06:19 +03:00
|
|
|
static volatile pid_t child_pid = -1;
|
|
|
|
|
2023-07-13 19:06:08 +03:00
|
|
|
static struct termios g_save;
|
|
|
|
|
|
|
|
static ErrorOr<void> setup_tty()
|
|
|
|
{
|
|
|
|
// Save previous tty settings.
|
|
|
|
g_save = TRY(Core::System::tcgetattr(STDOUT_FILENO));
|
|
|
|
|
|
|
|
struct termios raw = g_save;
|
|
|
|
raw.c_lflag &= ~(ECHO | ICANON);
|
|
|
|
|
|
|
|
// Disable echo and line buffering
|
|
|
|
TRY(Core::System::tcsetattr(STDOUT_FILENO, TCSAFLUSH, raw));
|
|
|
|
|
|
|
|
// Save cursor and switch to alternate buffer.
|
|
|
|
out("\e[s\e[?1047h");
|
|
|
|
return {};
|
|
|
|
}
|
|
|
|
|
|
|
|
static void teardown_tty()
|
|
|
|
{
|
|
|
|
auto maybe_error = Core::System::tcsetattr(STDOUT_FILENO, TCSAFLUSH, g_save);
|
|
|
|
if (maybe_error.is_error())
|
|
|
|
warnln("Failed to reset original terminal state: {}", strerror(maybe_error.error().code()));
|
|
|
|
|
|
|
|
out("\e[?1047l\e[u");
|
|
|
|
}
|
|
|
|
|
2023-12-16 17:19:34 +03:00
|
|
|
static ByteString build_header_string(Vector<ByteString> const& command, Duration const& interval)
|
2020-06-29 17:06:19 +03:00
|
|
|
{
|
|
|
|
StringBuilder builder;
|
2023-05-29 09:22:09 +03:00
|
|
|
auto interval_seconds = interval.to_truncated_seconds();
|
|
|
|
auto interval_fractional_seconds = (interval.to_truncated_milliseconds() % 1000) / 100;
|
|
|
|
builder.appendff("Every {}.{}s: \x1b[1m", interval_seconds, interval_fractional_seconds);
|
2020-06-29 17:06:19 +03:00
|
|
|
builder.join(' ', command);
|
2022-07-11 20:32:29 +03:00
|
|
|
builder.append("\x1b[0m"sv);
|
2023-12-16 17:19:34 +03:00
|
|
|
return builder.to_byte_string();
|
2020-06-29 17:06:19 +03:00
|
|
|
}
|
|
|
|
|
2023-12-16 17:19:34 +03:00
|
|
|
static ByteString build_header_string(Vector<ByteString> const& command, Vector<ByteString> const& filenames)
|
2021-10-03 02:12:39 +03:00
|
|
|
{
|
|
|
|
StringBuilder builder;
|
|
|
|
builder.appendff("Every time any of {} changes: \x1b[1m", filenames);
|
|
|
|
builder.join(' ', command);
|
2022-07-11 20:32:29 +03:00
|
|
|
builder.append("\x1b[0m"sv);
|
2023-12-16 17:19:34 +03:00
|
|
|
return builder.to_byte_string();
|
2021-10-03 02:12:39 +03:00
|
|
|
}
|
|
|
|
|
2020-08-11 00:48:37 +03:00
|
|
|
static void handle_signal(int signal)
|
2020-06-29 17:06:19 +03:00
|
|
|
{
|
|
|
|
if (child_pid > 0) {
|
|
|
|
if (kill(child_pid, signal) < 0) {
|
|
|
|
perror("kill");
|
|
|
|
}
|
|
|
|
int status;
|
|
|
|
if (waitpid(child_pid, &status, 0) < 0) {
|
|
|
|
perror("waitpid");
|
|
|
|
} else if (!WIFEXITED(status) || WEXITSTATUS(status) != 0) {
|
|
|
|
exit_code = 1;
|
|
|
|
}
|
|
|
|
}
|
2023-07-13 19:06:08 +03:00
|
|
|
auto is_a_tty_or_error = Core::System::isatty(STDOUT_FILENO);
|
|
|
|
if (!is_a_tty_or_error.is_error() && is_a_tty_or_error.value())
|
|
|
|
teardown_tty();
|
|
|
|
|
2020-06-29 17:06:19 +03:00
|
|
|
exit(exit_code);
|
|
|
|
}
|
|
|
|
|
2023-12-16 17:19:34 +03:00
|
|
|
static int run_command(Vector<ByteString> const& command)
|
2020-06-29 17:06:19 +03:00
|
|
|
{
|
2023-03-01 00:01:46 +03:00
|
|
|
Vector<char const*> argv;
|
|
|
|
argv.ensure_capacity(command.size() + 1);
|
|
|
|
for (auto& arg : command)
|
|
|
|
argv.unchecked_append(arg.characters());
|
|
|
|
argv.unchecked_append(nullptr);
|
2023-07-13 01:23:34 +03:00
|
|
|
auto child_pid_or_error = Core::System::posix_spawnp(command[0], nullptr, nullptr, const_cast<char**>(argv.data()), environ);
|
|
|
|
if (child_pid_or_error.is_error()) {
|
2020-06-29 17:06:19 +03:00
|
|
|
exit_code = 1;
|
2023-07-13 01:23:34 +03:00
|
|
|
warnln("posix_spawn: {}", strerror(child_pid_or_error.error().code()));
|
|
|
|
return child_pid_or_error.error().code();
|
2020-06-29 17:06:19 +03:00
|
|
|
}
|
2020-07-05 17:01:05 +03:00
|
|
|
|
2023-07-13 01:23:34 +03:00
|
|
|
child_pid = child_pid_or_error.release_value();
|
|
|
|
|
2020-08-11 00:48:37 +03:00
|
|
|
// Wait for the child to terminate, then return its exit code.
|
2023-07-13 01:23:34 +03:00
|
|
|
Core::System::WaitPidResult waitpid_result;
|
|
|
|
int error_code = 0;
|
2020-08-11 00:48:37 +03:00
|
|
|
do {
|
2023-07-13 01:23:34 +03:00
|
|
|
auto result_or_error = Core::System::waitpid(child_pid, 0);
|
|
|
|
if (result_or_error.is_error())
|
|
|
|
error_code = result_or_error.error().code();
|
|
|
|
else
|
|
|
|
waitpid_result = result_or_error.release_value();
|
|
|
|
} while (waitpid_result.pid < 0 && error_code == EINTR);
|
|
|
|
VERIFY(waitpid_result.pid == child_pid);
|
2020-08-11 00:48:37 +03:00
|
|
|
child_pid = -1;
|
2023-07-13 01:23:34 +03:00
|
|
|
if (error_code > 0) {
|
|
|
|
warnln("waitpid: {}", strerror(error_code));
|
2020-08-11 00:48:37 +03:00
|
|
|
return 1;
|
|
|
|
}
|
2023-07-13 01:23:34 +03:00
|
|
|
if (WIFEXITED(waitpid_result.status)) {
|
|
|
|
return WEXITSTATUS(waitpid_result.status);
|
2020-08-11 00:48:37 +03:00
|
|
|
}
|
2023-07-13 01:23:34 +03:00
|
|
|
return 1;
|
2020-06-29 17:06:19 +03:00
|
|
|
}
|
|
|
|
|
2021-11-24 18:36:07 +03:00
|
|
|
ErrorOr<int> serenity_main(Main::Arguments arguments)
|
2020-06-29 17:06:19 +03:00
|
|
|
{
|
2023-07-13 19:06:08 +03:00
|
|
|
TRY(Core::System::pledge("stdio proc exec rpath tty sigaction"));
|
2020-06-29 17:06:19 +03:00
|
|
|
|
2023-12-16 17:19:34 +03:00
|
|
|
Vector<ByteString> files_to_watch;
|
|
|
|
Vector<ByteString> command;
|
2020-06-29 17:06:19 +03:00
|
|
|
Core::ArgsParser args_parser;
|
2021-06-07 22:56:33 +03:00
|
|
|
args_parser.set_stop_on_first_non_option(true);
|
2020-12-05 18:22:58 +03:00
|
|
|
args_parser.set_general_help("Execute a command repeatedly, and watch its output over time.");
|
2020-06-29 17:06:19 +03:00
|
|
|
args_parser.add_option(opt_interval, "Amount of time between updates", "interval", 'n', "seconds");
|
|
|
|
args_parser.add_option(flag_noheader, "Turn off the header describing the command and interval", "no-title", 't');
|
|
|
|
args_parser.add_option(flag_beep_on_fail, "Beep if the command has a non-zero exit code", "beep", 'b');
|
2021-10-03 02:12:39 +03:00
|
|
|
Core::ArgsParser::Option file_arg {
|
2022-07-12 23:13:38 +03:00
|
|
|
.argument_mode = Core::ArgsParser::OptionArgumentMode::Required,
|
2021-10-03 02:12:39 +03:00
|
|
|
.help_string = "Run command whenever this file changes. Can be used multiple times.",
|
|
|
|
.long_name = "file",
|
|
|
|
.short_name = 'f',
|
|
|
|
.value_name = "file",
|
|
|
|
.accept_value = [&files_to_watch](auto filename) {
|
|
|
|
files_to_watch.append(filename);
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
};
|
|
|
|
args_parser.add_option(move(file_arg));
|
2020-06-29 17:06:19 +03:00
|
|
|
args_parser.add_positional_argument(command, "Command to run", "command");
|
2021-11-24 18:36:07 +03:00
|
|
|
args_parser.parse(arguments);
|
2020-06-29 17:06:19 +03:00
|
|
|
|
2023-07-13 19:06:08 +03:00
|
|
|
if (TRY(Core::System::isatty(STDOUT_FILENO)))
|
|
|
|
TRY(setup_tty());
|
|
|
|
|
|
|
|
struct sigaction quit_action;
|
|
|
|
quit_action.sa_handler = handle_signal;
|
|
|
|
TRY(Core::System::sigaction(SIGTERM, &quit_action, nullptr));
|
|
|
|
TRY(Core::System::sigaction(SIGINT, &quit_action, nullptr));
|
|
|
|
|
2023-12-16 17:19:34 +03:00
|
|
|
ByteString header;
|
2021-10-03 02:12:39 +03:00
|
|
|
|
|
|
|
auto watch_callback = [&] {
|
2020-06-29 17:06:19 +03:00
|
|
|
// Clear the screen, then reset the cursor position to the top left.
|
2023-08-04 01:13:37 +03:00
|
|
|
out("\033[H\033[2J");
|
2020-06-29 17:06:19 +03:00
|
|
|
// Print the header.
|
|
|
|
if (!flag_noheader) {
|
2023-08-04 01:13:37 +03:00
|
|
|
outln("{}", header);
|
|
|
|
outln();
|
2020-06-29 17:06:19 +03:00
|
|
|
} else {
|
2023-08-04 01:13:37 +03:00
|
|
|
fflush(stdout);
|
2020-06-29 17:06:19 +03:00
|
|
|
}
|
|
|
|
if (run_command(command) != 0) {
|
|
|
|
exit_code = 1;
|
|
|
|
if (flag_beep_on_fail) {
|
2023-08-04 01:13:37 +03:00
|
|
|
out("\a");
|
|
|
|
fflush(stdout);
|
2020-06-29 17:06:19 +03:00
|
|
|
}
|
|
|
|
}
|
2021-10-03 02:12:39 +03:00
|
|
|
};
|
|
|
|
|
|
|
|
if (!files_to_watch.is_empty()) {
|
|
|
|
header = build_header_string(command, files_to_watch);
|
|
|
|
|
|
|
|
auto file_watcher = Core::BlockingFileWatcher();
|
|
|
|
for (auto const& file : files_to_watch) {
|
2023-03-21 18:35:30 +03:00
|
|
|
if (!FileSystem::exists(file)) {
|
2021-10-03 02:12:39 +03:00
|
|
|
warnln("Cannot watch '{}', it does not exist.", file);
|
|
|
|
return 1;
|
|
|
|
}
|
|
|
|
if (!file_watcher.is_watching(file)) {
|
2021-11-24 18:36:07 +03:00
|
|
|
auto could_add_to_watch = TRY(file_watcher.add_watch(file, Core::FileWatcherEvent::Type::MetadataModified));
|
|
|
|
if (!could_add_to_watch) {
|
2021-10-03 02:12:39 +03:00
|
|
|
warnln("Could not add '{}' to watch list.", file);
|
|
|
|
return 1;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
watch_callback();
|
|
|
|
while (true) {
|
|
|
|
auto maybe_event = file_watcher.wait_for_event();
|
|
|
|
if (maybe_event.has_value()) {
|
|
|
|
watch_callback();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} else {
|
2023-07-13 19:06:08 +03:00
|
|
|
TRY(Core::System::pledge("stdio proc exec tty"));
|
2021-10-03 02:12:39 +03:00
|
|
|
|
2023-05-29 09:22:09 +03:00
|
|
|
Duration interval;
|
2021-10-03 02:12:39 +03:00
|
|
|
if (opt_interval <= 0) {
|
2023-05-29 09:22:09 +03:00
|
|
|
interval = Duration::from_milliseconds(100);
|
2021-10-03 02:12:39 +03:00
|
|
|
} else {
|
2023-05-29 09:22:09 +03:00
|
|
|
interval = Duration::from_seconds(opt_interval);
|
2021-10-03 02:12:39 +03:00
|
|
|
}
|
|
|
|
|
2023-05-29 09:22:09 +03:00
|
|
|
auto now = MonotonicTime::now();
|
2021-10-03 02:12:39 +03:00
|
|
|
auto next_run_time = now;
|
|
|
|
header = build_header_string(command, interval);
|
|
|
|
while (true) {
|
2023-05-29 09:22:09 +03:00
|
|
|
auto duration_to_sleep = (next_run_time - now).to_timespec();
|
|
|
|
timespec remaining_sleep {};
|
|
|
|
do {
|
|
|
|
clock_nanosleep(CLOCK_MONOTONIC, 0, &duration_to_sleep, &remaining_sleep);
|
|
|
|
} while (remaining_sleep.tv_sec || remaining_sleep.tv_nsec);
|
2021-10-03 02:12:39 +03:00
|
|
|
|
|
|
|
watch_callback();
|
|
|
|
|
2023-05-29 09:22:09 +03:00
|
|
|
now = MonotonicTime::now();
|
|
|
|
next_run_time = next_run_time + interval;
|
|
|
|
if (next_run_time < now) {
|
2021-10-03 02:12:39 +03:00
|
|
|
// The next execution is overdue, so we set next_run_time to now to prevent drift.
|
|
|
|
next_run_time = now;
|
|
|
|
}
|
2020-06-29 17:06:19 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|