When QEMU is executed as part of a test case or from a script, it is usually desirable to exit if the parent process terminates. This ensures that "leaked" QEMU processes do not continue consuming resources after their parent has died.
This patch adds the -chardev exit-on-eof option causing socket and pipe chardevs to exit QEMU upon close. This happens when a parent process deliberately closes its file descriptor but also when the kernel cleans up a crashed process. Signed-off-by: Stefan Hajnoczi <stefa...@redhat.com> --- include/sysemu/char.h | 1 + qapi-schema.json | 23 ++++++++++++++++------- qemu-char.c | 33 +++++++++++++++++++++++++++------ qemu-options.hx | 19 +++++++++++++------ 4 files changed, 57 insertions(+), 19 deletions(-) diff --git a/include/sysemu/char.h b/include/sysemu/char.h index 0bbd631..382b320 100644 --- a/include/sysemu/char.h +++ b/include/sysemu/char.h @@ -86,6 +86,7 @@ struct CharDriverState { guint fd_in_tag; QemuOpts *opts; QTAILQ_ENTRY(CharDriverState) next; + bool exit_on_eof; }; /** diff --git a/qapi-schema.json b/qapi-schema.json index b11aad2..9b13da1 100644 --- a/qapi-schema.json +++ b/qapi-schema.json @@ -2630,10 +2630,13 @@ # @device: The name of the special file for the device, # i.e. /dev/ttyS0 on Unix or COM1: on Windows # @type: What kind of device this is. +# @exit-on-eof: #optional terminate when other side closes the pipe +# (default: false, since: 2.2) # # Since: 1.4 ## -{ 'type': 'ChardevHostdev', 'data': { 'device' : 'str' } } +{ 'type': 'ChardevHostdev', 'data': { 'device' : 'str', + '*exit-on-eof' : 'bool' } } ## # @ChardevSocket: @@ -2648,14 +2651,17 @@ # @nodelay: #optional set TCP_NODELAY socket option (default: false) # @telnet: #optional enable telnet protocol on server # sockets (default: false) +# @exit-on-eof: #optional terminate when other side closes socket +# (default: false, since: 2.2) # # Since: 1.4 ## -{ 'type': 'ChardevSocket', 'data': { 'addr' : 'SocketAddress', - '*server' : 'bool', - '*wait' : 'bool', - '*nodelay' : 'bool', - '*telnet' : 'bool' } } +{ 'type': 'ChardevSocket', 'data': { 'addr' : 'SocketAddress', + '*server' : 'bool', + '*wait' : 'bool', + '*nodelay' : 'bool', + '*telnet' : 'bool', + '*exit-on-eof' : 'bool' } } ## # @ChardevUdp: @@ -2689,10 +2695,13 @@ # @signal: #optional Allow signals (such as SIGINT triggered by ^C) # be delivered to qemu. Default: true in -nographic mode, # false otherwise. +# @exit-on-eof: #optional terminate when other side sends EOF +# (default: false, since: 2.2) # # Since: 1.5 ## -{ 'type': 'ChardevStdio', 'data': { '*signal' : 'bool' } } +{ 'type': 'ChardevStdio', 'data': { '*signal' : 'bool', + '*exit-on-eof' : 'bool' } } ## # @ChardevSpiceChannel: diff --git a/qemu-char.c b/qemu-char.c index 7acc03f..1974b8f 100644 --- a/qemu-char.c +++ b/qemu-char.c @@ -110,9 +110,15 @@ void qemu_chr_be_event(CharDriverState *s, int event) break; } - if (!s->chr_event) - return; - s->chr_event(s->handler_opaque, event); + if (s->chr_event) { + s->chr_event(s->handler_opaque, event); + } + + if (s->exit_on_eof && event == CHR_EVENT_CLOSED) { + fprintf(stderr, "qemu: terminating due to eof on chardev '%s'\n", + s->label); + qemu_system_shutdown_force(); + } } void qemu_chr_be_generic_open(CharDriverState *s) @@ -991,6 +997,7 @@ static CharDriverState *qemu_chr_open_pipe(ChardevHostdev *opts) int fd_in, fd_out; char filename_in[256], filename_out[256]; const char *filename = opts->device; + CharDriverState *chr; if (filename == NULL) { fprintf(stderr, "chardev: pipe: no filename given\n"); @@ -1011,7 +1018,9 @@ static CharDriverState *qemu_chr_open_pipe(ChardevHostdev *opts) return NULL; } } - return qemu_chr_open_fd(fd_in, fd_out); + chr = qemu_chr_open_fd(fd_in, fd_out); + chr->exit_on_eof = opts->has_exit_on_eof && opts->exit_on_eof; + return chr; } /* init terminal so that we can grab keys */ @@ -2893,6 +2902,7 @@ static void tcp_chr_close(CharDriverState *chr) static CharDriverState *qemu_chr_open_socket_fd(int fd, bool do_nodelay, bool is_listen, bool is_telnet, bool is_waitconnect, + bool is_exit_on_eof, Error **errp) { CharDriverState *chr = NULL; @@ -2955,6 +2965,7 @@ static CharDriverState *qemu_chr_open_socket_fd(int fd, bool do_nodelay, chr->chr_update_read_handler = tcp_chr_update_read_handler; /* be isn't opened until we get a connection */ chr->explicit_be_open = true; + chr->exit_on_eof = is_exit_on_eof; if (is_listen) { s->listen_fd = fd; @@ -2991,6 +3002,7 @@ static CharDriverState *qemu_chr_open_socket(QemuOpts *opts) bool is_telnet = qemu_opt_get_bool(opts, "telnet", false); bool do_nodelay = !qemu_opt_get_bool(opts, "delay", true); bool is_unix = qemu_opt_get(opts, "path") != NULL; + bool is_exit_on_eof = qemu_opt_get_bool(opts, "exit-on-eof", false); if (is_unix) { if (is_listen) { @@ -3013,7 +3025,7 @@ static CharDriverState *qemu_chr_open_socket(QemuOpts *opts) qemu_set_nonblock(fd); chr = qemu_chr_open_socket_fd(fd, do_nodelay, is_listen, is_telnet, - is_waitconnect, &local_err); + is_waitconnect, is_exit_on_eof, &local_err); if (local_err) { goto fail; } @@ -3376,6 +3388,8 @@ static void qemu_chr_parse_stdio(QemuOpts *opts, ChardevBackend *backend, backend->stdio = g_new0(ChardevStdio, 1); backend->stdio->has_signal = true; backend->stdio->signal = qemu_opt_get_bool(opts, "signal", true); + backend->stdio->has_exit_on_eof = true; + backend->stdio->exit_on_eof = qemu_opt_get_bool(opts, "exit-on-eof", false); } static void qemu_chr_parse_serial(QemuOpts *opts, ChardevBackend *backend, @@ -3415,6 +3429,8 @@ static void qemu_chr_parse_pipe(QemuOpts *opts, ChardevBackend *backend, } backend->pipe = g_new0(ChardevHostdev, 1); backend->pipe->device = g_strdup(device); + backend->pipe->has_exit_on_eof = true; + backend->pipe->exit_on_eof = qemu_opt_get_bool(opts, "exit-on-eof", false); } static void qemu_chr_parse_ringbuf(QemuOpts *opts, ChardevBackend *backend, @@ -3848,6 +3864,9 @@ QemuOptsList qemu_chardev_opts = { },{ .name = "chardev", .type = QEMU_OPT_STRING, + },{ + .name = "exit-on-eof", + .type = QEMU_OPT_BOOL, }, { /* end of list */ } }, @@ -3967,6 +3986,7 @@ static CharDriverState *qmp_chardev_open_socket(ChardevSocket *sock, bool is_listen = sock->has_server ? sock->server : true; bool is_telnet = sock->has_telnet ? sock->telnet : false; bool is_waitconnect = sock->has_wait ? sock->wait : false; + bool is_exit_on_eof = sock->has_exit_on_eof ? sock->exit_on_eof : false; int fd; if (is_listen) { @@ -3978,7 +3998,8 @@ static CharDriverState *qmp_chardev_open_socket(ChardevSocket *sock, return NULL; } return qemu_chr_open_socket_fd(fd, do_nodelay, is_listen, - is_telnet, is_waitconnect, errp); + is_telnet, is_waitconnect, + is_exit_on_eof, errp); } static CharDriverState *qmp_chardev_open_udp(ChardevUdp *udp, diff --git a/qemu-options.hx b/qemu-options.hx index 9e54686..4b4da4f 100644 --- a/qemu-options.hx +++ b/qemu-options.hx @@ -1927,8 +1927,9 @@ ETEXI DEF("chardev", HAS_ARG, QEMU_OPTION_chardev, "-chardev null,id=id[,mux=on|off]\n" "-chardev socket,id=id[,host=host],port=host[,to=to][,ipv4][,ipv6][,nodelay]\n" - " [,server][,nowait][,telnet][,mux=on|off] (tcp)\n" - "-chardev socket,id=id,path=path[,server][,nowait][,telnet],[mux=on|off] (unix)\n" + " [,server][,nowait][,telnet][,mux=on|off][,exit-on-eof] (tcp)\n" + "-chardev socket,id=id,path=path[,server][,nowait][,telnet],[mux=on|off]\n" + " [,exit-on-eof] (unix)\n" "-chardev udp,id=id[,host=host],port=port[,localaddr=localaddr]\n" " [,localport=localport][,ipv4][,ipv6][,mux=on|off]\n" "-chardev msmouse,id=id[,mux=on|off]\n" @@ -1936,13 +1937,13 @@ DEF("chardev", HAS_ARG, QEMU_OPTION_chardev, " [,mux=on|off]\n" "-chardev ringbuf,id=id[,size=size]\n" "-chardev file,id=id,path=path[,mux=on|off]\n" - "-chardev pipe,id=id,path=path[,mux=on|off]\n" + "-chardev pipe,id=id,path=path[,mux=on|off][,exit-on-eof]\n" #ifdef _WIN32 "-chardev console,id=id[,mux=on|off]\n" "-chardev serial,id=id,path=path[,mux=on|off]\n" #else "-chardev pty,id=id[,mux=on|off]\n" - "-chardev stdio,id=id[,mux=on|off][,signal=on|off]\n" + "-chardev stdio,id=id[,mux=on|off][,signal=on|off][,exit-on-eof]\n" #endif #ifdef CONFIG_BRLAPI "-chardev braille,id=id[,mux=on|off]\n" @@ -2000,7 +2001,7 @@ Options to each backend are described below. A void device. This device will not emit any data, and will drop any data it receives. The null backend does not take any options. -@item -chardev socket ,id=@var{id} [@var{TCP options} or @var{unix options}] [,server] [,nowait] [,telnet] +@item -chardev socket ,id=@var{id} [@var{TCP options} or @var{unix options}] [,server] [,nowait] [,telnet] [,exit-on-eof] Create a two-way stream socket, which can be either a TCP or a unix socket. A unix socket will be created if @option{path} is specified. Behaviour is @@ -2014,6 +2015,8 @@ connect to a listening socket. @option{telnet} specifies that traffic on the socket should interpret telnet escape sequences. +@option{exit-on-eof} specifies that QEMU should terminate upon disconnect + TCP and unix socket options are given below: @table @option @@ -2094,7 +2097,7 @@ Log all traffic received from the guest to a file. created if it does not already exist, and overwritten if it does. @option{path} is required. -@item -chardev pipe ,id=@var{id} ,path=@var{path} +@item -chardev pipe ,id=@var{id} ,path=@var{path} [,exit-on-eof] Create a two-way connection to the guest. The behaviour differs slightly between Windows hosts and other hosts: @@ -2111,6 +2114,8 @@ be present. @option{path} forms part of the pipe path as described above. @option{path} is required. +@option{exit-on-eof} specifies that QEMU should terminate upon disconnect + @item -chardev console ,id=@var{id} Send traffic from the guest to QEMU's standard output. @option{console} does not @@ -2141,6 +2146,8 @@ Connect to standard input and standard output of the QEMU process. exiting QEMU with the key sequence @key{Control-c}. This option is enabled by default, use @option{signal=off} to disable it. +@option{exit-on-eof} specifies that QEMU should terminate upon EOF + @option{stdio} is not available on Windows hosts. @item -chardev braille ,id=@var{id} -- 1.9.3