diff --git a/Makefile b/Makefile index f8ace071..8a34ecfc 100644 --- a/Makefile +++ b/Makefile @@ -18,7 +18,7 @@ else UNUSED:=$(shell $(MAKE) loglevels.h) endif -SUBDIRS:=i3-msg i3-input i3-nagbar i3-config-wizard i3bar +SUBDIRS:=i3-msg i3-input i3-nagbar i3-config-wizard i3bar i3-dump-log # Depend on the specific file (.c for each .o) and on all headers src/%.o: src/%.c ${HEADERS} @@ -100,7 +100,7 @@ dist: distclean [ ! -e i3-${VERSION}.tar.bz2 ] || rm i3-${VERSION}.tar.bz2 mkdir i3-${VERSION} cp i3-migrate-config-to-v4 i3-sensible-* i3.config.keycodes DEPENDS GOALS LICENSE PACKAGE-MAINTAINER RELEASE-NOTES-${VERSION} i3.config i3.desktop i3.welcome pseudo-doc.doxygen i3-wsbar Makefile i3-${VERSION} - cp -r src libi3 i3-msg i3-nagbar i3-config-wizard i3bar yajl-fallback include man i3-${VERSION} + cp -r src libi3 i3-msg i3-nagbar i3-config-wizard i3bar i3-dump-log yajl-fallback include man i3-${VERSION} # Only copy toplevel documentation (important stuff) mkdir i3-${VERSION}/docs # Pre-generate documentation diff --git a/common.mk b/common.mk index 4394bc41..537d4dda 100644 --- a/common.mk +++ b/common.mk @@ -68,6 +68,7 @@ CPPFLAGS += -DPCRE_HAS_UCP=1 endif LIBS += -lm +LIBS += -lrt LIBS += -L $(TOPDIR)/libi3 -li3 LIBS += $(call ldflags_for_lib, xcb-event,xcb-event) LIBS += $(call ldflags_for_lib, xcb-keysyms,xcb-keysyms) diff --git a/docs/ipc b/docs/ipc index fc46590e..c76d0154 100644 --- a/docs/ipc +++ b/docs/ipc @@ -1,7 +1,7 @@ IPC interface (interprocess communication) ========================================== Michael Stapelberg -October 2011 +December 2011 This document describes how to interface with i3 from a separate process. This is useful for example to remote-control i3 (to write test cases for example) or @@ -68,6 +68,10 @@ GET_BAR_CONFIG (6):: Gets the configuration (as JSON map) of the workspace bar with the given ID. If no ID is provided, an array with all configured bar IDs is returned instead. +GET_LOG_MARKERS (7):: + Gets the SHM log markers for the current position, the last wrap, the + SHM segment name and segment size. This is necessary for tools like + i3-dump-log which want to display the SHM log. So, a typical message could look like this: -------------------------------------------------- @@ -111,18 +115,20 @@ The following reply types are implemented: COMMAND (0):: Confirmation/Error code for the COMMAND message. -GET_WORKSPACES (1):: +WORKSPACES (1):: Reply to the GET_WORKSPACES message. SUBSCRIBE (2):: Confirmation/Error code for the SUBSCRIBE message. -GET_OUTPUTS (3):: +OUTPUTS (3):: Reply to the GET_OUTPUTS message. -GET_TREE (4):: +TREE (4):: Reply to the GET_TREE message. -GET_MARKS (5):: +MARKS (5):: Reply to the GET_MARKS message. -GET_BAR_CONFIG (6):: +BAR_CONFIG (6):: Reply to the GET_BAR_CONFIG message. +LOG_MARKERS (7):: + Reply to the GET_LOG_MARKERS message. === COMMAND reply @@ -134,7 +140,7 @@ property is +success (bool)+, but this will be expanded in future versions. { "success": true } ------------------- -=== GET_WORKSPACES reply +=== WORKSPACES reply The reply consists of a serialized list of workspaces. Each workspace has the following properties: @@ -248,7 +254,7 @@ rect (map):: ] ------------------- -=== GET_TREE reply +=== TREE reply The reply consists of a serialized tree. Each node in the tree (representing one container) has at least the properties listed below. While the nodes might @@ -431,7 +437,7 @@ JSON dump: } ------------------------ -=== GET_MARKS reply +=== MARKS reply The reply consists of a single array of strings for each container that has a mark. The order of that array is undefined. If more than one container has the @@ -440,7 +446,7 @@ contents are not unique). If no window has a mark the response will be the empty array []. -=== GET_BAR_CONFIG reply +=== BAR_CONFIG reply This can be used by third-party workspace bars (especially i3bar, but others are free to implement compatible alternatives) to get the +bar+ block @@ -524,6 +530,40 @@ urgent_workspace_text/urgent_workspace_bar:: } -------------- +=== LOG_MARKERS reply + +Gets the SHM log markers for the current position, the last wrap, the +SHM segment name and segment size. This is necessary for tools like +i3-dump-log which want to display the SHM log. + +The reply is a JSON map with the following entries: + +shmname (string):: + The name of the SHM segment, will be of the format +/i3-log-+. +size (integer):: + The size (in bytes) of the SHM segment. If this is 0, SHM logging is + disabled. +offset_next_write (integer):: + The offset in the SHM segment at which the next write will happen. + Tools should start printing lines from here, since the bytes following + this offset are the oldest log lines. However, the first line might be + garbled, so it makes sense to skip all bytes until the first \0. +offset_last_wrap (integer):: + The offset in the SHM segment at which the last wrap occured. i3 only + stores entire messages in the SHM log, so it might waste a few bytes at + the end to be more efficient. Tools should not print content after the + offset_last_wrap. + +*Example*: +----------------------------- +{ + "offset_next_write":132839, + "offset_last_wrap":26214400, + "shmname":"/i3-log-3392", + "size":26214400 +} +----------------------------- + == Events [[events]] diff --git a/i3-dump-log/Makefile b/i3-dump-log/Makefile new file mode 100644 index 00000000..18076e51 --- /dev/null +++ b/i3-dump-log/Makefile @@ -0,0 +1,32 @@ +# Default value so one can compile i3-dump-log standalone +TOPDIR=.. + +include $(TOPDIR)/common.mk + +CFLAGS += -I$(TOPDIR)/include + +# Depend on the object files of all source-files in src/*.c and on all header files +FILES=$(patsubst %.c,%.o,$(wildcard *.c)) +HEADERS=$(wildcard *.h) + +# Depend on the specific file (.c for each .o) and on all headers +%.o: %.c ${HEADERS} + echo "[i3-dump-log] CC $<" + $(CC) $(CPPFLAGS) $(CFLAGS) -c -o $@ $< + +all: i3-dump-log + +i3-dump-log: ${FILES} + echo "[i3-dump-log] LINK i3-dump-log" + $(CC) $(LDFLAGS) -o i3-dump-log ${FILES} $(LIBS) + +install: all + echo "[i3-dump-log] INSTALL" + $(INSTALL) -d -m 0755 $(DESTDIR)$(PREFIX)/bin + $(INSTALL) -m 0755 i3-dump-log $(DESTDIR)$(PREFIX)/bin/ + +clean: + rm -f *.o + +distclean: clean + rm -f i3-dump-log diff --git a/i3-dump-log/main.c b/i3-dump-log/main.c new file mode 100644 index 00000000..68cbdb46 --- /dev/null +++ b/i3-dump-log/main.c @@ -0,0 +1,160 @@ +/* + * vim:ts=4:sw=4:expandtab + * + * i3 - an improved dynamic tiling window manager + * © 2009-2011 Michael Stapelberg and contributors (see also: LICENSE) + * + * i3-dump-log/main.c: Dumps the i3 SHM log to stdout. + * + */ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "libi3.h" +#include + +int main(int argc, char *argv[]) { + char *socket_path = getenv("I3SOCK"); + int o, option_index = 0; + int message_type = I3_IPC_MESSAGE_TYPE_GET_LOG_MARKERS; + bool verbose = false; + + static struct option long_options[] = { + {"socket", required_argument, 0, 's'}, + {"version", no_argument, 0, 'v'}, + {"verbose", no_argument, 0, 'V'}, + {"help", no_argument, 0, 'h'}, + {0, 0, 0, 0} + }; + + char *options_string = "s:vVh"; + + while ((o = getopt_long(argc, argv, options_string, long_options, &option_index)) != -1) { + if (o == 's') { + if (socket_path != NULL) + free(socket_path); + socket_path = sstrdup(optarg); + } else if (o == 'v') { + printf("i3-dump-log " I3_VERSION "\n"); + return 0; + } else if (o == 'V') { + verbose = true; + } else if (o == 'h') { + printf("i3-dump-log " I3_VERSION "\n"); + printf("i3-dump-log [-s ]\n"); + return 0; + } + } + + if (socket_path == NULL) + socket_path = socket_path_from_x11(); + + /* Fall back to the default socket path */ + if (socket_path == NULL) + socket_path = sstrdup("/tmp/i3-ipc.sock"); + + int sockfd = socket(AF_LOCAL, SOCK_STREAM, 0); + if (sockfd == -1) + err(EXIT_FAILURE, "Could not create socket"); + + struct sockaddr_un addr; + memset(&addr, 0, sizeof(struct sockaddr_un)); + addr.sun_family = AF_LOCAL; + strncpy(addr.sun_path, socket_path, sizeof(addr.sun_path) - 1); + if (connect(sockfd, (const struct sockaddr*)&addr, sizeof(struct sockaddr_un)) < 0) + err(EXIT_FAILURE, "Could not connect to i3"); + + if (ipc_send_message(sockfd, 0, message_type, NULL) == -1) + err(EXIT_FAILURE, "IPC: write()"); + + uint32_t reply_length; + uint8_t *reply; + int ret; + if ((ret = ipc_recv_message(sockfd, message_type, &reply_length, &reply)) != 0) { + if (ret == -1) + err(EXIT_FAILURE, "IPC: read()"); + exit(1); + } + char *buffer = NULL; + sasprintf(&buffer, "%.*s", reply_length, reply); + /* The reply will look like this: + * {"offset_next_write":1729,"offset_last_wrap":1996,"size":2048,"shmname":"/i3-log-399"} + * IMO, it’s not worth linking a JSON parser in just for this. If the + * structure changes in the future, this decision needs to be re-evaluated + * :). */ + int offset_next_write, offset_last_wrap, logbuffer_size; + char *next_write_str = strstr(buffer, "offset_next_write"), + *last_wrap_str = strstr(buffer, "offset_last_wrap"), + *size_str = strstr(buffer, "size"), + *shmname = strstr(buffer, "shmname"); + if (!next_write_str || + !last_wrap_str || + !size_str || + !shmname || + sscanf(next_write_str, "offset_next_write\":%d", &offset_next_write) != 1 || + sscanf(last_wrap_str, "offset_last_wrap\":%d", &offset_last_wrap) != 1 || + sscanf(size_str, "size\":%d", &logbuffer_size) != 1) + errx(EXIT_FAILURE, "invalid IPC reply: %s\n", buffer); + + shmname += strlen("shmname\":\""); + char *quote = strchr(shmname, '"'); + if (!quote) + errx(EXIT_FAILURE, "invalid IPC reply: %s\n", buffer); + *quote = '\0'; + + if (verbose) + printf("next_write = %d, last_wrap = %d, logbuffer_size = %d, shmname = %s\n", + offset_next_write, offset_last_wrap, logbuffer_size, shmname); + + if (*shmname == '\0') + errx(EXIT_FAILURE, "Cannot dump log: SHM logging is disabled in i3."); + + int logbuffer_shm = shm_open(shmname, O_RDONLY, 0); + if (logbuffer_shm == -1) + err(EXIT_FAILURE, "Could not shm_open SHM segment for the i3 log (%s)", shmname); + + char *logbuffer = mmap(NULL, logbuffer_size, PROT_READ, MAP_SHARED, logbuffer_shm, 0); + if (logbuffer == MAP_FAILED) + err(EXIT_FAILURE, "Could not mmap SHM segment for the i3 log"); + + int chars; + char *walk = logbuffer + offset_next_write; + /* Skip the first line, it very likely is mangled. Not a problem, though, + * the log is chatty enough to have plenty lines left. */ + while (*walk != '\0') + walk++; + + /* Print the oldest log lines. We use printf("%s") to stop on \0. */ + while (walk < (logbuffer + offset_last_wrap)) { + chars = printf("%s", walk); + /* Shortcut: If there are two consecutive \0 bytes, this part of the + * buffer was never touched. To not call printf() for every byte of the + * buffer, we directly exit the loop. */ + if (*walk == '\0' && *(walk+1) == '\0') + break; + walk += (chars > 0 ? chars : 1); + } + + /* Then start from the beginning and print the newer lines */ + walk = logbuffer; + while (walk < (logbuffer + offset_next_write)) { + chars = printf("%s", walk); + walk += (chars > 0 ? chars : 1); + } + + return 0; +} diff --git a/i3-msg/main.c b/i3-msg/main.c index 6a3b29d3..e16f6943 100644 --- a/i3-msg/main.c +++ b/i3-msg/main.c @@ -73,9 +73,11 @@ int main(int argc, char *argv[]) { message_type = I3_IPC_MESSAGE_TYPE_GET_MARKS; else if (strcasecmp(optarg, "get_bar_config") == 0) message_type = I3_IPC_MESSAGE_TYPE_GET_BAR_CONFIG; + else if (strcasecmp(optarg, "get_log_markers") == 0) + message_type = I3_IPC_MESSAGE_TYPE_GET_LOG_MARKERS; else { printf("Unknown message type\n"); - printf("Known types: command, get_workspaces, get_outputs, get_tree, get_marks, get_bar_config\n"); + printf("Known types: command, get_workspaces, get_outputs, get_tree, get_marks, get_bar_config, get_log_markers\n"); exit(EXIT_FAILURE); } } else if (o == 'q') { diff --git a/include/i3.h b/include/i3.h index 75b7a9bf..d6453254 100644 --- a/include/i3.h +++ b/include/i3.h @@ -28,6 +28,8 @@ * this before starting any other process, since we set RLIMIT_CORE to * RLIM_INFINITY for i3 debugging versions. */ extern struct rlimit original_rlimit_core; +/** Whether this version of i3 is a debug build or a release build. */ +extern bool debug_build; extern xcb_connection_t *conn; extern int conn_screen; /** The last timestamp we got from X11 (timestamps are included in some events diff --git a/include/i3/ipc.h b/include/i3/ipc.h index bfadf4cf..e8de2e1e 100644 --- a/include/i3/ipc.h +++ b/include/i3/ipc.h @@ -40,6 +40,9 @@ /** Request the configuration for a specific 'bar' */ #define I3_IPC_MESSAGE_TYPE_GET_BAR_CONFIG 6 +/** Request the SHM debug log start/wrap markers */ +#define I3_IPC_MESSAGE_TYPE_GET_LOG_MARKERS 7 + /* * Messages from i3 to clients * @@ -66,6 +69,9 @@ /** Bar config reply type */ #define I3_IPC_REPLY_TYPE_BAR_CONFIG 6 +/** Request the SHM debug log start/wrap markers */ +#define I3_IPC_REPLY_TYPE_LOG_MARKERS 7 + /* * Events from i3 to clients. Events have the first bit set high. * diff --git a/include/log.h b/include/log.h index ef6deb20..ef24bda8 100644 --- a/include/log.h +++ b/include/log.h @@ -21,6 +21,8 @@ extern char *loglevels[]; extern char *errorfilename; +extern char *shmlogname; +extern int shmlog_size; /** * Initializes logging by creating an error logfile in /tmp (or @@ -35,6 +37,13 @@ void init_logging(); */ void add_loglevel(const char *level); +/** + * Returns the offsets for the next write and for the last wrap. + * Necessary to print the i3 SHM log in the correct order. + * + */ +void get_log_markers(int *offset_next_write, int *offset_last_wrap, int *size); + /** * Set verbosity of i3. If verbose is set to true, informative messages will * be printed to stdout. If verbose is set to false, only errors will be diff --git a/man/Makefile b/man/Makefile index 44b2df6e..5c374735 100644 --- a/man/Makefile +++ b/man/Makefile @@ -1,6 +1,6 @@ A2M:=a2x -f manpage --asciidoc-opts="-f asciidoc.conf" -all: i3.1 i3-msg.1 i3-input.1 i3-nagbar.1 i3-wsbar.1 i3-config-wizard.1 i3-migrate-config-to-v4.1 i3-sensible-editor.1 i3-sensible-pager.1 i3-sensible-terminal.1 +all: i3.1 i3-msg.1 i3-input.1 i3-nagbar.1 i3-wsbar.1 i3-config-wizard.1 i3-migrate-config-to-v4.1 i3-sensible-editor.1 i3-sensible-pager.1 i3-sensible-terminal.1 i3-dump-log.1 %.1: %.man asciidoc.conf ${A2M} $< @@ -9,7 +9,7 @@ i3-wsbar.1: ../i3-wsbar pod2man $^ > $@ clean: - for file in $$(echo i3 i3-msg i3-input i3-nagbar i3-wsbar i3-config-wizard i3-migrate-config-to-v4 i3-sensible-editor i3-sensible-pager i3-sensible-terminal); \ + for file in $$(echo i3 i3-msg i3-input i3-nagbar i3-wsbar i3-config-wizard i3-migrate-config-to-v4 i3-sensible-editor i3-sensible-pager i3-sensible-terminal i3-dump-log); \ do \ rm -f $${file}.1 $${file}.html $${file}.xml; \ done diff --git a/man/i3-dump-log.man b/man/i3-dump-log.man new file mode 100644 index 00000000..8e9094ff --- /dev/null +++ b/man/i3-dump-log.man @@ -0,0 +1,32 @@ +i3-dump-log(1) +============== +Michael Stapelberg +v4.1, December 2011 + +== NAME + +i3-dump-log - dumps the i3 SHM log + +== SYNOPSIS + +i3-dump-log [-s ] + +== DESCRIPTION + +Debug versions of i3 automatically use 1% of your RAM (but 25 MiB max) to store +full debug loglevel log output. This is extremely helpful for bugreports and +figuring out what is going on, without permanently logging to a file. + +With i3-dump-log, you can dump the SHM log to stdout. + +== EXAMPLE + +i3-dump-log | gzip -9 > /tmp/i3-log.gz + +== SEE ALSO + +i3(1) + +== AUTHOR + +Michael Stapelberg and contributors diff --git a/src/ipc.c b/src/ipc.c index fe1464e6..60f456c3 100644 --- a/src/ipc.c +++ b/src/ipc.c @@ -612,6 +612,48 @@ IPC_HANDLER(get_bar_config) { y(free); } +/* + * Formats the reply message for a GET_LOG_MARKERS request and sends it to the + * client. + * + */ +IPC_HANDLER(get_log_markers) { +#if YAJL_MAJOR >= 2 + yajl_gen gen = yajl_gen_alloc(NULL); +#else + yajl_gen gen = yajl_gen_alloc(NULL, NULL); +#endif + + int offset_next_write, offset_last_wrap, logsize; + get_log_markers(&offset_next_write, &offset_last_wrap, &logsize); + + y(map_open); + ystr("offset_next_write"); + y(integer, offset_next_write); + + ystr("offset_last_wrap"); + y(integer, offset_last_wrap); + + ystr("shmname"); + ystr(shmlogname); + + ystr("size"); + y(integer, logsize); + + y(map_close); + + const unsigned char *payload; +#if YAJL_MAJOR >= 2 + size_t length; +#else + unsigned int length; +#endif + y(get_buf, &payload, &length); + + ipc_send_message(fd, length, I3_IPC_REPLY_TYPE_LOG_MARKERS, payload); + y(free); +} + /* * Callback for the YAJL parser (will be called when a string is parsed). * @@ -697,14 +739,15 @@ IPC_HANDLER(subscribe) { /* The index of each callback function corresponds to the numeric * value of the message type (see include/i3/ipc.h) */ -handler_t handlers[7] = { +handler_t handlers[8] = { handle_command, handle_get_workspaces, handle_subscribe, handle_get_outputs, handle_tree, handle_get_marks, - handle_get_bar_config + handle_get_bar_config, + handle_get_log_markers }; /* diff --git a/src/log.c b/src/log.c index a6159780..05a235fa 100644 --- a/src/log.c +++ b/src/log.c @@ -15,9 +15,14 @@ #include #include #include +#include +#include +#include #include "util.h" #include "log.h" +#include "i3.h" +#include "libi3.h" /* loglevels.h is autogenerated at make time */ #include "loglevels.h" @@ -27,21 +32,79 @@ static bool verbose = false; static FILE *errorfile; char *errorfilename; +/* SHM logging variables */ + +/* The name for the SHM (/i3-log-%pid). Will end up on /dev/shm on most + * systems. Global so that we can clean up at exit. */ +char *shmlogname = ""; +/* Size limit for the SHM log, by default 25 MiB. Can be overwritten using the + * flag --shmlog-size. */ +int shmlog_size = 0; +/* If enabled, logbuffer will point to a memory mapping of the i3 SHM log. */ +static char *logbuffer; +/* A pointer (within logbuffer) where data will be written to next. */ +static char *logwalk; +/* A pointer to the byte where we last wrapped. Necessary to not print the + * left-overs at the end of the ringbuffer. */ +static char *loglastwrap; +/* Size (in bytes) of the i3 SHM log. */ +static int logbuffer_size; +/* File descriptor for shm_open. */ +static int logbuffer_shm; + /* * Initializes logging by creating an error logfile in /tmp (or * XDG_RUNTIME_DIR, see get_process_filename()). * + * Will be called twice if --shmlog-size is specified. + * */ void init_logging() { - errorfilename = get_process_filename("errorlog"); - if (errorfilename == NULL) { - ELOG("Could not initialize errorlog\n"); - return; + if (!errorfilename) { + if (!(errorfilename = get_process_filename("errorlog"))) + ELOG("Could not initialize errorlog\n"); + else { + errorfile = fopen(errorfilename, "w"); + if (fcntl(fileno(errorfile), F_SETFD, FD_CLOEXEC)) { + ELOG("Could not set close-on-exec flag\n"); + } + } } - errorfile = fopen(errorfilename, "w"); - if (fcntl(fileno(errorfile), F_SETFD, FD_CLOEXEC)) { - ELOG("Could not set close-on-exec flag\n"); + /* If this is a debug build (not a release version), we will enable SHM + * logging by default, unless the user turned it off explicitly. */ + if (logbuffer == NULL && shmlog_size > 0) { + /* Reserve 1% of the RAM for the logfile, but at max 25 MiB. + * For 512 MiB of RAM this will lead to a 5 MiB log buffer. + * At the moment (2011-12-10), no testcase leads to an i3 log + * of more than ~ 600 KiB. */ + long long physical_mem_bytes = (long long)sysconf(_SC_PHYS_PAGES) * + sysconf(_SC_PAGESIZE); + logbuffer_size = min(physical_mem_bytes * 0.01, shmlog_size); + sasprintf(&shmlogname, "/i3-log-%d", getpid()); + logbuffer_shm = shm_open(shmlogname, O_RDWR | O_CREAT | O_TRUNC, S_IREAD | S_IWRITE); + if (logbuffer_shm == -1) { + ELOG("Could not shm_open SHM segment for the i3 log: %s\n", strerror(errno)); + return; + } + + if (ftruncate(logbuffer_shm, logbuffer_size) == -1) { + close(logbuffer_shm); + shm_unlink("/i3-log-"); + ELOG("Could not ftruncate SHM segment for the i3 log: %s\n", strerror(errno)); + return; + } + + logbuffer = mmap(NULL, logbuffer_size, PROT_READ | PROT_WRITE, MAP_SHARED, logbuffer_shm, 0); + if (logbuffer == MAP_FAILED) { + close(logbuffer_shm); + shm_unlink("/i3-log-"); + ELOG("Could not mmap SHM segment for the i3 log: %s\n", strerror(errno)); + logbuffer = NULL; + return; + } + logwalk = logbuffer; + loglastwrap = logbuffer + logbuffer_size; } } @@ -79,28 +142,78 @@ void add_loglevel(const char *level) { } /* - * Logs the given message to stdout while prefixing the current time to it. + * Returns the offsets for the next write and for the last wrap. + * Necessary to print the i3 SHM log in the correct order. + * + */ +void get_log_markers(int *offset_next_write, int *offset_last_wrap, int *size) { + *offset_next_write = (logwalk - logbuffer); + *offset_last_wrap = (loglastwrap - logbuffer); + *size = logbuffer_size; +} + +/* + * Logs the given message to stdout (if print is true) while prefixing the + * current time to it. Additionally, the message will be saved in the i3 SHM + * log if enabled. * This is to be called by *LOG() which includes filename/linenumber/function. * */ -void vlog(char *fmt, va_list args) { - static char timebuf[64]; +static void vlog(const bool print, const char *fmt, va_list args) { + /* Precisely one page to not consume too much memory but to hold enough + * data to be useful. */ + static char message[4096]; static struct tm result; + static time_t t; + static struct tm *tmp; + static size_t len; /* Get current time */ - time_t t = time(NULL); + t = time(NULL); /* Convert time to local time (determined by the locale) */ - struct tm *tmp = localtime_r(&t, &result); + tmp = localtime_r(&t, &result); /* Generate time prefix */ - strftime(timebuf, sizeof(timebuf), "%x %X - ", tmp); + len = strftime(message, sizeof(message), "%x %X - ", tmp); + + /* + * logbuffer print + * ---------------- + * true true format message, save, print + * true false format message, save + * false true print message only + * false false INVALID, never called + */ + if (!logbuffer) { #ifdef DEBUG_TIMING - struct timeval tv; - gettimeofday(&tv, NULL); - printf("%s%d.%d - ", timebuf, tv.tv_sec, tv.tv_usec); + struct timeval tv; + gettimeofday(&tv, NULL); + printf("%s%d.%d - ", message, tv.tv_sec, tv.tv_usec); #else - printf("%s", timebuf); + printf("%s", message); #endif - vprintf(fmt, args); + vprintf(fmt, args); + } else { + len += vsnprintf(message + len, sizeof(message) - len, fmt, args); + if (len == sizeof(message)) { + fprintf(stderr, "BUG: single log message > 4k\n"); + } + /* If there is no space for the current message (plus trailing + * nullbyte) in the ringbuffer, we need to wrap and write to the + * beginning again. */ + if ((len+1) >= (logbuffer_size - (logwalk - logbuffer))) { + loglastwrap = logwalk; + logwalk = logbuffer; + } + + /* Copy the buffer, terminate it, move the write pointer to the byte after + * our current message. */ + strncpy(logwalk, message, len); + logwalk[len] = '\0'; + logwalk += len + 1; + + if (print) + fwrite(message, len, 1, stdout); + } } /* @@ -111,11 +224,11 @@ void vlog(char *fmt, va_list args) { void verboselog(char *fmt, ...) { va_list args; - if (!verbose) + if (!logbuffer && !verbose) return; va_start(args, fmt); - vlog(fmt, args); + vlog(verbose, fmt, args); va_end(args); } @@ -127,7 +240,7 @@ void errorlog(char *fmt, ...) { va_list args; va_start(args, fmt); - vlog(fmt, args); + vlog(true, fmt, args); va_end(args); /* also log to the error logfile, if opened */ @@ -146,10 +259,10 @@ void errorlog(char *fmt, ...) { void debuglog(uint64_t lev, char *fmt, ...) { va_list args; - if ((loglevel & lev) == 0) + if (!logbuffer && !(loglevel & lev)) return; va_start(args, fmt); - vlog(fmt, args); + vlog((loglevel & lev), fmt, args); va_end(args); } diff --git a/src/main.c b/src/main.c index 38412d53..45cb162c 100644 --- a/src/main.c +++ b/src/main.c @@ -14,6 +14,8 @@ #include #include #include +#include +#include #include "all.h" #include "sd-daemon.h" @@ -23,6 +25,9 @@ * RLIM_INFINITY for i3 debugging versions. */ struct rlimit original_rlimit_core; +/* Whether this version of i3 is a debug build or a release build. */ +bool debug_build = false; + static int xkb_event_base; int xkb_current_group; @@ -205,6 +210,28 @@ static void i3_exit() { #if EV_VERSION_MAJOR >= 4 ev_loop_destroy(main_loop); #endif + + if (*shmlogname != '\0') { + fprintf(stderr, "Closing SHM log \"%s\"\n", shmlogname); + fflush(stderr); + shm_unlink(shmlogname); + } +} + +/* + * (One-shot) Handler for all signals with default action "Term", see signal(7) + * + * Unlinks the SHM log and re-raises the signal. + * + */ +static void handle_signal(int sig, siginfo_t *info, void *data) { + fprintf(stderr, "Received signal %d, terminating\n", sig); + if (*shmlogname != '\0') { + fprintf(stderr, "Closing SHM log \"%s\"\n", shmlogname); + shm_unlink(shmlogname); + } + fflush(stderr); + raise(sig); } int main(int argc, char *argv[]) { @@ -224,6 +251,8 @@ int main(int argc, char *argv[]) { {"force-xinerama", no_argument, 0, 0}, {"force_xinerama", no_argument, 0, 0}, {"disable-signalhandler", no_argument, 0, 0}, + {"shmlog-size", required_argument, 0, 0}, + {"shmlog_size", required_argument, 0, 0}, {"get-socketpath", no_argument, 0, 0}, {"get_socketpath", no_argument, 0, 0}, {0, 0, 0, 0} @@ -242,8 +271,21 @@ int main(int argc, char *argv[]) { srand(time(NULL)); + /* Init logging *before* initializing debug_build to guarantee early + * (file) logging. */ init_logging(); + /* I3_VERSION contains either something like this: + * "4.0.2 (2011-11-11, branch "release")". + * or: "4.0.2-123-gCOFFEEBABE (2011-11-11, branch "next")". + * + * So we check for the offset of the first opening round bracket to + * determine whether this is a git version or a release version. */ + debug_build = ((strchr(I3_VERSION, '(') - I3_VERSION) > 10); + + /* On non-release builds, disable SHM logging by default. */ + shmlog_size = (debug_build ? 25 * 1024 * 1024 : 0); + start_argv = argv; while ((opt = getopt_long(argc, argv, "c:CvaL:hld:V", long_options, &option_index)) != -1) { @@ -300,6 +342,14 @@ int main(int argc, char *argv[]) { } return 1; + } else if (strcmp(long_options[option_index].name, "shmlog-size") == 0 || + strcmp(long_options[option_index].name, "shmlog_size") == 0) { + shmlog_size = atoi(optarg); + /* Re-initialize logging immediately to get as many + * logmessages as possible into the SHM log. */ + init_logging(); + LOG("Limiting SHM log size to %d bytes\n", shmlog_size); + break; } else if (strcmp(long_options[option_index].name, "restart") == 0) { FREE(layout_path); layout_path = sstrdup(optarg); @@ -326,6 +376,11 @@ int main(int argc, char *argv[]) { fprintf(stderr, "\t--get-socketpath\n" "\tRetrieve the i3 IPC socket path from X11, print it, then exit.\n"); fprintf(stderr, "\n"); + fprintf(stderr, "\t--shmlog-size \n" + "\tLimits the size of the i3 SHM log to bytes. Setting this\n" + "\tto 0 disables SHM logging entirely.\n" + "\tThe default is %d bytes.\n", shmlog_size); + fprintf(stderr, "\n"); fprintf(stderr, "If you pass plain text arguments, i3 will interpret them as a command\n" "to send to a currently running i3 (like i3-msg). This allows you to\n" "use nice and logical commands, such as:\n" @@ -395,13 +450,11 @@ int main(int argc, char *argv[]) { return 0; } - /* I3_VERSION contains either something like this: - * "4.0.2 (2011-11-11, branch "release")". - * or: "4.0.2-123-gCOFFEEBABE (2011-11-11, branch "next")". - * - * So we check for the offset of the first opening round bracket to - * determine whether this is a git version or a release version. */ - if ((strchr(I3_VERSION, '(') - I3_VERSION) > 10) { + /* Enable logging to handle the case when the user did not specify --shmlog-size */ + init_logging(); + + /* Try to enable core dumps by default when running a debug build */ + if (debug_build) { struct rlimit limit = { RLIM_INFINITY, RLIM_INFINITY }; setrlimit(RLIMIT_CORE, &limit); @@ -662,8 +715,31 @@ int main(int argc, char *argv[]) { manage_existing_windows(root); + struct sigaction action; + + action.sa_sigaction = handle_signal; + action.sa_flags = SA_NODEFER | SA_RESETHAND | SA_SIGINFO; + sigemptyset(&action.sa_mask); + if (!disable_signalhandler) setup_signal_handler(); + else { + /* Catch all signals with default action "Core", see signal(7) */ + if (sigaction(SIGQUIT, &action, NULL) == -1 || + sigaction(SIGILL, &action, NULL) == -1 || + sigaction(SIGABRT, &action, NULL) == -1 || + sigaction(SIGFPE, &action, NULL) == -1 || + sigaction(SIGSEGV, &action, NULL) == -1) + ELOG("Could not setup signal handler"); + } + + /* Catch all signals with default action "Term", see signal(7) */ + if (sigaction(SIGHUP, &action, NULL) == -1 || + sigaction(SIGINT, &action, NULL) == -1 || + sigaction(SIGALRM, &action, NULL) == -1 || + sigaction(SIGUSR1, &action, NULL) == -1 || + sigaction(SIGUSR2, &action, NULL) == -1) + ELOG("Could not setup signal handler"); /* Ignore SIGPIPE to survive errors when an IPC client disconnects * while we are sending him a message */ diff --git a/src/sighandler.c b/src/sighandler.c index ca74813d..86366413 100644 --- a/src/sighandler.c +++ b/src/sighandler.c @@ -199,8 +199,11 @@ void setup_signal_handler() { action.sa_flags = SA_NODEFER | SA_RESETHAND | SA_SIGINFO; sigemptyset(&action.sa_mask); - if (sigaction(SIGSEGV, &action, NULL) == -1 || + /* Catch all signals with default action "Core", see signal(7) */ + if (sigaction(SIGQUIT, &action, NULL) == -1 || + sigaction(SIGILL, &action, NULL) == -1 || sigaction(SIGABRT, &action, NULL) == -1 || - sigaction(SIGFPE, &action, NULL) == -1) + sigaction(SIGFPE, &action, NULL) == -1 || + sigaction(SIGSEGV, &action, NULL) == -1) ELOG("Could not setup signal handler"); } diff --git a/testcases/lib/SocketActivation.pm b/testcases/lib/SocketActivation.pm index da1dda33..36197794 100644 --- a/testcases/lib/SocketActivation.pm +++ b/testcases/lib/SocketActivation.pm @@ -85,7 +85,8 @@ sub activate_i3 { # Construct the command to launch i3. Use maximum debug level, disable # the interactive signalhandler to make it crash immediately instead. - my $i3cmd = abs_path("../i3") . " -V -d all --disable-signalhandler"; + # Also disable logging to SHM since we want to redirect the logs anyways. + my $i3cmd = abs_path("../i3") . " -V -d all --disable-signalhandler --shmlog-size=0"; # For convenience: my $outdir = $args{outdir};