2626#include < fcntl.h>
2727#include < sys/wait.h>
2828#include < unistd.h>
29+ #include < poll.h>
30+ #include < signal.h>
2931#endif
3032
3133#ifndef _WIN32
@@ -258,9 +260,11 @@ namespace vix::cli::build
258260 return r;
259261 }
260262
263+ // Fork
261264 pid_t pid = ::fork ();
262265 if (pid == 0 )
263266 {
267+ // Child: redirect stdout+stderr to pipe write end
264268 ::dup2 (pipefd[1 ], STDOUT_FILENO );
265269 ::dup2 (pipefd[1 ], STDERR_FILENO );
266270
@@ -280,6 +284,7 @@ namespace vix::cli::build
280284 _exit (127 );
281285 }
282286
287+ // Parent
283288 ::close (pipefd[1 ]);
284289
285290 std::string firstLine;
@@ -308,9 +313,7 @@ namespace vix::cli::build
308313 return false ;
309314
310315 if (filterCMakeSummary)
311- {
312316 return !is_cmake_configure_summary_line (line);
313- }
314317
315318 if (!progressOnly)
316319 return true ;
@@ -329,56 +332,118 @@ namespace vix::cli::build
329332
330333 std::string buf (16 * 1024 , ' \0 ' );
331334
335+ // Heartbeat state
336+ const auto startTs = std::chrono::steady_clock::now ();
337+ auto lastOutputTs = startTs;
338+ auto lastHeartbeatTs = startTs;
339+
340+ // poll loop
332341 while (true )
333342 {
334- ssize_t n = ::read (pipefd[0 ], &buf[0 ], buf.size ());
335- if (n <= 0 )
336- break ;
343+ struct pollfd pfd;
344+ pfd.fd = pipefd[0 ];
345+ pfd.events = POLLIN | POLLHUP | POLLERR ;
346+ pfd.revents = 0 ;
337347
338- r.producedOutput = true ;
348+ // 250ms tick -> lets us print heartbeat & not look frozen
349+ const int pr = ::poll (&pfd, 1 , 250 );
339350
340- write_all_fd (logfd, buf.data (), static_cast <std::size_t >(n));
351+ if (pr < 0 )
352+ {
353+ if (errno == EINTR )
354+ continue ;
355+ break ;
356+ }
341357
342- if (!gotFirstLine)
358+ // If data is ready, read it
359+ if (pr > 0 && (pfd.revents & POLLIN ))
343360 {
344- for (ssize_t i = 0 ; i < n; ++i)
361+ const ssize_t n = ::read (pipefd[0 ], &buf[0 ], buf.size ());
362+ if (n <= 0 )
363+ break ;
364+
365+ r.producedOutput = true ;
366+ lastOutputTs = std::chrono::steady_clock::now ();
367+
368+ write_all_fd (logfd, buf.data (), static_cast <std::size_t >(n));
369+
370+ if (!gotFirstLine)
345371 {
346- char c = buf[static_cast <std::size_t >(i)];
347- if (c == ' \n ' )
372+ for (ssize_t i = 0 ; i < n; ++i)
348373 {
349- gotFirstLine = true ;
350- break ;
374+ char c = buf[static_cast <std::size_t >(i)];
375+ if (c == ' \n ' )
376+ {
377+ gotFirstLine = true ;
378+ break ;
379+ }
380+ if (firstLine.size () < 200 )
381+ firstLine.push_back (c);
351382 }
352- if (firstLine.size () < 200 )
353- firstLine.push_back (c);
354383 }
355- }
356384
357- if (quiet)
358- continue ;
385+ if (!quiet)
386+ {
387+ consoleBuf.append (buf.data (), static_cast <std::size_t >(n));
359388
360- consoleBuf.append (buf.data (), static_cast <std::size_t >(n));
389+ std::size_t start = 0 ;
390+ while (true )
391+ {
392+ std::size_t nl = consoleBuf.find (' \n ' , start);
393+ if (nl == std::string::npos)
394+ break ;
395+
396+ std::string line = consoleBuf.substr (start, nl - start);
397+ if (should_echo_line (line))
398+ {
399+ line.push_back (' \n ' );
400+ write_all_fd (STDOUT_FILENO , line.data (), line.size ());
401+ }
402+ start = nl + 1 ;
403+ }
361404
362- std::size_t start = 0 ;
363- while (true )
364- {
365- std::size_t nl = consoleBuf.find (' \n ' , start);
366- if (nl == std::string::npos)
367- break ;
405+ if (start > 0 )
406+ consoleBuf.erase (0 , start);
407+ }
368408
369- std::string line = consoleBuf.substr (start, nl - start);
409+ continue ;
410+ }
370411
371- if (should_echo_line (line))
412+ // EOF/HUP
413+ if (pr > 0 && (pfd.revents & (POLLHUP | POLLERR )))
414+ break ;
415+
416+ // No output tick -> heartbeat (avoid "looks frozen")
417+ if (!quiet)
418+ {
419+ const auto now = std::chrono::steady_clock::now ();
420+ const auto silenceMs =
421+ std::chrono::duration_cast<std::chrono::milliseconds>(now - lastOutputTs).count ();
422+ const auto hbMs =
423+ std::chrono::duration_cast<std::chrono::milliseconds>(now - lastHeartbeatTs).count ();
424+
425+ // print heartbeat every 5s of silence
426+ if (silenceMs >= 5000 && hbMs >= 5000 )
372427 {
373- line. push_back ( ' \n ' ) ;
374- write_all_fd ( STDOUT_FILENO , line. data (), line. size ());
375- }
428+ lastHeartbeatTs = now ;
429+ const auto elapsedMs =
430+ std::chrono::duration_cast<std::chrono::milliseconds>(now - startTs). count ();
376431
377- start = nl + 1 ;
432+ std::string msg =
433+ " \r [building] still running… (" + util::format_seconds (elapsedMs) + " ) " ;
434+ write_all_fd (STDOUT_FILENO , msg.data (), msg.size ());
435+ }
378436 }
437+ }
379438
380- if (start > 0 )
381- consoleBuf.erase (0 , start);
439+ // flush a possible last partial line when process ends
440+ if (!quiet && !consoleBuf.empty ())
441+ {
442+ if (should_echo_line (consoleBuf))
443+ {
444+ std::string tail = consoleBuf + " \n " ;
445+ write_all_fd (STDOUT_FILENO , tail.data (), tail.size ());
446+ }
382447 }
383448
384449 ::close (pipefd[0 ]);
@@ -391,11 +456,17 @@ namespace vix::cli::build
391456 return r;
392457 }
393458
459+ if (!quiet)
460+ {
461+ // clear heartbeat line if any
462+ const std::string clear = " \r " ;
463+ write_all_fd (STDOUT_FILENO , clear.data (), clear.size ());
464+ }
465+
394466 r.exitCode = process::normalize_exit_code (status);
395467 r.capturedFirstLine = util::trim (firstLine);
396468 return r;
397469 }
398-
399470#else // _WIN32
400471
401472 process::ExecResult run_process_capture (
0 commit comments