Browse Source

Implement output capture to buffer

The new 'log-type = buffer' setting can be used to specify that output
should be captured to a buffer in memory rather than to file. The
maximum size (in bytes) can be specified via 'log-buffer-size'.

"dinitctl catlog <service-name>" can be used to view the captured output
from a service.

Documentation pending.
Davin McCall 1 year ago
parent
commit
c66a24450b

+ 212 - 124
src/baseproc-service.cc

@@ -106,129 +106,194 @@ bool base_process_service::start_ps_process(const std::vector<const char *> &cmd
     }
 
     const char * logfile = this->logfile.c_str();
-    if (*logfile == 0) {
-        logfile = "/dev/null";
-    }
-
-    bool child_status_registered = false;
-    control_conn_t *control_conn = nullptr;
-
-    int control_socket[2] = {-1, -1};
-    int notify_pipe[2] = {-1, -1};
-    bool have_notify = !notification_var.empty() || force_notification_fd != -1;
-    ready_notify_watcher * rwatcher = have_notify ? get_ready_watcher() : nullptr;
-    bool ready_watcher_registered = false;
-
-    if (onstart_flags.pass_cs_fd) {
-        if (dinit_socketpair(AF_UNIX, SOCK_STREAM, /* protocol */ 0, control_socket, SOCK_NONBLOCK)) {
-            log(loglevel_t::ERROR, get_name(), ": can't create control socket: ", strerror(errno));
-            goto out_p;
-        }
-
-        // Make the server side socket close-on-exec:
-        int fdflags = bp_sys::fcntl(control_socket[0], F_GETFD);
-        bp_sys::fcntl(control_socket[0], F_SETFD, fdflags | FD_CLOEXEC);
-
-        try {
-            control_conn = new control_conn_t(event_loop, services, control_socket[0]);
-        }
-        catch (std::exception &exc) {
-            log(loglevel_t::ERROR, get_name(), ": can't launch process; out of memory");
-            goto out_cs;
-        }
-    }
-
-    if (have_notify) {
-        // Create a notification pipe:
-        if (bp_sys::pipe2(notify_pipe, 0) != 0) {
-            log(loglevel_t::ERROR, get_name(), ": can't create notification pipe: ", strerror(errno));
-            goto out_cs_h;
-        }
-
-        // Set the read side as close-on-exec:
-        int fdflags = bp_sys::fcntl(notify_pipe[0], F_GETFD);
-        bp_sys::fcntl(notify_pipe[0], F_SETFD, fdflags | FD_CLOEXEC);
-
-        // add, but don't yet enable, readiness watcher:
-        try {
-            rwatcher->add_watch(event_loop, notify_pipe[0], dasynq::IN_EVENTS, false);
-            ready_watcher_registered = true;
-        }
-        catch (std::exception &exc) {
-            log(loglevel_t::ERROR, get_name(), ": can't add notification watch: ", exc.what());
-        }
-    }
-
-    // Set up complete, now fork and exec:
-
-    pid_t forkpid;
-
-    try {
-        child_status_listener.add_watch(event_loop, pipefd[0], dasynq::IN_EVENTS);
-        child_status_registered = true;
-
-        // We specify a high priority (i.e. low priority value) so that process termination is
-        // handled early. This means we have always recorded that the process is terminated by the
-        // time that we handle events that might otherwise cause us to signal the process, so we
-        // avoid sending a signal to an invalid (and possibly recycled) process ID.
-        forkpid = child_listener.fork(event_loop, reserved_child_watch, dasynq::DEFAULT_PRIORITY - 10);
-        reserved_child_watch = true;
-    }
-    catch (std::exception &e) {
-        log(loglevel_t::ERROR, get_name(), ": could not fork: ", e.what());
-        goto out_cs_h;
-    }
-
-    if (forkpid == 0) {
-        const char * working_dir_c = nullptr;
-        if (! working_dir.empty()) working_dir_c = working_dir.c_str();
-        after_fork(getpid());
-        run_proc_params run_params{cmd.data(), working_dir_c, logfile, pipefd[1], run_as_uid, run_as_gid, rlimits};
-        run_params.on_console = on_console;
-        run_params.in_foreground = !onstart_flags.shares_console;
-        run_params.csfd = control_socket[1];
-        run_params.socket_fd = socket_fd;
-        run_params.notify_fd = notify_pipe[1];
-        run_params.force_notify_fd = force_notification_fd;
-        run_params.notify_var = notification_var.c_str();
-        run_params.env_file = env_file.c_str();
-        #if SUPPORT_CGROUPS
-        run_params.run_in_cgroup = run_in_cgroup.c_str();
-        #endif
-        run_child_proc(run_params);
-    }
-    else {
-        // Parent process
-        pid = forkpid;
-
-        bp_sys::close(pipefd[1]); // close the 'other end' fd
-        if (control_socket[1] != -1) bp_sys::close(control_socket[1]);
-        if (notify_pipe[1] != -1) bp_sys::close(notify_pipe[1]);
-        notification_fd = notify_pipe[0];
-        waiting_for_execstat = true;
-        return true;
-    }
-
-    // Failure exit:
-
-    out_cs_h:
-    if (child_status_registered) {
-        child_status_listener.deregister(event_loop);
-    }
-
-    if (notify_pipe[0] != -1) bp_sys::close(notify_pipe[0]);
-    if (notify_pipe[1] != -1) bp_sys::close(notify_pipe[1]);
-    if (ready_watcher_registered) {
-        rwatcher->deregister(event_loop);
-    }
-
-    if (onstart_flags.pass_cs_fd) {
-        delete control_conn;
-
-        out_cs:
-        bp_sys::close(control_socket[0]);
-        bp_sys::close(control_socket[1]);
-    }
+    if (this->log_type == log_type_id::LOGFILE) {
+    	if (*logfile == 0) {
+    		logfile = "/dev/null";
+    	}
+    }
+    else /* log_type_id::BUFFER */ {
+    	if (this->log_output_fd == -1) {
+    		int logfd[2];
+    		// Note: we set CLOEXEC on the file descriptors here; when the output file descriptor is dup'd
+    		// to stdout, this will be effectively removed for the output end
+    	    if (bp_sys::pipe2(logfd, O_CLOEXEC)) {
+    	        log(loglevel_t::ERROR, get_name(), ": can't create output pipe: ", strerror(errno));
+    	        goto out_p;
+    	    }
+    	    this->log_input_fd = logfd[0];
+    	    this->log_output_fd = logfd[1];
+    	    try {
+    	    	this->log_output_listener.add_watch(event_loop, logfd[0], dasynq::IN_EVENTS,
+    	    			false /* not enabled */);
+    	    }
+    	    catch (...) {
+    	    	log(loglevel_t::ERROR, get_name(), ": can't add output watch (insufficient resources)");
+    	    	bp_sys::close(this->log_input_fd);
+    	    	bp_sys::close(this->log_output_fd);
+    	    	this->log_input_fd = -1;
+    	    	this->log_output_fd = -1;
+    	    	goto out_p;
+    	    }
+    	}
+    	// (More is done below, after we have performed additional setup)
+    }
+
+    {
+		bool child_status_registered = false;
+		control_conn_t *control_conn = nullptr;
+
+		int control_socket[2] = {-1, -1};
+		int notify_pipe[2] = {-1, -1};
+		bool have_notify = !notification_var.empty() || force_notification_fd != -1;
+		ready_notify_watcher * rwatcher = have_notify ? get_ready_watcher() : nullptr;
+		bool ready_watcher_registered = false;
+
+		if (onstart_flags.pass_cs_fd) {
+			if (dinit_socketpair(AF_UNIX, SOCK_STREAM, /* protocol */ 0, control_socket, SOCK_NONBLOCK)) {
+				log(loglevel_t::ERROR, get_name(), ": can't create control socket: ", strerror(errno));
+				goto out_lfd;
+			}
+
+			// Make the server side socket close-on-exec:
+			int fdflags = bp_sys::fcntl(control_socket[0], F_GETFD);
+			bp_sys::fcntl(control_socket[0], F_SETFD, fdflags | FD_CLOEXEC);
+
+			try {
+				control_conn = new control_conn_t(event_loop, services, control_socket[0]);
+			}
+			catch (std::exception &exc) {
+				log(loglevel_t::ERROR, get_name(), ": can't launch process; out of memory");
+				goto out_cs;
+			}
+		}
+
+		if (have_notify) {
+			// Create a notification pipe:
+			if (bp_sys::pipe2(notify_pipe, 0) != 0) {
+				log(loglevel_t::ERROR, get_name(), ": can't create notification pipe: ", strerror(errno));
+				goto out_cs_h;
+			}
+
+			// Set the read side as close-on-exec:
+			int fdflags = bp_sys::fcntl(notify_pipe[0], F_GETFD);
+			bp_sys::fcntl(notify_pipe[0], F_SETFD, fdflags | FD_CLOEXEC);
+
+			// add, but don't yet enable, readiness watcher:
+			try {
+				rwatcher->add_watch(event_loop, notify_pipe[0], dasynq::IN_EVENTS, false);
+				ready_watcher_registered = true;
+			}
+			catch (std::exception &exc) {
+				log(loglevel_t::ERROR, get_name(), ": can't add notification watch: ", exc.what());
+				goto out_cs_h;
+			}
+		}
+
+		if (log_type == log_type_id::BUFFER) {
+	    	// Set watcher enabled if space in buffer
+			if (log_buf_size > 0) {
+				// Append a "restarted" message to buffer contents
+				const char *restarting_msg = "\n(dinit: note: service restarted)\n";
+				unsigned restarting_msg_len = strlen(restarting_msg);
+				bool trailing_nl = log_buffer[log_buf_size - 1] == '\n';
+				if (trailing_nl) {
+					++restarting_msg; // trim leading newline
+					--restarting_msg_len;
+				}
+				if (log_buf_size + restarting_msg_len >= log_buf_max) {
+					goto skip_enable_log_watch;
+				}
+				if (!ensure_log_buffer_backing(log_buf_size + restarting_msg_len)) {
+					goto skip_enable_log_watch;
+				}
+				memcpy(log_buffer.data() + log_buf_size, restarting_msg, restarting_msg_len);
+				log_buf_size += restarting_msg_len;
+			}
+			log_output_listener.set_enabled(event_loop, true);
+		}
+		skip_enable_log_watch: ;
+
+		// Set up complete, now fork and exec:
+
+		pid_t forkpid;
+
+		try {
+			child_status_listener.add_watch(event_loop, pipefd[0], dasynq::IN_EVENTS);
+			child_status_registered = true;
+
+			// We specify a high priority (i.e. low priority value) so that process termination is
+			// handled early. This means we have always recorded that the process is terminated by the
+			// time that we handle events that might otherwise cause us to signal the process, so we
+			// avoid sending a signal to an invalid (and possibly recycled) process ID.
+			forkpid = child_listener.fork(event_loop, reserved_child_watch, dasynq::DEFAULT_PRIORITY - 10);
+			reserved_child_watch = true;
+		}
+		catch (std::exception &e) {
+			log(loglevel_t::ERROR, get_name(), ": could not fork: ", e.what());
+			goto out_cs_h;
+		}
+
+		if (forkpid == 0) {
+			const char * working_dir_c = nullptr;
+			if (! working_dir.empty()) working_dir_c = working_dir.c_str();
+			after_fork(getpid());
+			run_proc_params run_params{cmd.data(), working_dir_c, logfile, pipefd[1], run_as_uid, run_as_gid, rlimits};
+			run_params.on_console = on_console;
+			run_params.in_foreground = !onstart_flags.shares_console;
+			run_params.csfd = control_socket[1];
+			run_params.socket_fd = socket_fd;
+			run_params.notify_fd = notify_pipe[1];
+			run_params.force_notify_fd = force_notification_fd;
+			run_params.notify_var = notification_var.c_str();
+			run_params.env_file = env_file.c_str();
+			run_params.output_fd = log_output_fd;
+			#if SUPPORT_CGROUPS
+			run_params.run_in_cgroup = run_in_cgroup.c_str();
+			#endif
+			run_child_proc(run_params);
+		}
+		else {
+			// Parent process
+			pid = forkpid;
+
+			bp_sys::close(pipefd[1]); // close the 'other end' fd
+			if (control_socket[1] != -1) bp_sys::close(control_socket[1]);
+			if (notify_pipe[1] != -1) bp_sys::close(notify_pipe[1]);
+			notification_fd = notify_pipe[0];
+			waiting_for_execstat = true;
+			return true;
+		}
+
+		// Failure exit:
+
+		out_cs_h:
+		if (child_status_registered) {
+			child_status_listener.deregister(event_loop);
+		}
+
+		if (notify_pipe[0] != -1) bp_sys::close(notify_pipe[0]);
+		if (notify_pipe[1] != -1) bp_sys::close(notify_pipe[1]);
+		if (ready_watcher_registered) {
+			rwatcher->deregister(event_loop);
+		}
+
+		if (onstart_flags.pass_cs_fd) {
+			delete control_conn;
+
+			out_cs:
+			bp_sys::close(control_socket[0]);
+			bp_sys::close(control_socket[1]);
+		}
+    }
+
+    out_lfd:
+	if (log_input_fd != -1) {
+		log_output_listener.deregister(event_loop);
+		bp_sys::close(log_input_fd);
+		bp_sys::close(log_output_fd);
+		log_input_fd = -1;
+		log_output_fd = -1;
+	}
 
     out_p:
     bp_sys::close(pipefd[0]);
