git/list[1] front-page[2] threads[3] people[4] search[5] about
wed 2026-10-07 18:15 UTC

[PATCH 4/9] run-command: add support for timeout in command finisher

From
Siddh Raman Pant <siddh.raman.pant@oracle.com>
Date
May 19, 2026, 16:30 UTC
Message-ID
<f58c8c522814dce9257f64733e9fbc9bd9f446c0.1779207350.git.siddh.raman.pant@oracle.com>
In-Reply-To
<cover.1779207350.git.siddh.raman.pant@oracle.com>

A called command may not respond to the initial signal and will get stuck in finish_command() -> wait_or_whine().

So let's add timeout support into the finisher so that if a deadline occurs, we can send a force-kill signal.

The force-kill signal is in the argument because a program may trap a signal, so it is the responsibility of caller to pass the correct kill signal.

Assisted-by: Codex:gpt-5.5-xhigh-fast
Signed-off-by: Siddh Raman Pant <siddh.raman.pant@oracle.com>
---
 run-command.c | 92 +++++++++++++++++++++++++++++++++++++++++++++++----
 run-command.h | 13 ++++++++
 2 files changed, 98 insertions(+), 7 deletions(-)
diff --git a/run-command.c b/run-command.c
index c146a56532a1..60b84610d1f0 100644
--- a/run-command.c
+++ b/run-command.c
@@ -554,16 +554,63 @@ static inline void set_cloexec(int fd)
 		fcntl(fd, F_SETFD, flags | FD_CLOEXEC);
 }
 
