zoukankan      html  css  js  c++  java
  • 通过Python调用Spice-gtk

    序言

    通过Virt Manager研究学习Spice gtk的Python方法

    你将学到什么

    Virt Manager研究

    显示代码定位

    首先我们使用Virt Manager来观察桌面连接窗口

    然后我们使用glade打开Virt Manager源码目录下的ui目录下的文件进行比对,发现details.ui就是我们前面见到的窗口,对应的处理代码为details.py,并且我们知道对应的处理信号为on_control_vm_console_toggled

    接着我们在details.py文件找到对应处理函数

    # 绑定信号处理函数
    "on_control_vm_console_toggled": self.details_console_changed,
    # 处理函数核心逻辑
    pages.set_current_page(DETAILS_PAGE_CONSOLE)
    

    分析函数我们知道信号的处理逻辑就是切换到DETAILS_PAGE_CONSOLE页面,然后我们找到对应的页面知道对应的处理信号为on_console_pages_switch_page

    查找信号对应的处理函数,我们知道最终的显示代码位于console.py中,继续分析UI文件,我们知道我们最终要关注的部件就是console-gfx-viewport,接下来我们来研究下console.py文件的vmmConsolePages

    vmmConsolePages类分析

    首先分析类的初始化代码,我们发现如下注释

    def __init__(self, vm, builder, topwin)
        # Initialize display widget
        self._viewer = None
    

    很显然这个_viewer就是用作显示的widget,查找代码找到_viewer赋值处

    def _init_viewer(self):
    # 省略部分代码
    try:
        if ginfo.gtype == "vnc":
            viewer_class = VNCViewer
        elif ginfo.gtype == "spice":
            if have_spice_gtk:
                viewer_class = SpiceViewer
            else:
                raise RuntimeError("Error opening Spice console, "
     "SpiceClientGtk missing")
    
        self._viewer = viewer_class(self.vm, ginfo)
        self._connect_viewer_signals()
    
        self._refresh_enable_accel()
    
        self._viewer.console_open()
    except Exception, e:
        logging.exception("Error connection to graphical console")
        self._activate_unavailable_page(
                _("Error connecting to graphical console") + ":
    %s" % e)
    

    很显然SpiceViewer就是实现spice gtk调用的类,它位于viewers.py文件中,在研究这个类之前我们要先研究下传入的两个参数viewer_class(self.vm, ginfo),其中self.vm是一个vmmDomain类对象(位于domain.py文件),ginfo变量是一个ConnectionInfo类对象(位于sshtunnels.py文件)。ConnectionInfo类对象作用就是保存套接字服务端信息,vmmDomain类对象作用就是保存libvirt虚拟机信息。

    SpiceViewer代码分析

    首先我们来看下官方文档的描述

    SpiceSession处理所有SpiceChannel连接,它同时还保存了连接信息比如服务器地址和端口。你也可以简单的设置"uri"比如 spice://127.0.0.1?port=5930 来设置你的连接信息。通过关联channel-new信号到SpiceSession对象,你将会收到连接就绪通知,并进行互动。比如SpiceInputsChannel准备就绪并且收到SPICE_CHANNEL_OPENED事件,你就可以通过spice_inputs_key_press()来发送按键事件;如果SpiceMainChannel处于可用状态,你就可以开始共享剪切板了等等。一旦SpiceSession对象属性设置完成,我们就能通过调用spice_session_connect()开始与Spice server的通信。

    通过官网的描述使用SpiceSession的步骤如下:

    • 创建SpiceSession对象
    • 关联channel-new信号
    • 设置连接信息
    • 调用spice_session_connect

    SpiceViewer的调用顺序为console_open -> _open -> _open_host/_open_fd(完成了后两步) -> _create_spice_session(完成了前两步),接下来我们主要分析channel-new信号的处理函数_channel_new_cb里面描述了对Display Channel的处理。

    最简单的显示代码

    # -*- coding: utf-8 -*-
    import gi
    gi.require_version('SpiceClientGtk', '3.0')
    gi.require_version('Gtk', '3.0')
    from gi.repository import SpiceClientGtk,SpiceClientGLib,Gtk,GObject
    
    class Spicy(Gtk.Window):
        def __init__(self, host, port):
            Gtk.Window.__init__(self, title="Spicy")
            self._display = None
            self._spice_session = SpiceClientGLib.Session()
            GObject.GObject.connect(self._spice_session, "channel-new", self._channel_new_cb)
            self._spice_session.set_property("host", str(host))
            self._spice_session.set_property("port", str(port))
            self._spice_session.connect()
    
        def _channel_new_cb(self, session, channel):
            if (type(channel) == SpiceClientGLib.DisplayChannel and not self._display):
                channel_id = channel.get_property("channel-id")
                self._display_channel = channel
                self._display = SpiceClientGtk.Display.new(self._spice_session, channel_id)
                self._display.show()
                self.add(self._display)
    
        def _fd_channel_event_cb(self, channel, event):
            channel.disconnect_by_func(self._fd_channel_event_cb)
    
        def _channel_open_fd_request(self, channel, tls_ignore):
            channel.connect_after("channel-event", self._fd_channel_event_cb)
    
            fd = self._get_fd_for_open()
            channel.open_fd(fd)
    
    window = Spicy("127.0.0.1", "5900")
    window.connect("delete-event", Gtk.main_quit)
    window.show_all()
    Gtk.main()
    

    编写Spicy

    类名 说明 用户需要处理的信号 用户需要读取的属性
    Spice Session 负责客户端和服务器的套接字连接 channel-new: 根据服务器的配置在套接字上建立对应的通道 channel-destroy: 关闭通道 name: SPICE服务名 password: TLS密码 host: SPICE主机地址 port: 未加密会话端口 tls-port: 加密会话端口
    Spice Channel 负责功能模块的通信 channel-event: 监听通道事件 open-fd:通道与套接字的绑定
    Spice Display 负责显示部件的建立 mouse-grab: 鼠标捕获与释放

    Viewer: 远程桌面连接客户端的Gtk.Widget,对Spice Display的封装,需要提供以下功能

    • 根据传入的服务器信息,能建立和关闭与服务器的会话连接
    • 在Spice Display显示部件完成初始化后能发出通知,使得父窗口能自动完成显示部件添加动作

    基类Viewer

    公共方法

    方法名 参数 说明
    def open(self) None 建立与服务器的会话连接,分为两种方式,一种是使用用户传入的套接字建立会话,一种是协议自身根据传入的服务器信息建立套接字,然后建立会话
    def close(self) None 关闭与服务器的会话连接,需要子类根据自身资源重写
    def remove_display_widget(self, widget) widget: 包含显示部件的父窗口 关闭与服务器的会话连接

    私有方法

    方法名 参数 说明
    def _get_fd_for_open(self) None 获取用户传入的套接字,如果用户只传入服务器信息则返回None
    def _open_fd(self, fd) fd: 用户传入的套接字句柄 使用用户传入的套接字建立会话
    def _open_host(self) None 协议自身根据传入的服务器信息建立套接字并建立会话

    SpiceViewer

    重写方法

    方法名 参数 说明
    def close(self) None 释放显示部件,并断开Spice会话连接
    def _open_fd(self, fd) fd: 用户传入的套接字句柄 使用用户传入的套接字建立Spice会话
    def _open_host(self) None 根据传入的服务器信息设置Spice会话属性,Spice会话将根据设置的服务器属性自动建立会话连接
    def _create_session(self) None 建立Spice会话对象并监听channel创建消息
    def _channel_new_cb(self, session, channel) session: Spice会话 channel: Spice通道 针对不同通道的建立做不同处理,例如当检测到显示通道建立时,创建显示部件,并触发显示部件添加消息
    def _channel_open_fd_cb(self, channel, with_tls) channel: 没有fd关联的Spice通道 with_tls: 是不是SSH连接 Spice通道在连接时如果没有检测到Spice会话关联的fd,则触发此回调,根据Spice会话保存的服务器信息创建会话连接,然后将连接的套接字句柄关联到Spice通道上,然后再次执行通道连接动作
    def _channel_event_cb(self, channel, event) channel: Spice通道 event: 事件 如果通道关闭时出现异常就会触发此回调
    def _mouse_grab_cb(self, display, status) display: 显示部件 status
    : 鼠标捕获状态 显示部件捕获丢失鼠标时触发此回调,status为1表示捕获鼠标,0表示丢失鼠标,我们需要把这消息继续路由出去,让使用者能做更多控制
    def _grab_keys_pressed_cb(self, display) display: 显示部件 当释放鼠标键盘的组合键被按下时触发此回调,我们还要继续把这消息路由出去,让使用者能做更多控制

    样例代码

    viewers.py文件

    # -*- coding: utf-8 -*-
    
    import gi
    gi.require_version('SpiceClientGtk', '3.0')
    gi.require_version('Gtk', '3.0')
    from gi.repository import SpiceClientGtk, SpiceClientGLib, GObject, Gtk
    
    class Viewer(GObject.GObject):
    
        __gsignals__ = {
            "add-display-widget": (GObject.SignalFlags.RUN_FIRST, None, [object]),
            "size-allocate": (GObject.SignalFlags.RUN_FIRST, None, [object]),
            "focus-in-event": (GObject.SignalFlags.RUN_FIRST, None, [object]),
            "focus-out-event": (GObject.SignalFlags.RUN_FIRST, None, [object]),
            "pointer-grab": (GObject.SignalFlags.RUN_FIRST, None, []),
            "pointer-ungrab": (GObject.SignalFlags.RUN_FIRST, None, []),
            "connected": (GObject.SignalFlags.RUN_FIRST, None, []),
            "disconnected": (GObject.SignalFlags.RUN_FIRST, None, [str, str]),
            "auth-error": (GObject.SignalFlags.RUN_FIRST, None, [str, bool]),
            "auth-rejected": (GObject.SignalFlags.RUN_FIRST, None, [str]),
            "need-auth": (GObject.SignalFlags.RUN_FIRST, None, [bool, bool]),
            "agent-connected": (GObject.SignalFlags.RUN_FIRST, None, []),
            "grab-keys-pressed": (GObject.SignalFlags.RUN_FIRST, None, []),
        }
    
        def __init__(self, info):
            GObject.GObject.__init__(self)
            self._display = None
            self._info = info # 服务器信息
    
        # 建立与服务器的会话连接
        def open(self):
            fd = self._get_fd_for_open()
            if fd is not None:
                self._open_fd(fd)
            else:
                self._open_host()
    
        # 关闭与服务器的会话连接
        def close(self):
            raise NotImplementedError()
    
        # 获取用户创建的与服务器的套接字连接
        def _get_fd_for_open(self):
            None
    
        def remove_display_widget(self, widget):
            if self._display and self._display in widget.get_children():
                widget.remove(self._display)
    
        #######################################################
    
        # Internal API that will be overwritten by subclasses #
    
        #######################################################
    
        # 使用用户创建的套接字来建立会话连接
        def _open_fd(self, fd):
            raise NotImplementedError()
    
        # 使用连接协议自动创建的套接字来建立会话连接
        def _open_host(self):
            raise NotImplementedError()
    
    class SpiceViewer(Viewer):
    
        def __init__(self, *args, **kwargs):
            Viewer.__init__(self, *args, **kwargs)
            self._spice_session = None
    
        def close(self):
            if self._display:
                self._display.destroy()
            self._spice_session.disconnect()
    
        def _open_fd(self, fd):
            self._create_session()
            self._spice_session.open_fd(fd)
    
        def _open_host(self):
            self._create_session()
            host, port, tlsport = self._info.get_conn_host()
            self._spice_session.set_property("host", str(host))
            if port:
                self._spice_session.set_property("port", str(port))
            if tlsport:
                self._spice_session.set_property("tls-port", str(tlsport))
            self._spice_session.connect()
    
        # 创建spice会话对象
        def _create_session(self):
            self._spice_session = SpiceClientGLib.Session.new()
            GObject.GObject.connect(self._spice_session, "channel-new", self._channel_new_cb)
    
        # channel创建信号回调函数
        def _channel_new_cb(self, session, channel):
            GObject.GObject.connect(channel, "open-fd", self._channel_open_fd_cb)
    
            if (type(channel) == SpiceClientGLib.MainChannel):
                GObject.GObject.connect(channel, "channel-event", self._channel_event_cb)
            elif (type(channel) == SpiceClientGLib.DisplayChannel and not self._display):
                # 创建显示部件
                channel_id = channel.get_property("channel-id")
                self._display = SpiceClientGtk.Display.new(session, channel_id)
                self.emit("add-display-widget", self._display)
                self._display.realize()
                self._display.connect("mouse-grab", self._mouse_grab_cb)
                self._display.connect("grab-keys-pressed", self._grab_keys_pressed_cb)
                self._display.show()
                channel.connect()
            elif (type(channel) == SpiceClientGLib.InputsChannel):
                None
            elif (type(channel) == SpiceClientGLib.PlaybackChannel):
                None
    
        # channel关联套接字句柄信号回调函数,当channel.connect()发现spice会话对象没有可用的套接字句柄时会触发此信号
        def _channel_open_fd_cb(self, channel, with_tls):
            None
    
        # channel事件信号回调函数
        def _channel_event_cb(self, channel, event):
            if event == SpiceClientGLib.ChannelEvent.CLOSED:
                self.emit("disconnected", None, None)
            elif event == SpiceClientGLib.ChannelEvent.ERROR_AUTH:
                if not self._spice_session.get_property("password"):
                    self.emit("need-auth", True, False)
                else:
                    self.emit("auth-error", channel.get_error().message, False)
            elif "ERROR" in str(event):
                self.emit("disconnected", channel.get_error().message, None)
    
        # 鼠标捕获信号回调函数
        def _mouse_grab_cb(self, display, status):
            if status:
                self.emit("pointer-grab")
            else:
                self.emit("pointer-ungrab")
    
        # 捕获键按下信号回调函数
        def _grab_keys_pressed_cb(self, display):
            self.emit("grab-keys-pressed")
    

    main.py文件

    # -*- coding: utf-8 -*-
    
    import gi
    gi.require_version('Gtk', '3.0')
    from gi.repository import Gtk,GObject
    from viewers import SpiceViewer
    
    class HostInfo():
        def __init__(self):
            self.gaddr = "127.0.0.1"
            self.gport = 5900
            self.gtlsport = None
    
        def get_conn_host(self):
            host = self.gaddr
            port = self.gport
            tlsport = self.gtlsport
            return host, port, tlsport
    
    def add_display_widget_cb(viewer, display):
        win.add(display)
        win.fullscreen()
    
    def grab_keys_pressed_cb(viewer):
        win.unfullscreen()
    
    win = Gtk.Window(title="Spicy")
    info = HostInfo()
    viewer = SpiceViewer(info)
    viewer.open()
    viewer.connect("add-display-widget", add_display_widget_cb)
    viewer.connect("grab-keys-pressed", grab_keys_pressed_cb)
    win.connect("delete-event", Gtk.main_quit)
    win.show_all()
    Gtk.main()
    
  • 相关阅读:
    Xamarin.Forms
    Docker Azure Kubernetes
    出现( linker command failed with exit code 1)错误总结(http://blog.csdn.net/hengshujiyi/article/details/21182813)
    UITextView学习笔记
    UIScrollView学习笔记
    如何学习ios(摘自知乎https://www.zhihu.com/question/20016551)
    iOS手势操作,拖动,轻击,捏合,旋转,长按,自定义(http://www.cnblogs.com/huangjianwu/p/4675648.html)
    触屏事件
    给UITextView添加链接
    IOS绘图详解(http://blog.163.com/wkyuyang_001/blog/static/10802122820133190545227/)
  • 原文地址:https://www.cnblogs.com/silvermagic/p/7666216.html
Copyright © 2011-2022 走看看