@@ -242,7 +307,7 @@ base_process_service::base_process_service(service_set *sset, string name,
         const std::list<std::pair<unsigned,unsigned>> &command_offsets,
         const std::list<prelim_dep> &deplist_p)
      : service_record(sset, name, service_type_p, deplist_p), child_listener(this),
-       child_status_listener(this), process_timer(this)
+       child_status_listener(this), process_timer(this), log_output_listener(this)
 {
     program_name = std::move(command);
     exec_arg_parts = separate_args(program_name, command_offsets);
@@ -476,3 +541,26 @@ bool base_process_service::open_socket() noexcept
     socket_fd = sockfd;
     return true;
 }
+
+bool base_process_service::ensure_log_buffer_backing(unsigned new_size) noexcept
+{
+	//  Note: we manage capacity manually to avoid it exceeding maximum
+    if (log_buffer.size() < new_size) {
+        if (log_buffer.capacity() < new_size) {
+        	try {
+				unsigned new_capacity = std::max((unsigned)log_buffer.capacity() * 2, new_size);
+				new_capacity = std::min(new_capacity, log_buf_max);
+				log_buffer.reserve(new_capacity);
+				log_buffer.resize(new_capacity);
+        	}
+        	catch (std::bad_alloc &badalloc) {
+        		log(loglevel_t::WARN, get_name(), ": cannot increase log buffer; out-of-memory");
+        		return false;
+        	}
+        }
+        else {
+        	log_buffer.resize(new_size);
+        }
+    }
+    return true;
+}