-static int wait_or_whine(pid_t pid, const char *argv0, int in_signal)
+#define NS_IN_10MS 10000000ULL	/* 10 ms = 10^-2 s = 10^(9-2) ns = 10^7 ns */
+
+/* If timeout_ns == 0, no timeout happens (the timeout path is not taken). */
+static int wait_or_whine_timeout(pid_t pid, const char *argv0, int in_signal,
+				 uint64_t timeout_ns)
 {
 	int status, code = -1;
 	pid_t waiting;
 	int failed_errno = 0;
+	int flags = timeout_ns ? WNOHANG : 0;
+	bool timed_out = false;
+	uint64_t deadline_ns = getnanotime() + timeout_ns;
+
+	while(1) {
+		uint64_t current_time_ns, remaining_ns;
+		waiting = waitpid(pid, &status, flags);
+
+		/* Retry if interrupted. */
+		if (waiting < 0 && errno == EINTR)
+			continue;
+
+		/* Break if exited. */
+		if (waiting)
+			break;
+
+		/* If no timeout is specified, retry till it exits. */
+		if (!timeout_ns)
+			continue;
 
-	while ((waiting = waitpid(pid, &status, 0)) < 0 && errno == EINTR)
-		;	/* nothing */
+		current_time_ns = getnanotime();
+
+		/* If we are past the deadline, set errno and break. */
+		if (deadline_ns <= current_time_ns) {
+			errno = ETIMEDOUT;
+			timed_out = true;
+			break;
+		}
+
+		/**
+		 * Retry after a sleep(min(remaining, default_chunk)).
+		 *
+		 * We don't blindly sleep for the entire remaining time because
+		 * the process can exit early.
+		 *
+		 * The subtraction of uint64_t is safe here since we have
+		 * already established that deadline_ns > current_time_ns.
+		 */
+		remaining_ns = deadline_ns - current_time_ns;
+		sleep_nanosec(remaining_ns < NS_IN_10MS ?
+			      remaining_ns : NS_IN_10MS);
+	}
 
-	if (waiting < 0) {
+	if (timed_out) {
+		failed_errno = errno;
+		if (!in_signal)
+			error_errno("waitpid for %s timed out", argv0);
+	} else if (waiting < 0) {
 		failed_errno = errno;
 		if (!in_signal)
 			error_errno("waitpid for %s failed", argv0);
@@ -587,13 +634,28 @@ static int wait_or_whine(pid_t pid, const char *argv0, int in_signal)
 			error("waitpid is confused (%s)", argv0);
 	}
 
-	if (!in_signal)
+	/**
+	 * Signal handlers use the cleanup list while reaping children, so only
+	 * non-signal waiters (in_signal != 0) should update it.
+	 *
+	 * In case of a timeout, we keep the child registered since it is
+	 * actually not reaped so removing would be wrong. It is the
+	 * responsibility of the caller to detect the timeout and do cleanup,
+	 * like sending a kill signal using this function without a timeout.
+	 */
+	if (!in_signal && !timed_out)
 		clear_child_for_cleanup(pid);
 
 	errno = failed_errno;
 	return code;
 }
 
+/* Non-timeout wrapper for compatibility. */
+static int wait_or_whine(pid_t pid, const char *argv0, int in_signal)
+{
+	return wait_or_whine_timeout(pid, argv0, in_signal, 0);
+}
+
 static void trace_add_env(struct strbuf *dst, const char *const *deltaenv)
 {
 	struct string_list envs = STRING_LIST_INIT_DUP;
@@ -989,15 +1051,31 @@ int start_command(struct child_process *cmd)
 	return 0;
 }
 
-int finish_command(struct child_process *cmd)
+/* See comment in the header file for executive summary. */
+int finish_command_with_timeout(struct child_process *cmd, uint64_t timeout_ns,
+				int signal_on_timeout)
 {
-	int ret = wait_or_whine(cmd->pid, cmd->args.v[0], 0);
+	int ret = wait_or_whine_timeout(cmd->pid, cmd->args.v[0], 0,
+					timeout_ns);
+
+	if (timeout_ns && ret < 0 && errno == ETIMEDOUT) {
+		kill(cmd->pid, signal_on_timeout);
+		ret = wait_or_whine(cmd->pid, cmd->args.v[0], 0);
+	}
+
 	trace2_child_exit(cmd, ret);
 	child_process_clear(cmd);
 	invalidate_lstat_cache();
 	return ret;
 }
 
+/* Non-timeout wrapper for compatibility. */
+int finish_command(struct child_process *cmd)
+{
+	return finish_command_with_timeout(cmd, 0, 0);
+}
+
+
 int finish_command_in_signal(struct child_process *cmd)
 {
 	int ret = wait_or_whine(cmd->pid, cmd->args.v[0], 1);
diff --git a/run-command.h b/run-command.h
index 8ca496d7bdeb..cb1c8ba4ec01 100644
--- a/run-command.h
+++ b/run-command.h
@@ -215,6 +215,19 @@ int start_command(struct child_process *);
  */
 int finish_command(struct child_process *);
 
+/**
+ * Wait for the completion of a sub-process that was started with
+ * start_command(), but uptil a given timeout duration timeout_ns.
+ *
+ * If it has not exited after timeout_ns, signal_on_timeout is sent to the
+ * process. We don't enforce a timeout for the second wait after sending
+ * the signal (as the process cleanup needs to happen), so it will block there.
+ *
+ * If timeout_ns == 0, no timeout happens and signal_on_timeout is ignored.
+ */
+int finish_command_with_timeout(struct child_process *cmd, uint64_t timeout_ns,
+				int signal_on_timeout);
+
 int finish_command_in_signal(struct child_process *);
 
 /**
-- 
2.53.0
Previous: Siddh Raman PantNext: Siddh Raman Pant
Message 3 of 29 in “Add support for an external command for fetching notes”
  1. 0/9 Add support for an external command for fetching notesSiddh Raman Pant, May 19, 2026
  2. 1/9 Documentation/git-range-diff: add missing notes options in synopsisSiddh Raman Pant, May 19, 2026
  3. 4/9 run-command: add support for timeout in command finisherSiddh Raman Pant, May 19, 2026
  4. 5/9 wrapper: add support for timeout and deadline in read helpersSiddh Raman Pant, May 19, 2026
  5. 6/9 t3301: cover generic displayed notes behaviorSiddh Raman Pant, May 19, 2026
  6. 7/9 notes: support an external command to display notesSiddh Raman Pant, May 19, 2026
  7. 2/9 notes: convert raw arg in format_display_notes() to boolSiddh Raman Pant, May 19, 2026
  8. 3/9 wrapper: add sleep_nanosecSiddh Raman Pant, May 19, 2026
  9. 8/9 Documentation: document external notes command optionsSiddh Raman Pant, May 19, 2026
  10. 9/9 t: add tests for external notes commandSiddh Raman Pant, May 19, 2026
  11. Junio C HamanoMay 19, 2026
  12. Junio C HamanoMay 19, 2026
  13. Junio C HamanoMay 20, 2026
  14. Siddh Raman PantMay 20, 2026
  15. Siddh Raman PantMay 20, 2026
  16. Siddh Raman PantMay 20, 2026
  17. Junio C HamanoMay 21, 2026
  18. brian m. carlsonMay 21, 2026
  19. Siddh Raman PantMay 21, 2026
  20. Siddh Raman PantMay 21, 2026
  21. Johannes SixtMay 21, 2026
  22. Oswald BuddenhagenMay 21, 2026
  23. Siddh Raman PantMay 21, 2026
  24. Johannes SixtMay 21, 2026
  25. brian m. carlsonMay 21, 2026
  26. Junio C HamanoMay 22, 2026
  27. Jeff KingMay 22, 2026
  28. Siddh Raman PantMay 22, 2026
  29. Siddh Raman PantMay 22, 2026

Read the whole thread, see it on lore, or plain text.

$ cat FOOTERMessages come from the public archive at lore.kernel.org/git, fetched every hour. The front page is chosen and written each morning by an AI editor and can be wrong; the threads themselves are the record. About and API. For agents: an MCP server at https://gitlist.dev/mcp, and any thread, story or person page as Markdown by adding .md to its URL (or sending Accept: text/markdown). Details in /llms.txt.