Coverage for /home/antoine/projects/xpra-git/dist/python3/lib64/python/xpra/client/gobject_client_base.py : 51%
Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1# This file is part of Xpra.
2# Copyright (C) 2010-2020 Antoine Martin <antoine@xpra.org>
3# Copyright (C) 2008, 2010 Nathaniel Smith <njs@pobox.com>
4# Xpra is released under the terms of the GNU GPL v2, or, at your option, any
5# later version. See the file COPYING for details.
7import os.path
8import sys
10from gi.repository import GLib
11from gi.repository import GObject
13from xpra.util import (
14 nonl, sorted_nicely, print_nested_dict, envint, flatten_dict, typedict,
15 disconnect_is_an_error, ellipsizer, DONE, first_time,
16 )
17from xpra.os_util import bytestostr, strtobytes, get_hex_uuid, POSIX, OSX, hexstr
18from xpra.simple_stats import std_unit
19from xpra.client.client_base import XpraClientBase, EXTRA_TIMEOUT
20from xpra.exit_codes import (
21 EXIT_OK, EXIT_CONNECTION_LOST, EXIT_TIMEOUT, EXIT_INTERNAL_ERROR,
22 EXIT_FAILURE, EXIT_UNSUPPORTED, EXIT_REMOTE_ERROR, EXIT_FILE_TOO_BIG,
23 )
24from xpra.log import Logger
26log = Logger("gobject", "client")
28FLATTEN_INFO = envint("XPRA_FLATTEN_INFO", 1)
31def errwrite(msg):
32 try:
33 sys.stderr.write(msg)
34 sys.stderr.flush()
35 except (OSError, AttributeError):
36 pass
39class GObjectXpraClient(GObject.GObject, XpraClientBase):
40 """
41 Utility superclass for GObject clients
42 """
43 COMMAND_TIMEOUT = EXTRA_TIMEOUT
45 def __init__(self):
46 self.idle_add = GLib.idle_add
47 self.timeout_add = GLib.timeout_add
48 self.source_remove = GLib.source_remove
49 GObject.GObject.__init__(self)
50 XpraClientBase.__init__(self)
52 def init(self, opts):
53 XpraClientBase.init(self, opts)
54 self.glib_init()
56 def get_scheduler(self):
57 return GLib
60 def install_signal_handlers(self):
61 from xpra.gtk_common.gobject_compat import install_signal_handlers
62 install_signal_handlers("%s Client" % self.client_type(), self.handle_app_signal)
65 def setup_connection(self, conn):
66 protocol = super().setup_connection(conn)
67 protocol._log_stats = False
68 GLib.idle_add(self.send_hello)
69 return protocol
72 def client_type(self):
73 #overriden in subclasses!
74 return "Python3/GObject"
77 def init_packet_handlers(self):
78 XpraClientBase.init_packet_handlers(self)
79 def noop(*args): # pragma: no cover
80 log("ignoring packet: %s", args)
81 #ignore the following packet types without error:
82 #(newer servers should avoid sending us any of those)
83 for t in (
84 "new-window", "new-override-redirect",
85 "draw", "cursor", "bell",
86 "notify_show", "notify_close",
87 "ping", "ping_echo",
88 "window-metadata", "configure-override-redirect",
89 "lost-window",
90 ):
91 self._packet_handlers[t] = noop
93 def run(self):
94 XpraClientBase.run(self)
95 self.glib_mainloop = GLib.MainLoop()
96 self.glib_mainloop.run()
97 return self.exit_code
99 def make_hello(self):
100 capabilities = XpraClientBase.make_hello(self)
101 capabilities["keyboard"] = False
102 return capabilities
104 def quit(self, exit_code):
105 log("quit(%s) current exit_code=%s", exit_code, self.exit_code)
106 if self.exit_code is None:
107 self.exit_code = exit_code
108 self.cleanup()
109 GLib.timeout_add(50, self.exit_loop)
111 def exit_loop(self):
112 self.glib_mainloop.quit()
115class CommandConnectClient(GObjectXpraClient):
116 """
117 Utility superclass for clients that only send one command
118 via the hello packet.
119 """
121 def __init__(self, opts):
122 super().__init__()
123 super().init(opts)
124 self.display_desc = {}
125 #not used by command line clients,
126 #so don't try probing for printers, etc
127 self.file_transfer = False
128 self.printing = False
129 self.command_timeout = None
130 #don't bother with many of these things for one-off commands:
131 for x in ("ui_client", "wants_aliases", "wants_encodings",
132 "wants_versions", "wants_features", "wants_sound", "windows",
133 "webcam", "keyboard", "mouse", "network-state",
134 ):
135 self.hello_extra[x] = False
137 def setup_connection(self, conn):
138 protocol = super().setup_connection(conn)
139 if conn.timeout>0:
140 self.command_timeout = GLib.timeout_add((conn.timeout + self.COMMAND_TIMEOUT) * 1000, self.timeout)
141 return protocol
143 def timeout(self, *_args):
144 log.warn("timeout!") # pragma: no cover
146 def cancel_command_timeout(self):
147 ct = self.command_timeout
148 if ct:
149 self.command_timeout = None
150 GLib.source_remove(ct)
152 def _process_connection_lost(self, _packet):
153 #override so we don't log a warning
154 #"command clients" are meant to exit quickly by losing the connection
155 p = self._protocol
156 if p and p.input_packetcount==0:
157 self.quit(EXIT_CONNECTION_LOST)
158 else:
159 self.quit(EXIT_OK)
161 def server_connection_established(self, caps : typedict):
162 #don't bother parsing the network caps:
163 #* it could cause errors if the caps are missing
164 #* we don't care about sending anything back after hello
165 log("server_capabilities: %s", ellipsizer(caps))
166 log("protocol state: %s", self._protocol.save_state())
167 self.cancel_command_timeout()
168 self.do_command(caps)
169 return True
171 def do_command(self, caps : typedict):
172 raise NotImplementedError()
175class SendCommandConnectClient(CommandConnectClient):
176 """
177 Utility superclass for clients that only send at least one more packet
178 after the hello packet.
179 So unlike CommandConnectClient, we do need the network and encryption to be setup.
180 """
182 def server_connection_established(self, caps):
183 assert self.parse_encryption_capabilities(caps), "encryption failure"
184 assert self.parse_network_capabilities(caps), "network capabilities failure"
185 return super().server_connection_established(caps)
188class HelloRequestClient(SendCommandConnectClient):
189 """
190 Utility superclass for clients that send a server request
191 as part of the hello packet.
192 """
194 def make_hello_base(self):
195 caps = super().make_hello_base()
196 caps.update(self.hello_request())
197 return caps
199 def timeout(self, *_args):
200 self.warn_and_quit(EXIT_TIMEOUT, "timeout: server did not disconnect us")
202 def hello_request(self): # pragma: no cover
203 raise NotImplementedError()
205 def do_command(self, caps : typedict):
206 self.quit(EXIT_OK)
208 def _process_disconnect(self, packet):
209 #overriden method so we can avoid printing a warning,
210 #we haven't received the hello back from the server
211 #but that's fine for a request client
212 info = tuple(nonl(bytestostr(x)) for x in packet[1:])
213 reason = info[0]
214 if disconnect_is_an_error(reason):
215 self.server_disconnect_warning(*info)
216 elif self.exit_code is None:
217 #we're not in the process of exiting already,
218 #tell the user why the server is disconnecting us
219 self.server_disconnect(*info)
222class ScreenshotXpraClient(CommandConnectClient):
223 """ This client does one thing only:
224 it sends the hello packet with a screenshot request
225 and exits when the resulting image is received (or timedout)
226 """
228 def __init__(self, opts, screenshot_filename):
229 self.screenshot_filename = screenshot_filename
230 super().__init__(opts)
231 self.hello_extra["screenshot_request"] = True
232 self.hello_extra["request"] = "screenshot"
234 def timeout(self, *_args):
235 self.warn_and_quit(EXIT_TIMEOUT, "timeout: did not receive the screenshot")
237 def _process_screenshot(self, packet):
238 (w, h, encoding, _, img_data) = packet[1:6]
239 assert encoding==b"png", "expected png screenshot data but got %s" % bytestostr(encoding)
240 if not img_data:
241 self.warn_and_quit(EXIT_OK,
242 "screenshot is empty and has not been saved (maybe there are no windows or they are not currently shown)")
243 return
244 if self.screenshot_filename=="-":
245 output = os.fdopen(sys.stdout.fileno(), "wb", closefd=False)
246 else:
247 output = open(self.screenshot_filename, "wb")
248 with output:
249 output.write(img_data)
250 output.flush()
251 self.warn_and_quit(EXIT_OK, "screenshot %sx%s saved to: %s" % (w, h, self.screenshot_filename))
253 def init_packet_handlers(self):
254 super().init_packet_handlers()
255 self._ui_packet_handlers["screenshot"] = self._process_screenshot
258class InfoXpraClient(CommandConnectClient):
259 """ This client does one thing only:
260 it queries the server with an 'info' request
261 """
263 def __init__(self, opts):
264 super().__init__(opts)
265 self.hello_extra["info_request"] = True
266 self.hello_extra["request"] = "info"
267 if FLATTEN_INFO>=1:
268 self.hello_extra["info-namespace"] = True
270 def timeout(self, *_args):
271 self.warn_and_quit(EXIT_TIMEOUT, "timeout: did not receive the info")
273 def do_command(self, caps : typedict):
274 if caps:
275 if FLATTEN_INFO<2:
276 #compatibility mode:
277 c = flatten_dict(caps)
278 for k in sorted_nicely(c.keys()):
279 v = c.get(k)
280 #FIXME: this is a nasty and horrible python3 workaround (yet again)
281 #we want to print bytes as strings without the ugly 'b' prefix..
282 #it assumes that all the strings are raw or in (possibly nested) lists or tuples only
283 #we assume that all strings we get are utf-8,
284 #and fallback to the bytestostr hack if that fails
285 def fixvalue(w):
286 if isinstance(w, bytes):
287 if k.endswith(".data"):
288 return hexstr(w)
289 try:
290 return w.decode("utf-8")
291 except:
292 return bytestostr(w)
293 elif isinstance(w, (tuple,list)):
294 return type(w)([fixvalue(x) for x in w])
295 return w
296 v = fixvalue(v)
297 k = fixvalue(k)
298 log.info("%s=%s", k, nonl(v))
299 else:
300 print_nested_dict(caps)
301 self.quit(EXIT_OK)
303class IDXpraClient(InfoXpraClient):
305 def __init__(self, *args):
306 super().__init__(*args)
307 self.hello_extra["request"] = "id"
310class ConnectTestXpraClient(CommandConnectClient):
311 """ This client does one thing only:
312 it queries the server with an 'info' request
313 """
315 def __init__(self, opts, **kwargs):
316 super().__init__(opts)
317 self.value = get_hex_uuid()
318 self.hello_extra.update({
319 "connect_test_request" : self.value,
320 "request" : "connect_test",
321 #tells proxy servers we don't want to connect to the real / new instance:
322 "connect" : False,
323 #older servers don't know about connect-test,
324 #pretend that we're interested in info:
325 "info_request" : True,
326 "info-namespace" : True,
327 })
328 self.hello_extra.update(kwargs)
330 def timeout(self, *_args):
331 self.warn_and_quit(EXIT_TIMEOUT, "timeout: no server response")
333 def _process_connection_lost(self, _packet):
334 #we should always receive a hello back and call do_command,
335 #which sets the correct exit code, landing here is an error:
336 self.quit(EXIT_FAILURE)
338 def do_command(self, caps : typedict):
339 if caps:
340 ctr = caps.strget("connect_test_response")
341 log("do_command(..) expected connect test response='%s', got '%s'", self.value, ctr)
342 if ctr==self.value:
343 self.quit(EXIT_OK)
344 else:
345 self.quit(EXIT_INTERNAL_ERROR)
346 else:
347 self.quit(EXIT_FAILURE)
350class MonitorXpraClient(SendCommandConnectClient):
351 """ This client does one thing only:
352 it prints out events received from the server.
353 If the server does not support this feature it exits with an error.
354 """
356 def __init__(self, opts):
357 super().__init__(opts)
358 for x in ("wants_features", "wants_events", "event_request"):
359 self.hello_extra[x] = True
360 self.hello_extra["request"] = "event"
361 self.hello_extra["info-namespace"] = True
363 def timeout(self, *args):
364 pass
365 #self.warn_and_quit(EXIT_TIMEOUT, "timeout: did not receive the info")
367 def do_command(self, caps : typedict):
368 log.info("waiting for server events")
370 def _process_server_event(self, packet):
371 log.info(": ".join(bytestostr(x) for x in packet[1:]))
373 def init_packet_handlers(self):
374 super().init_packet_handlers()
375 self._packet_handlers["server-event"] = self._process_server_event
376 self._packet_handlers["ping"] = self._process_ping
378 def _process_ping(self, packet):
379 echotime = packet[1]
380 self.send("ping_echo", echotime, 0, 0, 0, -1)
383class ShellXpraClient(SendCommandConnectClient):
384 """
385 Provides an interactive shell with the socket it connects to
386 """
388 def __init__(self, opts):
389 super().__init__(opts)
390 self.stdin_io_watch = None
391 self.stdin_buffer = ""
392 self.hello_extra["shell"] = "True"
394 def timeout(self, *args):
395 pass
397 def cleanup(self):
398 siw = self.stdin_io_watch
399 if siw:
400 self.stdin_io_watch = None
401 self.source_remove(siw)
402 super().cleanup()
404 def do_command(self, caps : typedict):
405 if not caps.boolget("shell"):
406 msg = "this server does not support the 'shell' subcommand"
407 log.error(msg)
408 self.disconnect_and_quit(EXIT_UNSUPPORTED, msg)
409 return
410 #start reading from stdin:
411 self.install_signal_handlers()
412 stdin = sys.stdin
413 fileno = stdin.fileno()
414 import fcntl
415 fl = fcntl.fcntl(fileno, fcntl.F_GETFL)
416 fcntl.fcntl(fileno, fcntl.F_SETFL, fl | os.O_NONBLOCK)
417 self.stdin_io_watch = GLib.io_add_watch(sys.stdin,
418 GLib.PRIORITY_DEFAULT, GLib.IO_IN,
419 self.stdin_ready)
420 self.print_prompt()
422 def stdin_ready(self, *_args):
423 data = sys.stdin.read()
424 #log.warn("stdin=%r", data)
425 self.stdin_buffer += data
426 sent = 0
427 if self.stdin_buffer.endswith("\n"):
428 for line in self.stdin_buffer.splitlines():
429 if line:
430 if line.rstrip("\n\r") in ("quit", "exit"):
431 self.disconnect_and_quit(EXIT_OK, "user requested %s" % line)
432 self.stdin_io_watch = None
433 return False
434 self.send("shell-exec", line.encode())
435 sent += 1
436 self.stdin_buffer = ""
437 if not sent:
438 self.print_prompt()
439 return True
441 def init_packet_handlers(self):
442 super().init_packet_handlers()
443 self._packet_handlers["shell-reply"] = self._process_shell_reply
444 self._packet_handlers["ping"] = self._process_ping
446 def _process_ping(self, packet):
447 echotime = packet[1]
448 self.send("ping_echo", echotime, 0, 0, 0, -1)
450 def _process_shell_reply(self, packet):
451 fd = packet[1]
452 message = packet[2]
453 if fd==1:
454 stream = sys.stdout
455 elif fd==2:
456 stream = sys.stderr
457 else:
458 raise Exception("invalid file descriptor %i" % fd)
459 s = message.decode("utf8")
460 if s.endswith("\n"):
461 s = s[:-1]
462 stream.write("%s" % s)
463 stream.flush()
464 if fd==2:
465 stream.write("\n")
466 self.print_prompt()
468 def print_prompt(self):
469 sys.stdout.write("> ")
470 sys.stdout.flush()
473class VersionXpraClient(HelloRequestClient):
474 """ This client does one thing only:
475 it queries the server for version information and prints it out
476 """
478 def hello_request(self):
479 return {
480 "version_request" : True,
481 "request" : "version",
482 "full-version-request" : True,
483 }
485 def parse_network_capabilities(self, *_args):
486 #don't bother checking anything - this could generate warnings
487 return True
489 def do_command(self, caps : typedict):
490 v = caps.strget(b"version")
491 if not v:
492 self.warn_and_quit(EXIT_FAILURE, "server did not provide the version information")
493 else:
494 sys.stdout.write("%s\n" % (v,))
495 sys.stdout.flush()
496 self.quit(EXIT_OK)
499class ControlXpraClient(CommandConnectClient):
500 """ Allows us to send commands to a server.
501 """
502 def set_command_args(self, command):
503 self.command = command
505 def timeout(self, *_args):
506 self.warn_and_quit(EXIT_TIMEOUT, "timeout: server did not respond")
508 def do_command(self, caps : typedict):
509 cr = caps.tupleget("command_response")
510 if cr is None:
511 self.warn_and_quit(EXIT_UNSUPPORTED, "server does not support control command")
512 return
513 code, text = cr
514 text = bytestostr(text)
515 if code!=0:
516 log.warn("server returned error code %s", code)
517 self.warn_and_quit(EXIT_REMOTE_ERROR, " %s" % text)
518 return
519 self.warn_and_quit(EXIT_OK, text)
521 def make_hello(self):
522 capabilities = super().make_hello()
523 log("make_hello() adding command request '%s' to %s", self.command, capabilities)
524 def b(s):
525 try:
526 return s.encode("utf8")
527 except:
528 return strtobytes(s)
529 capabilities["command_request"] = tuple(b(x) for x in self.command)
530 capabilities["request"] = "command"
531 return capabilities
534class PrintClient(SendCommandConnectClient):
535 """ Allows us to send a file to the server for printing.
536 """
537 def set_command_args(self, command):
538 log("set_command_args(%s)", command)
539 self.filename = command[0]
540 #print command arguments:
541 #filename, file_data, mimetype, source_uuid, title, printer, no_copies, print_options_str = packet[1:9]
542 self.command = command[1:]
543 #TODO: load as needed...
544 def sizeerr(size):
545 self.warn_and_quit(EXIT_FILE_TOO_BIG,
546 "the file is too large: %sB (the file size limit is %sB)" % (
547 std_unit(size), std_unit(self.file_size_limit)))
548 return
549 if self.filename=="-":
550 #replace with filename proposed
551 self.filename = command[2]
552 #read file from stdin
553 with open(sys.stdin.fileno(), mode='rb', closefd=False) as stdin_binary:
554 self.file_data = stdin_binary.read()
555 log("read %i bytes from stdin", len(self.file_data))
556 else:
557 size = os.path.getsize(self.filename)
558 if size>self.file_size_limit:
559 sizeerr(size)
560 return
561 from xpra.os_util import load_binary_file
562 self.file_data = load_binary_file(self.filename)
563 log("read %i bytes from %s", len(self.file_data), self.filename)
564 size = len(self.file_data)
565 if size>self.file_size_limit:
566 sizeerr(size)
567 return
568 assert self.file_data, "no data found for '%s'" % self.filename
570 def client_type(self):
571 return "Python/GObject/Print"
573 def timeout(self, *_args):
574 self.warn_and_quit(EXIT_TIMEOUT, "timeout: server did not respond")
576 def do_command(self, caps : typedict):
577 printing = caps.boolget("printing")
578 if not printing:
579 self.warn_and_quit(EXIT_UNSUPPORTED, "server does not support printing")
580 return
581 #TODO: compress file data? (this should run locally most of the time anyway)
582 from xpra.net.compression import Compressed
583 blob = Compressed("print", self.file_data)
584 self.send("print", self.filename, blob, *self.command)
585 log("print: sending %s as %s for printing", self.filename, blob)
586 self.idle_add(self.send, "disconnect", DONE, "detaching")
588 def make_hello(self):
589 capabilities = super().make_hello()
590 capabilities["wants_features"] = True #so we know if printing is supported or not
591 capabilities["print_request"] = True #marker to skip full setup
592 capabilities["request"] = "print"
593 return capabilities
596class ExitXpraClient(HelloRequestClient):
597 """ This client does one thing only:
598 it asks the server to terminate (like stop),
599 but without killing the Xvfb or clients.
600 """
602 def hello_request(self):
603 return {
604 "exit_request" : True,
605 "request" : "exit",
606 }
608 def do_command(self, caps : typedict):
609 self.idle_add(self.send, "exit-server")
612class StopXpraClient(HelloRequestClient):
613 """ stop a server """
615 def hello_request(self):
616 return {
617 "stop_request" : True,
618 "request" : "stop",
619 }
621 def do_command(self, caps : typedict):
622 if not self.server_client_shutdown:
623 log.error("Error: cannot shutdown this server")
624 log.error(" the feature is disable on the server")
625 self.quit(EXIT_FAILURE)
626 return
627 self.timeout_add(1000, self.send_shutdown_server)
628 #self.idle_add(self.send_shutdown_server)
629 #not exiting the client here,
630 #the server should send us the shutdown disconnection message anyway
631 #and if not, we will then hit the timeout to tell us something went wrong
634class DetachXpraClient(HelloRequestClient):
635 """ run the detach subcommand """
637 def hello_request(self):
638 return {
639 "detach_request" : True,
640 "request" : "detach",
641 }
643 def do_command(self, caps : typedict):
644 self.idle_add(self.send, "disconnect", DONE, "detaching")
645 #not exiting the client here,
646 #the server should disconnect us with the response
648class WaitForDisconnectXpraClient(DetachXpraClient):
649 """ we just want the connection to close """
651 def _process_disconnect(self, _packet):
652 self.quit(EXIT_OK)
655class RequestStartClient(HelloRequestClient):
656 """ request the system proxy server to start a new session for us """
657 #wait longer for this command to return:
658 from xpra.scripts.main import WAIT_SERVER_TIMEOUT
659 COMMAND_TIMEOUT = EXTRA_TIMEOUT+WAIT_SERVER_TIMEOUT
661 def dots(self):
662 errwrite(".")
663 return not self.connection_established
665 def _process_connection_lost(self, packet):
666 errwrite("\n")
667 super()._process_connection_lost(packet)
669 def hello_request(self):
670 if first_time("hello-request"):
671 #this can be called again if we receive a challenge,
672 #but only print this message once:
673 errwrite("requesting new session, please wait")
674 self.timeout_add(1*1000, self.dots)
675 return {
676 "start-new-session" : self.start_new_session,
677 #tells proxy servers we don't want to connect to the real / new instance:
678 "connect" : False,
679 }
681 def server_connection_established(self, caps : typedict):
682 #the server should respond with the display chosen
683 log("server_connection_established() exit_code=%s", self.exit_code)
684 display = caps.strget("display")
685 if display:
686 mode = caps.strget("mode")
687 session_type = {
688 "start" : "seamless ",
689 "start-desktop" : "desktop ",
690 "shadow" : "shadow ",
691 }.get(mode, "")
692 try:
693 errwrite("\n%ssession now available on display %s\n" % (session_type, display))
694 if POSIX and not OSX and self.displayfd>0 and display and display.startswith(b":"):
695 from xpra.platform.displayfd import write_displayfd
696 log("writing display %s to displayfd=%s", display, self.displayfd)
697 write_displayfd(self.displayfd, display[1:])
698 except OSError:
699 log("server_connection_established(..)", exc_info=True)
700 if not self.exit_code:
701 self.quit(0)
702 return True
704 def __init__(self, opts):
705 super().__init__(opts)
706 try:
707 self.displayfd = int(opts.displayfd)
708 except (ValueError, TypeError):
709 self.displayfd = 0