+ 46 - 1
src/control.cc

@@ -12,7 +12,7 @@
 
 // Control protocol versions:
 // 1 - dinit 0.16 and prior
-// 2 - dinit 0.17 (adds DINIT_CP_SETTRIGGER)
+// 2 - dinit 0.17 (adds DINIT_CP_SETTRIGGER, DINIT_CP_CATLOG)
 
 namespace {
     constexpr auto OUT_EVENTS = dasynq::OUT_EVENTS;
@@ -116,6 +116,9 @@ bool control_conn_t::process_packet()
     if (pktType == DINIT_CP_SETTRIGGER) {
         return process_set_trigger();
     }
+    if (pktType == DINIT_CP_CATLOG) {
+    	return process_catlog();
+    }
 
     // Unrecognized: give error response
     char outbuf[] = { DINIT_RP_BADREQ };
@@ -958,6 +961,48 @@ bool control_conn_t::process_set_trigger()
     return queue_packet(ack_rep, 1);
 }
 
+bool control_conn_t::process_catlog()
+{
+	// 1 byte packet type
+	// 1 byte reserved for future use
+	// handle
+    constexpr int pkt_size = 2 + sizeof(handle_t);
+
+    if (rbuf.get_length() < pkt_size) {
+        chklen = pkt_size;
+        return true;
+    }
+
+    handle_t handle;
+
+    rbuf.extract(&handle, 1, sizeof(handle));
+    rbuf.consume(pkt_size);
+    chklen = 0;
+
+    service_record *service = find_service_for_key(handle);
+    if (service == nullptr || (service->get_type() != service_type_t::PROCESS
+    		&& service->get_type() != service_type_t::BGPROCESS
+			&& service->get_type() != service_type_t::SCRIPTED)) {
+        char nak_rep[] = { DINIT_RP_NAK };
+        return queue_packet(nak_rep, 1);
+    }
+
+    base_process_service *bps = static_cast<base_process_service *>(service);
+    if (bps->get_log_mode() != log_type_id::BUFFER) {
+        char nak_rep[] = { DINIT_RP_NAK };
+        return queue_packet(nak_rep, 1);
+    }
+
+    auto buffer_details = bps->get_log_buffer();
+    const char *bufaddr = buffer_details.first;
+    unsigned buflen = buffer_details.second;
+
+    std::vector<char> pkt = { (char)DINIT_RP_SERVICE_LOG };
+    pkt.insert(pkt.end(), (char *)(&buflen), (char *)(&buflen + 1));
+    pkt.insert(pkt.end(), bufaddr, bufaddr + buflen);
+    return queue_packet(std::move(pkt));
+}
+
 bool control_conn_t::query_load_mech()
 {
     rbuf.consume(1);

+ 70 - 0
src/dinitctl.cc

@@ -52,6 +52,7 @@ static int enable_disable_service(int socknum, cpbuffer_t &rbuffer, const char *
         bool enable, bool verbose);
 static int do_setenv(int socknum, cpbuffer_t &rbuffer, std::vector<const char *> &env_names);
 static int trigger_service(int socknum, cpbuffer_t &rbuffer, const char *service_name, bool trigger_value);
+static int cat_service_log(int socknum, cpbuffer_t &rbuffer, const char *service_name);
 
 static const char * describeState(bool stopped)
 {
@@ -83,6 +84,7 @@ enum class command_t {
     SETENV,
     SET_TRIGGER,
     UNSET_TRIGGER,
+	CAT_LOG,
 };
 
 class dinit_protocol_error
@@ -228,6 +230,9 @@ int dinitctl_main(int argc, char **argv)
             else if (strcmp(argv[i], "untrigger") == 0) {
                 command = command_t::UNSET_TRIGGER;
             }
+            else if (strcmp(argv[i], "catlog") == 0) {
+            	command = command_t::CAT_LOG;
+            }
             else {
                 cerr << "dinitctl: unrecognized command: " << argv[i] << " (use --help for help)\n";
                 return 1;
@@ -338,6 +343,7 @@ int dinitctl_main(int argc, char **argv)
           "    dinitctl [options] trigger <service-name>\n"
           "    dinitctl [options] untrigger <service-name>\n"
           "    dinitctl [options] setenv [name[=value] ...]\n"
+          "    dinitctl [options] catlog <service-name>\n"
           "\n"
           "Note: An activated service continues running when its dependents stop.\n"
           "\n"
@@ -434,6 +440,12 @@ int dinitctl_main(int argc, char **argv)
             }
             return trigger_service(socknum, rbuffer, service_name, (command == command_t::SET_TRIGGER));
         }
+        else if (command == command_t::CAT_LOG) {
+            if (daemon_protocol_ver < 2) {
+                throw cp_old_server_exception();
+            }
+            return cat_service_log(socknum, rbuffer, service_name);
+        }
         else {
             return start_stop_service(socknum, rbuffer, service_name, command, do_pin, do_force,
                     wait_for_service, ignore_unstarted, verbose);
@@ -1703,3 +1715,61 @@ static int trigger_service(int socknum, cpbuffer_t &rbuffer, const char *service
 
     return 0;
 }
+
+static int cat_service_log(int socknum, cpbuffer_t &rbuffer, const char *service_name)
+{
+    using namespace std;
+
+    handle_t handle;
+    if (!load_service(socknum, rbuffer, service_name, &handle, nullptr, true)) {
+        return 1;
+    }
+
+    // Issue CATLOG
+    auto m = membuf()
+    		 .append<char>(DINIT_CP_CATLOG)
+			 .append<char>(0)
+			 .append(handle);
+    write_all_x(socknum, m);
+
+    wait_for_reply(rbuffer, socknum);
+    if (rbuffer[0] == DINIT_RP_NAK) {
+        cerr << "dinitctl: cannot cat log for service not configured to buffer output.\n";
+        return 1;
+    }
+    if (rbuffer[0] != DINIT_RP_SERVICE_LOG) {
+        cerr << "dinitctl: protocol error.\n";
+        return 1;
+    }
+
+    fill_buffer_to(rbuffer, socknum, 1 + sizeof(unsigned));
+    unsigned bufsize;
+    rbuffer.extract(&bufsize, 1, sizeof(unsigned));
+    rbuffer.consume(1 + sizeof(unsigned));
+
+    // output the log
+    if (bufsize > 0) {
+		cout << flush;
+
+		bool trailing_nl = false;
+		char output_buf[rbuffer.get_size()];
+		while (bufsize > 0) {
+			unsigned l = rbuffer.get_length();
+			if (l == 0) {
+				fill_buffer_to(rbuffer, socknum, 1);
+			}
+			l = std::min(rbuffer.get_length(), bufsize);
+			rbuffer.extract(output_buf, 0, l);
+			write(STDOUT_FILENO, output_buf, l);
+			rbuffer.consume(l);
+			bufsize -= l;
+			trailing_nl = (output_buf[l - 1] == '\n');
+		}
+
+		if (!trailing_nl) {
+			cout << "\n(last line is truncated or incomplete)\n";
+		}
+    }
+
+    return 0;
+}

+ 6 - 0
src/includes/control-cmds.h

@@ -57,6 +57,9 @@ constexpr static int DINIT_CP_SERVICESTATUS = 18;
 // Set trigger value for triggered services
 constexpr static int DINIT_CP_SETTRIGGER = 19;
 
+// Retrieve buffered output
+constexpr static int DINIT_CP_CATLOG = 20;
+
 
 // Replies:
 
@@ -119,6 +122,9 @@ constexpr static int DINIT_RP_SERVICE_DESC_ERR = 71;
 // Service load error (general):
 constexpr static int DINIT_RP_SERVICE_LOAD_ERR = 72;
 
+// Service log:
+constexpr static int DINIT_RP_SERVICE_LOG = 73;
+
 
 // Information (out-of-band):
 

+ 3 - 0
src/includes/control.h

@@ -158,6 +158,9 @@ class control_conn_t : private service_listener
     // Process a SETTRIGGER packet.
     bool process_set_trigger();
 
+    // Process a CATLOG packet.
+    bool process_catlog();
+
     // List all loaded services and their state.
     bool list_services();
 

+ 39 - 0
src/includes/load-service.h

@@ -759,7 +759,9 @@ class service_settings_wrapper
     service_type_t service_type = service_type_t::INTERNAL;
     list<dep_type> depends;
     list<std::string> before_svcs;
+    log_type_id log_type = log_type_id::NONE;
     string logfile;
+    unsigned max_log_buffer_sz = 4096;
     service_flags_t onstart_flags;
     int term_signal = SIGTERM;  // termination signal
     bool auto_restart = false;
@@ -849,6 +851,18 @@ class service_settings_wrapper
             if (onstart_flags.skippable) {
                 report_lint("option 'skippable' was specified, but ignored for the specified (or default) service type.");
             }
+            if (log_type != log_type_id::NONE) {
+                report_lint("option 'log_type' was specified, but ignored for the specified (or default) service type.");
+            }
+        }
+
+        if (do_report_lint) {
+            if (log_type != log_type_id::LOGFILE && !logfile.empty()) {
+                report_lint("option 'logfile' was specified, but selected log type is not 'file'");
+            }
+            if (log_type == log_type_id::LOGFILE && logfile.empty()) {
+                report_lint("option 'logfile' not set, but selected log type is 'file'");
+            }
         }
 
         if (service_type == service_type_t::BGPROCESS) {
@@ -994,6 +1008,31 @@ void process_service_line(settings_wrapper &settings, const char *name, string &
     }
     else if (setting == "logfile") {
         settings.logfile = read_setting_value(line_num, i, end);
+        if (!settings.logfile.empty() && settings.log_type == log_type_id::NONE) {
+            settings.log_type = log_type_id::LOGFILE;
+        }
+    }
+    else if (setting == "log-type") {
+        string log_type_str = read_setting_value(line_num, i, end);
+        if (log_type_str == "file") {
+            settings.log_type = log_type_id::LOGFILE;
+        }
+        else if (log_type_str == "buffer") {
+            settings.log_type = log_type_id::BUFFER;
+        }
+        else if (log_type_str == "none") {
+            settings.log_type = log_type_id::NONE;
+        }
+        else {
+            throw service_description_exc(name, "log type must be one of: \"file\", \"buffer\" or \"none\"",
+                    line_num);
+        }
+    }
+    else if (setting == "log-buffer-size") {
+        string log_buffer_size_str = read_setting_value(line_num, i, end);
+        unsigned bufsize = (unsigned)parse_unum_param(line_num, log_buffer_size_str, name,
+                std::numeric_limits<unsigned>::max() / 2);
+        settings.max_log_buffer_sz = bufsize;
     }
     else if (setting == "restart") {
         string restart = read_setting_value(line_num, i, end);

+ 69 - 13
src/includes/proc-service.h

@@ -29,7 +29,7 @@ struct run_proc_params
     const char *logfile;      // log file or nullptr (stdout/stderr); must be valid if !on_console
     const char *env_file;     // file with environment settings (or nullptr)
     #if SUPPORT_CGROUPS
-    const char *run_in_cgroup; //  cgroup path
+    const char *run_in_cgroup = nullptr; //  cgroup path
     #endif
     bool on_console;          // whether to run on console
     bool in_foreground;       // if on console: whether to run in foreground
@@ -38,6 +38,7 @@ struct run_proc_params
     int socket_fd;            // pre-opened socket fd (or -1); may be moved
     int notify_fd;            // pipe for readiness notification message (or -1); may be moved
     int force_notify_fd;      // if not -1, notification fd must be moved to this fd
+    int output_fd;            // if not -1, output will be directed here (rather than logfile)
     const char *notify_var;   // environment variable name where notification fd will be stored, or nullptr
     uid_t uid;
     gid_t gid;
@@ -47,7 +48,7 @@ struct run_proc_params
             uid_t uid, gid_t gid, const std::vector<service_rlimits> &rlimits)
             : args(args), working_dir(working_dir), logfile(logfile), env_file(nullptr), on_console(false),
               in_foreground(false), wpipefd(wpipefd), csfd(-1), socket_fd(-1), notify_fd(-1),
-              force_notify_fd(-1), notify_var(nullptr), uid(uid), gid(gid), rlimits(rlimits)
+              force_notify_fd(-1), output_fd(-1), notify_var(nullptr), uid(uid), gid(gid), rlimits(rlimits)
     { }
 };
 
@@ -65,7 +66,7 @@ class base_process_service;
 class process_restart_timer : public eventloop_t::timer_impl<process_restart_timer>
 {
     public:
-    base_process_service * service;
+    base_process_service *service;
 
     explicit process_restart_timer(base_process_service *service_p)
         : service(service_p)
@@ -79,7 +80,7 @@ class process_restart_timer : public eventloop_t::timer_impl<process_restart_tim
 class exec_status_pipe_watcher : public eventloop_t::fd_watcher_impl<exec_status_pipe_watcher>
 {
     public:
-    base_process_service * service;
+    base_process_service *service;
     dasynq::rearm fd_event(eventloop_t &eloop, int fd, int flags) noexcept;
 
     exec_status_pipe_watcher(base_process_service * sr) noexcept : service(sr) { }
@@ -92,7 +93,7 @@ class exec_status_pipe_watcher : public eventloop_t::fd_watcher_impl<exec_status
 class stop_status_pipe_watcher : public eventloop_t::fd_watcher_impl<stop_status_pipe_watcher>
 {
     public:
-    process_service * service;
+    process_service *service;
     dasynq::rearm fd_event(eventloop_t &eloop, int fd, int flags) noexcept;
 
     stop_status_pipe_watcher(process_service * sr) noexcept : service(sr) { }
@@ -105,7 +106,7 @@ class stop_status_pipe_watcher : public eventloop_t::fd_watcher_impl<stop_status
 class ready_notify_watcher : public eventloop_t::fd_watcher_impl<ready_notify_watcher>
 {
     public:
-    base_process_service * service;
+    base_process_service *service;
     dasynq::rearm fd_event(eventloop_t &eloop, int fd, int flags) noexcept;
 
     ready_notify_watcher(base_process_service * sr) noexcept : service(sr) { }
@@ -118,7 +119,7 @@ class ready_notify_watcher : public eventloop_t::fd_watcher_impl<ready_notify_wa
 class service_child_watcher : public eventloop_t::child_proc_watcher_impl<service_child_watcher>
 {
     public:
-    base_process_service * service;
+    base_process_service *service;
     dasynq::rearm status_change(eventloop_t &eloop, pid_t child, int status) noexcept;
 
     service_child_watcher(base_process_service * sr) noexcept : service(sr) { }
@@ -131,7 +132,7 @@ class service_child_watcher : public eventloop_t::child_proc_watcher_impl<servic
 class stop_child_watcher : public eventloop_t::child_proc_watcher_impl<stop_child_watcher>
 {
     public:
-    process_service * service;
+    process_service *service;
     dasynq::rearm status_change(eventloop_t &eloop, pid_t child, int status) noexcept;
 
     stop_child_watcher(process_service * sr) noexcept : service(sr) { }
@@ -140,6 +141,19 @@ class stop_child_watcher : public eventloop_t::child_proc_watcher_impl<stop_chil
     void operator=(const service_child_watcher &) = delete;
 };
 
+class log_output_watcher : public eventloop_t::fd_watcher_impl<log_output_watcher>
+{
+    public:
+    base_process_service *service;
+
+    dasynq::rearm fd_event(eventloop_t &eloop, int fd, int flags) noexcept;
+
+    log_output_watcher(base_process_service * sr) noexcept : service(sr) { }
+
+    log_output_watcher(const ready_notify_watcher &) = delete;
+    void operator=(const ready_notify_watcher &) = delete;
+};
+
 // Base class for process-based services.
 class base_process_service : public service_record
 {
@@ -147,10 +161,7 @@ class base_process_service : public service_record
     friend class exec_status_pipe_watcher;
     friend class base_process_service_test;
     friend class ready_notify_watcher;
-
-    private:
-    // Re-launch process
-    void do_restart() noexcept;
+    friend class log_output_watcher;
 
     protected:
     ha_string program_name;          // storage for program/script and arguments
@@ -164,6 +175,12 @@ class base_process_service : public service_record
     string working_dir;       // working directory (or empty)
     string env_file;          // file with environment settings for this service
 
+    log_type_id log_type = log_type_id::NONE;
+    string logfile;           // log file name, empty string specifies /dev/null
+    unsigned log_buf_max = 0; // log buffer maximum size
+    unsigned log_buf_size = 0; // log buffer current size
+    std::vector<char, default_init_allocator<char>> log_buffer;
+
     std::vector<service_rlimits> rlimits; // resource limits
 
 #if SUPPORT_CGROUPS
@@ -173,6 +190,7 @@ class base_process_service : public service_record
     service_child_watcher child_listener;
     exec_status_pipe_watcher child_status_listener;
     process_restart_timer process_timer; // timer is used for start, stop and restart
+    log_output_watcher log_output_listener;
     time_val last_start_time;
 
     // Restart interval time and restart count are used to track the number of automatic restarts
@@ -202,6 +220,8 @@ class base_process_service : public service_record
     bp_sys::exit_status exit_status; // Exit status, if the process has exited (pid == -1).
     int socket_fd = -1;  // For socket-activation services, this is the file descriptor for the socket.
     int notification_fd = -1;  // If readiness notification is via fd
+    int log_output_fd = -1; // If logging via buffer, write end of the log pipe
+    int log_input_fd = -1; // If logging via buffer, read end of the log pipe
 
     // Only one of waiting_restart_timer and waiting_stopstart_timer should be set at any time.
     // They indicate that the process timer is armed (and why).
@@ -214,6 +234,11 @@ class base_process_service : public service_record
     // If executing child process failed, information about the error
     run_proc_err exec_err_info;
 
+    private:
+    // Re-launch process
+    void do_restart() noexcept;
+
+    protected:
     // Run a child process (call after forking). Note that some parameters specify file descriptors,
     // but in general file descriptors may be moved before the exec call.
     void run_child_proc(run_proc_params params) noexcept;
@@ -271,6 +296,8 @@ class base_process_service : public service_record
         return nullptr;
     }
 
+    bool ensure_log_buffer_backing(unsigned size) noexcept;
+
     public:
     // Constructor for a base_process_service. Note that the various parameters not specified here must in
     // general be set separately (using the appropriate set_xxx function for each).
@@ -318,6 +345,36 @@ class base_process_service : public service_record
         stop_arg_parts = std::move(command_parts);
     }
 
+    // Set logfile (used if log mode is FILE)
+    void set_log_file(std::string &&logfile) noexcept
+    {
+        this->logfile = std::move(logfile);
+    }
+
+    // Set log buffer maximum size (for if mode is BUFFER). Maximum allowed size is UINT_MAX / 2
+    // (must be checked by caller).
+    void set_log_buf_max(unsigned max_size) noexcept
+    {
+        this->log_buf_max = max_size;
+    }
+
+    // Set log mode (NONE, BUFFER, FILE)
+    void set_log_mode(log_type_id log_type) noexcept
+    {
+        this->log_type = log_type;
+    }
+
+    log_type_id get_log_mode() noexcept
+    {
+    	return this->log_type;
+    }
+
+    // Get the log buffer (address, length)
+    std::pair<const char *, unsigned> get_log_buffer() noexcept
+    {
+    	return {log_buffer.data(), log_buf_size};
+    }
+
     void set_env_file(const std::string &env_file_p)
     {
         env_file = env_file_p;
@@ -450,7 +507,6 @@ class process_service : public base_process_service
     stop_child_watcher stop_watcher;
     stop_status_pipe_watcher stop_pipe_watcher;
 
-    protected:
     bool doing_smooth_recovery = false; // if we are performing smooth recovery
 
 #if USE_UTMPX

+ 7 - 0
src/includes/service-constants.h

@@ -112,6 +112,13 @@ enum class dependency_type
     AFTER       // "after" ordering constraint (specified via the "from" service)
 };
 
+enum class log_type_id
+{
+    NONE,     // discard all output
+    LOGFILE,  // log to a file
+    BUFFER    // log to a buffer in memory
+};
+
 // Service set type identifiers:
 constexpr int SSET_TYPE_NONE = 0;
 constexpr int SSET_TYPE_DIRLOAD = 1;

+ 0 - 13
src/includes/service.h

@@ -248,8 +248,6 @@ class service_record
 
     protected:
     service_flags_t onstart_flags;
-
-    string logfile;           // log file name, empty string specifies /dev/null
     
     bool auto_restart : 1;    // whether to restart this (process) if it dies unexpectedly
     bool smooth_recovery : 1; // whether the service process can restart without bringing down service
@@ -521,17 +519,6 @@ class service_record
         return start_explicit;
     }
 
-    // Set logfile, should be done before service is started
-    void set_log_file(const string &logfile)
-    {
-        this->logfile = logfile;
-    }
-    
-    void set_log_file(std::string &&logfile) noexcept
-    {
-        this->logfile = std::move(logfile);
-    }
-
     // Set whether this service should automatically restart when it dies
     void set_auto_restart(bool auto_restart) noexcept
     {

+ 9 - 1
src/load-service.cc

@@ -496,6 +496,9 @@ service_record * dirload_service_set::load_reload_service(const char *name, serv
             rvalps->set_run_as_uid_gid(settings.run_as_uid, settings.run_as_gid);
             rvalps->set_notification_fd(settings.readiness_fd);
             rvalps->set_notification_var(std::move(settings.readiness_var));
+            rvalps->set_log_file(std::move(settings.logfile));
+            rvalps->set_log_buf_max(settings.max_log_buffer_sz);
+            rvalps->set_log_mode(settings.log_type);
             #if USE_UTMPX
             rvalps->set_utmp_id(settings.inittab_id);
             rvalps->set_utmp_line(settings.inittab_line);
@@ -533,6 +536,9 @@ service_record * dirload_service_set::load_reload_service(const char *name, serv
             rvalps->set_start_timeout(settings.start_timeout);
             rvalps->set_extra_termination_signal(settings.term_signal);
             rvalps->set_run_as_uid_gid(settings.run_as_uid, settings.run_as_gid);
+            rvalps->set_log_file(std::move(settings.logfile));
+            rvalps->set_log_buf_max(settings.max_log_buffer_sz);
+            rvalps->set_log_mode(settings.log_type);
             settings.onstart_flags.runs_on_console = false;
         }
         else if (service_type == service_type_t::SCRIPTED) {
@@ -564,6 +570,9 @@ service_record * dirload_service_set::load_reload_service(const char *name, serv
             rvalps->set_start_timeout(settings.start_timeout);
             rvalps->set_extra_termination_signal(settings.term_signal);
             rvalps->set_run_as_uid_gid(settings.run_as_uid, settings.run_as_gid);
+            rvalps->set_log_file(std::move(settings.logfile));
+            rvalps->set_log_buf_max(settings.max_log_buffer_sz);
+            rvalps->set_log_mode(settings.log_type);
         }
         else {
             if (create_new_record) {
@@ -584,7 +593,6 @@ service_record * dirload_service_set::load_reload_service(const char *name, serv
             }
         }
 
-        rval->set_log_file(std::move(settings.logfile));
         rval->set_auto_restart(settings.auto_restart);
         rval->set_smooth_recovery(settings.smooth_recovery);
         rval->set_flags(settings.onstart_flags);

+ 42 - 0
src/proc-service.cc

@@ -257,6 +257,48 @@ dasynq::rearm stop_child_watcher::status_change(eventloop_t &loop, pid_t child,
     return dasynq::rearm::NOOP;
 }
 
+rearm log_output_watcher::fd_event(eventloop_t &eloop, int fd, int flags) noexcept
+{
+	// In case buffer size has been decreased, check if we are already at the limit:
+	if (service->log_buf_size >= service->log_buf_max) {
+		return rearm::DISARM;
+	}
+
+    size_t max_read = std::min(service->log_buf_max / 8, service->log_buf_max - service->log_buf_size);
+
+    // ensure vector has size sufficient to read
+    unsigned new_size = service->log_buf_size + max_read;
+    if (!service->ensure_log_buffer_backing(new_size)) {
+    	return rearm::DISARM;
+    }
+
+    max_read = service->log_buffer.size() - service->log_buf_size;
+
+    int r = bp_sys::read(fd, service->log_buffer.data() + service->log_buf_size, max_read);
+    if (r == -1) {
+    	if (errno == EAGAIN || errno == EWOULDBLOCK || errno == EINTR) {
+    		return rearm::REARM;
+    	}
+    	log(loglevel_t::WARN, "Service ", service->get_name(), " output not readable: ", strerror(errno));
+    }
+
+    if (r <= 0) {
+    	deregister(eloop);
+        close(fd);
+        close(service->log_output_fd);
+        service->log_input_fd = -1;
+        service->log_output_fd = -1;
+        return rearm::REMOVED;
+    }
+
+    service->log_buf_size += r;
+    if (service->log_buf_size >= service->log_buf_max) {
+    	return rearm::DISARM;
+    }
+
+    return rearm::REARM;
+}
+
 void process_service::handle_exit_status(bp_sys::exit_status exit_status) noexcept
 {
     bool did_exit = exit_status.did_exit();

+ 7 - 2
src/run-child-proc.cc

@@ -61,6 +61,7 @@ void base_process_service::run_child_proc(run_proc_params params) noexcept
     uid_t uid = params.uid;
     gid_t gid = params.gid;
     const std::vector<service_rlimits> &rlimits = params.rlimits;
+    int output_fd = params.output_fd;
 
     // If the console already has a session leader, presumably it is us. On the other hand
     // if it has no session leader, and we don't create one, then control inputs such as
@@ -210,15 +211,19 @@ void base_process_service::run_child_proc(run_proc_params params) noexcept
         if (notify_fd == 0 || move_fd(open("/dev/null", O_RDONLY), 0) == 0) {
             // stdin = 0. That's what we should have; proceed with opening stdout and stderr. We have to
             // take care not to clobber the notify_fd.
+        	if (output_fd == -1) {
+        		output_fd = open(logfile, O_WRONLY | O_CREAT | O_APPEND, S_IRUSR | S_IWUSR);
+        		if (output_fd == -1) goto failure_out;
+        	}
             if (notify_fd != 1) {
-                if (move_fd(open(logfile, O_WRONLY | O_CREAT | O_APPEND, S_IRUSR | S_IWUSR), 1) != 0) {
+                if (move_fd(output_fd, 1) != 0) {
                     goto failure_out;
                 }
                 if (notify_fd != 2 && dup2(1, 2) != 2) {
                     goto failure_out;
                 }
             }
-            else if (move_fd(open(logfile, O_WRONLY | O_CREAT | O_APPEND, S_IRUSR | S_IWUSR), 2) != 0) {
+            else if (move_fd(output_fd, 2) != 0) {
                 goto failure_out;
             }
         }