mirror of
https://github.com/qemu/qemu.git
synced 2024-12-02 16:23:35 +08:00
fd4a5fd463
Currently any client which can complete the TLS handshake is able to use a chardev server. The server admin can turn on the 'verify-peer' option for the x509 creds to require the client to provide a x509 certificate. This means the client will have to acquire a certificate from the CA before they are permitted to use the chardev server. This is still a fairly low bar. This adds a 'tls-authz=OBJECT-ID' option to the socket chardev backend which takes the ID of a previously added 'QAuthZ' object instance. This will be used to validate the client's x509 distinguished name. Clients failing the check will not be permitted to use the chardev server. For example to setup authorization that only allows connection from a client whose x509 certificate distinguished name contains 'CN=fred', you would use: $QEMU -object tls-creds-x509,id=tls0,dir=/home/berrange/qemutls,\ endpoint=server,verify-peer=yes \ -object authz-simple,id=authz0,identity=CN=laptop.example.com,,\ O=Example Org,,L=London,,ST=London,,C=GB \ -chardev socket,host=127.0.0.1,port=9000,server,\ tls-creds=tls0,tls-authz=authz0 \ ...other qemu args... Signed-off-by: Daniel P. Berrange <berrange@redhat.com> Reviewed-by: Marc-André Lureau <marcandre.lureau@redhat.com>
1170 lines
31 KiB
C
1170 lines
31 KiB
C
/*
|
|
* QEMU System Emulator
|
|
*
|
|
* Copyright (c) 2003-2008 Fabrice Bellard
|
|
*
|
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
* of this software and associated documentation files (the "Software"), to deal
|
|
* in the Software without restriction, including without limitation the rights
|
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
* copies of the Software, and to permit persons to whom the Software is
|
|
* furnished to do so, subject to the following conditions:
|
|
*
|
|
* The above copyright notice and this permission notice shall be included in
|
|
* all copies or substantial portions of the Software.
|
|
*
|
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
|
|
* THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
* THE SOFTWARE.
|
|
*/
|
|
|
|
#include "qemu/osdep.h"
|
|
#include "qemu/cutils.h"
|
|
#include "monitor/monitor.h"
|
|
#include "sysemu/sysemu.h"
|
|
#include "qemu/config-file.h"
|
|
#include "qemu/error-report.h"
|
|
#include "chardev/char.h"
|
|
#include "qapi/error.h"
|
|
#include "qapi/qapi-commands-char.h"
|
|
#include "qapi/qmp/qerror.h"
|
|
#include "sysemu/replay.h"
|
|
#include "qemu/help_option.h"
|
|
#include "qemu/option.h"
|
|
|
|
#include "chardev/char-mux.h"
|
|
|
|
/***********************************************************/
|
|
/* character device */
|
|
|
|
static Object *get_chardevs_root(void)
|
|
{
|
|
return container_get(object_get_root(), "/chardevs");
|
|
}
|
|
|
|
static void chr_be_event(Chardev *s, int event)
|
|
{
|
|
CharBackend *be = s->be;
|
|
|
|
if (!be || !be->chr_event) {
|
|
return;
|
|
}
|
|
|
|
be->chr_event(be->opaque, event);
|
|
}
|
|
|
|
void qemu_chr_be_event(Chardev *s, int event)
|
|
{
|
|
/* Keep track if the char device is open */
|
|
switch (event) {
|
|
case CHR_EVENT_OPENED:
|
|
s->be_open = 1;
|
|
break;
|
|
case CHR_EVENT_CLOSED:
|
|
s->be_open = 0;
|
|
break;
|
|
}
|
|
|
|
CHARDEV_GET_CLASS(s)->chr_be_event(s, event);
|
|
}
|
|
|
|
/* Not reporting errors from writing to logfile, as logs are
|
|
* defined to be "best effort" only */
|
|
static void qemu_chr_write_log(Chardev *s, const uint8_t *buf, size_t len)
|
|
{
|
|
size_t done = 0;
|
|
ssize_t ret;
|
|
|
|
if (s->logfd < 0) {
|
|
return;
|
|
}
|
|
|
|
while (done < len) {
|
|
retry:
|
|
ret = write(s->logfd, buf + done, len - done);
|
|
if (ret == -1 && errno == EAGAIN) {
|
|
g_usleep(100);
|
|
goto retry;
|
|
}
|
|
|
|
if (ret <= 0) {
|
|
return;
|
|
}
|
|
done += ret;
|
|
}
|
|
}
|
|
|
|
static int qemu_chr_write_buffer(Chardev *s,
|
|
const uint8_t *buf, int len,
|
|
int *offset, bool write_all)
|
|
{
|
|
ChardevClass *cc = CHARDEV_GET_CLASS(s);
|
|
int res = 0;
|
|
*offset = 0;
|
|
|
|
qemu_mutex_lock(&s->chr_write_lock);
|
|
while (*offset < len) {
|
|
retry:
|
|
res = cc->chr_write(s, buf + *offset, len - *offset);
|
|
if (res < 0 && errno == EAGAIN && write_all) {
|
|
g_usleep(100);
|
|
goto retry;
|
|
}
|
|
|
|
if (res <= 0) {
|
|
break;
|
|
}
|
|
|
|
*offset += res;
|
|
if (!write_all) {
|
|
break;
|
|
}
|
|
}
|
|
if (*offset > 0) {
|
|
qemu_chr_write_log(s, buf, *offset);
|
|
}
|
|
qemu_mutex_unlock(&s->chr_write_lock);
|
|
|
|
return res;
|
|
}
|
|
|
|
int qemu_chr_write(Chardev *s, const uint8_t *buf, int len, bool write_all)
|
|
{
|
|
int offset = 0;
|
|
int res;
|
|
|
|
if (qemu_chr_replay(s) && replay_mode == REPLAY_MODE_PLAY) {
|
|
replay_char_write_event_load(&res, &offset);
|
|
assert(offset <= len);
|
|
qemu_chr_write_buffer(s, buf, offset, &offset, true);
|
|
return res;
|
|
}
|
|
|
|
res = qemu_chr_write_buffer(s, buf, len, &offset, write_all);
|
|
|
|
if (qemu_chr_replay(s) && replay_mode == REPLAY_MODE_RECORD) {
|
|
replay_char_write_event_save(res, offset);
|
|
}
|
|
|
|
if (res < 0) {
|
|
return res;
|
|
}
|
|
return offset;
|
|
}
|
|
|
|
int qemu_chr_be_can_write(Chardev *s)
|
|
{
|
|
CharBackend *be = s->be;
|
|
|
|
if (!be || !be->chr_can_read) {
|
|
return 0;
|
|
}
|
|
|
|
return be->chr_can_read(be->opaque);
|
|
}
|
|
|
|
void qemu_chr_be_write_impl(Chardev *s, uint8_t *buf, int len)
|
|
{
|
|
CharBackend *be = s->be;
|
|
|
|
if (be && be->chr_read) {
|
|
be->chr_read(be->opaque, buf, len);
|
|
}
|
|
}
|
|
|
|
void qemu_chr_be_write(Chardev *s, uint8_t *buf, int len)
|
|
{
|
|
if (qemu_chr_replay(s)) {
|
|
if (replay_mode == REPLAY_MODE_PLAY) {
|
|
return;
|
|
}
|
|
replay_chr_be_write(s, buf, len);
|
|
} else {
|
|
qemu_chr_be_write_impl(s, buf, len);
|
|
}
|
|
}
|
|
|
|
void qemu_chr_be_update_read_handlers(Chardev *s,
|
|
GMainContext *context)
|
|
{
|
|
ChardevClass *cc = CHARDEV_GET_CLASS(s);
|
|
|
|
assert(qemu_chr_has_feature(s, QEMU_CHAR_FEATURE_GCONTEXT)
|
|
|| !context);
|
|
s->gcontext = context;
|
|
if (cc->chr_update_read_handler) {
|
|
cc->chr_update_read_handler(s);
|
|
}
|
|
}
|
|
|
|
int qemu_chr_add_client(Chardev *s, int fd)
|
|
{
|
|
return CHARDEV_GET_CLASS(s)->chr_add_client ?
|
|
CHARDEV_GET_CLASS(s)->chr_add_client(s, fd) : -1;
|
|
}
|
|
|
|
static void qemu_char_open(Chardev *chr, ChardevBackend *backend,
|
|
bool *be_opened, Error **errp)
|
|
{
|
|
ChardevClass *cc = CHARDEV_GET_CLASS(chr);
|
|
/* Any ChardevCommon member would work */
|
|
ChardevCommon *common = backend ? backend->u.null.data : NULL;
|
|
|
|
if (common && common->has_logfile) {
|
|
int flags = O_WRONLY | O_CREAT;
|
|
if (common->has_logappend &&
|
|
common->logappend) {
|
|
flags |= O_APPEND;
|
|
} else {
|
|
flags |= O_TRUNC;
|
|
}
|
|
chr->logfd = qemu_open(common->logfile, flags, 0666);
|
|
if (chr->logfd < 0) {
|
|
error_setg_errno(errp, errno,
|
|
"Unable to open logfile %s",
|
|
common->logfile);
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (cc->open) {
|
|
cc->open(chr, backend, be_opened, errp);
|
|
}
|
|
}
|
|
|
|
static void char_init(Object *obj)
|
|
{
|
|
Chardev *chr = CHARDEV(obj);
|
|
|
|
chr->logfd = -1;
|
|
qemu_mutex_init(&chr->chr_write_lock);
|
|
|
|
/*
|
|
* Assume if chr_update_read_handler is implemented it will
|
|
* take the updated gcontext into account.
|
|
*/
|
|
if (CHARDEV_GET_CLASS(chr)->chr_update_read_handler) {
|
|
qemu_chr_set_feature(chr, QEMU_CHAR_FEATURE_GCONTEXT);
|
|
}
|
|
|
|
}
|
|
|
|
static int null_chr_write(Chardev *chr, const uint8_t *buf, int len)
|
|
{
|
|
return len;
|
|
}
|
|
|
|
static void char_class_init(ObjectClass *oc, void *data)
|
|
{
|
|
ChardevClass *cc = CHARDEV_CLASS(oc);
|
|
|
|
cc->chr_write = null_chr_write;
|
|
cc->chr_be_event = chr_be_event;
|
|
}
|
|
|
|
static void char_finalize(Object *obj)
|
|
{
|
|
Chardev *chr = CHARDEV(obj);
|
|
|
|
if (chr->be) {
|
|
chr->be->chr = NULL;
|
|
}
|
|
g_free(chr->filename);
|
|
g_free(chr->label);
|
|
if (chr->logfd != -1) {
|
|
close(chr->logfd);
|
|
}
|
|
qemu_mutex_destroy(&chr->chr_write_lock);
|
|
}
|
|
|
|
static const TypeInfo char_type_info = {
|
|
.name = TYPE_CHARDEV,
|
|
.parent = TYPE_OBJECT,
|
|
.instance_size = sizeof(Chardev),
|
|
.instance_init = char_init,
|
|
.instance_finalize = char_finalize,
|
|
.abstract = true,
|
|
.class_size = sizeof(ChardevClass),
|
|
.class_init = char_class_init,
|
|
};
|
|
|
|
static int chardev_machine_done_notify_one(Object *child, void *opaque)
|
|
{
|
|
Chardev *chr = (Chardev *)child;
|
|
ChardevClass *class = CHARDEV_GET_CLASS(chr);
|
|
|
|
if (class->chr_machine_done) {
|
|
return class->chr_machine_done(chr);
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static void chardev_machine_done_hook(Notifier *notifier, void *unused)
|
|
{
|
|
int ret = object_child_foreach(get_chardevs_root(),
|
|
chardev_machine_done_notify_one, NULL);
|
|
|
|
if (ret) {
|
|
error_report("Failed to call chardev machine_done hooks");
|
|
exit(1);
|
|
}
|
|
}
|
|
|
|
static Notifier chardev_machine_done_notify = {
|
|
.notify = chardev_machine_done_hook,
|
|
};
|
|
|
|
static bool qemu_chr_is_busy(Chardev *s)
|
|
{
|
|
if (CHARDEV_IS_MUX(s)) {
|
|
MuxChardev *d = MUX_CHARDEV(s);
|
|
return d->mux_cnt >= 0;
|
|
} else {
|
|
return s->be != NULL;
|
|
}
|
|
}
|
|
|
|
int qemu_chr_wait_connected(Chardev *chr, Error **errp)
|
|
{
|
|
ChardevClass *cc = CHARDEV_GET_CLASS(chr);
|
|
|
|
if (cc->chr_wait_connected) {
|
|
return cc->chr_wait_connected(chr, errp);
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
QemuOpts *qemu_chr_parse_compat(const char *label, const char *filename,
|
|
bool permit_mux_mon)
|
|
{
|
|
char host[65], port[33], width[8], height[8];
|
|
int pos;
|
|
const char *p;
|
|
QemuOpts *opts;
|
|
Error *local_err = NULL;
|
|
|
|
opts = qemu_opts_create(qemu_find_opts("chardev"), label, 1, &local_err);
|
|
if (local_err) {
|
|
error_report_err(local_err);
|
|
return NULL;
|
|
}
|
|
|
|
if (strstart(filename, "mon:", &p)) {
|
|
if (!permit_mux_mon) {
|
|
error_report("mon: isn't supported in this context");
|
|
return NULL;
|
|
}
|
|
filename = p;
|
|
qemu_opt_set(opts, "mux", "on", &error_abort);
|
|
if (strcmp(filename, "stdio") == 0) {
|
|
/* Monitor is muxed to stdio: do not exit on Ctrl+C by default
|
|
* but pass it to the guest. Handle this only for compat syntax,
|
|
* for -chardev syntax we have special option for this.
|
|
* This is what -nographic did, redirecting+muxing serial+monitor
|
|
* to stdio causing Ctrl+C to be passed to guest. */
|
|
qemu_opt_set(opts, "signal", "off", &error_abort);
|
|
}
|
|
}
|
|
|
|
if (strcmp(filename, "null") == 0 ||
|
|
strcmp(filename, "pty") == 0 ||
|
|
strcmp(filename, "msmouse") == 0 ||
|
|
strcmp(filename, "wctablet") == 0 ||
|
|
strcmp(filename, "braille") == 0 ||
|
|
strcmp(filename, "testdev") == 0 ||
|
|
strcmp(filename, "stdio") == 0) {
|
|
qemu_opt_set(opts, "backend", filename, &error_abort);
|
|
return opts;
|
|
}
|
|
if (strstart(filename, "vc", &p)) {
|
|
qemu_opt_set(opts, "backend", "vc", &error_abort);
|
|
if (*p == ':') {
|
|
if (sscanf(p+1, "%7[0-9]x%7[0-9]", width, height) == 2) {
|
|
/* pixels */
|
|
qemu_opt_set(opts, "width", width, &error_abort);
|
|
qemu_opt_set(opts, "height", height, &error_abort);
|
|
} else if (sscanf(p+1, "%7[0-9]Cx%7[0-9]C", width, height) == 2) {
|
|
/* chars */
|
|
qemu_opt_set(opts, "cols", width, &error_abort);
|
|
qemu_opt_set(opts, "rows", height, &error_abort);
|
|
} else {
|
|
goto fail;
|
|
}
|
|
}
|
|
return opts;
|
|
}
|
|
if (strcmp(filename, "con:") == 0) {
|
|
qemu_opt_set(opts, "backend", "console", &error_abort);
|
|
return opts;
|
|
}
|
|
if (strstart(filename, "COM", NULL)) {
|
|
qemu_opt_set(opts, "backend", "serial", &error_abort);
|
|
qemu_opt_set(opts, "path", filename, &error_abort);
|
|
return opts;
|
|
}
|
|
if (strstart(filename, "file:", &p)) {
|
|
qemu_opt_set(opts, "backend", "file", &error_abort);
|
|
qemu_opt_set(opts, "path", p, &error_abort);
|
|
return opts;
|
|
}
|
|
if (strstart(filename, "pipe:", &p)) {
|
|
qemu_opt_set(opts, "backend", "pipe", &error_abort);
|
|
qemu_opt_set(opts, "path", p, &error_abort);
|
|
return opts;
|
|
}
|
|
if (strstart(filename, "tcp:", &p) ||
|
|
strstart(filename, "telnet:", &p) ||
|
|
strstart(filename, "tn3270:", &p) ||
|
|
strstart(filename, "websocket:", &p)) {
|
|
if (sscanf(p, "%64[^:]:%32[^,]%n", host, port, &pos) < 2) {
|
|
host[0] = 0;
|
|
if (sscanf(p, ":%32[^,]%n", port, &pos) < 1)
|
|
goto fail;
|
|
}
|
|
qemu_opt_set(opts, "backend", "socket", &error_abort);
|
|
qemu_opt_set(opts, "host", host, &error_abort);
|
|
qemu_opt_set(opts, "port", port, &error_abort);
|
|
if (p[pos] == ',') {
|
|
qemu_opts_do_parse(opts, p+pos+1, NULL, &local_err);
|
|
if (local_err) {
|
|
error_report_err(local_err);
|
|
goto fail;
|
|
}
|
|
}
|
|
if (strstart(filename, "telnet:", &p)) {
|
|
qemu_opt_set(opts, "telnet", "on", &error_abort);
|
|
} else if (strstart(filename, "tn3270:", &p)) {
|
|
qemu_opt_set(opts, "tn3270", "on", &error_abort);
|
|
} else if (strstart(filename, "websocket:", &p)) {
|
|
qemu_opt_set(opts, "websocket", "on", &error_abort);
|
|
}
|
|
return opts;
|
|
}
|
|
if (strstart(filename, "udp:", &p)) {
|
|
qemu_opt_set(opts, "backend", "udp", &error_abort);
|
|
if (sscanf(p, "%64[^:]:%32[^@,]%n", host, port, &pos) < 2) {
|
|
host[0] = 0;
|
|
if (sscanf(p, ":%32[^@,]%n", port, &pos) < 1) {
|
|
goto fail;
|
|
}
|
|
}
|
|
qemu_opt_set(opts, "host", host, &error_abort);
|
|
qemu_opt_set(opts, "port", port, &error_abort);
|
|
if (p[pos] == '@') {
|
|
p += pos + 1;
|
|
if (sscanf(p, "%64[^:]:%32[^,]%n", host, port, &pos) < 2) {
|
|
host[0] = 0;
|
|
if (sscanf(p, ":%32[^,]%n", port, &pos) < 1) {
|
|
goto fail;
|
|
}
|
|
}
|
|
qemu_opt_set(opts, "localaddr", host, &error_abort);
|
|
qemu_opt_set(opts, "localport", port, &error_abort);
|
|
}
|
|
return opts;
|
|
}
|
|
if (strstart(filename, "unix:", &p)) {
|
|
qemu_opt_set(opts, "backend", "socket", &error_abort);
|
|
qemu_opts_do_parse(opts, p, "path", &local_err);
|
|
if (local_err) {
|
|
error_report_err(local_err);
|
|
goto fail;
|
|
}
|
|
return opts;
|
|
}
|
|
if (strstart(filename, "/dev/parport", NULL) ||
|
|
strstart(filename, "/dev/ppi", NULL)) {
|
|
qemu_opt_set(opts, "backend", "parallel", &error_abort);
|
|
qemu_opt_set(opts, "path", filename, &error_abort);
|
|
return opts;
|
|
}
|
|
if (strstart(filename, "/dev/", NULL)) {
|
|
qemu_opt_set(opts, "backend", "serial", &error_abort);
|
|
qemu_opt_set(opts, "path", filename, &error_abort);
|
|
return opts;
|
|
}
|
|
|
|
error_report("'%s' is not a valid char driver", filename);
|
|
|
|
fail:
|
|
qemu_opts_del(opts);
|
|
return NULL;
|
|
}
|
|
|
|
void qemu_chr_parse_common(QemuOpts *opts, ChardevCommon *backend)
|
|
{
|
|
const char *logfile = qemu_opt_get(opts, "logfile");
|
|
|
|
backend->has_logfile = logfile != NULL;
|
|
backend->logfile = g_strdup(logfile);
|
|
|
|
backend->has_logappend = true;
|
|
backend->logappend = qemu_opt_get_bool(opts, "logappend", false);
|
|
}
|
|
|
|
static const ChardevClass *char_get_class(const char *driver, Error **errp)
|
|
{
|
|
ObjectClass *oc;
|
|
const ChardevClass *cc;
|
|
char *typename = g_strdup_printf("chardev-%s", driver);
|
|
|
|
oc = object_class_by_name(typename);
|
|
g_free(typename);
|
|
|
|
if (!object_class_dynamic_cast(oc, TYPE_CHARDEV)) {
|
|
error_setg(errp, "'%s' is not a valid char driver name", driver);
|
|
return NULL;
|
|
}
|
|
|
|
if (object_class_is_abstract(oc)) {
|
|
error_setg(errp, QERR_INVALID_PARAMETER_VALUE, "driver",
|
|
"abstract device type");
|
|
return NULL;
|
|
}
|
|
|
|
cc = CHARDEV_CLASS(oc);
|
|
if (cc->internal) {
|
|
error_setg(errp, "'%s' is not a valid char driver name", driver);
|
|
return NULL;
|
|
}
|
|
|
|
return cc;
|
|
}
|
|
|
|
static const struct ChardevAlias {
|
|
const char *typename;
|
|
const char *alias;
|
|
} chardev_alias_table[] = {
|
|
#ifdef HAVE_CHARDEV_PARPORT
|
|
{ "parallel", "parport" },
|
|
#endif
|
|
#ifdef HAVE_CHARDEV_SERIAL
|
|
{ "serial", "tty" },
|
|
#endif
|
|
};
|
|
|
|
typedef struct ChadevClassFE {
|
|
void (*fn)(const char *name, void *opaque);
|
|
void *opaque;
|
|
} ChadevClassFE;
|
|
|
|
static void
|
|
chardev_class_foreach(ObjectClass *klass, void *opaque)
|
|
{
|
|
ChadevClassFE *fe = opaque;
|
|
|
|
assert(g_str_has_prefix(object_class_get_name(klass), "chardev-"));
|
|
if (CHARDEV_CLASS(klass)->internal) {
|
|
return;
|
|
}
|
|
|
|
fe->fn(object_class_get_name(klass) + 8, fe->opaque);
|
|
}
|
|
|
|
static void
|
|
chardev_name_foreach(void (*fn)(const char *name, void *opaque), void *opaque)
|
|
{
|
|
ChadevClassFE fe = { .fn = fn, .opaque = opaque };
|
|
int i;
|
|
|
|
object_class_foreach(chardev_class_foreach, TYPE_CHARDEV, false, &fe);
|
|
|
|
for (i = 0; i < (int)ARRAY_SIZE(chardev_alias_table); i++) {
|
|
fn(chardev_alias_table[i].alias, opaque);
|
|
}
|
|
}
|
|
|
|
static void
|
|
help_string_append(const char *name, void *opaque)
|
|
{
|
|
GString *str = opaque;
|
|
|
|
g_string_append_printf(str, "\n %s", name);
|
|
}
|
|
|
|
static const char *chardev_alias_translate(const char *name)
|
|
{
|
|
int i;
|
|
for (i = 0; i < (int)ARRAY_SIZE(chardev_alias_table); i++) {
|
|
if (g_strcmp0(chardev_alias_table[i].alias, name) == 0) {
|
|
return chardev_alias_table[i].typename;
|
|
}
|
|
}
|
|
return name;
|
|
}
|
|
|
|
ChardevBackend *qemu_chr_parse_opts(QemuOpts *opts, Error **errp)
|
|
{
|
|
Error *local_err = NULL;
|
|
const ChardevClass *cc;
|
|
ChardevBackend *backend = NULL;
|
|
const char *name = chardev_alias_translate(qemu_opt_get(opts, "backend"));
|
|
|
|
if (name == NULL) {
|
|
error_setg(errp, "chardev: \"%s\" missing backend",
|
|
qemu_opts_id(opts));
|
|
return NULL;
|
|
}
|
|
|
|
cc = char_get_class(name, errp);
|
|
if (cc == NULL) {
|
|
return NULL;
|
|
}
|
|
|
|
backend = g_new0(ChardevBackend, 1);
|
|
backend->type = CHARDEV_BACKEND_KIND_NULL;
|
|
|
|
if (cc->parse) {
|
|
cc->parse(opts, backend, &local_err);
|
|
if (local_err) {
|
|
error_propagate(errp, local_err);
|
|
qapi_free_ChardevBackend(backend);
|
|
return NULL;
|
|
}
|
|
} else {
|
|
ChardevCommon *ccom = g_new0(ChardevCommon, 1);
|
|
qemu_chr_parse_common(opts, ccom);
|
|
backend->u.null.data = ccom; /* Any ChardevCommon member would work */
|
|
}
|
|
|
|
return backend;
|
|
}
|
|
|
|
Chardev *qemu_chr_new_from_opts(QemuOpts *opts, GMainContext *context,
|
|
Error **errp)
|
|
{
|
|
const ChardevClass *cc;
|
|
Chardev *chr = NULL;
|
|
ChardevBackend *backend = NULL;
|
|
const char *name = chardev_alias_translate(qemu_opt_get(opts, "backend"));
|
|
const char *id = qemu_opts_id(opts);
|
|
char *bid = NULL;
|
|
|
|
if (name && is_help_option(name)) {
|
|
GString *str = g_string_new("");
|
|
|
|
chardev_name_foreach(help_string_append, str);
|
|
|
|
error_printf("Available chardev backend types: %s\n", str->str);
|
|
g_string_free(str, true);
|
|
return NULL;
|
|
}
|
|
|
|
if (id == NULL) {
|
|
error_setg(errp, "chardev: no id specified");
|
|
return NULL;
|
|
}
|
|
|
|
backend = qemu_chr_parse_opts(opts, errp);
|
|
if (backend == NULL) {
|
|
return NULL;
|
|
}
|
|
|
|
cc = char_get_class(name, errp);
|
|
if (cc == NULL) {
|
|
goto out;
|
|
}
|
|
|
|
if (qemu_opt_get_bool(opts, "mux", 0)) {
|
|
bid = g_strdup_printf("%s-base", id);
|
|
}
|
|
|
|
chr = qemu_chardev_new(bid ? bid : id,
|
|
object_class_get_name(OBJECT_CLASS(cc)),
|
|
backend, context, errp);
|
|
|
|
if (chr == NULL) {
|
|
goto out;
|
|
}
|
|
|
|
if (bid) {
|
|
Chardev *mux;
|
|
qapi_free_ChardevBackend(backend);
|
|
backend = g_new0(ChardevBackend, 1);
|
|
backend->type = CHARDEV_BACKEND_KIND_MUX;
|
|
backend->u.mux.data = g_new0(ChardevMux, 1);
|
|
backend->u.mux.data->chardev = g_strdup(bid);
|
|
mux = qemu_chardev_new(id, TYPE_CHARDEV_MUX, backend, context, errp);
|
|
if (mux == NULL) {
|
|
object_unparent(OBJECT(chr));
|
|
chr = NULL;
|
|
goto out;
|
|
}
|
|
chr = mux;
|
|
}
|
|
|
|
out:
|
|
qapi_free_ChardevBackend(backend);
|
|
g_free(bid);
|
|
return chr;
|
|
}
|
|
|
|
Chardev *qemu_chr_new_noreplay(const char *label, const char *filename,
|
|
bool permit_mux_mon, GMainContext *context)
|
|
{
|
|
const char *p;
|
|
Chardev *chr;
|
|
QemuOpts *opts;
|
|
Error *err = NULL;
|
|
|
|
if (strstart(filename, "chardev:", &p)) {
|
|
return qemu_chr_find(p);
|
|
}
|
|
|
|
opts = qemu_chr_parse_compat(label, filename, permit_mux_mon);
|
|
if (!opts)
|
|
return NULL;
|
|
|
|
chr = qemu_chr_new_from_opts(opts, context, &err);
|
|
if (!chr) {
|
|
error_report_err(err);
|
|
goto out;
|
|
}
|
|
|
|
if (qemu_opt_get_bool(opts, "mux", 0)) {
|
|
assert(permit_mux_mon);
|
|
monitor_init(chr, MONITOR_USE_READLINE);
|
|
}
|
|
|
|
out:
|
|
qemu_opts_del(opts);
|
|
return chr;
|
|
}
|
|
|
|
static Chardev *qemu_chr_new_permit_mux_mon(const char *label,
|
|
const char *filename,
|
|
bool permit_mux_mon,
|
|
GMainContext *context)
|
|
{
|
|
Chardev *chr;
|
|
chr = qemu_chr_new_noreplay(label, filename, permit_mux_mon, context);
|
|
if (chr) {
|
|
if (replay_mode != REPLAY_MODE_NONE) {
|
|
qemu_chr_set_feature(chr, QEMU_CHAR_FEATURE_REPLAY);
|
|
}
|
|
if (qemu_chr_replay(chr) && CHARDEV_GET_CLASS(chr)->chr_ioctl) {
|
|
error_report("Replay: ioctl is not supported "
|
|
"for serial devices yet");
|
|
}
|
|
replay_register_char_driver(chr);
|
|
}
|
|
return chr;
|
|
}
|
|
|
|
Chardev *qemu_chr_new(const char *label, const char *filename,
|
|
GMainContext *context)
|
|
{
|
|
return qemu_chr_new_permit_mux_mon(label, filename, false, context);
|
|
}
|
|
|
|
Chardev *qemu_chr_new_mux_mon(const char *label, const char *filename,
|
|
GMainContext *context)
|
|
{
|
|
return qemu_chr_new_permit_mux_mon(label, filename, true, context);
|
|
}
|
|
|
|
static int qmp_query_chardev_foreach(Object *obj, void *data)
|
|
{
|
|
Chardev *chr = CHARDEV(obj);
|
|
ChardevInfoList **list = data;
|
|
ChardevInfoList *info = g_malloc0(sizeof(*info));
|
|
|
|
info->value = g_malloc0(sizeof(*info->value));
|
|
info->value->label = g_strdup(chr->label);
|
|
info->value->filename = g_strdup(chr->filename);
|
|
info->value->frontend_open = chr->be && chr->be->fe_open;
|
|
|
|
info->next = *list;
|
|
*list = info;
|
|
|
|
return 0;
|
|
}
|
|
|
|
ChardevInfoList *qmp_query_chardev(Error **errp)
|
|
{
|
|
ChardevInfoList *chr_list = NULL;
|
|
|
|
object_child_foreach(get_chardevs_root(),
|
|
qmp_query_chardev_foreach, &chr_list);
|
|
|
|
return chr_list;
|
|
}
|
|
|
|
static void
|
|
qmp_prepend_backend(const char *name, void *opaque)
|
|
{
|
|
ChardevBackendInfoList **list = opaque;
|
|
ChardevBackendInfoList *info = g_malloc0(sizeof(*info));
|
|
|
|
info->value = g_malloc0(sizeof(*info->value));
|
|
info->value->name = g_strdup(name);
|
|
info->next = *list;
|
|
*list = info;
|
|
}
|
|
|
|
ChardevBackendInfoList *qmp_query_chardev_backends(Error **errp)
|
|
{
|
|
ChardevBackendInfoList *backend_list = NULL;
|
|
|
|
chardev_name_foreach(qmp_prepend_backend, &backend_list);
|
|
|
|
return backend_list;
|
|
}
|
|
|
|
Chardev *qemu_chr_find(const char *name)
|
|
{
|
|
Object *obj = object_resolve_path_component(get_chardevs_root(), name);
|
|
|
|
return obj ? CHARDEV(obj) : NULL;
|
|
}
|
|
|
|
QemuOptsList qemu_chardev_opts = {
|
|
.name = "chardev",
|
|
.implied_opt_name = "backend",
|
|
.head = QTAILQ_HEAD_INITIALIZER(qemu_chardev_opts.head),
|
|
.desc = {
|
|
{
|
|
.name = "backend",
|
|
.type = QEMU_OPT_STRING,
|
|
},{
|
|
.name = "path",
|
|
.type = QEMU_OPT_STRING,
|
|
},{
|
|
.name = "host",
|
|
.type = QEMU_OPT_STRING,
|
|
},{
|
|
.name = "port",
|
|
.type = QEMU_OPT_STRING,
|
|
},{
|
|
.name = "fd",
|
|
.type = QEMU_OPT_STRING,
|
|
},{
|
|
.name = "localaddr",
|
|
.type = QEMU_OPT_STRING,
|
|
},{
|
|
.name = "localport",
|
|
.type = QEMU_OPT_STRING,
|
|
},{
|
|
.name = "to",
|
|
.type = QEMU_OPT_NUMBER,
|
|
},{
|
|
.name = "ipv4",
|
|
.type = QEMU_OPT_BOOL,
|
|
},{
|
|
.name = "ipv6",
|
|
.type = QEMU_OPT_BOOL,
|
|
},{
|
|
.name = "wait",
|
|
.type = QEMU_OPT_BOOL,
|
|
},{
|
|
.name = "server",
|
|
.type = QEMU_OPT_BOOL,
|
|
},{
|
|
.name = "delay",
|
|
.type = QEMU_OPT_BOOL,
|
|
},{
|
|
.name = "reconnect",
|
|
.type = QEMU_OPT_NUMBER,
|
|
},{
|
|
.name = "telnet",
|
|
.type = QEMU_OPT_BOOL,
|
|
},{
|
|
.name = "tn3270",
|
|
.type = QEMU_OPT_BOOL,
|
|
},{
|
|
.name = "tls-creds",
|
|
.type = QEMU_OPT_STRING,
|
|
},{
|
|
.name = "tls-authz",
|
|
.type = QEMU_OPT_STRING,
|
|
},{
|
|
.name = "websocket",
|
|
.type = QEMU_OPT_BOOL,
|
|
},{
|
|
.name = "width",
|
|
.type = QEMU_OPT_NUMBER,
|
|
},{
|
|
.name = "height",
|
|
.type = QEMU_OPT_NUMBER,
|
|
},{
|
|
.name = "cols",
|
|
.type = QEMU_OPT_NUMBER,
|
|
},{
|
|
.name = "rows",
|
|
.type = QEMU_OPT_NUMBER,
|
|
},{
|
|
.name = "mux",
|
|
.type = QEMU_OPT_BOOL,
|
|
},{
|
|
.name = "signal",
|
|
.type = QEMU_OPT_BOOL,
|
|
},{
|
|
.name = "name",
|
|
.type = QEMU_OPT_STRING,
|
|
},{
|
|
.name = "debug",
|
|
.type = QEMU_OPT_NUMBER,
|
|
},{
|
|
.name = "size",
|
|
.type = QEMU_OPT_SIZE,
|
|
},{
|
|
.name = "chardev",
|
|
.type = QEMU_OPT_STRING,
|
|
},{
|
|
.name = "append",
|
|
.type = QEMU_OPT_BOOL,
|
|
},{
|
|
.name = "logfile",
|
|
.type = QEMU_OPT_STRING,
|
|
},{
|
|
.name = "logappend",
|
|
.type = QEMU_OPT_BOOL,
|
|
},
|
|
{ /* end of list */ }
|
|
},
|
|
};
|
|
|
|
bool qemu_chr_has_feature(Chardev *chr,
|
|
ChardevFeature feature)
|
|
{
|
|
return test_bit(feature, chr->features);
|
|
}
|
|
|
|
void qemu_chr_set_feature(Chardev *chr,
|
|
ChardevFeature feature)
|
|
{
|
|
return set_bit(feature, chr->features);
|
|
}
|
|
|
|
Chardev *qemu_chardev_new(const char *id, const char *typename,
|
|
ChardevBackend *backend,
|
|
GMainContext *gcontext,
|
|
Error **errp)
|
|
{
|
|
Object *obj;
|
|
Chardev *chr = NULL;
|
|
Error *local_err = NULL;
|
|
bool be_opened = true;
|
|
|
|
assert(g_str_has_prefix(typename, "chardev-"));
|
|
|
|
obj = object_new(typename);
|
|
chr = CHARDEV(obj);
|
|
chr->label = g_strdup(id);
|
|
chr->gcontext = gcontext;
|
|
|
|
qemu_char_open(chr, backend, &be_opened, &local_err);
|
|
if (local_err) {
|
|
goto end;
|
|
}
|
|
|
|
if (!chr->filename) {
|
|
chr->filename = g_strdup(typename + 8);
|
|
}
|
|
if (be_opened) {
|
|
qemu_chr_be_event(chr, CHR_EVENT_OPENED);
|
|
}
|
|
|
|
if (id) {
|
|
object_property_add_child(get_chardevs_root(), id, obj, &local_err);
|
|
if (local_err) {
|
|
goto end;
|
|
}
|
|
object_unref(obj);
|
|
}
|
|
|
|
end:
|
|
if (local_err) {
|
|
error_propagate(errp, local_err);
|
|
object_unref(obj);
|
|
return NULL;
|
|
}
|
|
|
|
return chr;
|
|
}
|
|
|
|
ChardevReturn *qmp_chardev_add(const char *id, ChardevBackend *backend,
|
|
Error **errp)
|
|
{
|
|
const ChardevClass *cc;
|
|
ChardevReturn *ret;
|
|
Chardev *chr;
|
|
|
|
cc = char_get_class(ChardevBackendKind_str(backend->type), errp);
|
|
if (!cc) {
|
|
return NULL;
|
|
}
|
|
|
|
chr = qemu_chardev_new(id, object_class_get_name(OBJECT_CLASS(cc)),
|
|
backend, NULL, errp);
|
|
if (!chr) {
|
|
return NULL;
|
|
}
|
|
|
|
ret = g_new0(ChardevReturn, 1);
|
|
if (CHARDEV_IS_PTY(chr)) {
|
|
ret->pty = g_strdup(chr->filename + 4);
|
|
ret->has_pty = true;
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
ChardevReturn *qmp_chardev_change(const char *id, ChardevBackend *backend,
|
|
Error **errp)
|
|
{
|
|
CharBackend *be;
|
|
const ChardevClass *cc;
|
|
Chardev *chr, *chr_new;
|
|
bool closed_sent = false;
|
|
ChardevReturn *ret;
|
|
|
|
chr = qemu_chr_find(id);
|
|
if (!chr) {
|
|
error_setg(errp, "Chardev '%s' does not exist", id);
|
|
return NULL;
|
|
}
|
|
|
|
if (CHARDEV_IS_MUX(chr)) {
|
|
error_setg(errp, "Mux device hotswap not supported yet");
|
|
return NULL;
|
|
}
|
|
|
|
if (qemu_chr_replay(chr)) {
|
|
error_setg(errp,
|
|
"Chardev '%s' cannot be changed in record/replay mode", id);
|
|
return NULL;
|
|
}
|
|
|
|
be = chr->be;
|
|
if (!be) {
|
|
/* easy case */
|
|
object_unparent(OBJECT(chr));
|
|
return qmp_chardev_add(id, backend, errp);
|
|
}
|
|
|
|
if (!be->chr_be_change) {
|
|
error_setg(errp, "Chardev user does not support chardev hotswap");
|
|
return NULL;
|
|
}
|
|
|
|
cc = char_get_class(ChardevBackendKind_str(backend->type), errp);
|
|
if (!cc) {
|
|
return NULL;
|
|
}
|
|
|
|
chr_new = qemu_chardev_new(NULL, object_class_get_name(OBJECT_CLASS(cc)),
|
|
backend, chr->gcontext, errp);
|
|
if (!chr_new) {
|
|
return NULL;
|
|
}
|
|
chr_new->label = g_strdup(id);
|
|
|
|
if (chr->be_open && !chr_new->be_open) {
|
|
qemu_chr_be_event(chr, CHR_EVENT_CLOSED);
|
|
closed_sent = true;
|
|
}
|
|
|
|
chr->be = NULL;
|
|
qemu_chr_fe_init(be, chr_new, &error_abort);
|
|
|
|
if (be->chr_be_change(be->opaque) < 0) {
|
|
error_setg(errp, "Chardev '%s' change failed", chr_new->label);
|
|
chr_new->be = NULL;
|
|
qemu_chr_fe_init(be, chr, &error_abort);
|
|
if (closed_sent) {
|
|
qemu_chr_be_event(chr, CHR_EVENT_OPENED);
|
|
}
|
|
object_unref(OBJECT(chr_new));
|
|
return NULL;
|
|
}
|
|
|
|
object_unparent(OBJECT(chr));
|
|
object_property_add_child(get_chardevs_root(), chr_new->label,
|
|
OBJECT(chr_new), &error_abort);
|
|
object_unref(OBJECT(chr_new));
|
|
|
|
ret = g_new0(ChardevReturn, 1);
|
|
if (CHARDEV_IS_PTY(chr_new)) {
|
|
ret->pty = g_strdup(chr_new->filename + 4);
|
|
ret->has_pty = true;
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
void qmp_chardev_remove(const char *id, Error **errp)
|
|
{
|
|
Chardev *chr;
|
|
|
|
chr = qemu_chr_find(id);
|
|
if (chr == NULL) {
|
|
error_setg(errp, "Chardev '%s' not found", id);
|
|
return;
|
|
}
|
|
if (qemu_chr_is_busy(chr)) {
|
|
error_setg(errp, "Chardev '%s' is busy", id);
|
|
return;
|
|
}
|
|
if (qemu_chr_replay(chr)) {
|
|
error_setg(errp,
|
|
"Chardev '%s' cannot be unplugged in record/replay mode", id);
|
|
return;
|
|
}
|
|
object_unparent(OBJECT(chr));
|
|
}
|
|
|
|
void qmp_chardev_send_break(const char *id, Error **errp)
|
|
{
|
|
Chardev *chr;
|
|
|
|
chr = qemu_chr_find(id);
|
|
if (chr == NULL) {
|
|
error_setg(errp, "Chardev '%s' not found", id);
|
|
return;
|
|
}
|
|
qemu_chr_be_event(chr, CHR_EVENT_BREAK);
|
|
}
|
|
|
|
/*
|
|
* Add a timeout callback for the chardev (in milliseconds), return
|
|
* the GSource object created. Please use this to add timeout hook for
|
|
* chardev instead of g_timeout_add() and g_timeout_add_seconds(), to
|
|
* make sure the gcontext that the task bound to is correct.
|
|
*/
|
|
GSource *qemu_chr_timeout_add_ms(Chardev *chr, guint ms,
|
|
GSourceFunc func, void *private)
|
|
{
|
|
GSource *source = g_timeout_source_new(ms);
|
|
|
|
assert(func);
|
|
g_source_set_callback(source, func, private, NULL);
|
|
g_source_attach(source, chr->gcontext);
|
|
|
|
return source;
|
|
}
|
|
|
|
void qemu_chr_cleanup(void)
|
|
{
|
|
object_unparent(get_chardevs_root());
|
|
}
|
|
|
|
static void register_types(void)
|
|
{
|
|
type_register_static(&char_type_info);
|
|
|
|
/* this must be done after machine init, since we register FEs with muxes
|
|
* as part of realize functions like serial_isa_realizefn when -nographic
|
|
* is specified
|
|
*/
|
|
qemu_add_machine_init_done_notifier(&chardev_machine_done_notify);
|
|
}
|
|
|
|
type_init(register_types);
|