diff options
41 files changed, 2176 insertions, 179 deletions
diff --git a/MAINTAINERS b/MAINTAINERS index 75e0f2d750..a77f246569 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -2525,6 +2525,7 @@ Benchmark util M: Vladimir Sementsov-Ogievskiy <vsementsov@virtuozzo.com> S: Maintained F: scripts/simplebench/ +T: git https://src.openvz.org/scm/~vsementsov/qemu.git simplebench Transactions helper M: Vladimir Sementsov-Ogievskiy <vsementsov@virtuozzo.com> diff --git a/chardev/char.c b/chardev/char.c index a4ebfcc5ac..d959eec522 100644 --- a/chardev/char.c +++ b/chardev/char.c @@ -931,6 +931,12 @@ QemuOptsList qemu_chardev_opts = { },{ .name = "logappend", .type = QEMU_OPT_BOOL, + },{ + .name = "mouse", + .type = QEMU_OPT_BOOL, + },{ + .name = "clipboard", + .type = QEMU_OPT_BOOL, #ifdef CONFIG_LINUX },{ .name = "tight", diff --git a/configure b/configure index 9470fff09a..676239c697 100755 --- a/configure +++ b/configure @@ -389,6 +389,7 @@ qom_cast_debug="yes" trace_backends="log" trace_file="trace" spice="$default_feature" +spice_protocol="auto" rbd="auto" smartcard="$default_feature" u2f="auto" @@ -1132,7 +1133,15 @@ for opt do ;; --disable-spice) spice="no" ;; - --enable-spice) spice="yes" + --enable-spice) + spice_protocol="yes" + spice="yes" + ;; + --disable-spice-protocol) + spice_protocol="no" + spice="no" + ;; + --enable-spice-protocol) spice_protocol="yes" ;; --disable-libiscsi) libiscsi="disabled" ;; @@ -1870,6 +1879,7 @@ disabled with --disable-FEATURE, default is enabled if available vhost-user-blk-server vhost-user-blk server support vhost-vdpa vhost-vdpa kernel backend support spice spice + spice-protocol spice-protocol rbd rados block device (rbd) libiscsi iscsi support libnfs nfs support @@ -4153,6 +4163,19 @@ fi ########################################## # spice probe +if test "$spice_protocol" != "no" ; then + spice_protocol_cflags=$($pkg_config --cflags spice-protocol 2>/dev/null) + if $pkg_config --atleast-version=0.12.3 spice-protocol; then + spice_protocol="yes" + else + if test "$spice_protocol" = "yes" ; then + feature_not_found "spice_protocol" \ + "Install spice-protocol(>=0.12.3) devel" + fi + spice_protocol="no" + fi +fi + if test "$spice" != "no" ; then cat > $TMPC << EOF #include <spice.h> @@ -4161,13 +4184,13 @@ EOF spice_cflags=$($pkg_config --cflags spice-protocol spice-server 2>/dev/null) spice_libs=$($pkg_config --libs spice-protocol spice-server 2>/dev/null) if $pkg_config --atleast-version=0.12.5 spice-server && \ - $pkg_config --atleast-version=0.12.3 spice-protocol && \ + test "$spice_protocol" = "yes" && \ compile_prog "$spice_cflags" "$spice_libs" ; then spice="yes" else if test "$spice" = "yes" ; then feature_not_found "spice" \ - "Install spice-server(>=0.12.5) and spice-protocol(>=0.12.3) devel" + "Install spice-server(>=0.12.5) devel" fi spice="no" fi @@ -5836,9 +5859,14 @@ fi if test "$posix_memalign" = "yes" ; then echo "CONFIG_POSIX_MEMALIGN=y" >> $config_host_mak fi + +if test "$spice_protocol" = "yes" ; then + echo "CONFIG_SPICE_PROTOCOL=y" >> $config_host_mak + echo "SPICE_PROTOCOL_CFLAGS=$spice_protocol_cflags" >> $config_host_mak +fi if test "$spice" = "yes" ; then echo "CONFIG_SPICE=y" >> $config_host_mak - echo "SPICE_CFLAGS=$spice_cflags" >> $config_host_mak + echo "SPICE_CFLAGS=$spice_cflags $spice_protocol_cflags" >> $config_host_mak echo "SPICE_LIBS=$spice_libs" >> $config_host_mak fi diff --git a/docs/devel/index.rst b/docs/devel/index.rst index 6cf7e2d233..cbdbb90491 100644 --- a/docs/devel/index.rst +++ b/docs/devel/index.rst @@ -36,6 +36,7 @@ Contents: multi-thread-tcg tcg-plugins bitops + ui reset s390-dasd-ipl clocks diff --git a/docs/devel/ui.rst b/docs/devel/ui.rst new file mode 100644 index 0000000000..06c7d622ce --- /dev/null +++ b/docs/devel/ui.rst @@ -0,0 +1,8 @@ +================= +Qemu UI subsystem +================= + +Qemu Clipboard +-------------- + +.. kernel-doc:: include/ui/clipboard.h diff --git a/include/ui/clipboard.h b/include/ui/clipboard.h new file mode 100644 index 0000000000..e5bcb365ed --- /dev/null +++ b/include/ui/clipboard.h @@ -0,0 +1,193 @@ +#ifndef QEMU_CLIPBOARD_H +#define QEMU_CLIPBOARD_H + +#include "qemu/notify.h" + +/** + * DOC: Introduction + * + * The header ``ui/clipboard.h`` declares the qemu clipboard interface. + * + * All qemu elements which want use the clipboard can register as + * clipboard peer. Subsequently they can set the clipboard content + * and get notifications for clipboard updates. + * + * Typical users are user interfaces (gtk), remote access protocols + * (vnc) and devices talking to the guest (vdagent). + * + * Even though the design allows different data types only plain text + * is supported for now. + */ + +typedef enum QemuClipboardType QemuClipboardType; +typedef enum QemuClipboardSelection QemuClipboardSelection; +typedef struct QemuClipboardPeer QemuClipboardPeer; +typedef struct QemuClipboardInfo QemuClipboardInfo; + +/** + * enum QemuClipboardType + * + * @QEMU_CLIPBOARD_TYPE_TEXT: text/plain; charset=utf-8 + * @QEMU_CLIPBOARD_TYPE__COUNT: type count. + */ +enum QemuClipboardType { + QEMU_CLIPBOARD_TYPE_TEXT, + QEMU_CLIPBOARD_TYPE__COUNT, +}; + +/* same as VD_AGENT_CLIPBOARD_SELECTION_* */ +/** + * enum QemuClipboardSelection + * + * @QEMU_CLIPBOARD_SELECTION_CLIPBOARD: clipboard (explitcit cut+paste). + * @QEMU_CLIPBOARD_SELECTION_PRIMARY: primary selection (select + middle mouse button). + * @QEMU_CLIPBOARD_SELECTION_SECONDARY: secondary selection (dunno). + * @QEMU_CLIPBOARD_SELECTION__COUNT: selection count. + */ +enum QemuClipboardSelection { + QEMU_CLIPBOARD_SELECTION_CLIPBOARD, + QEMU_CLIPBOARD_SELECTION_PRIMARY, + QEMU_CLIPBOARD_SELECTION_SECONDARY, + QEMU_CLIPBOARD_SELECTION__COUNT, +}; + +/** + * struct QemuClipboardPeer + * + * @name: peer name. + * @update: notifier for clipboard updates. + * @request: callback for clipboard data requests. + * + * Clipboard peer description. + */ +struct QemuClipboardPeer { + const char *name; + Notifier update; + void (*request)(QemuClipboardInfo *info, + QemuClipboardType type); +}; + +/** + * struct QemuClipboardInfo + * + * @refcount: reference counter. + * @owner: clipboard owner. + * @selection: clipboard selection. + * @types: clipboard data array (one entry per type). + * + * Clipboard content data and metadata. + */ +struct QemuClipboardInfo { + uint32_t refcount; + QemuClipboardPeer *owner; + QemuClipboardSelection selection; + struct { + bool available; + bool requested; + size_t size; + void *data; + } types[QEMU_CLIPBOARD_TYPE__COUNT]; +}; + +/** + * qemu_clipboard_peer_register + * + * @peer: peer information. + * + * Register clipboard peer. Registering is needed for both active + * (set+grab clipboard) and passive (watch clipboard for updates) + * interaction with the qemu clipboard. + */ +void qemu_clipboard_peer_register(QemuClipboardPeer *peer); + +/** + * qemu_clipboard_peer_unregister + * + * @peer: peer information. + * + * Unregister clipboard peer. + */ +void qemu_clipboard_peer_unregister(QemuClipboardPeer *peer); + +/** + * qemu_clipboard_info_new + * + * @owner: clipboard owner. + * @selection: clipboard selection. + * + * Allocate a new QemuClipboardInfo and initialize it with the given + * @owner and @selection. + * + * QemuClipboardInfo is a reference-counted struct. The new struct is + * returned with a reference already taken (i.e. reference count is + * one). + */ +QemuClipboardInfo *qemu_clipboard_info_new(QemuClipboardPeer *owner, + QemuClipboardSelection selection); +/** + * qemu_clipboard_info_ref + * + * @info: clipboard info. + * + * Increase @info reference count. + */ +QemuClipboardInfo *qemu_clipboard_info_ref(QemuClipboardInfo *info); + +/** + * qemu_clipboard_info_unref + * + * @info: clipboard info. + * + * Decrease @info reference count. When the count goes down to zero + * free the @info struct itself and all clipboard data. + */ +void qemu_clipboard_info_unref(QemuClipboardInfo *info); + +/** + * qemu_clipboard_update + * + * @info: clipboard info. + * + * Update the qemu clipboard. Notify all registered peers (including + * the clipboard owner) that the qemu clipboard has been updated. + * + * This is used for both new completely clipboard content and for + * clipboard data updates in response to qemu_clipboard_request() + * calls. + */ +void qemu_clipboard_update(QemuClipboardInfo *info); + +/** + * qemu_clipboard_request + * + * @info: clipboard info. + * @type: clipboard data type. + * + * Request clipboard content. Typically the clipboard owner only + * advertises the available data types and provides the actual data + * only on request. + */ +void qemu_clipboard_request(QemuClipboardInfo *info, + QemuClipboardType type); + +/** + * qemu_clipboard_set_data + * + * @peer: clipboard peer. + * @info: clipboard info. + * @type: clipboard data type. + * @size: data size. + * @data: data blob. + * @update: notify peers about the update. + * + * Set clipboard content for the given @type. This function will make + * a copy of the content data and store that. + */ +void qemu_clipboard_set_data(QemuClipboardPeer *peer, + QemuClipboardInfo *info, + QemuClipboardType type, + uint32_t size, + void *data, + bool update); + +#endif /* QEMU_CLIPBOARD_H */ diff --git a/include/ui/gtk.h b/include/ui/gtk.h index 5ae0ad60a6..9516670ebc 100644 --- a/include/ui/gtk.h +++ b/include/ui/gtk.h @@ -18,12 +18,16 @@ #include <gdk/gdkwayland.h> #endif +#include "ui/clipboard.h" +#include "ui/console.h" #include "ui/kbd-state.h" #if defined(CONFIG_OPENGL) #include "ui/egl-helpers.h" #include "ui/egl-context.h" #endif +#define MAX_VCS 10 + typedef struct GtkDisplayState GtkDisplayState; typedef struct VirtualGfxConsole { @@ -83,6 +87,66 @@ typedef struct VirtualConsole { }; } VirtualConsole; +struct GtkDisplayState { + GtkWidget *window; + + GtkWidget *menu_bar; + + GtkAccelGroup *accel_group; + + GtkWidget *machine_menu_item; + GtkWidget *machine_menu; + GtkWidget *pause_item; + GtkWidget *reset_item; + GtkWidget *powerdown_item; + GtkWidget *quit_item; + + GtkWidget *view_menu_item; + GtkWidget *view_menu; + GtkWidget *full_screen_item; + GtkWidget *copy_item; + GtkWidget *zoom_in_item; + GtkWidget *zoom_out_item; + GtkWidget *zoom_fixed_item; + GtkWidget *zoom_fit_item; + GtkWidget *grab_item; + GtkWidget *grab_on_hover_item; + + int nb_vcs; + VirtualConsole vc[MAX_VCS]; + + GtkWidget *show_tabs_item; + GtkWidget *untabify_item; + GtkWidget *show_menubar_item; + + GtkWidget *vbox; + GtkWidget *notebook; + int button_mask; + gboolean last_set; + int last_x; + int last_y; + int grab_x_root; + int grab_y_root; + VirtualConsole *kbd_owner; + VirtualConsole *ptr_owner; + + gboolean full_screen; + + GdkCursor *null_cursor; + Notifier mouse_mode_notifier; + gboolean free_scale; + + bool external_pause_update; + + QemuClipboardPeer cbpeer; + QemuClipboardInfo *cbinfo[QEMU_CLIPBOARD_SELECTION__COUNT]; + uint32_t cbpending[QEMU_CLIPBOARD_SELECTION__COUNT]; + GtkClipboard *gtkcb[QEMU_CLIPBOARD_SELECTION__COUNT]; + bool cbowner[QEMU_CLIPBOARD_SELECTION__COUNT]; + + DisplayOptions *opts; +}; + extern bool gtk_use_gl_area; /* ui/gtk.c */ @@ -150,4 +214,7 @@ void gtk_gl_area_init(void); int gd_gl_area_make_current(DisplayChangeListener *dcl, QEMUGLContext ctx); +/* gtk-clipboard.c */ +void gd_clipboard_init(GtkDisplayState *gd); + #endif /* UI_GTK_H */ diff --git a/meson.build b/meson.build index 1559e8d873..632b380738 100644 --- a/meson.build +++ b/meson.build @@ -458,11 +458,15 @@ if 'CONFIG_LIBJACK' in config_host endif spice = not_found spice_headers = not_found +spice_protocol = not_found if 'CONFIG_SPICE' in config_host spice = declare_dependency(compile_args: config_host['SPICE_CFLAGS'].split(), link_args: config_host['SPICE_LIBS'].split()) spice_headers = declare_dependency(compile_args: config_host['SPICE_CFLAGS'].split()) endif +if 'CONFIG_SPICE_PROTOCOL' in config_host + spice_protocol = declare_dependency(compile_args: config_host['SPICE_PROTOCOL_CFLAGS'].split()) +endif rt = cc.find_library('rt', required: false) libdl = not_found if 'CONFIG_PLUGIN' in config_host diff --git a/qapi/char.json b/qapi/char.json index 6413970fa7..adf2685f68 100644 --- a/qapi/char.json +++ b/qapi/char.json @@ -391,11 +391,28 @@ 'base': 'ChardevCommon' } ## +# @ChardevQemuVDAgent: +# +# Configuration info for qemu vdagent implementation. +# +# @mouse: enable/disable mouse, default is enabled. +# @clipboard: enable/disable clipboard, default is disabled. +# +# Since: 6.1 +# +## +{ 'struct': 'ChardevQemuVDAgent', + 'data': { '*mouse': 'bool', + '*clipboard': 'bool' }, + 'base': 'ChardevCommon', + 'if': 'defined(CONFIG_SPICE_PROTOCOL)' } + +## # @ChardevBackend: # # Configuration info for the new chardev backend. # -# Since: 1.4 (testdev since 2.2, wctablet since 2.9) +# Since: 1.4 (testdev since 2.2, wctablet since 2.9, vdagent since 6.1) ## { 'union': 'ChardevBackend', 'data': { 'file': 'ChardevFile', @@ -417,6 +434,8 @@ 'if': 'defined(CONFIG_SPICE)' }, 'spiceport': { 'type': 'ChardevSpicePort', 'if': 'defined(CONFIG_SPICE)' }, + 'qemu-vdagent': { 'type': 'ChardevQemuVDAgent', + 'if': 'defined(CONFIG_SPICE_PROTOCOL)' }, 'vc': 'ChardevVC', 'ringbuf': 'ChardevRingbuf', # next one is just for compatibility diff --git a/scripts/qapi/common.py b/scripts/qapi/common.py index cbd3fd81d3..6ad1eeb61d 100644 --- a/scripts/qapi/common.py +++ b/scripts/qapi/common.py @@ -12,7 +12,7 @@ # See the COPYING file in the top-level directory. import re -from typing import Optional, Sequence +from typing import Match, Optional, Sequence #: Magic string that gets removed along with all space to its right. @@ -210,3 +210,9 @@ def gen_endif(ifcond: Sequence[str]) -> str: #endif /* %(cond)s */ ''', cond=ifc) return ret + + +def must_match(pattern: str, string: str) -> Match[str]: + match = re.match(pattern, string) + assert match is not None + return match diff --git a/scripts/qapi/main.py b/scripts/qapi/main.py index 703e7ed1ed..f2ea6e0ce4 100644 --- a/scripts/qapi/main.py +++ b/scripts/qapi/main.py @@ -8,11 +8,11 @@ This is the main entry point for generating C code from the QAPI schema. """ import argparse -import re import sys from typing import Optional from .commands import gen_commands +from .common import must_match from .error import QAPIError from .events import gen_events from .introspect import gen_introspect @@ -22,9 +22,7 @@ from .visit import gen_visit def invalid_prefix_char(prefix: str) -> Optional[str]: - match = re.match(r'([A-Za-z_.-][A-Za-z0-9_.-]*)?', prefix) - # match cannot be None, but mypy cannot infer that. - assert match is not None + match = must_match(r'([A-Za-z_.-][A-Za-z0-9_.-]*)?', prefix) if match.end() != len(prefix): return prefix[match.end()] return None diff --git a/scripts/qapi/parser.py b/scripts/qapi/parser.py index ca5e8e18e0..f03ba2cfec 100644 --- a/scripts/qapi/parser.py +++ b/scripts/qapi/parser.py @@ -17,14 +17,26 @@ from collections import OrderedDict import os import re - +from typing import ( + Dict, + List, + Optional, + Set, + Union, +) + +from .common import must_match from .error import QAPISemError, QAPISourceError from .source import QAPISourceInfo +# Return value alias for get_expr(). +_ExprValue = Union[List[object], Dict[str, object], str, bool] + + class QAPIParseError(QAPISourceError): """Error class for all QAPI schema parsing errors.""" - def __init__(self, parser, msg): + def __init__(self, parser: 'QAPISchemaParser', msg: str): col = 1 for ch in parser.src[parser.line_pos:parser.pos]: if ch == '\t': @@ -35,31 +47,69 @@ class QAPIParseError(QAPISourceError): class QAPISchemaParser: + """ + Parse QAPI schema source. - def __init__(self, fname, previously_included=None, incl_info=None): - previously_included = previously_included or set() - previously_included.add(os.path.abspath(fname)) + Parse a JSON-esque schema file and process directives. See + qapi-code-gen.txt section "Schema Syntax" for the exact syntax. + Grammatical validation is handled later by `expr.check_exprs()`. - try: - fp = open(fname, 'r', encoding='utf-8') - self.src = fp.read() - except IOError as e: - raise QAPISemError(incl_info or QAPISourceInfo(None, None, None), - "can't read %s file '%s': %s" - % ("include" if incl_info else "schema", - fname, - e.strerror)) + :param fname: Source file name. + :param previously_included: + The absolute names of previously included source files, + if being invoked from another parser. + :param incl_info: + `QAPISourceInfo` belonging to the parent module. + ``None`` implies this is the root module. - if self.src == '' or self.src[-1] != '\n': - self.src += '\n' + :ivar exprs: Resulting parsed expressions. + :ivar docs: Resulting parsed documentation blocks. + + :raise OSError: For problems reading the root schema document. + :raise QAPIError: For errors in the schema source. + """ + def __init__(self, + fname: str, + previously_included: Optional[Set[str]] = None, + incl_info: Optional[QAPISourceInfo] = None): + self._fname = fname + self._included = previously_included or set() + self._included.add(os.path.abspath(self._fname)) + self.src = '' + + # Lexer state (see `accept` for details): + self.info = QAPISourceInfo(self._fname, incl_info) + self.tok: Union[None, str] = None + self.pos = 0 self.cursor = 0 - self.info = QAPISourceInfo(fname, 1, incl_info) + self.val: Optional[Union[bool, str]] = None self.line_pos = 0 - self.exprs = [] - self.docs = [] - self.accept() + + # Parser output: + self.exprs: List[Dict[str, object]] = [] + self.docs: List[QAPIDoc] = [] + + # Showtime! + self._parse() + + def _parse(self) -> None: + """ + Parse the QAPI schema document. + + :return: None. Results are stored in ``.exprs`` and ``.docs``. + """ cur_doc = None + # May raise OSError; allow the caller to handle it. + with open(self._fname, 'r', encoding='utf-8') as fp: + self.src = fp.read() + if self.src == '' or self.src[-1] != '\n': + self.src += '\n' + + # Prime the lexer: + self.accept() + + # Parse until done: while self.tok is not None: info = self.info if self.tok == '#': @@ -68,7 +118,11 @@ class QAPISchemaParser: self.docs.append(cur_doc) continue - expr = self.get_expr(False) + expr = self.get_expr() + if not isinstance(expr, dict): + raise QAPISemError( + info, "top-level expression must be an object") + if 'include' in expr: self.reject_expr_doc(cur_doc) if len(expr) != 1: @@ -77,12 +131,12 @@ class QAPISchemaParser: if not isinstance(include, str): raise QAPISemError(info, "value of 'include' must be a string") - incl_fname = os.path.join(os.path.dirname(fname), + incl_fname = os.path.join(os.path.dirname(self._fname), include) self.exprs.append({'expr': {'include': incl_fname}, 'info': info}) exprs_include = self._include(include, info, incl_fname, - previously_included) + self._included) if exprs_include: self.exprs.extend(exprs_include.exprs) self.docs.extend(exprs_include.docs) @@ -109,17 +163,22 @@ class QAPISchemaParser: self.reject_expr_doc(cur_doc) @staticmethod - def reject_expr_doc(doc): + def reject_expr_doc(doc: Optional['QAPIDoc']) -> None: if doc and doc.symbol: raise QAPISemError( doc.info, "documentation for '%s' is not followed by the definition" % doc.symbol) - def _include(self, include, info, incl_fname, previously_included): + @staticmethod + def _include(include: str, + info: QAPISourceInfo, + incl_fname: str, + previously_included: Set[str] + ) -> Optional['QAPISchemaParser']: incl_abs_fname = os.path.abspath(incl_fname) # catch inclusion cycle - inf = info + inf: Optional[QAPISourceInfo] = info while inf: if incl_abs_fname == os.path.abspath(inf.fname): raise QAPISemError(info, "inclusion loop for %s" % include) @@ -129,34 +188,86 @@ class QAPISchemaParser: if incl_abs_fname in previously_included: return None - return QAPISchemaParser(incl_fname, previously_included, info) - - def _check_pragma_list_of_str(self, name, value, info): - if (not isinstance(value, list) - or any([not isinstance(elt, str) for elt in value])): + try: + return QAPISchemaParser(incl_fname, previously_included, info) + except OSError as err: raise QAPISemError( info, - "pragma %s must be a list of strings" % name) + f"can't read include file '{incl_fname}': {err.strerror}" + ) from err + + @staticmethod + def _pragma(name: str, value: object, info: QAPISourceInfo) -> None: + + def check_list_str(name: str, value: object) -> List[str]: + if (not isinstance(value, list) or + any(not isinstance(elt, str) for elt in value)): + raise QAPISemError( + info, + "pragma %s must be a list of strings" % name) + return value + + pragma = info.pragma - def _pragma(self, name, value, info): if name == 'doc-required': if not isinstance(value, bool): raise QAPISemError(info, "pragma 'doc-required' must be boolean") - info.pragma.doc_required = value + pragma.doc_required = value elif name == 'command-name-exceptions': - self._check_pragma_list_of_str(name, value, info) - info.pragma.command_name_exceptions = value + pragma.command_name_exceptions = check_list_str(name, value) elif name == 'command-returns-exceptions': - self._check_pragma_list_of_str(name, value, info) - info.pragma.command_returns_exceptions = value + pragma.command_returns_exceptions = check_list_str(name, value) elif name == 'member-name-exceptions': - self._check_pragma_list_of_str(name, value, info) - info.pragma.member_name_exceptions = value + pragma.member_name_exceptions = check_list_str(name, value) else: raise QAPISemError(info, "unknown pragma '%s'" % name) - def accept(self, skip_comment=True): + def accept(self, skip_comment: bool = True) -> None: + """ + Read and store the next token. + + :param skip_comment: + When false, return COMMENT tokens ("#"). + This is used when reading documentation blocks. + + :return: + None. Several instance attributes are updated instead: + + - ``.tok`` represents the token type. See below for values. + - ``.info`` describes the token's source location. + - ``.val`` is the token's value, if any. See below. + - ``.pos`` is the buffer index of the first character of + the token. + + * Single-character tokens: + + These are "{", "}", ":", ",", "[", and "]". + ``.tok`` holds the single character and ``.val`` is None. + + * Multi-character tokens: + + * COMMENT: + + This token is not normally returned by the lexer, but it can + be when ``skip_comment`` is False. ``.tok`` is "#", and + ``.val`` is a string including all chars until end-of-line, + including the "#" itself. + + * STRING: + + ``.tok`` is "'", the single quote. ``.val`` contains the + string, excluding the surrounding quotes. + + * TRUE and FALSE: + + ``.tok`` is either "t" or "f", ``.val`` will be the + corresponding bool value. + + * EOF: + + ``.tok`` and ``.val`` will both be None at EOF. + """ while True: self.tok = self.src[self.cursor] self.pos = self.cursor @@ -216,12 +327,12 @@ class QAPISchemaParser: elif not self.tok.isspace(): # Show up to next structural, whitespace or quote # character - match = re.match('[^[\\]{}:,\\s\'"]+', - self.src[self.cursor-1:]) + match = must_match('[^[\\]{}:,\\s\'"]+', + self.src[self.cursor-1:]) raise QAPIParseError(self, "stray '%s'" % match.group(0)) - def get_members(self): - expr = OrderedDict() + def get_members(self) -> Dict[str, object]: + expr: Dict[str, object] = OrderedDict() if self.tok == '}': self.accept() return expr @@ -229,13 +340,15 @@ class QAPISchemaParser: raise QAPIParseError(self, "expected string or '}'") while True: key = self.val + assert isinstance(key, str) # Guaranteed by tok == "'" + self.accept() if self.tok != ':': raise QAPIParseError(self, "expected ':'") self.accept() if key in expr: raise QAPIParseError(self, "duplicate key '%s'" % key) - expr[key] = self.get_expr(True) + expr[key] = self.get_expr() if self.tok == '}': self.accept() return expr @@ -245,16 +358,16 @@ class QAPISchemaParser: if self.tok != "'": raise QAPIParseError(self, "expected string") - def get_values(self): - expr = [] + def get_values(self) -> List[object]: + expr: List[object] = [] if self.tok == ']': self.accept() return expr - if self.tok not in "{['tf": + if self.tok not in tuple("{['tf"): raise QAPIParseError( self, "expected '{', '[', ']', string, or boolean") while True: - expr.append(self.get_expr(True)) + expr.append(self.get_expr()) if self.tok == ']': self.accept() return expr @@ -262,16 +375,16 @@ class QAPISchemaParser: raise QAPIParseError(self, "expected ',' or ']'") self.accept() - def get_expr(self, nested): - if self.tok != '{' and not nested: - raise QAPIParseError(self, "expected '{'") + def get_expr(self) -> _ExprValue: + expr: _ExprValue if self.tok == '{': self.accept() expr = self.get_members() elif self.tok == '[': self.accept() expr = self.get_values() - elif self.tok in "'tf": + elif self.tok in tuple("'tf"): + assert isinstance(self.val, (str, bool)) expr = self.val self.accept() else: @@ -279,7 +392,7 @@ class QAPISchemaParser: self, "expected '{', '[', string, or boolean") return expr - def get_doc(self, info): + def get_doc(self, info: QAPISourceInfo) -> List['QAPIDoc']: if self.val != '##': raise QAPIParseError( self, "junk after '##' at start of documentation comment") @@ -288,6 +401,7 @@ class QAPISchemaParser: cur_doc = QAPIDoc(self, info) self.accept(False) while self.tok == '#': + assert isinstance(self.val, str) if self.val.startswith('##'): # End of doc comment if self.val != '##': @@ -346,7 +460,7 @@ class QAPIDoc: # Strip leading spaces corresponding to the expected indent level # Blank lines are always OK. if line: - indent = re.match(r'\s*', line).end() + indent = must_match(r'\s*', line).end() if indent < self._indent: raise QAPIParseError( self._parser, @@ -482,7 +596,7 @@ class QAPIDoc: # from line and replace it with spaces so that 'f' has the # same index as it did in the original line and can be # handled the same way we will handle following lines. - indent = re.match(r'@\S*:\s*', line).end() + indent = must_match(r'@\S*:\s*', line).end() line = line[indent:] if not line: # Line was just the "@arg:" header; following lines @@ -517,7 +631,7 @@ class QAPIDoc: # from line and replace it with spaces so that 'f' has the # same index as it did in the original line and can be # handled the same way we will handle following lines. - indent = re.match(r'@\S*:\s*', line).end() + indent = must_match(r'@\S*:\s*', line).end() line = line[indent:] if not line: # Line was just the "@arg:" header; following lines @@ -563,7 +677,7 @@ class QAPIDoc: # from line and replace it with spaces so that 'f' has the # same index as it did in the original line and can be # handled the same way we will handle following lines. - indent = re.match(r'\S*:\s*', line).end() + indent = must_match(r'\S*:\s*', line).end() line = line[indent:] if not line: # Line was just the "Section:" header; following lines diff --git a/scripts/qapi/pylintrc b/scripts/qapi/pylintrc index 88efbf71cb..c5275d5f59 100644 --- a/scripts/qapi/pylintrc +++ b/scripts/qapi/pylintrc @@ -43,6 +43,7 @@ good-names=i, _, fp, # fp = open(...) fd, # fd = os.open(...) + ch, [VARIABLES] diff --git a/scripts/qapi/schema.py b/scripts/qapi/schema.py index 3a4172fb74..d1d27ff7ee 100644 --- a/scripts/qapi/schema.py +++ b/scripts/qapi/schema.py @@ -20,7 +20,7 @@ import re from typing import Optional from .common import POINTER_SUFFIX, c_name -from .error import QAPISemError, QAPISourceError +from .error import QAPIError, QAPISemError, QAPISourceError from .expr import check_exprs from .parser import QAPISchemaParser @@ -849,7 +849,14 @@ class QAPISchemaEvent(QAPISchemaEntity): class QAPISchema: def __init__(self, fname): self.fname = fname - parser = QAPISchemaParser(fname) + + try: + parser = QAPISchemaParser(fname) + except OSError as err: + raise QAPIError( + f"can't read schema file '{fname}': {err.strerror}" + ) from err + exprs = check_exprs(parser.exprs) self.docs = parser.docs self._entity_list = [] diff --git a/scripts/qapi/source.py b/scripts/qapi/source.py index 03b6ede082..04193cc964 100644 --- a/scripts/qapi/source.py +++ b/scripts/qapi/source.py @@ -10,7 +10,6 @@ # See the COPYING file in the top-level directory. import copy -import sys from typing import List, Optional, TypeVar @@ -32,10 +31,9 @@ class QAPISchemaPragma: class QAPISourceInfo: T = TypeVar('T', bound='QAPISourceInfo') - def __init__(self, fname: str, line: int, - parent: Optional['QAPISourceInfo']): + def __init__(self, fname: str, parent: Optional['QAPISourceInfo']): self.fname = fname - self.line = line + self.line = 1 self.parent = parent self.pragma: QAPISchemaPragma = ( parent.pragma if parent else QAPISchemaPragma() @@ -53,12 +51,7 @@ class QAPISourceInfo: return info def loc(self) -> str: - if self.fname is None: - return sys.argv[0] - ret = self.fname - if self.line is not None: - ret += ':%d' % self.line - return ret + return f"{self.fname}:{self.line}" def in_defn(self) -> str: if self.defn_name: diff --git a/scripts/simplebench/bench-backup.py b/scripts/simplebench/bench-backup.py index 33a1ecfefa..5a0675c593 100755 --- a/scripts/simplebench/bench-backup.py +++ b/scripts/simplebench/bench-backup.py @@ -23,7 +23,7 @@ import json import simplebench from results_to_text import results_to_text -from bench_block_job import bench_block_copy, drv_file, drv_nbd +from bench_block_job import bench_block_copy, drv_file, drv_nbd, drv_qcow2 def bench_func(env, case): @@ -37,29 +37,56 @@ def bench_func(env, case): def bench(args): test_cases = [] - sources = {} - targets = {} - for d in args.dir: - label, path = d.split(':') # paths with colon not supported - sources[label] = drv_file(path + '/test-source') - targets[label] = drv_file(path + '/test-target') + # paths with colon not supported, so we just split by ':' + dirs = dict(d.split(':') for d in args.dir) + nbd_drv = None if args.nbd: nbd = args.nbd.split(':') host = nbd[0] port = '10809' if len(nbd) == 1 else nbd[1] - drv = drv_nbd(host, port) - sources['nbd'] = drv - targets['nbd'] = drv + nbd_drv = drv_nbd(host, port) for t in args.test: src, dst = t.split(':') - test_cases.append({ - 'id': t, - 'source': sources[src], - 'target': targets[dst] - }) + if src == 'nbd' and dst == 'nbd': + raise ValueError("Can't use 'nbd' label for both src and dst") + + if (src == 'nbd' or dst == 'nbd') and not nbd_drv: + raise ValueError("'nbd' label used but --nbd is not given") + + if src == 'nbd': + source = nbd_drv + elif args.qcow2_sources: + source = drv_qcow2(drv_file(dirs[src] + '/test-source.qcow2')) + else: + source = drv_file(dirs[src] + '/test-source') + + if dst == 'nbd': + test_cases.append({'id': t, 'source': source, 'target': nbd_drv}) + continue + + if args.target_cache == 'both': + target_caches = ['direct', 'cached'] + else: + target_caches = [args.target_cache] + + for c in target_caches: + o_direct = c == 'direct' + fname = dirs[dst] + '/test-target' + if args.compressed: + fname += '.qcow2' + target = drv_file(fname, o_direct=o_direct) + if args.compressed: + target = drv_qcow2(target) + + test_id = t + if args.target_cache == 'both': + test_id += f'({c})' + + test_cases.append({'id': test_id, 'source': source, + 'target': target}) binaries = [] # list of (<label>, <path>, [<options>]) for i, q in enumerate(args.env): @@ -106,6 +133,13 @@ def bench(args): elif opt.startswith('max-workers='): x_perf['max-workers'] = int(opt.split('=')[1]) + backup_options = {} + if x_perf: + backup_options['x-perf'] = x_perf + + if args.compressed: + backup_options['compress'] = True + if is_mirror: assert not x_perf test_envs.append({ @@ -117,11 +151,13 @@ def bench(args): test_envs.append({ 'id': f'backup({label})\n' + '\n'.join(opts), 'cmd': 'blockdev-backup', - 'cmd-options': {'x-perf': x_perf} if x_perf else {}, + 'cmd-options': backup_options, 'qemu-binary': path }) - result = simplebench.bench(bench_func, test_envs, test_cases, count=3) + result = simplebench.bench(bench_func, test_envs, test_cases, + count=args.count, initial_run=args.initial_run, + drop_caches=args.drop_caches) with open('results.json', 'w') as f: json.dump(result, f, indent=4) print(results_to_text(result)) @@ -163,5 +199,30 @@ default port 10809). Use it in tests, label is "nbd" p.add_argument('--test', nargs='+', help='''\ Tests, in form source-dir-label:target-dir-label''', action=ExtendAction) + p.add_argument('--compressed', help='''\ +Use compressed backup. It automatically means +automatically creating qcow2 target with +lazy_refcounts for each test run''', action='store_true') + p.add_argument('--qcow2-sources', help='''\ +Use test-source.qcow2 images as sources instead of +test-source raw images''', action='store_true') + p.add_argument('--target-cache', help='''\ +Setup cache for target nodes. Options: + direct: default, use O_DIRECT and aio=native + cached: use system cache (Qemu default) and aio=threads (Qemu default) + both: generate two test cases for each src:dst pair''', + default='direct', choices=('direct', 'cached', 'both')) + + p.add_argument('--count', type=int, default=3, help='''\ +Number of test runs per table cell''') + + # BooleanOptionalAction helps to support --no-initial-run option + p.add_argument('--initial-run', action=argparse.BooleanOptionalAction, + help='''\ +Do additional initial run per cell which doesn't count in result, +default true''') + + p.add_argument('--drop-caches', action='store_true', help='''\ +Do "sync; echo 3 > /proc/sys/vm/drop_caches" before each test run''') bench(p.parse_args()) diff --git a/scripts/simplebench/bench_block_job.py b/scripts/simplebench/bench_block_job.py index 7332845c1c..4f03c12169 100755 --- a/scripts/simplebench/bench_block_job.py +++ b/scripts/simplebench/bench_block_job.py @@ -21,6 +21,7 @@ import sys import os +import subprocess import socket import json @@ -69,6 +70,10 @@ def bench_block_job(cmd, cmd_args, qemu_args): vm.shutdown() return {'error': 'block-job failed: ' + str(e), 'vm-log': vm.get_log()} + if 'error' in e['data']: + vm.shutdown() + return {'error': 'block-job failed: ' + e['data']['error'], + 'vm-log': vm.get_log()} end_ms = e['timestamp']['seconds'] * 1000000 + \ e['timestamp']['microseconds'] finally: @@ -77,11 +82,34 @@ def bench_block_job(cmd, cmd_args, qemu_args): return {'seconds': (end_ms - start_ms) / 1000000.0} +def get_image_size(path): + out = subprocess.run(['qemu-img', 'info', '--out=json', path], + stdout=subprocess.PIPE, check=True).stdout + return json.loads(out)['virtual-size'] + + +def get_blockdev_size(obj): + img = obj['filename'] if 'filename' in obj else obj['file']['filename'] + return get_image_size(img) + + # Bench backup or mirror def bench_block_copy(qemu_binary, cmd, cmd_options, source, target): """Helper to run bench_block_job() for mirror or backup""" assert cmd in ('blockdev-backup', 'blockdev-mirror') + if target['driver'] == 'qcow2': + try: + os.remove(target['file']['filename']) + except OSError: + pass + + subprocess.run(['qemu-img', 'create', '-f', 'qcow2', + target['file']['filename'], + str(get_blockdev_size(source))], + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, check=True) + source['node-name'] = 'source' target['node-name'] = 'target' @@ -96,9 +124,13 @@ def bench_block_copy(qemu_binary, cmd, cmd_options, source, target): '-blockdev', json.dumps(target)]) -def drv_file(filename): - return {'driver': 'file', 'filename': filename, - 'cache': {'direct': True}, 'aio': 'native'} +def drv_file(filename, o_direct=True): + node = {'driver': 'file', 'filename': filename} + if o_direct: + node['cache'] = {'direct': True} + node['aio'] = 'native' + + return node def drv_nbd(host, port): @@ -106,6 +138,10 @@ def drv_nbd(host, port): 'server': {'type': 'inet', 'host': host, 'port': port}} +def drv_qcow2(file): + return {'driver': 'qcow2', 'file': file} + + if __name__ == '__main__': import sys diff --git a/scripts/simplebench/simplebench.py b/scripts/simplebench/simplebench.py index f61513af90..8efca2af98 100644 --- a/scripts/simplebench/simplebench.py +++ b/scripts/simplebench/simplebench.py @@ -19,9 +19,17 @@ # import statistics +import subprocess +import time -def bench_one(test_func, test_env, test_case, count=5, initial_run=True): +def do_drop_caches(): + subprocess.run('sync; echo 3 > /proc/sys/vm/drop_caches', shell=True, + check=True) + + +def bench_one(test_func, test_env, test_case, count=5, initial_run=True, + slow_limit=100, drop_caches=False): """Benchmark one test-case test_func -- benchmarking function with prototype @@ -36,6 +44,9 @@ def bench_one(test_func, test_env, test_case, count=5, initial_run=True): test_case -- test case - opaque second argument for test_func count -- how many times to call test_func, to calculate average initial_run -- do initial run of test_func, which don't get into result + slow_limit -- stop at slow run (that exceedes the slow_limit by seconds). + (initial run is not measured) + drop_caches -- drop caches before each run Returns dict with the following fields: 'runs': list of test_func results @@ -49,15 +60,25 @@ def bench_one(test_func, test_env, test_case, count=5, initial_run=True): """ if initial_run: print(' #initial run:') + do_drop_caches() print(' ', test_func(test_env, test_case)) runs = [] for i in range(count): + t = time.time() + print(' #run {}'.format(i+1)) + do_drop_caches() res = test_func(test_env, test_case) print(' ', res) runs.append(res) + if time.time() - t > slow_limit: + print(' - run is too slow, stop here') + break + + count = len(runs) + result = {'runs': runs} succeeded = [r for r in runs if ('seconds' in r or 'iops' in r)] @@ -71,7 +92,10 @@ def bench_one(test_func, test_env, test_case, count=5, initial_run=True): dim = 'seconds' result['dimension'] = dim result['average'] = statistics.mean(r[dim] for r in succeeded) - result['stdev'] = statistics.stdev(r[dim] for r in succeeded) + if len(succeeded) == 1: + result['stdev'] = 0 + else: + result['stdev'] = statistics.stdev(r[dim] for r in succeeded) if len(succeeded) < count: result['n-failed'] = count - len(succeeded) diff --git a/tests/qapi-schema/meson.build b/tests/qapi-schema/meson.build index d7163e6601..9e8f658ce3 100644 --- a/tests/qapi-schema/meson.build +++ b/tests/qapi-schema/meson.build @@ -134,9 +134,11 @@ schemas = [ 'indented-expr.json', 'leading-comma-list.json', 'leading-comma-object.json', + 'missing-array-rsqb.json', 'missing-colon.json', 'missing-comma-list.json', 'missing-comma-object.json', + 'missing-object-member-element.json', 'missing-type.json', 'nested-struct-data.json', 'nested-struct-data-invalid-dict.json', @@ -199,11 +201,16 @@ schemas = [ 'unknown-escape.json', 'unknown-expr-key.json', ] +schemas = files(schemas) + +# Intentionally missing schema file test -- not passed through files(): +schemas += [meson.current_source_dir() / 'missing-schema.json'] # Because people may want to use test-qapi.py from the command line, we # are not using the "#! /usr/bin/env python3" trick here. See # docs/devel/build-system.txt -test('QAPI schema regression tests', python, args: files('test-qapi.py', schemas), +test('QAPI schema regression tests', python, + args: files('test-qapi.py') + schemas, env: test_env, suite: ['qapi-schema', 'qapi-frontend']) diff = find_program('diff') diff --git a/tests/qapi-schema/missing-array-rsqb.err b/tests/qapi-schema/missing-array-rsqb.err new file mode 100644 index 0000000000..b5f58b8c12 --- /dev/null +++ b/tests/qapi-schema/missing-array-rsqb.err @@ -0,0 +1 @@ +missing-array-rsqb.json:1:44: expected '{', '[', string, or boolean diff --git a/tests/qapi-schema/missing-array-rsqb.json b/tests/qapi-schema/missing-array-rsqb.json new file mode 100644 index 0000000000..7fca1df923 --- /dev/null +++ b/tests/qapi-schema/missing-array-rsqb.json @@ -0,0 +1 @@ +['Daisy,', 'Daisy,', 'Give me your answer', diff --git a/tests/qapi-schema/missing-array-rsqb.out b/tests/qapi-schema/missing-array-rsqb.out new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/tests/qapi-schema/missing-array-rsqb.out diff --git a/tests/qapi-schema/missing-object-member-element.err b/tests/qapi-schema/missing-object-member-element.err new file mode 100644 index 0000000000..c08a3dc307 --- /dev/null +++ b/tests/qapi-schema/missing-object-member-element.err @@ -0,0 +1 @@ +missing-object-member-element.json:1:8: expected '{', '[', string, or boolean diff --git a/tests/qapi-schema/missing-object-member-element.json b/tests/qapi-schema/missing-object-member-element.json new file mode 100644 index 0000000000..f52d0106f3 --- /dev/null +++ b/tests/qapi-schema/missing-object-member-element.json @@ -0,0 +1 @@ +{'key': diff --git a/tests/qapi-schema/missing-object-member-element.out b/tests/qapi-schema/missing-object-member-element.out new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/tests/qapi-schema/missing-object-member-element.out diff --git a/tests/qapi-schema/missing-schema.err b/tests/qapi-schema/missing-schema.err new file mode 100644 index 0000000000..b4d9ff1fb2 --- /dev/null +++ b/tests/qapi-schema/missing-schema.err @@ -0,0 +1 @@ +can't read schema file 'missing-schema.json': No such file or directory diff --git a/tests/qapi-schema/missing-schema.out b/tests/qapi-schema/missing-schema.out new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/tests/qapi-schema/missing-schema.out diff --git a/tests/qapi-schema/non-objects.err b/tests/qapi-schema/non-objects.err index 3a4ea36966..23bdb69c71 100644 --- a/tests/qapi-schema/non-objects.err +++ b/tests/qapi-schema/non-objects.err @@ -1 +1 @@ -non-objects.json:1:1: expected '{' +non-objects.json:1: top-level expression must be an object diff --git a/tests/qapi-schema/quoted-structural-chars.err b/tests/qapi-schema/quoted-structural-chars.err index 07d1561d1f..af6c1e173d 100644 --- a/tests/qapi-schema/quoted-structural-chars.err +++ b/tests/qapi-schema/quoted-structural-chars.err @@ -1 +1 @@ -quoted-structural-chars.json:1:1: expected '{' +quoted-structural-chars.json:1: top-level expression must be an object diff --git a/tests/qapi-schema/test-qapi.py b/tests/qapi-schema/test-qapi.py index e8db9d09d9..f1c4deb9a5 100755 --- a/tests/qapi-schema/test-qapi.py +++ b/tests/qapi-schema/test-qapi.py @@ -128,9 +128,6 @@ def test_and_diff(test_name, dir_name, update): try: test_frontend(os.path.join(dir_name, test_name + '.json')) except QAPIError as err: - if err.info.fname is None: - print("%s" % err, file=sys.stderr) - return 2 errstr = str(err) + '\n' if dir_name: errstr = errstr.replace(dir_name + '/', '') diff --git a/ui/clipboard.c b/ui/clipboard.c new file mode 100644 index 0000000000..abf2b98f1f --- /dev/null +++ b/ui/clipboard.c @@ -0,0 +1,92 @@ +#include "qemu/osdep.h" +#include "ui/clipboard.h" + +static NotifierList clipboard_notifiers = + NOTIFIER_LIST_INITIALIZER(clipboard_notifiers); + +void qemu_clipboard_peer_register(QemuClipboardPeer *peer) +{ + notifier_list_add(&clipboard_notifiers, &peer->update); +} + +void qemu_clipboard_peer_unregister(QemuClipboardPeer *peer) +{ + notifier_remove(&peer->update); +} + +void qemu_clipboard_update(QemuClipboardInfo *info) +{ + notifier_list_notify(&clipboard_notifiers, info); +} + +QemuClipboardInfo *qemu_clipboard_info_new(QemuClipboardPeer *owner, + QemuClipboardSelection selection) +{ + QemuClipboardInfo *info = g_new0(QemuClipboardInfo, 1); + + info->owner = owner; + info->selection = selection; + info->refcount = 1; + + return info; +} + +QemuClipboardInfo *qemu_clipboard_info_ref(QemuClipboardInfo *info) +{ + info->refcount++; + return info; +} + +void qemu_clipboard_info_unref(QemuClipboardInfo *info) +{ + uint32_t type; + + if (!info) { + return; + } + + info->refcount--; + if (info->refcount > 0) { + return; + } + + for (type = 0; type < QEMU_CLIPBOARD_TYPE__COUNT; type++) { + g_free(info->types[type].data); + } + g_free(info); +} + +void qemu_clipboard_request(QemuClipboardInfo *info, + QemuClipboardType type) +{ + if (info->types[type].data || + info->types[type].requested || + !info->types[type].available || + !info->owner) + return; + + info->types[type].requested = true; + info->owner->request(info, type); +} + +void qemu_clipboard_set_data(QemuClipboardPeer *peer, + QemuClipboardInfo *info, + QemuClipboardType type, + uint32_t size, + void *data, + bool update) +{ + if (!info || + info->owner != peer) { + return; + } + + g_free(info->types[type].data); + info->types[type].data = g_memdup(data, size); + info->types[type].size = size; + info->types[type].available = true; + + if (update) { + qemu_clipboard_update(info); + } +} diff --git a/ui/gtk-clipboard.c b/ui/gtk-clipboard.c new file mode 100644 index 0000000000..bff28d2030 --- /dev/null +++ b/ui/gtk-clipboard.c @@ -0,0 +1,192 @@ +/* + * GTK UI -- clipboard support + * + * Copyright (C) 2021 Gerd Hoffmann <kraxel@redhat.com> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see <http://www.gnu.org/licenses/>. + * + */ + +#include "qemu/osdep.h" +#include "qemu-common.h" +#include "qemu/main-loop.h" + +#include "ui/gtk.h" + +static QemuClipboardSelection gd_find_selection(GtkDisplayState *gd, + GtkClipboard *clipboard) +{ + QemuClipboardSelection s; + + for (s = 0; s < QEMU_CLIPBOARD_SELECTION__COUNT; s++) { + if (gd->gtkcb[s] == clipboard) { + return s; + } + } + return QEMU_CLIPBOARD_SELECTION_CLIPBOARD; +} + +static void gd_clipboard_get_data(GtkClipboard *clipboard, + GtkSelectionData *selection_data, + guint selection_info, + gpointer data) +{ + GtkDisplayState *gd = data; + QemuClipboardSelection s = gd_find_selection(gd, clipboard); + QemuClipboardType type = QEMU_CLIPBOARD_TYPE_TEXT; + QemuClipboardInfo *info = qemu_clipboard_info_ref(gd->cbinfo[s]); + + qemu_clipboard_request(info, type); + while (info == gd->cbinfo[s] && + info->types[type].available && + info->types[type].data == NULL) { + main_loop_wait(false); + } + + if (info == gd->cbinfo[s] && gd->cbowner[s]) { + gtk_selection_data_set_text(selection_data, + info->types[type].data, + info->types[type].size); + } else { + /* clipboard owner changed while waiting for the data */ + } + + qemu_clipboard_info_unref(info); +} + +static void gd_clipboard_clear(GtkClipboard *clipboard, + gpointer data) +{ + GtkDisplayState *gd = data; + QemuClipboardSelection s = gd_find_selection(gd, clipboard); + + gd->cbowner[s] = false; +} + +static void gd_clipboard_notify(Notifier *notifier, void *data) +{ + GtkDisplayState *gd = container_of(notifier, GtkDisplayState, cbpeer.update); + QemuClipboardInfo *info = data; + QemuClipboardSelection s = info->selection; + bool self_update = info->owner == &gd->cbpeer; + + if (info != gd->cbinfo[s]) { + qemu_clipboard_info_unref(gd->cbinfo[s]); + gd->cbinfo[s] = qemu_clipboard_info_ref(info); + gd->cbpending[s] = 0; + if (!self_update) { + GtkTargetList *list; + GtkTargetEntry *targets; + gint n_targets; + + list = gtk_target_list_new(NULL, 0); + if (info->types[QEMU_CLIPBOARD_TYPE_TEXT].available) { + gtk_target_list_add_text_targets(list, 0); + } + targets = gtk_target_table_new_from_list(list, &n_targets); + + gtk_clipboard_clear(gd->gtkcb[s]); + gd->cbowner[s] = true; + gtk_clipboard_set_with_data(gd->gtkcb[s], + targets, n_targets, + gd_clipboard_get_data, + gd_clipboard_clear, + gd); + + gtk_target_table_free(targets, n_targets); + gtk_target_list_unref(list); + } + return; + } + + if (self_update) { + return; + } + + /* + * Clipboard got updated, with data probably. No action here, we + * are waiting for updates in gd_clipboard_get_data(). + */ +} + +static void gd_clipboard_request(QemuClipboardInfo *info, + QemuClipboardType type) +{ + GtkDisplayState *gd = container_of(info->owner, GtkDisplayState, cbpeer); + char *text; + + switch (type) { + case QEMU_CLIPBOARD_TYPE_TEXT: + text = gtk_clipboard_wait_for_text(gd->gtkcb[info->selection]); + if (text) { + qemu_clipboard_set_data(&gd->cbpeer, info, type, + strlen(text), text, true); + g_free(text); + } + break; + default: + break; + } +} + +static void gd_owner_change(GtkClipboard *clipboard, + GdkEvent *event, + gpointer data) +{ + GtkDisplayState *gd = data; + QemuClipboardSelection s = gd_find_selection(gd, clipboard); + QemuClipboardInfo *info; + + if (gd->cbowner[s]) { + /* ignore notifications about our own grabs */ + return; + } + + + switch (event->owner_change.reason) { + case GDK_SETTING_ACTION_NEW: + info = qemu_clipboard_info_new(&gd->cbpeer, s); + if (gtk_clipboard_wait_is_text_available(clipboard)) { + info->types[QEMU_CLIPBOARD_TYPE_TEXT].available = true; + } + + qemu_clipboard_update(info); + qemu_clipboard_info_unref(info); + break; + default: + break; + } +} + +void gd_clipboard_init(GtkDisplayState *gd) +{ + gd->cbpeer.name = "gtk"; + gd->cbpeer.update.notify = gd_clipboard_notify; + gd->cbpeer.request = gd_clipboard_request; + qemu_clipboard_peer_register(&gd->cbpeer); + + gd->gtkcb[QEMU_CLIPBOARD_SELECTION_CLIPBOARD] = + gtk_clipboard_get(gdk_atom_intern("CLIPBOARD", FALSE)); + gd->gtkcb[QEMU_CLIPBOARD_SELECTION_PRIMARY] = + gtk_clipboard_get(gdk_atom_intern("PRIMARY", FALSE)); + gd->gtkcb[QEMU_CLIPBOARD_SELECTION_SECONDARY] = + gtk_clipboard_get(gdk_atom_intern("SECONDARY", FALSE)); + + g_signal_connect(gd->gtkcb[QEMU_CLIPBOARD_SELECTION_CLIPBOARD], + "owner-change", G_CALLBACK(gd_owner_change), gd); + g_signal_connect(gd->gtkcb[QEMU_CLIPBOARD_SELECTION_PRIMARY], + "owner-change", G_CALLBACK(gd_owner_change), gd); + g_signal_connect(gd->gtkcb[QEMU_CLIPBOARD_SELECTION_SECONDARY], + "owner-change", G_CALLBACK(gd_owner_change), gd); +} diff --git a/ui/gtk.c b/ui/gtk.c index 1ea1253528..98046f577b 100644 --- a/ui/gtk.c +++ b/ui/gtk.c @@ -60,7 +60,6 @@ #include "chardev/char.h" #include "qom/object.h" -#define MAX_VCS 10 #define VC_WINDOW_X_MIN 320 #define VC_WINDOW_Y_MIN 240 #define VC_TERM_X_MIN 80 @@ -119,60 +118,6 @@ static const guint16 *keycode_map; static size_t keycode_maplen; -struct GtkDisplayState { - GtkWidget *window; - - GtkWidget *menu_bar; - - GtkAccelGroup *accel_group; - - GtkWidget *machine_menu_item; - GtkWidget *machine_menu; - GtkWidget *pause_item; - GtkWidget *reset_item; - GtkWidget *powerdown_item; - GtkWidget *quit_item; - - GtkWidget *view_menu_item; - GtkWidget *view_menu; - GtkWidget *full_screen_item; - GtkWidget *copy_item; - GtkWidget *zoom_in_item; - GtkWidget *zoom_out_item; - GtkWidget *zoom_fixed_item; - GtkWidget *zoom_fit_item; - GtkWidget *grab_item; - GtkWidget *grab_on_hover_item; - - int nb_vcs; - VirtualConsole vc[MAX_VCS]; - - GtkWidget *show_tabs_item; - GtkWidget *untabify_item; - GtkWidget *show_menubar_item; - - GtkWidget *vbox; - GtkWidget *notebook; - int button_mask; - gboolean last_set; - int last_x; - int last_y; - int grab_x_root; - int grab_y_root; - VirtualConsole *kbd_owner; - VirtualConsole *ptr_owner; - - gboolean full_screen; - - GdkCursor *null_cursor; - Notifier mouse_mode_notifier; - gboolean free_scale; - - bool external_pause_update; - - DisplayOptions *opts; -}; - struct VCChardev { Chardev parent; VirtualConsole *console; @@ -2322,6 +2267,7 @@ static void gtk_display_init(DisplayState *ds, DisplayOptions *opts) opts->u.gtk.grab_on_hover) { gtk_menu_item_activate(GTK_MENU_ITEM(s->grab_on_hover_item)); } + gd_clipboard_init(s); } static void early_gtk_display_init(DisplayOptions *opts) diff --git a/ui/meson.build b/ui/meson.build index e8d3ff41b9..b5aed14886 100644 --- a/ui/meson.build +++ b/ui/meson.build @@ -2,6 +2,7 @@ softmmu_ss.add(pixman) specific_ss.add(when: ['CONFIG_SOFTMMU'], if_true: pixman) # for the include path softmmu_ss.add(files( + 'clipboard.c', 'console.c', 'cursor.c', 'input-keymap.c', @@ -13,6 +14,7 @@ softmmu_ss.add(files( 'qemu-pixman.c', )) softmmu_ss.add([spice_headers, files('spice-module.c')]) +softmmu_ss.add(when: spice_protocol, if_true: files('vdagent.c')) softmmu_ss.add(when: 'CONFIG_LINUX', if_true: files('input-linux.c')) softmmu_ss.add(when: cocoa, if_true: files('cocoa.m')) @@ -28,6 +30,7 @@ vnc_ss.add(files( 'vnc-auth-vencrypt.c', 'vnc-ws.c', 'vnc-jobs.c', + 'vnc-clipboard.c', )) vnc_ss.add(zlib, png, jpeg, gnutls) vnc_ss.add(when: sasl, if_true: files('vnc-auth-sasl.c')) @@ -62,7 +65,7 @@ if gtk.found() softmmu_ss.add(when: 'CONFIG_WIN32', if_true: files('win32-kbd-hook.c')) gtk_ss = ss.source_set() - gtk_ss.add(gtk, vte, pixman, files('gtk.c')) + gtk_ss.add(gtk, vte, pixman, files('gtk.c', 'gtk-clipboard.c')) gtk_ss.add(when: x11, if_true: files('x_keymap.c')) gtk_ss.add(when: [opengl, 'CONFIG_OPENGL'], if_true: files('gtk-gl-area.c')) gtk_ss.add(when: [x11, opengl, 'CONFIG_OPENGL'], if_true: files('gtk-egl.c')) diff --git a/ui/spice-display.c b/ui/spice-display.c index d22781a23d..f59c69882d 100644 --- a/ui/spice-display.c +++ b/ui/spice-display.c @@ -561,6 +561,10 @@ static void interface_release_resource(QXLInstance *sin, SimpleSpiceCursor *cursor; QXLCommandExt *ext; + if (!rext.info) { + return; + } + ext = (void *)(intptr_t)(rext.info->id); switch (ext->cmd.type) { case QXL_CMD_DRAW: diff --git a/ui/trace-events b/ui/trace-events index 5d1da6f236..c86542e2b6 100644 --- a/ui/trace-events +++ b/ui/trace-events @@ -124,3 +124,13 @@ xkeymap_extension(const char *name) "extension '%s'" xkeymap_vendor(const char *name) "vendor '%s'" xkeymap_keycodes(const char *name) "keycodes '%s'" xkeymap_keymap(const char *name) "keymap '%s'" + +# vdagent.c +vdagent_open(void) "" +vdagent_close(void) "" +vdagent_send(const char *name) "msg %s" +vdagent_recv_chunk(uint32_t size) "size %d" +vdagent_recv_msg(const char *name, uint32_t size) "msg %s, size %d" +vdagent_peer_cap(const char *name) "cap %s" +vdagent_cb_grab_selection(const char *name) "selection %s" +vdagent_cb_grab_type(const char *name) "type %s" diff --git a/ui/vdagent.c b/ui/vdagent.c new file mode 100644 index 0000000000..a253a8fe63 --- /dev/null +++ b/ui/vdagent.c @@ -0,0 +1,803 @@ +#include "qemu/osdep.h" +#include "qapi/error.h" +#include "include/qemu-common.h" +#include "chardev/char.h" +#include "qemu/buffer.h" +#include "qemu/option.h" +#include "qemu/units.h" +#include "hw/qdev-core.h" +#include "ui/clipboard.h" +#include "ui/console.h" +#include "ui/input.h" +#include "trace.h" + +#include "qapi/qapi-types-char.h" +#include "qapi/qapi-types-ui.h" + +#include "spice/vd_agent.h" + +#define VDAGENT_BUFFER_LIMIT (1 * MiB) +#define VDAGENT_MOUSE_DEFAULT true +#define VDAGENT_CLIPBOARD_DEFAULT false + +struct VDAgentChardev { + Chardev parent; + + /* config */ + bool mouse; + bool clipboard; + + /* guest vdagent */ + uint32_t caps; + VDIChunkHeader chunk; + uint32_t chunksize; + uint8_t *msgbuf; + uint32_t msgsize; + uint8_t *xbuf; + uint32_t xoff, xsize; + Buffer outbuf; + + /* mouse */ + DeviceState mouse_dev; + uint32_t mouse_x; + uint32_t mouse_y; + uint32_t mouse_btn; + uint32_t mouse_display; + QemuInputHandlerState *mouse_hs; + + /* clipboard */ + QemuClipboardPeer cbpeer; + QemuClipboardInfo *cbinfo[QEMU_CLIPBOARD_SELECTION__COUNT]; + uint32_t cbpending[QEMU_CLIPBOARD_SELECTION__COUNT]; +}; +typedef struct VDAgentChardev VDAgentChardev; + +#define TYPE_CHARDEV_QEMU_VDAGENT "chardev-qemu-vdagent" + +DECLARE_INSTANCE_CHECKER(VDAgentChardev, QEMU_VDAGENT_CHARDEV, + TYPE_CHARDEV_QEMU_VDAGENT); + +/* ------------------------------------------------------------------ */ +/* names, for debug logging */ + +static const char *cap_name[] = { + [VD_AGENT_CAP_MOUSE_STATE] = "mouse-state", + [VD_AGENT_CAP_MONITORS_CONFIG] = "monitors-config", + [VD_AGENT_CAP_REPLY] = "reply", + [VD_AGENT_CAP_CLIPBOARD] = "clipboard", + [VD_AGENT_CAP_DISPLAY_CONFIG] = "display-config", + [VD_AGENT_CAP_CLIPBOARD_BY_DEMAND] = "clipboard-by-demand", + [VD_AGENT_CAP_CLIPBOARD_SELECTION] = "clipboard-selection", + [VD_AGENT_CAP_SPARSE_MONITORS_CONFIG] = "sparse-monitors-config", + [VD_AGENT_CAP_GUEST_LINEEND_LF] = "guest-lineend-lf", + [VD_AGENT_CAP_GUEST_LINEEND_CRLF] = "guest-lineend-crlf", + [VD_AGENT_CAP_MAX_CLIPBOARD] = "max-clipboard", + [VD_AGENT_CAP_AUDIO_VOLUME_SYNC] = "audio-volume-sync", + [VD_AGENT_CAP_MONITORS_CONFIG_POSITION] = "monitors-config-position", + [VD_AGENT_CAP_FILE_XFER_DISABLED] = "file-xfer-disabled", + [VD_AGENT_CAP_FILE_XFER_DETAILED_ERRORS] = "file-xfer-detailed-errors", +#if 0 + [VD_AGENT_CAP_GRAPHICS_DEVICE_INFO] = "graphics-device-info", + [VD_AGENT_CAP_CLIPBOARD_NO_RELEASE_ON_REGRAB] = "clipboard-no-release-on-regrab", + [VD_AGENT_CAP_CLIPBOARD_GRAB_SERIAL] = "clipboard-grab-serial", +#endif +}; + +static const char *msg_name[] = { + [VD_AGENT_MOUSE_STATE] = "mouse-state", + [VD_AGENT_MONITORS_CONFIG] = "monitors-config", + [VD_AGENT_REPLY] = "reply", + [VD_AGENT_CLIPBOARD] = "clipboard", + [VD_AGENT_DISPLAY_CONFIG] = "display-config", + [VD_AGENT_ANNOUNCE_CAPABILITIES] = "announce-capabilities", + [VD_AGENT_CLIPBOARD_GRAB] = "clipboard-grab", + [VD_AGENT_CLIPBOARD_REQUEST] = "clipboard-request", + [VD_AGENT_CLIPBOARD_RELEASE] = "clipboard-release", + [VD_AGENT_FILE_XFER_START] = "file-xfer-start", + [VD_AGENT_FILE_XFER_STATUS] = "file-xfer-status", + [VD_AGENT_FILE_XFER_DATA] = "file-xfer-data", + [VD_AGENT_CLIENT_DISCONNECTED] = "client-disconnected", + [VD_AGENT_MAX_CLIPBOARD] = "max-clipboard", + [VD_AGENT_AUDIO_VOLUME_SYNC] = "audio-volume-sync", +#if 0 + [VD_AGENT_GRAPHICS_DEVICE_INFO] = "graphics-device-info", +#endif +}; + +static const char *sel_name[] = { + [VD_AGENT_CLIPBOARD_SELECTION_CLIPBOARD] = "clipboard", + [VD_AGENT_CLIPBOARD_SELECTION_PRIMARY] = "primary", + [VD_AGENT_CLIPBOARD_SELECTION_SECONDARY] = "secondary", +}; + +static const char *type_name[] = { + [VD_AGENT_CLIPBOARD_NONE] = "none", + [VD_AGENT_CLIPBOARD_UTF8_TEXT] = "text", + [VD_AGENT_CLIPBOARD_IMAGE_PNG] = "png", + [VD_AGENT_CLIPBOARD_IMAGE_BMP] = "bmp", + [VD_AGENT_CLIPBOARD_IMAGE_TIFF] = "tiff", + [VD_AGENT_CLIPBOARD_IMAGE_JPG] = "jpg", +#if 0 + [VD_AGENT_CLIPBOARD_FILE_LIST] = "files", +#endif +}; + +#define GET_NAME(_m, _v) \ + (((_v) < ARRAY_SIZE(_m) && (_m[_v])) ? (_m[_v]) : "???") + +/* ------------------------------------------------------------------ */ +/* send messages */ + +static void vdagent_send_buf(VDAgentChardev *vd) +{ + uint32_t len; + + while (!buffer_empty(&vd->outbuf)) { + len = qemu_chr_be_can_write(CHARDEV(vd)); + if (len == 0) { + return; + } + if (len > vd->outbuf.offset) { + len = vd->outbuf.offset; + } + qemu_chr_be_write(CHARDEV(vd), vd->outbuf.buffer, len); + buffer_advance(&vd->outbuf, len); + } +} + +static void vdagent_send_msg(VDAgentChardev *vd, VDAgentMessage *msg) +{ + uint8_t *msgbuf = (void *)msg; + uint32_t msgsize = sizeof(VDAgentMessage) + msg->size; + uint32_t msgoff = 0; + VDIChunkHeader chunk; + + trace_vdagent_send(GET_NAME(msg_name, msg->type)); + + msg->protocol = VD_AGENT_PROTOCOL; + + if (vd->outbuf.offset + msgsize > VDAGENT_BUFFER_LIMIT) { + error_report("buffer full, dropping message"); + return; + } + + while (msgoff < msgsize) { + chunk.port = VDP_CLIENT_PORT; + chunk.size = msgsize - msgoff; + if (chunk.size > 1024) { + chunk.size = 1024; + } + buffer_reserve(&vd->outbuf, sizeof(chunk) + chunk.size); + buffer_append(&vd->outbuf, &chunk, sizeof(chunk)); + buffer_append(&vd->outbuf, msgbuf + msgoff, chunk.size); + msgoff += chunk.size; + } + vdagent_send_buf(vd); +} + +static void vdagent_send_caps(VDAgentChardev *vd) +{ + g_autofree VDAgentMessage *msg = g_malloc0(sizeof(VDAgentMessage) + + sizeof(VDAgentAnnounceCapabilities) + + sizeof(uint32_t)); + VDAgentAnnounceCapabilities *caps = (void *)msg->data; + + msg->type = VD_AGENT_ANNOUNCE_CAPABILITIES; + msg->size = sizeof(VDAgentAnnounceCapabilities) + sizeof(uint32_t); + if (vd->mouse) { + caps->caps[0] |= (1 << VD_AGENT_CAP_MOUSE_STATE); + } + if (vd->clipboard) { + caps->caps[0] |= (1 << VD_AGENT_CAP_CLIPBOARD_BY_DEMAND); + caps->caps[0] |= (1 << VD_AGENT_CAP_CLIPBOARD_SELECTION); + } + + vdagent_send_msg(vd, msg); +} + +/* ------------------------------------------------------------------ */ +/* mouse events */ + +static bool have_mouse(VDAgentChardev *vd) +{ + return vd->mouse && + (vd->caps & (1 << VD_AGENT_CAP_MOUSE_STATE)); +} + +static void vdagent_send_mouse(VDAgentChardev *vd) +{ + g_autofree VDAgentMessage *msg = g_malloc0(sizeof(VDAgentMessage) + + sizeof(VDAgentMouseState)); + VDAgentMouseState *mouse = (void *)msg->data; + + msg->type = VD_AGENT_MOUSE_STATE; + msg->size = sizeof(VDAgentMouseState); + + mouse->x = vd->mouse_x; + mouse->y = vd->mouse_y; + mouse->buttons = vd->mouse_btn; + mouse->display_id = vd->mouse_display; + + vdagent_send_msg(vd, msg); +} + +static void vdagent_pointer_event(DeviceState *dev, QemuConsole *src, + InputEvent *evt) +{ + static const int bmap[INPUT_BUTTON__MAX] = { + [INPUT_BUTTON_LEFT] = VD_AGENT_LBUTTON_MASK, + [INPUT_BUTTON_RIGHT] = VD_AGENT_RBUTTON_MASK, + [INPUT_BUTTON_MIDDLE] = VD_AGENT_MBUTTON_MASK, + [INPUT_BUTTON_WHEEL_UP] = VD_AGENT_UBUTTON_MASK, + [INPUT_BUTTON_WHEEL_DOWN] = VD_AGENT_DBUTTON_MASK, +#ifdef VD_AGENT_EBUTTON_MASK + [INPUT_BUTTON_SIDE] = VD_AGENT_SBUTTON_MASK, + [INPUT_BUTTON_EXTRA] = VD_AGENT_EBUTTON_MASK, +#endif + }; + + VDAgentChardev *vd = container_of(dev, struct VDAgentChardev, mouse_dev); + InputMoveEvent *move; + InputBtnEvent *btn; + uint32_t xres, yres; + + switch (evt->type) { + case INPUT_EVENT_KIND_ABS: + move = evt->u.abs.data; + xres = qemu_console_get_width(src, 1024); + yres = qemu_console_get_height(src, 768); + if (move->axis == INPUT_AXIS_X) { + vd->mouse_x = qemu_input_scale_axis(move->value, + INPUT_EVENT_ABS_MIN, + INPUT_EVENT_ABS_MAX, + 0, xres); + } else if (move->axis == INPUT_AXIS_Y) { + vd->mouse_y = qemu_input_scale_axis(move->value, + INPUT_EVENT_ABS_MIN, + INPUT_EVENT_ABS_MAX, + 0, yres); + } + vd->mouse_display = qemu_console_get_index(src); + break; + + case INPUT_EVENT_KIND_BTN: + btn = evt->u.btn.data; + if (btn->down) { + vd->mouse_btn |= bmap[btn->button]; + } else { + vd->mouse_btn &= ~bmap[btn->button]; + } + break; + + default: + /* keep gcc happy */ + break; + } +} + +static void vdagent_pointer_sync(DeviceState *dev) +{ + VDAgentChardev *vd = container_of(dev, struct VDAgentChardev, mouse_dev); + + if (vd->caps & (1 << VD_AGENT_CAP_MOUSE_STATE)) { + vdagent_send_mouse(vd); + } +} + +static QemuInputHandler vdagent_mouse_handler = { + .name = "vdagent mouse", + .mask = INPUT_EVENT_MASK_BTN | INPUT_EVENT_MASK_ABS, + .event = vdagent_pointer_event, + .sync = vdagent_pointer_sync, +}; + +/* ------------------------------------------------------------------ */ +/* clipboard */ + +static bool have_clipboard(VDAgentChardev *vd) +{ + return vd->clipboard && + (vd->caps & (1 << VD_AGENT_CAP_CLIPBOARD_BY_DEMAND)); +} + +static bool have_selection(VDAgentChardev *vd) +{ + return vd->caps & (1 << VD_AGENT_CAP_CLIPBOARD_SELECTION); +} + +static uint32_t type_qemu_to_vdagent(enum QemuClipboardType type) +{ + switch (type) { + case QEMU_CLIPBOARD_TYPE_TEXT: + return VD_AGENT_CLIPBOARD_UTF8_TEXT; + default: + return VD_AGENT_CLIPBOARD_NONE; + } +} + +static void vdagent_send_clipboard_grab(VDAgentChardev *vd, + QemuClipboardInfo *info) +{ + g_autofree VDAgentMessage *msg = + g_malloc0(sizeof(VDAgentMessage) + + sizeof(uint32_t) * (QEMU_CLIPBOARD_TYPE__COUNT + 1)); + uint8_t *s = msg->data; + uint32_t *data = (uint32_t *)msg->data; + uint32_t q, type; + + if (have_selection(vd)) { + *s = info->selection; + data++; + msg->size += sizeof(uint32_t); + } else if (info->selection != QEMU_CLIPBOARD_SELECTION_CLIPBOARD) { + return; + } + + for (q = 0; q < QEMU_CLIPBOARD_TYPE__COUNT; q++) { + type = type_qemu_to_vdagent(q); + if (type != VD_AGENT_CLIPBOARD_NONE && info->types[q].available) { + *data = type; + data++; + msg->size += sizeof(uint32_t); + } + } + + msg->type = VD_AGENT_CLIPBOARD_GRAB; + vdagent_send_msg(vd, msg); +} + +static void vdagent_send_clipboard_data(VDAgentChardev *vd, + QemuClipboardInfo *info, + QemuClipboardType type) +{ + g_autofree VDAgentMessage *msg = g_malloc0(sizeof(VDAgentMessage) + + sizeof(uint32_t) * 2 + + info->types[type].size); + + uint8_t *s = msg->data; + uint32_t *data = (uint32_t *)msg->data; + + if (have_selection(vd)) { + *s = info->selection; + data++; + msg->size += sizeof(uint32_t); + } else if (info->selection != QEMU_CLIPBOARD_SELECTION_CLIPBOARD) { + return; + } + + *data = type_qemu_to_vdagent(type); + data++; + msg->size += sizeof(uint32_t); + + memcpy(data, info->types[type].data, info->types[type].size); + msg->size += info->types[type].size; + + msg->type = VD_AGENT_CLIPBOARD; + vdagent_send_msg(vd, msg); +} + +static void vdagent_clipboard_notify(Notifier *notifier, void *data) +{ + VDAgentChardev *vd = container_of(notifier, VDAgentChardev, cbpeer.update); + QemuClipboardInfo *info = data; + QemuClipboardSelection s = info->selection; + QemuClipboardType type; + bool self_update = info->owner == &vd->cbpeer; + + if (info != vd->cbinfo[s]) { + qemu_clipboard_info_unref(vd->cbinfo[s]); + vd->cbinfo[s] = qemu_clipboard_info_ref(info); + vd->cbpending[s] = 0; + if (!self_update) { + vdagent_send_clipboard_grab(vd, info); + } + return; + } + + if (self_update) { + return; + } + + for (type = 0; type < QEMU_CLIPBOARD_TYPE__COUNT; type++) { + if (vd->cbpending[s] & (1 << type)) { + vd->cbpending[s] &= ~(1 << type); + vdagent_send_clipboard_data(vd, info, type); + } + } +} + +static void vdagent_clipboard_request(QemuClipboardInfo *info, + QemuClipboardType qtype) +{ + VDAgentChardev *vd = container_of(info->owner, VDAgentChardev, cbpeer); + g_autofree VDAgentMessage *msg = g_malloc0(sizeof(VDAgentMessage) + + sizeof(uint32_t) * 2); + uint32_t type = type_qemu_to_vdagent(qtype); + uint8_t *s = msg->data; + uint32_t *data = (uint32_t *)msg->data; + + if (type == VD_AGENT_CLIPBOARD_NONE) { + return; + } + + if (have_selection(vd)) { + *s = info->selection; + data++; + msg->size += sizeof(uint32_t); + } + + *data = type; + msg->size += sizeof(uint32_t); + + msg->type = VD_AGENT_CLIPBOARD_REQUEST; + vdagent_send_msg(vd, msg); +} + +static void vdagent_chr_recv_clipboard(VDAgentChardev *vd, VDAgentMessage *msg) +{ + uint8_t s = VD_AGENT_CLIPBOARD_SELECTION_CLIPBOARD; + uint32_t size = msg->size; + void *data = msg->data; + QemuClipboardInfo *info; + QemuClipboardType type; + + if (have_selection(vd)) { + if (size < 4) { + return; + } + s = *(uint8_t *)data; + if (s >= QEMU_CLIPBOARD_SELECTION__COUNT) { + return; + } + data += 4; + size -= 4; + } + + switch (msg->type) { + case VD_AGENT_CLIPBOARD_GRAB: + trace_vdagent_cb_grab_selection(GET_NAME(sel_name, s)); + info = qemu_clipboard_info_new(&vd->cbpeer, s); + if (size > sizeof(uint32_t) * 10) { + /* + * spice has 6 types as of 2021. Limiting to 10 entries + * so we we have some wiggle room. + */ + return; + } + while (size >= sizeof(uint32_t)) { + trace_vdagent_cb_grab_type(GET_NAME(type_name, *(uint32_t *)data)); + switch (*(uint32_t *)data) { + case VD_AGENT_CLIPBOARD_UTF8_TEXT: + info->types[QEMU_CLIPBOARD_TYPE_TEXT].available = true; + break; + default: + break; + } + data += sizeof(uint32_t); + size -= sizeof(uint32_t); + } + qemu_clipboard_update(info); + qemu_clipboard_info_unref(info); + break; + case VD_AGENT_CLIPBOARD_REQUEST: + if (size < sizeof(uint32_t)) { + return; + } + switch (*(uint32_t *)data) { + case VD_AGENT_CLIPBOARD_UTF8_TEXT: + type = QEMU_CLIPBOARD_TYPE_TEXT; + break; + default: + return; + } + if (vd->cbinfo[s] && + vd->cbinfo[s]->types[type].available && + vd->cbinfo[s]->owner != &vd->cbpeer) { + if (vd->cbinfo[s]->types[type].data) { + vdagent_send_clipboard_data(vd, vd->cbinfo[s], type); + } else { + vd->cbpending[s] |= (1 << type); + qemu_clipboard_request(vd->cbinfo[s], type); + } + } + break; + case VD_AGENT_CLIPBOARD: /* data */ + if (size < sizeof(uint32_t)) { + return; + } + switch (*(uint32_t *)data) { + case VD_AGENT_CLIPBOARD_UTF8_TEXT: + type = QEMU_CLIPBOARD_TYPE_TEXT; + break; + default: + return; + } + data += 4; + size -= 4; + qemu_clipboard_set_data(&vd->cbpeer, vd->cbinfo[s], type, + size, data, true); + break; + case VD_AGENT_CLIPBOARD_RELEASE: /* data */ + if (vd->cbinfo[s] && + vd->cbinfo[s]->owner == &vd->cbpeer) { + /* set empty clipboard info */ + info = qemu_clipboard_info_new(NULL, s); + qemu_clipboard_update(info); + qemu_clipboard_info_unref(info); + } + break; + } +} + +/* ------------------------------------------------------------------ */ +/* chardev backend */ + +static void vdagent_chr_open(Chardev *chr, + ChardevBackend *backend, + bool *be_opened, + Error **errp) +{ + VDAgentChardev *vd = QEMU_VDAGENT_CHARDEV(chr); + ChardevQemuVDAgent *cfg = backend->u.qemu_vdagent.data; + +#if defined(HOST_WORDS_BIGENDIAN) + /* + * TODO: vdagent protocol is defined to be LE, + * so we have to byteswap everything on BE hosts. + */ + error_setg(errp, "vdagent is not supported on bigendian hosts"); + return; +#endif + + vd->mouse = VDAGENT_MOUSE_DEFAULT; + if (cfg->has_mouse) { + vd->mouse = cfg->mouse; + } + + vd->clipboard = VDAGENT_CLIPBOARD_DEFAULT; + if (cfg->has_clipboard) { + vd->clipboard = cfg->clipboard; + } + + if (vd->mouse) { + vd->mouse_hs = qemu_input_handler_register(&vd->mouse_dev, + &vdagent_mouse_handler); + } + + *be_opened = true; +} + +static void vdagent_chr_recv_caps(VDAgentChardev *vd, VDAgentMessage *msg) +{ + VDAgentAnnounceCapabilities *caps = (void *)msg->data; + int i; + + if (msg->size < (sizeof(VDAgentAnnounceCapabilities) + + sizeof(uint32_t))) { + return; + } + + for (i = 0; i < ARRAY_SIZE(cap_name); i++) { + if (caps->caps[0] & (1 << i)) { + trace_vdagent_peer_cap(GET_NAME(cap_name, i)); + } + } + + vd->caps = caps->caps[0]; + if (caps->request) { + vdagent_send_caps(vd); + } + if (have_mouse(vd) && vd->mouse_hs) { + qemu_input_handler_activate(vd->mouse_hs); + } + if (have_clipboard(vd) && vd->cbpeer.update.notify == NULL) { + vd->cbpeer.name = "vdagent"; + vd->cbpeer.update.notify = vdagent_clipboard_notify; + vd->cbpeer.request = vdagent_clipboard_request; + qemu_clipboard_peer_register(&vd->cbpeer); + } +} + +static void vdagent_chr_recv_msg(VDAgentChardev *vd, VDAgentMessage *msg) +{ + trace_vdagent_recv_msg(GET_NAME(msg_name, msg->type), msg->size); + + switch (msg->type) { + case VD_AGENT_ANNOUNCE_CAPABILITIES: + vdagent_chr_recv_caps(vd, msg); + break; + case VD_AGENT_CLIPBOARD: + case VD_AGENT_CLIPBOARD_GRAB: + case VD_AGENT_CLIPBOARD_REQUEST: + case VD_AGENT_CLIPBOARD_RELEASE: + if (have_clipboard(vd)) { + vdagent_chr_recv_clipboard(vd, msg); + } + break; + default: + break; + } +} + +static void vdagent_reset_xbuf(VDAgentChardev *vd) +{ + g_clear_pointer(&vd->xbuf, g_free); + vd->xoff = 0; + vd->xsize = 0; +} + +static void vdagent_chr_recv_chunk(VDAgentChardev *vd) +{ + VDAgentMessage *msg = (void *)vd->msgbuf; + + if (!vd->xsize) { + if (vd->msgsize < sizeof(*msg)) { + error_report("%s: message too small: %d < %zd", __func__, + vd->msgsize, sizeof(*msg)); + return; + } + if (vd->msgsize == msg->size + sizeof(*msg)) { + vdagent_chr_recv_msg(vd, msg); + return; + } + } + + if (!vd->xsize) { + vd->xsize = msg->size + sizeof(*msg); + vd->xbuf = g_malloc0(vd->xsize); + } + + if (vd->xoff + vd->msgsize > vd->xsize) { + error_report("%s: Oops: %d+%d > %d", __func__, + vd->xoff, vd->msgsize, vd->xsize); + vdagent_reset_xbuf(vd); + return; + } + + memcpy(vd->xbuf + vd->xoff, vd->msgbuf, vd->msgsize); + vd->xoff += vd->msgsize; + if (vd->xoff < vd->xsize) { + return; + } + + msg = (void *)vd->xbuf; + vdagent_chr_recv_msg(vd, msg); + vdagent_reset_xbuf(vd); +} + +static void vdagent_reset_bufs(VDAgentChardev *vd) +{ + memset(&vd->chunk, 0, sizeof(vd->chunk)); + vd->chunksize = 0; + g_free(vd->msgbuf); + vd->msgbuf = NULL; + vd->msgsize = 0; +} + +static int vdagent_chr_write(Chardev *chr, const uint8_t *buf, int len) +{ + VDAgentChardev *vd = QEMU_VDAGENT_CHARDEV(chr); + uint32_t copy, ret = len; + + while (len) { + if (vd->chunksize < sizeof(vd->chunk)) { + copy = sizeof(vd->chunk) - vd->chunksize; + if (copy > len) { + copy = len; + } + memcpy((void *)(&vd->chunk) + vd->chunksize, buf, copy); + vd->chunksize += copy; + buf += copy; + len -= copy; + if (vd->chunksize < sizeof(vd->chunk)) { + break; + } + + assert(vd->msgbuf == NULL); + vd->msgbuf = g_malloc0(vd->chunk.size); + } + + copy = vd->chunk.size - vd->msgsize; + if (copy > len) { + copy = len; + } + memcpy(vd->msgbuf + vd->msgsize, buf, copy); + vd->msgsize += copy; + buf += copy; + len -= copy; + + if (vd->msgsize == vd->chunk.size) { + trace_vdagent_recv_chunk(vd->chunk.size); + vdagent_chr_recv_chunk(vd); + vdagent_reset_bufs(vd); + } + } + + return ret; +} + +static void vdagent_chr_accept_input(Chardev *chr) +{ + VDAgentChardev *vd = QEMU_VDAGENT_CHARDEV(chr); + + vdagent_send_buf(vd); +} + +static void vdagent_chr_set_fe_open(struct Chardev *chr, int fe_open) +{ + VDAgentChardev *vd = QEMU_VDAGENT_CHARDEV(chr); + + if (!fe_open) { + trace_vdagent_close(); + /* reset state */ + vdagent_reset_bufs(vd); + vd->caps = 0; + if (vd->mouse_hs) { + qemu_input_handler_deactivate(vd->mouse_hs); + } + if (vd->cbpeer.update.notify) { + qemu_clipboard_peer_unregister(&vd->cbpeer); + memset(&vd->cbpeer, 0, sizeof(vd->cbpeer)); + } + return; + } + + trace_vdagent_open(); +} + +static void vdagent_chr_parse(QemuOpts *opts, ChardevBackend *backend, + Error **errp) +{ + ChardevQemuVDAgent *cfg; + + backend->type = CHARDEV_BACKEND_KIND_QEMU_VDAGENT; + cfg = backend->u.qemu_vdagent.data = g_new0(ChardevQemuVDAgent, 1); + qemu_chr_parse_common(opts, qapi_ChardevQemuVDAgent_base(cfg)); + cfg->has_mouse = true; + cfg->mouse = qemu_opt_get_bool(opts, "mouse", VDAGENT_MOUSE_DEFAULT); + cfg->has_clipboard = true; + cfg->clipboard = qemu_opt_get_bool(opts, "clipboard", VDAGENT_CLIPBOARD_DEFAULT); +} + +/* ------------------------------------------------------------------ */ + +static void vdagent_chr_class_init(ObjectClass *oc, void *data) +{ + ChardevClass *cc = CHARDEV_CLASS(oc); + + cc->parse = vdagent_chr_parse; + cc->open = vdagent_chr_open; + cc->chr_write = vdagent_chr_write; + cc->chr_set_fe_open = vdagent_chr_set_fe_open; + cc->chr_accept_input = vdagent_chr_accept_input; +} + +static void vdagent_chr_init(Object *obj) +{ + VDAgentChardev *vd = QEMU_VDAGENT_CHARDEV(obj); + + buffer_init(&vd->outbuf, "vdagent-outbuf"); +} + +static void vdagent_chr_fini(Object *obj) +{ + VDAgentChardev *vd = QEMU_VDAGENT_CHARDEV(obj); + + buffer_free(&vd->outbuf); +} + +static const TypeInfo vdagent_chr_type_info = { + .name = TYPE_CHARDEV_QEMU_VDAGENT, + .parent = TYPE_CHARDEV, + .instance_size = sizeof(VDAgentChardev), + .instance_init = vdagent_chr_init, + .instance_finalize = vdagent_chr_fini, + .class_init = vdagent_chr_class_init, +}; + +static void register_types(void) +{ + type_register_static(&vdagent_chr_type_info); +} + +type_init(register_types); diff --git a/ui/vnc-clipboard.c b/ui/vnc-clipboard.c new file mode 100644 index 0000000000..9f077965d0 --- /dev/null +++ b/ui/vnc-clipboard.c @@ -0,0 +1,323 @@ +/* + * QEMU VNC display driver -- clipboard support + * + * Copyright (C) 2021 Gerd Hoffmann <kraxel@redhat.com> + * + * 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-common.h" +#include "vnc.h" +#include "vnc-jobs.h" + +static uint8_t *inflate_buffer(uint8_t *in, uint32_t in_len, uint32_t *size) +{ + z_stream stream = { + .next_in = in, + .avail_in = in_len, + .zalloc = Z_NULL, + .zfree = Z_NULL, + }; + uint32_t out_len = 8; + uint8_t *out = g_malloc(out_len); + int ret; + + stream.next_out = out + stream.total_out; + stream.avail_out = out_len - stream.total_out; + + ret = inflateInit(&stream); + if (ret != Z_OK) { + goto err; + } + + while (stream.avail_in) { + ret = inflate(&stream, Z_FINISH); + switch (ret) { + case Z_OK: + case Z_STREAM_END: + break; + case Z_BUF_ERROR: + out_len <<= 1; + if (out_len > (1 << 20)) { + goto err_end; + } + out = g_realloc(out, out_len); + stream.next_out = out + stream.total_out; + stream.avail_out = out_len - stream.total_out; + break; + default: + goto err_end; + } + } + + *size = stream.total_out; + inflateEnd(&stream); + + return out; + +err_end: + inflateEnd(&stream); +err: + g_free(out); + return NULL; +} + +static uint8_t *deflate_buffer(uint8_t *in, uint32_t in_len, uint32_t *size) +{ + z_stream stream = { + .next_in = in, + .avail_in = in_len, + .zalloc = Z_NULL, + .zfree = Z_NULL, + }; + uint32_t out_len = 8; + uint8_t *out = g_malloc(out_len); + int ret; + + stream.next_out = out + stream.total_out; + stream.avail_out = out_len - stream.total_out; + + ret = deflateInit(&stream, Z_DEFAULT_COMPRESSION); + if (ret != Z_OK) { + goto err; + } + + while (ret != Z_STREAM_END) { + ret = deflate(&stream, Z_FINISH); + switch (ret) { + case Z_OK: + case Z_STREAM_END: + break; + case Z_BUF_ERROR: + out_len <<= 1; + if (out_len > (1 << 20)) { + goto err_end; + } + out = g_realloc(out, out_len); + stream.next_out = out + stream.total_out; + stream.avail_out = out_len - stream.total_out; + break; + default: + goto err_end; + } + } + + *size = stream.total_out; + deflateEnd(&stream); + + return out; + +err_end: + deflateEnd(&stream); +err: + g_free(out); + return NULL; +} + +static void vnc_clipboard_send(VncState *vs, uint32_t count, uint32_t *dwords) +{ + int i; + + vnc_lock_output(vs); + vnc_write_u8(vs, VNC_MSG_SERVER_CUT_TEXT); + vnc_write_u8(vs, 0); + vnc_write_u8(vs, 0); + vnc_write_u8(vs, 0); + vnc_write_s32(vs, -(count * sizeof(uint32_t))); /* -(message length) */ + for (i = 0; i < count; i++) { + vnc_write_u32(vs, dwords[i]); + } + vnc_unlock_output(vs); + vnc_flush(vs); +} + +static void vnc_clipboard_provide(VncState *vs, + QemuClipboardInfo *info, + QemuClipboardType type) +{ + uint32_t flags = 0; + g_autofree uint8_t *buf = NULL; + g_autofree void *zbuf = NULL; + uint32_t zsize; + + switch (type) { + case QEMU_CLIPBOARD_TYPE_TEXT: + flags |= VNC_CLIPBOARD_TEXT; + break; + default: + return; + } + flags |= VNC_CLIPBOARD_PROVIDE; + + buf = g_malloc(info->types[type].size + 4); + buf[0] = (info->types[type].size >> 24) & 0xff; + buf[1] = (info->types[type].size >> 16) & 0xff; + buf[2] = (info->types[type].size >> 8) & 0xff; + buf[3] = (info->types[type].size >> 0) & 0xff; + memcpy(buf + 4, info->types[type].data, info->types[type].size); + zbuf = deflate_buffer(buf, info->types[type].size + 4, &zsize); + if (!zbuf) { + return; + } + + vnc_lock_output(vs); + vnc_write_u8(vs, VNC_MSG_SERVER_CUT_TEXT); + vnc_write_u8(vs, 0); + vnc_write_u8(vs, 0); + vnc_write_u8(vs, 0); + vnc_write_s32(vs, -(sizeof(uint32_t) + zsize)); /* -(message length) */ + vnc_write_u32(vs, flags); + vnc_write(vs, zbuf, zsize); + vnc_unlock_output(vs); + vnc_flush(vs); +} + +static void vnc_clipboard_notify(Notifier *notifier, void *data) +{ + VncState *vs = container_of(notifier, VncState, cbpeer.update); + QemuClipboardInfo *info = data; + QemuClipboardType type; + bool self_update = info->owner == &vs->cbpeer; + uint32_t flags = 0; + + if (info != vs->cbinfo) { + qemu_clipboard_info_unref(vs->cbinfo); + vs->cbinfo = qemu_clipboard_info_ref(info); + vs->cbpending = 0; + if (!self_update) { + if (info->types[QEMU_CLIPBOARD_TYPE_TEXT].available) { + flags |= VNC_CLIPBOARD_TEXT; + } + flags |= VNC_CLIPBOARD_NOTIFY; + vnc_clipboard_send(vs, 1, &flags); + } + return; + } + + if (self_update) { + return; + } + + for (type = 0; type < QEMU_CLIPBOARD_TYPE__COUNT; type++) { + if (vs->cbpending & (1 << type)) { + vs->cbpending &= ~(1 << type); + vnc_clipboard_provide(vs, info, type); + } + } +} + +static void vnc_clipboard_request(QemuClipboardInfo *info, + QemuClipboardType type) +{ + VncState *vs = container_of(info->owner, VncState, cbpeer); + uint32_t flags = 0; + + if (type == QEMU_CLIPBOARD_TYPE_TEXT) { + flags |= VNC_CLIPBOARD_TEXT; + } + if (!flags) { + return; + } + flags |= VNC_CLIPBOARD_REQUEST; + + vnc_clipboard_send(vs, 1, &flags); +} + +void vnc_client_cut_text_ext(VncState *vs, int32_t len, uint32_t flags, uint8_t *data) +{ + if (flags & VNC_CLIPBOARD_CAPS) { + /* need store caps somewhere ? */ + return; + } + + if (flags & VNC_CLIPBOARD_NOTIFY) { + QemuClipboardInfo *info = + qemu_clipboard_info_new(&vs->cbpeer, QEMU_CLIPBOARD_SELECTION_CLIPBOARD); + if (flags & VNC_CLIPBOARD_TEXT) { + info->types[QEMU_CLIPBOARD_TYPE_TEXT].available = true; + } + qemu_clipboard_update(info); + qemu_clipboard_info_unref(info); + return; + } + + if (flags & VNC_CLIPBOARD_PROVIDE && + vs->cbinfo && + vs->cbinfo->owner == &vs->cbpeer) { + uint32_t size = 0; + g_autofree uint8_t *buf = inflate_buffer(data, len - 4, &size); + if ((flags & VNC_CLIPBOARD_TEXT) && + buf && size >= 4) { + uint32_t tsize = read_u32(buf, 0); + uint8_t *tbuf = buf + 4; + if (tsize < size) { + qemu_clipboard_set_data(&vs->cbpeer, vs->cbinfo, + QEMU_CLIPBOARD_TYPE_TEXT, + tsize, tbuf, true); + } + } + } + + if (flags & VNC_CLIPBOARD_REQUEST && + vs->cbinfo && + vs->cbinfo->owner != &vs->cbpeer) { + if ((flags & VNC_CLIPBOARD_TEXT) && + vs->cbinfo->types[QEMU_CLIPBOARD_TYPE_TEXT].available) { + if (vs->cbinfo->types[QEMU_CLIPBOARD_TYPE_TEXT].data) { + vnc_clipboard_provide(vs, vs->cbinfo, QEMU_CLIPBOARD_TYPE_TEXT); + } else { + vs->cbpending |= (1 << QEMU_CLIPBOARD_TYPE_TEXT); + qemu_clipboard_request(vs->cbinfo, QEMU_CLIPBOARD_TYPE_TEXT); + } + } + } +} + +void vnc_client_cut_text(VncState *vs, size_t len, uint8_t *text) +{ + QemuClipboardInfo *info = + qemu_clipboard_info_new(&vs->cbpeer, QEMU_CLIPBOARD_SELECTION_CLIPBOARD); + + qemu_clipboard_set_data(&vs->cbpeer, info, QEMU_CLIPBOARD_TYPE_TEXT, + len, text, true); + qemu_clipboard_info_unref(info); +} + +void vnc_server_cut_text_caps(VncState *vs) +{ + uint32_t caps[2]; + + if (!vnc_has_feature(vs, VNC_FEATURE_CLIPBOARD_EXT)) { + return; + } + + caps[0] = (VNC_CLIPBOARD_PROVIDE | + VNC_CLIPBOARD_NOTIFY | + VNC_CLIPBOARD_REQUEST | + VNC_CLIPBOARD_CAPS | + VNC_CLIPBOARD_TEXT); + caps[1] = 0; + vnc_clipboard_send(vs, 2, caps); + + vs->cbpeer.name = "vnc"; + vs->cbpeer.update.notify = vnc_clipboard_notify; + vs->cbpeer.request = vnc_clipboard_request; + qemu_clipboard_peer_register(&vs->cbpeer); +} diff --git a/ui/vnc.c b/ui/vnc.c index 456db47d71..b3d4d7b9a5 100644 --- a/ui/vnc.c +++ b/ui/vnc.c @@ -25,6 +25,7 @@ */ #include "qemu/osdep.h" +#include "qemu-common.h" #include "vnc.h" #include "vnc-jobs.h" #include "trace.h" @@ -596,7 +597,7 @@ bool vnc_display_reload_certs(const char *id, Error **errp) } if (!vd->tlscreds) { - error_setg(errp, "vnc tls is not enable"); + error_setg(errp, "vnc tls is not enabled"); return false; } @@ -1352,6 +1353,9 @@ void vnc_disconnect_finish(VncState *vs) /* last client gone */ vnc_update_server_surface(vs->vd); } + if (vs->cbpeer.update.notify) { + qemu_clipboard_peer_unregister(&vs->cbpeer); + } vnc_unlock_output(vs); @@ -1777,10 +1781,6 @@ uint32_t read_u32(uint8_t *data, size_t offset) (data[offset + 2] << 8) | data[offset + 3]); } -static void client_cut_text(VncState *vs, size_t len, uint8_t *text) -{ -} - static void check_pointer_type_change(Notifier *notifier, void *data) { VncState *vs = container_of(notifier, VncState, mouse_mode_notifier); @@ -2222,6 +2222,10 @@ static void set_encodings(VncState *vs, int32_t *encodings, size_t n_encodings) send_xvp_message(vs, VNC_XVP_CODE_INIT); } break; + case VNC_ENCODING_CLIPBOARD_EXT: + vs->features |= VNC_FEATURE_CLIPBOARD_EXT_MASK; + vnc_server_cut_text_caps(vs); + break; case VNC_ENCODING_COMPRESSLEVEL0 ... VNC_ENCODING_COMPRESSLEVEL0 + 9: vs->tight->compression = (enc & 0x0F); break; @@ -2438,7 +2442,7 @@ static int protocol_client_msg(VncState *vs, uint8_t *data, size_t len) return 8; } if (len == 8) { - uint32_t dlen = read_u32(data, 4); + uint32_t dlen = abs(read_s32(data, 4)); if (dlen > (1 << 20)) { error_report("vnc: client_cut_text msg payload has %u bytes" " which exceeds our limit of 1MB.", dlen); @@ -2450,7 +2454,12 @@ static int protocol_client_msg(VncState *vs, uint8_t *data, size_t len) } } - client_cut_text(vs, read_u32(data, 4), data + 8); + if (read_s32(data, 4) < 0) { + vnc_client_cut_text_ext(vs, abs(read_s32(data, 4)), + read_u32(data, 8), data + 12); + break; + } + vnc_client_cut_text(vs, read_u32(data, 4), data + 8); break; case VNC_MSG_CLIENT_XVP: if (!(vs->features & VNC_FEATURE_XVP)) { diff --git a/ui/vnc.h b/ui/vnc.h index d4f3e15558..a7149831f9 100644 --- a/ui/vnc.h +++ b/ui/vnc.h @@ -29,6 +29,7 @@ #include "qemu/queue.h" #include "qemu/thread.h" +#include "ui/clipboard.h" #include "ui/console.h" #include "audio/audio.h" #include "qemu/bitmap.h" @@ -348,6 +349,10 @@ struct VncState Notifier mouse_mode_notifier; + QemuClipboardPeer cbpeer; + QemuClipboardInfo *cbinfo; + uint32_t cbpending; + QTAILQ_ENTRY(VncState) next; }; @@ -417,6 +422,7 @@ enum { #define VNC_ENCODING_XVP 0XFFFFFECB /* -309 */ #define VNC_ENCODING_ALPHA_CURSOR 0XFFFFFEC6 /* -314 */ #define VNC_ENCODING_WMVi 0x574D5669 +#define VNC_ENCODING_CLIPBOARD_EXT 0xc0a1e5ce /***************************************************************************** * @@ -458,6 +464,7 @@ enum VncFeatures { VNC_FEATURE_ZYWRLE, VNC_FEATURE_LED_STATE, VNC_FEATURE_XVP, + VNC_FEATURE_CLIPBOARD_EXT, }; #define VNC_FEATURE_RESIZE_MASK (1 << VNC_FEATURE_RESIZE) @@ -474,6 +481,7 @@ enum VncFeatures { #define VNC_FEATURE_ZYWRLE_MASK (1 << VNC_FEATURE_ZYWRLE) #define VNC_FEATURE_LED_STATE_MASK (1 << VNC_FEATURE_LED_STATE) #define VNC_FEATURE_XVP_MASK (1 << VNC_FEATURE_XVP) +#define VNC_FEATURE_CLIPBOARD_EXT_MASK (1 << VNC_FEATURE_CLIPBOARD_EXT) /* Client -> Server message IDs */ @@ -535,6 +543,17 @@ enum VncFeatures { #define VNC_XVP_ACTION_REBOOT 3 #define VNC_XVP_ACTION_RESET 4 +/* extended clipboard flags */ +#define VNC_CLIPBOARD_TEXT (1 << 0) +#define VNC_CLIPBOARD_RTF (1 << 1) +#define VNC_CLIPBOARD_HTML (1 << 2) +#define VNC_CLIPBOARD_DIB (1 << 3) +#define VNC_CLIPBOARD_FILES (1 << 4) +#define VNC_CLIPBOARD_CAPS (1 << 24) +#define VNC_CLIPBOARD_REQUEST (1 << 25) +#define VNC_CLIPBOARD_PEEK (1 << 26) +#define VNC_CLIPBOARD_NOTIFY (1 << 27) +#define VNC_CLIPBOARD_PROVIDE (1 << 28) /***************************************************************************** * @@ -618,4 +637,9 @@ int vnc_zrle_send_framebuffer_update(VncState *vs, int x, int y, int w, int h); int vnc_zywrle_send_framebuffer_update(VncState *vs, int x, int y, int w, int h); void vnc_zrle_clear(VncState *vs); +/* vnc-clipboard.c */ +void vnc_server_cut_text_caps(VncState *vs); +void vnc_client_cut_text(VncState *vs, size_t len, uint8_t *text); +void vnc_client_cut_text_ext(VncState *vs, int32_t len, uint32_t flags, uint8_t *data); + #endif /* QEMU_VNC_H */ diff --git a/util/qemu-sockets.c b/util/qemu-sockets.c index 8af0278f15..2463c49773 100644 --- a/util/qemu-sockets.c +++ b/util/qemu-sockets.c @@ -1116,14 +1116,10 @@ fail: return NULL; } -static int socket_get_fd(const char *fdstr, int num, Error **errp) +static int socket_get_fd(const char *fdstr, Error **errp) { Monitor *cur_mon = monitor_cur(); int fd; - if (num != 1) { - error_setg_errno(errp, EINVAL, "socket_get_fd: too many connections"); - return -1; - } if (cur_mon) { fd = monitor_get_fd(cur_mon, fdstr, errp); if (fd < 0) { @@ -1159,7 +1155,7 @@ int socket_connect(SocketAddress *addr, Error **errp) break; case SOCKET_ADDRESS_TYPE_FD: - fd = socket_get_fd(addr->u.fd.str, 1, errp); + fd = socket_get_fd(addr->u.fd.str, errp); break; case SOCKET_ADDRESS_TYPE_VSOCK: @@ -1187,7 +1183,26 @@ int socket_listen(SocketAddress *addr, int num, Error **errp) break; case SOCKET_ADDRESS_TYPE_FD: - fd = socket_get_fd(addr->u.fd.str, num, errp); + fd = socket_get_fd(addr->u.fd.str, errp); + if (fd < 0) { + return -1; + } + + /* + * If the socket is not yet in the listen state, then transition it to + * the listen state now. + * + * If it's already listening then this updates the backlog value as + * requested. + * + * If this socket cannot listen because it's already in another state + * (e.g. unbound or connected) then we'll catch the error here. + */ + if (listen(fd, num) != 0) { + error_setg_errno(errp, errno, "Failed to listen on fd socket"); + closesocket(fd); + return -1; + } break; case SOCKET_ADDRESS_TYPE_VSOCK: |