Coverage for /home/antoine/projects/xpra-git/dist/python3/lib64/python/xpra/server/server_base.py : 74%
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# -*- coding: utf-8 -*-
2# This file is part of Xpra.
3# Copyright (C) 2011 Serviware (Arthur Huillet, <ahuillet@serviware.com>)
4# Copyright (C) 2010-2019 Antoine Martin <antoine@xpra.org>
5# Copyright (C) 2008 Nathaniel Smith <njs@pobox.com>
6# Xpra is released under the terms of the GNU GPL v2, or, at your option, any
7# later version. See the file COPYING for details.
9import os
10from threading import Thread, Lock
12from xpra.server.server_core import ServerCore
13from xpra.server.mixins.server_base_controlcommands import ServerBaseControlCommands
14from xpra.server.background_worker import add_work_item
15from xpra.net.common import may_log_packet
16from xpra.os_util import monotonic_time, bytestostr, strtobytes, WIN32
17from xpra.util import (
18 typedict, flatten_dict, updict, merge_dicts, envbool, envint,
19 SERVER_EXIT, SERVER_ERROR, SERVER_SHUTDOWN, DETACH_REQUEST,
20 NEW_CLIENT, DONE, SESSION_BUSY,
21 )
22from xpra.net.bytestreams import set_socket_timeout
23from xpra.server import server_features
24from xpra.server import EXITING_CODE
25from xpra.log import Logger
27SERVER_BASES = [ServerCore, ServerBaseControlCommands]
28if server_features.notifications:
29 from xpra.server.mixins.notification_forwarder import NotificationForwarder
30 SERVER_BASES.append(NotificationForwarder)
31if server_features.webcam:
32 from xpra.server.mixins.webcam_server import WebcamServer
33 SERVER_BASES.append(WebcamServer)
34if server_features.clipboard:
35 from xpra.server.mixins.clipboard_server import ClipboardServer
36 SERVER_BASES.append(ClipboardServer)
37if server_features.audio:
38 from xpra.server.mixins.audio_server import AudioServer
39 SERVER_BASES.append(AudioServer)
40if server_features.fileprint:
41 from xpra.server.mixins.fileprint_server import FilePrintServer
42 SERVER_BASES.append(FilePrintServer)
43if server_features.mmap:
44 from xpra.server.mixins.mmap_server import MMAP_Server
45 SERVER_BASES.append(MMAP_Server)
46if server_features.input_devices:
47 from xpra.server.mixins.input_server import InputServer
48 SERVER_BASES.append(InputServer)
49if server_features.commands:
50 from xpra.server.mixins.child_command_server import ChildCommandServer
51 SERVER_BASES.append(ChildCommandServer)
52if server_features.dbus:
53 from xpra.server.mixins.dbusrpc_server import DBUS_RPC_Server
54 SERVER_BASES.append(DBUS_RPC_Server)
55if server_features.encoding:
56 from xpra.server.mixins.encoding_server import EncodingServer
57 SERVER_BASES.append(EncodingServer)
58if server_features.logging:
59 from xpra.server.mixins.logging_server import LoggingServer
60 SERVER_BASES.append(LoggingServer)
61if server_features.network_state:
62 from xpra.server.mixins.networkstate_server import NetworkStateServer
63 SERVER_BASES.append(NetworkStateServer)
64if server_features.shell:
65 from xpra.server.mixins.shell_server import ShellServer
66 SERVER_BASES.append(ShellServer)
67if server_features.display:
68 from xpra.server.mixins.display_manager import DisplayManager
69 SERVER_BASES.append(DisplayManager)
70if server_features.windows:
71 from xpra.server.mixins.window_server import WindowServer
72 SERVER_BASES.append(WindowServer)
73SERVER_BASES = tuple(SERVER_BASES)
74ServerBaseClass = type('ServerBaseClass', SERVER_BASES, {})
76log = Logger("server")
77netlog = Logger("network")
78httplog = Logger("http")
79timeoutlog = Logger("timeout")
80screenlog = Logger("screen")
82log("ServerBaseClass%s", SERVER_BASES)
84CLIENT_CAN_SHUTDOWN = envbool("XPRA_CLIENT_CAN_SHUTDOWN", True)
85INIT_THREAD_TIMEOUT = envint("XPRA_INIT_THREAD_TIMEOUT", 10)
86MDNS_CLIENT_COUNT = envbool("XPRA_MDNS_CLIENT_COUNT", True)
89"""
90This is the base class for seamless and desktop servers. (not proxy servers)
91It provides all the generic functions but is not tied
92to a specific backend (X11 or otherwise).
93See GTKServerBase/X11ServerBase and other platform specific subclasses.
94"""
95class ServerBase(ServerBaseClass):
97 def __init__(self):
98 for c in SERVER_BASES:
99 c.__init__(self)
100 log("ServerBase.__init__()")
101 self.init_uuid()
103 self._authenticated_packet_handlers = {}
104 self._authenticated_ui_packet_handlers = {}
106 self.display_pid = 0
107 self._server_sources = {}
108 self.client_properties = {}
109 self.ui_driver = None
110 self.sharing = None
111 self.lock = None
112 self.init_thread = None
113 self.init_thread_callbacks = []
114 self.init_thread_lock = Lock()
116 self.idle_timeout = 0
117 #duplicated from Server Source...
118 self.client_shutdown = CLIENT_CAN_SHUTDOWN
119 self.http_stream_check_timers = {}
121 self.init_packet_handlers()
122 self.init_aliases()
125 def idle_add(self, *args, **kwargs):
126 raise NotImplementedError()
128 def timeout_add(self, *args, **kwargs):
129 raise NotImplementedError()
131 def source_remove(self, timer):
132 raise NotImplementedError()
135 def server_event(self, *args):
136 for s in self._server_sources.values():
137 s.send_server_event(*args)
138 if self.dbus_server:
139 self.dbus_server.Event(str(args[0]), [str(x) for x in args[1:]])
141 def get_server_source(self, proto):
142 return self._server_sources.get(proto)
145 def init(self, opts):
146 #from now on, use the logger for parsing errors:
147 from xpra.scripts import config
148 config.warn = log.warn
149 for c in SERVER_BASES:
150 start = monotonic_time()
151 c.init(self, opts)
152 end = monotonic_time()
153 log("%3ims in %s.init", 1000*(end-start), c)
154 self.sharing = opts.sharing
155 self.lock = opts.lock
156 self.idle_timeout = opts.idle_timeout
157 self.bandwidth_detection = opts.bandwidth_detection
159 def setup(self):
160 log("starting component init")
161 for c in SERVER_BASES:
162 start = monotonic_time()
163 c.setup(self)
164 end = monotonic_time()
165 log("%3ims in %s.setup", 1000*(end-start), c)
166 self.init_thread = Thread(target=self.threaded_init)
167 self.init_thread.start()
169 def threaded_init(self):
170 log("threaded_init() start")
171 from xpra.platform import threaded_server_init
172 threaded_server_init()
173 for c in SERVER_BASES:
174 if c!=ServerCore:
175 try:
176 c.threaded_setup(self)
177 except Exception:
178 log.error("Error during threaded setup of %s", c, exc_info=True)
179 #populate the platform info cache:
180 from xpra.version_util import get_platform_info
181 get_platform_info()
182 with self.init_thread_lock:
183 for cb in self.init_thread_callbacks:
184 try:
185 cb()
186 except Exception as e:
187 log("threaded_init()", exc_info=True)
188 log.error("Error in initialization thread callback %s", cb)
189 log.error(" %s", e)
190 log("threaded_init() end")
192 def after_threaded_init(self, callback):
193 with self.init_thread_lock:
194 if not self.init_thread or not self.init_thread.is_alive():
195 callback()
196 else:
197 self.init_thread_callbacks.append(callback)
199 def wait_for_threaded_init(self):
200 if not self.init_thread:
201 #looks like we didn't make it as far as calling setup()
202 log("wait_for_threaded_init() no init thread")
203 return
204 log("wait_for_threaded_init() %s.is_alive()=%s", self.init_thread, self.init_thread.is_alive())
205 if self.init_thread.is_alive():
206 log.info("waiting for initialization thread to complete")
207 self.init_thread.join(INIT_THREAD_TIMEOUT)
208 if self.init_thread.is_alive():
209 log.warn("Warning: initialization thread is still active")
212 def server_is_ready(self):
213 ServerCore.server_is_ready(self)
214 self.server_event("ready")
217 def do_cleanup(self):
218 self.server_event("exit")
219 self.wait_for_threaded_init()
220 self.cancel_http_stream_check_timers()
221 for c in SERVER_BASES:
222 if c!=ServerCore:
223 c.cleanup(self)
226 ######################################################################
227 # shutdown / exit commands:
228 def _process_exit_server(self, _proto, _packet):
229 log.info("Exiting in response to client request")
230 self.cleanup_all_protocols(SERVER_EXIT)
231 self.timeout_add(500, self.clean_quit, EXITING_CODE)
233 def _process_shutdown_server(self, _proto, _packet):
234 if not self.client_shutdown:
235 log.warn("Warning: ignoring shutdown request")
236 return
237 log.info("Shutting down in response to client request")
238 self.cleanup_all_protocols(SERVER_SHUTDOWN)
239 self.timeout_add(500, self.clean_quit)
242 def get_mdns_info(self) -> dict:
243 mdns_info = ServerCore.get_mdns_info(self)
244 if MDNS_CLIENT_COUNT:
245 mdns_info["clients"] = len(self._server_sources)
246 return mdns_info
249 ######################################################################
250 # handle new connections:
251 def handle_sharing(self, proto, ui_client=True, detach_request=False, share=False, uuid=None):
252 share_count = 0
253 disconnected = 0
254 existing_sources = set(ss for p,ss in self._server_sources.items() if p!=proto)
255 is_existing_client = uuid and any(ss.uuid==uuid for ss in existing_sources)
256 log("handle_sharing%s lock=%s, sharing=%s, existing sources=%s, is existing client=%s",
257 (proto, ui_client, detach_request, share, uuid),
258 self.lock, self.sharing, existing_sources, is_existing_client)
259 #if other clients are connected, verify we can steal or share:
260 if existing_sources and not is_existing_client:
261 if self.sharing is True or (self.sharing is None and share and all(ss.share for ss in existing_sources)):
262 log("handle_sharing: sharing with %s", tuple(existing_sources))
263 elif self.lock is True:
264 self.disconnect_client(proto, SESSION_BUSY, "this session is locked")
265 return False, 0, 0
266 elif self.lock is not False and any(ss.lock for ss in existing_sources):
267 self.disconnect_client(proto, SESSION_BUSY, "a client has locked this session")
268 return False, 0, 0
269 for p,ss in tuple(self._server_sources.items()):
270 if detach_request and p!=proto:
271 self.disconnect_client(p, DETACH_REQUEST)
272 disconnected += 1
273 elif uuid and ss.uuid==uuid and ui_client and ss.ui_client:
274 self.disconnect_client(p, NEW_CLIENT, "new connection from the same uuid")
275 disconnected += 1
276 elif ui_client and ss.ui_client:
277 #check if existing sessions are willing to share:
278 if self.sharing is True:
279 share_count += 1
280 elif self.sharing is False:
281 self.disconnect_client(p, NEW_CLIENT, "this session does not allow sharing")
282 disconnected += 1
283 else:
284 assert self.sharing is None
285 if not share:
286 self.disconnect_client(p, NEW_CLIENT, "the new client does not wish to share")
287 disconnected += 1
288 elif not ss.share:
289 self.disconnect_client(p, NEW_CLIENT, "this client had not enabled sharing")
290 disconnected += 1
291 else:
292 share_count += 1
294 #don't accept this connection if we're going to exit-with-client:
295 accepted = True
296 if disconnected>0 and share_count==0 and self.exit_with_client:
297 self.disconnect_client(proto, SERVER_EXIT, "last client has exited")
298 accepted = False
299 return accepted, share_count, disconnected
301 def hello_oked(self, proto, packet, c, auth_caps):
302 if ServerCore.hello_oked(self, proto, packet, c, auth_caps):
303 #has been handled
304 return
305 if not c.boolget("steal", True) and self._server_sources:
306 self.disconnect_client(proto, SESSION_BUSY, "this session is already active")
307 return
308 if c.boolget("screenshot_request"):
309 self.send_screenshot(proto)
310 return
311 #added in 2.2:
312 generic_request = c.strget("request")
313 def is_req(mode):
314 return generic_request==mode or c.boolget("%s_request" % mode, False)
315 detach_request = is_req("detach")
316 stop_request = is_req("stop_request")
317 exit_request = is_req("exit_request")
318 event_request = is_req("event_request")
319 print_request = is_req("print_request")
320 is_request = detach_request or stop_request or exit_request or event_request or print_request
321 if not is_request:
322 #"normal" connection, so log welcome message:
323 log.info("Handshake complete; enabling connection")
324 else:
325 log("handling request %s", generic_request)
326 self.server_event("handshake-complete")
328 # Things are okay, we accept this connection, and may disconnect previous one(s)
329 # (but only if this is going to be a UI session - control sessions can co-exist)
330 ui_client = c.boolget("ui_client", True)
331 share = c.boolget("share")
332 uuid = c.strget("uuid")
333 accepted, share_count, disconnected = self.handle_sharing(proto, ui_client, detach_request, share, uuid)
334 if not accepted:
335 return
337 if detach_request:
338 self.disconnect_client(proto, DONE, "%i other clients have been disconnected" % disconnected)
339 return
341 if not is_request and ui_client:
342 #a bit of explanation:
343 #normally these things are synchronized using xsettings, which we handle already
344 #but non-posix clients have no such thing,
345 #and we don't want to expose that as an interface
346 #(it's not very nice and it is very X11 specific)
347 #also, clients may want to override what is in their xsettings..
348 #so if the client specifies what it wants to use, we patch the xsettings with it
349 #(the actual xsettings part is done in update_all_server_settings in the X11 specific subclasses)
350 if share_count>0:
351 log.info("sharing with %s other client(s)", share_count)
352 self.dpi = 0
353 self.xdpi = 0
354 self.ydpi = 0
355 self.double_click_time = -1
356 self.double_click_distance = -1, -1
357 self.antialias = {}
358 self.cursor_size = 24
359 else:
360 self.dpi = c.intget("dpi", 0)
361 self.xdpi = c.intget("dpi.x", 0)
362 self.ydpi = c.intget("dpi.y", 0)
363 self.double_click_time = c.intget("double_click.time", -1)
364 self.double_click_distance = c.intpair("double_click.distance", (-1, -1))
365 self.antialias = c.dictget("antialias", {})
366 self.cursor_size = c.intget("cursor.size", 0)
367 #FIXME: this belongs in DisplayManager!
368 screenlog("dpi=%s, dpi.x=%s, dpi.y=%s, antialias=%s, cursor_size=%s",
369 self.dpi, self.xdpi, self.ydpi, self.antialias, self.cursor_size)
370 log("double-click time=%s, distance=%s", self.double_click_time, self.double_click_distance)
371 #if we're not sharing, reset all the settings:
372 reset = share_count==0
373 self.update_all_server_settings(reset)
375 self.accept_client(proto, c)
376 #use blocking sockets from now on:
377 if not WIN32:
378 set_socket_timeout(proto._conn, None)
380 def drop_client(reason="unknown", *args):
381 self.disconnect_client(proto, reason, *args)
382 cc_class = self.get_client_connection_class(c)
383 ss = cc_class(proto, drop_client,
384 self.session_name, self,
385 self.idle_add, self.timeout_add, self.source_remove,
386 self.setting_changed,
387 self._socket_dir, self.unix_socket_paths, not is_request,
388 self.bandwidth_limit, self.bandwidth_detection,
389 )
390 log("process_hello clientconnection=%s", ss)
391 try:
392 ss.parse_hello(c)
393 except:
394 #close it already
395 ss.close()
396 raise
397 self._server_sources[proto] = ss
398 add_work_item(self.mdns_update, False)
399 #process ui half in ui thread:
400 send_ui = ui_client and not is_request
401 self.idle_add(self._process_hello_ui, ss, c, auth_caps, send_ui, share_count)
403 def get_client_connection_class(self, caps):
404 from xpra.server.source.client_connection_factory import get_client_connection_class
405 return get_client_connection_class(caps)
408 def _process_hello_ui(self, ss, c, auth_caps, send_ui : bool, share_count : int):
409 #adds try:except around parse hello ui code:
410 try:
411 if self._closing:
412 raise Exception("server is shutting down")
414 self.notify_new_user(ss)
416 self.parse_hello(ss, c, send_ui)
417 #send_hello will take care of sending the current and max screen resolutions
418 root_w, root_h = self.get_root_window_size()
419 self.send_hello(ss, root_w, root_h, auth_caps)
420 self.add_new_client(ss, c, send_ui, share_count)
421 self.send_initial_data(ss, c, send_ui, share_count)
422 self.client_startup_complete(ss)
424 if self._closing:
425 raise Exception("server is shutting down")
426 except Exception as e:
427 #log exception but don't disclose internal details to the client
428 p = ss.protocol
429 log("_process_hello_ui%s", (ss, c, auth_caps, send_ui, share_count), exc_info=True)
430 log.error("Error: processing new connection from %s:", p or ss)
431 log.error(" %s", e)
432 if p:
433 self.disconnect_client(p, SERVER_ERROR, "error accepting new connection")
435 def parse_hello(self, ss, c, send_ui):
436 for bc in SERVER_BASES:
437 if bc!=ServerCore:
438 bc.parse_hello(self, ss, c, send_ui)
440 def add_new_client(self, ss, c, send_ui, share_count):
441 for bc in SERVER_BASES:
442 if bc!=ServerCore:
443 bc.add_new_client(self, ss, c, send_ui, share_count)
445 def send_initial_data(self, ss, c, send_ui, share_count):
446 for bc in SERVER_BASES:
447 if bc!=ServerCore:
448 bc.send_initial_data(self, ss, c, send_ui, share_count)
450 def client_startup_complete(self, ss):
451 ss.startup_complete()
452 self.server_event("startup-complete", ss.uuid)
453 if not self.start_after_connect_done: #pylint: disable=access-member-before-definition
454 self.start_after_connect_done = True
455 self.exec_after_connect_commands()
456 self.exec_on_connect_commands()
458 def sanity_checks(self, proto, c):
459 server_uuid = c.strget("server_uuid")
460 if server_uuid:
461 if server_uuid==self.uuid:
462 self.send_disconnect(proto, "cannot connect a client running on the same display"
463 +" that the server it connects to is managing - this would create a loop!")
464 return False
465 log.warn("This client is running within the Xpra server %s", server_uuid)
466 return True
469 def update_all_server_settings(self, reset=False):
470 pass #may be overriden in subclasses (ie: x11 server)
473 ######################################################################
474 # hello:
475 def get_server_features(self, server_source=None):
476 #these are flags that have been added over time with new versions
477 #to expose new server features:
478 f = {
479 "toggle_keyboard_sync" : True, #v4.0 clients assume this is always available
480 }
481 for c in SERVER_BASES:
482 if c!=ServerCore:
483 merge_dicts(f, c.get_server_features(self, server_source))
484 return f
486 def make_hello(self, source):
487 capabilities = super().make_hello(source)
488 for c in SERVER_BASES:
489 if c!=ServerCore:
490 merge_dicts(capabilities, c.get_caps(self, source))
491 capabilities["server_type"] = "base"
492 if source.wants_display:
493 capabilities.update({
494 "max_desktop_size" : self.get_max_screen_size(),
495 "display" : os.environ.get("DISPLAY", "Main"),
496 })
497 if source.wants_features:
498 capabilities.update({
499 "client-shutdown" : self.client_shutdown,
500 "sharing" : self.sharing is not False,
501 "sharing-toggle" : self.sharing is None,
502 "lock" : self.lock is not False,
503 "lock-toggle" : self.lock is None,
504 "windows" : server_features.windows,
505 "keyboard" : server_features.input_devices,
506 "pointer" : server_features.input_devices,
507 })
508 capabilities.update(flatten_dict(self.get_server_features(source)))
509 capabilities["configure.pointer"] = True #v4 clients assume this is enabled
510 return capabilities
512 def send_hello(self, server_source, root_w, root_h, server_cipher):
513 capabilities = self.make_hello(server_source)
514 if server_source.wants_encodings and server_features.windows:
515 try:
516 from xpra.codecs.loader import codec_versions
517 except ImportError:
518 log("no codecs", exc_info=True)
519 else:
520 def add_encoding_caps(d):
521 updict(d, "encoding", codec_versions, "version")
522 for k,v in self.get_encoding_info().items():
523 if k=="":
524 k = "encodings"
525 else:
526 k = "encodings.%s" % k
527 d[k] = v
528 if server_source.encodings_packet:
529 #we can send it later,
530 #when the init thread has finished:
531 def send_encoding_caps():
532 d = {}
533 add_encoding_caps(d)
534 #make sure the 'hello' packet goes out first:
535 self.idle_add(server_source.send_async, "encodings", d)
536 self.after_threaded_init(send_encoding_caps)
537 else:
538 self.wait_for_threaded_init()
539 add_encoding_caps(capabilities)
540 #check for mmap:
541 if getattr(self, "mmap_size", 0)==0:
542 self.after_threaded_init(server_source.print_encoding_info)
543 if server_source.wants_display:
544 capabilities.update({
545 "actual_desktop_size" : (root_w, root_h),
546 "root_window_size" : (root_w, root_h),
547 })
548 if self._aliases and server_source.wants_aliases:
549 reverse_aliases = {}
550 for i, packet_type in self._aliases.items():
551 reverse_aliases[packet_type] = i
552 capabilities["aliases"] = reverse_aliases
553 if server_cipher:
554 capabilities.update(server_cipher)
555 server_source.send_hello(capabilities)
558 ######################################################################
559 # info:
560 def _process_info_request(self, proto, packet):
561 log("process_info_request(%s, %s)", proto, packet)
562 #ignoring the list of client uuids supplied in packet[1]
563 ss = self.get_server_source(proto)
564 if not ss:
565 return
566 categories = None
567 #if len(packet>=2):
568 # uuid = packet[1]
569 if len(packet)>=4:
570 categories = tuple(bytestostr(x) for x in packet[3])
571 def info_callback(_proto, info):
572 assert proto==_proto
573 if categories:
574 info = dict((k,v) for k,v in info.items() if k in categories)
575 ss.send_info_response(info)
576 self.get_all_info(info_callback, proto, None)
578 def send_hello_info(self, proto):
579 self.wait_for_threaded_init()
580 start = monotonic_time()
581 def cb(proto, info):
582 self.do_send_info(proto, info)
583 end = monotonic_time()
584 log.info("processed info request from %s in %ims",
585 proto._conn, (end-start)*1000)
586 self.get_all_info(cb, proto, None)
588 def get_ui_info(self, proto, client_uuids=None, *args) -> dict:
589 """ info that must be collected from the UI thread
590 (ie: things that query the display)
591 """
592 info = {"server" : {"max_desktop_size" : self.get_max_screen_size()}}
593 for c in SERVER_BASES:
594 try:
595 merge_dicts(info, c.get_ui_info(self, proto, client_uuids, *args))
596 except Exception:
597 log.error("Error gathering UI info on %s", c, exc_info=True)
598 return info
601 def get_info(self, proto=None, client_uuids=None) -> dict:
602 log("ServerBase.get_info%s", (proto, client_uuids))
603 start = monotonic_time()
604 info = ServerCore.get_info(self, proto)
605 if client_uuids:
606 sources = [ss for ss in self._server_sources.values() if ss.uuid in client_uuids]
607 else:
608 sources = tuple(self._server_sources.values())
609 log("info-request: sources=%s", sources)
610 dgi = self.do_get_info(proto, sources)
611 #ugly alert: merge nested dictionaries,
612 #ie: do_get_info may return a dictionary for "server" and we already have one,
613 # so we update it with the new values
614 for k,v in dgi.items():
615 cval = info.get(k)
616 if cval is None:
617 info[k] = v
618 continue
619 cval.update(v)
620 log("ServerBase.get_info took %.1fms", 1000.0*(monotonic_time()-start))
621 return info
623 def get_packet_handlers_info(self) -> dict:
624 info = ServerCore.get_packet_handlers_info(self)
625 info.update({
626 "authenticated" : sorted(self._authenticated_packet_handlers.keys()),
627 "ui" : sorted(self._authenticated_ui_packet_handlers.keys()),
628 })
629 return info
632 def get_features_info(self) -> dict:
633 i = {
634 "sharing" : self.sharing is not False,
635 "idle_timeout" : self.idle_timeout,
636 }
637 i.update(self.get_server_features())
638 return i
640 def do_get_info(self, proto, server_sources=None) -> dict:
641 start = monotonic_time()
642 info = {}
643 def up(prefix, d):
644 merge_dicts(info, {prefix : d})
646 for c in SERVER_BASES:
647 try:
648 merge_dicts(info, c.get_info(self, proto))
649 except Exception as e:
650 log("do_get_info%s", (proto, server_sources), exc_info=True)
651 log.error("Error collecting information from %s", c)
652 log.error(" %s", e)
654 up("features", self.get_features_info())
655 up("network", {
656 "sharing" : self.sharing is not False,
657 "sharing-toggle" : self.sharing is None,
658 "lock" : self.lock is not False,
659 "lock-toggle" : self.lock is None,
660 })
662 # other clients:
663 info["clients"] = {
664 "" : sum(1 for p in self._server_sources if p!=proto),
665 "unauthenticated" : sum(1 for p in self._potential_protocols if ((p is not proto) and (p not in self._server_sources))),
666 }
667 #find the server source to report on:
668 n = len(server_sources or [])
669 if n==1:
670 ss = server_sources[0]
671 up("client", ss.get_info())
672 elif n>1:
673 cinfo = {}
674 for i, ss in enumerate(server_sources):
675 sinfo = ss.get_info()
676 sinfo["ui-driver"] = self.ui_driver==ss.uuid
677 cinfo[i] = sinfo
678 up("client", cinfo)
679 log("ServerBase.do_get_info took %ims", (monotonic_time()-start)*1000)
680 return info
683 def _process_server_settings(self, proto, packet):
684 #only used by x11 servers
685 pass
688 def _set_client_properties(self, proto, wid, window, new_client_properties):
689 """
690 Allows us to keep window properties for a client after disconnection.
691 (we keep it in a map with the client's uuid as key)
692 """
693 ss = self.get_server_source(proto)
694 if ss:
695 ss.set_client_properties(wid, window, typedict(new_client_properties))
696 #filter out encoding properties, which are expected to be set everytime:
697 ncp = {}
698 for k,v in new_client_properties.items():
699 if v is None:
700 log.warn("removing invalid None property for %s", k)
701 continue
702 k = strtobytes(k)
703 if not k.startswith(b"encoding"):
704 ncp[k] = v
705 if ncp:
706 log("set_client_properties updating window %s of source %s with %s", wid, ss.uuid, ncp)
707 client_properties = self.client_properties.setdefault(wid, {}).setdefault(ss.uuid, {})
708 client_properties.update(ncp)
711 ######################################################################
712 # settings toggle:
713 def setting_changed(self, setting, value):
714 #tell all the clients (that can) about the new value for this setting
715 for ss in tuple(self._server_sources.values()):
716 ss.send_setting_change(setting, value)
718 def _process_set_deflate(self, proto, packet):
719 level = packet[1]
720 log("client has requested compression level=%s", level)
721 proto.set_compression_level(level)
722 #echo it back to the client:
723 ss = self.get_server_source(proto)
724 if ss:
725 ss.set_deflate(level)
727 def _process_sharing_toggle(self, proto, packet):
728 assert self.sharing is None
729 ss = self.get_server_source(proto)
730 if not ss:
731 return
732 sharing = bool(packet[1])
733 ss.share = sharing
734 if not sharing:
735 #disconnect other users:
736 for p,ss in tuple(self._server_sources.items()):
737 if p!=proto:
738 self.disconnect_client(p, DETACH_REQUEST,
739 "client %i no longer wishes to share the session" % ss.counter)
741 def _process_lock_toggle(self, proto, packet):
742 assert self.lock is None
743 ss = self.get_server_source(proto)
744 if ss:
745 ss.lock = bool(packet[1])
746 log("lock set to %s for client %i", ss.lock, ss.counter)
751 ######################################################################
752 # http server and http audio stream:
753 def get_http_info(self) -> dict:
754 info = ServerCore.get_http_info(self)
755 info["clients"] = len(self._server_sources)
756 return info
758 def get_http_scripts(self) -> dict:
759 scripts = {}
760 for c in SERVER_BASES:
761 scripts.update(c.get_http_scripts(self))
762 httplog("scripts=%s", scripts)
763 return scripts
765 def cancel_http_stream_check_timers(self):
766 for ts in tuple(self.http_stream_check_timers.keys()):
767 v = self.http_stream_check_timers.pop(ts, None)
768 if v:
769 timer, stop = v
770 self.source_remove(timer)
771 try:
772 stop()
773 except Exception:
774 httplog("error on %s", stop, exc_info=True)
777 ######################################################################
778 # client connections:
779 def init_sockets(self, sockets):
780 for c in SERVER_BASES:
781 c.init_sockets(self, sockets)
783 def cleanup_protocol(self, protocol):
784 netlog("cleanup_protocol(%s)", protocol)
785 #this ensures that from now on we ignore any incoming packets coming
786 #from this connection as these could potentially set some keys pressed, etc
787 try:
788 self._potential_protocols.remove(protocol)
789 except ValueError:
790 pass
791 source = self._server_sources.pop(protocol, None)
792 if source:
793 self.cleanup_source(source)
794 add_work_item(self.mdns_update, False)
795 for c in SERVER_BASES:
796 c.cleanup_protocol(self, protocol)
797 return source
799 def cleanup_source(self, source):
800 self.server_event("connection-lost", source.uuid)
801 remaining_sources = tuple(self._server_sources.values())
802 if self.ui_driver==source.uuid:
803 if len(remaining_sources)==1:
804 self.set_ui_driver(remaining_sources[0])
805 else:
806 self.set_ui_driver(None)
807 source.close()
808 netlog("cleanup_source(%s) remaining sources: %s", source, remaining_sources)
809 netlog.info("xpra client %i disconnected.", source.counter)
810 has_client = len(remaining_sources)>0
811 if not has_client:
812 self.idle_add(self.last_client_exited)
814 def last_client_exited(self):
815 #must run from the UI thread (modifies focus and keys)
816 netlog("last_client_exited() exit_with_client=%s", self.exit_with_client)
817 self.reset_server_timeout(True)
818 for c in SERVER_BASES:
819 if c!=ServerCore:
820 try:
821 c.last_client_exited(self)
822 except Exception:
823 log("last_client_exited calling %s", c.last_client_exited, exc_info=True)
824 if self.exit_with_client:
825 if not self._closing:
826 netlog.info("Last client has disconnected, terminating")
827 self.clean_quit(False)
830 def set_ui_driver(self, source):
831 if source and self.ui_driver==source.uuid:
832 return
833 log("new ui driver: %s", source)
834 if not source:
835 self.ui_driver = None
836 else:
837 self.ui_driver = source.uuid
838 for c in SERVER_BASES:
839 if c!=ServerCore:
840 c.set_session_driver(self, source)
843 def reset_focus(self):
844 for c in SERVER_BASES:
845 if c!=ServerCore:
846 c.reset_focus(self)
849 def get_all_protocols(self) -> list:
850 return list(self._potential_protocols) + list(self._server_sources.keys())
853 def is_timedout(self, protocol):
854 v = ServerCore.is_timedout(self, protocol) and protocol not in self._server_sources
855 netlog("is_timedout(%s)=%s", protocol, v)
856 return v
859 def _log_disconnect(self, proto, *args):
860 #skip logging of disconnection events for server sources
861 #we have tagged during hello ("info_request", "exit_request", etc..)
862 ss = self.get_server_source(proto)
863 if ss and not ss.log_disconnect:
864 #log at debug level only:
865 netlog(*args)
866 return
867 ServerCore._log_disconnect(self, proto, *args)
869 def _disconnect_proto_info(self, proto):
870 #only log protocol info if there is more than one client:
871 if len(self._server_sources)>1:
872 return " %s" % proto
873 return ""
875 ######################################################################
876 # packets:
877 def add_packet_handlers(self, defs, main_thread=True):
878 for packet_type, handler in defs.items():
879 self.add_packet_handler(packet_type, handler, main_thread)
881 def add_packet_handler(self, packet_type, handler, main_thread=True):
882 netlog("add_packet_handler%s", (packet_type, handler, main_thread))
883 if main_thread:
884 handlers = self._authenticated_ui_packet_handlers
885 else:
886 handlers = self._authenticated_packet_handlers
887 handlers[packet_type] = handler
889 def init_packet_handlers(self):
890 for c in SERVER_BASES:
891 c.init_packet_handlers(self)
892 #no need for main thread:
893 self.add_packet_handlers({
894 "sharing-toggle" : self._process_sharing_toggle,
895 "lock-toggle" : self._process_lock_toggle,
896 }, False)
897 #attributes / settings:
898 self.add_packet_handlers({
899 "server-settings" : self._process_server_settings,
900 "set_deflate" : self._process_set_deflate,
901 "shutdown-server" : self._process_shutdown_server,
902 "exit-server" : self._process_exit_server,
903 "info-request" : self._process_info_request,
904 })
906 def init_aliases(self):
907 packet_types = list(self._default_packet_handlers.keys())
908 packet_types += list(self._authenticated_packet_handlers.keys())
909 packet_types += list(self._authenticated_ui_packet_handlers.keys())
910 self.do_init_aliases(packet_types)
912 def process_packet(self, proto, packet):
913 try:
914 handler = None
915 packet_type = bytestostr(packet[0])
916 def call_handler():
917 may_log_packet(False, packet_type, packet)
918 handler(proto, packet)
919 if proto in self._server_sources:
920 handler = self._authenticated_ui_packet_handlers.get(packet_type)
921 if handler:
922 netlog("process ui packet %s", packet_type)
923 self.idle_add(call_handler)
924 return
925 handler = self._authenticated_packet_handlers.get(packet_type)
926 if handler:
927 netlog("process non-ui packet %s", packet_type)
928 call_handler()
929 return
930 handler = self._default_packet_handlers.get(packet_type)
931 if handler:
932 netlog("process default packet %s", packet_type)
933 call_handler()
934 return
935 def invalid_packet():
936 ss = self.get_server_source(proto)
937 if not self._closing and not proto.is_closed() and (ss is None or not ss.is_closed()):
938 netlog("invalid packet: %s", packet)
939 netlog.error("Error: unknown or invalid packet type '%s'", packet_type)
940 netlog.error(" received from %s", proto)
941 if not ss:
942 proto.close()
943 self.idle_add(invalid_packet)
944 except Exception:
945 netlog.error("Error processing a '%s' packet", packet_type)
946 netlog.error(" received from %s:", proto)
947 netlog.error(" using %s", handler, exc_info=True)