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.
6
7import os.path
8import sys
9from time import monotonic
10
11from gi.repository import GLib
12from gi.repository import GObject
13
14from xpra.util import (
15    u, net_utf8, nonl, sorted_nicely, print_nested_dict, envint, flatten_dict, typedict,
16    disconnect_is_an_error, ellipsizer, first_time, csv,
17    repr_ellipsized,
18    SERVER_EXIT, DONE,
19    )
20from xpra.os_util import (
21    bytestostr,
22    get_hex_uuid, hexstr,
23    POSIX, OSX,
24    )
25from xpra.simple_stats import std_unit
26from xpra.client.client_base import XpraClientBase, EXTRA_TIMEOUT
27from xpra.exit_codes import (
28    EXIT_OK, EXIT_CONNECTION_LOST, EXIT_TIMEOUT, EXIT_INTERNAL_ERROR,
29    EXIT_FAILURE, EXIT_UNSUPPORTED, EXIT_REMOTE_ERROR, EXIT_FILE_TOO_BIG,
30    EXIT_IO_ERROR, EXIT_NO_DATA,
31    )
32from xpra.log import Logger
33
34log = Logger("gobject", "client")
35
36FLATTEN_INFO = envint("XPRA_FLATTEN_INFO", 1)
37
38
39def errwrite(msg):
40    try:
41        sys.stderr.write(msg)
42        sys.stderr.flush()
43    except (OSError, AttributeError):
44        pass
45
46
47class GObjectXpraClient(GObject.GObject, XpraClientBase):
48    """
49        Utility superclass for GObject clients
50    """
51    COMMAND_TIMEOUT = EXTRA_TIMEOUT
52
53    def __init__(self):
54        self.idle_add = GLib.idle_add
55        self.timeout_add = GLib.timeout_add
56        self.source_remove = GLib.source_remove
57        GObject.GObject.__init__(self)
58        XpraClientBase.__init__(self)
59
60    def init(self, opts):
61        XpraClientBase.init(self, opts)
62        self.glib_init()
63
64    def get_scheduler(self):
65        return GLib
66
67
68    def install_signal_handlers(self):
69        from xpra.gtk_common.gobject_compat import install_signal_handlers
70        install_signal_handlers("%s Client" % self.client_type(), self.handle_app_signal)
71
72
73    def setup_connection(self, conn):
74        protocol = super().setup_connection(conn)
75        protocol._log_stats  = False
76        GLib.idle_add(self.send_hello)
77        return protocol
78
79
80    def client_type(self):
81        #overriden in subclasses!
82        return "Python3/GObject"
83
84
85    def init_packet_handlers(self):
86        XpraClientBase.init_packet_handlers(self)
87        def noop(*args):    # pragma: no cover
88            log("ignoring packet: %s", args)
89        #ignore the following packet types without error:
90        #(newer servers should avoid sending us any of those)
91        for t in (
92            "new-window", "new-override-redirect",
93            "draw", "cursor", "bell",
94            "notify_show", "notify_close",
95            "ping", "ping_echo",
96            "window-metadata", "configure-override-redirect",
97            "lost-window",
98            ):
99            self._packet_handlers[t] = noop
100
101    def run(self):
102        XpraClientBase.run(self)
103        self.run_loop()
104        return self.exit_code
105
106    def run_loop(self):
107        self.glib_mainloop = GLib.MainLoop()
108        self.glib_mainloop.run()
109
110    def make_hello(self):
111        capabilities = XpraClientBase.make_hello(self)
112        capabilities["keyboard"] = False
113        return capabilities
114
115    def quit(self, exit_code=0):
116        log("quit(%s) current exit_code=%s", exit_code, self.exit_code)
117        if self.exit_code is None:
118            self.exit_code = exit_code
119        self.cleanup()
120        GLib.timeout_add(50, self.exit_loop)
121
122    def exit_loop(self):
123        self.glib_mainloop.quit()
124
125
126class CommandConnectClient(GObjectXpraClient):
127    """
128        Utility superclass for clients that only send one command
129        via the hello packet.
130    """
131
132    def __init__(self, opts):
133        super().__init__()
134        super().init(opts)
135        self.display_desc = {}
136        #not used by command line clients,
137        #so don't try probing for printers, etc
138        self.file_transfer = False
139        self.printing = False
140        self.command_timeout = None
141        #don't bother with many of these things for one-off commands:
142        for x in ("ui_client", "wants_aliases", "wants_encodings",
143                  "wants_versions", "wants_features", "wants_sound", "windows",
144                  "webcam", "keyboard", "mouse", "network-state",
145                  ):
146            self.hello_extra[x] = False
147
148    def setup_connection(self, conn):
149        protocol = super().setup_connection(conn)
150        if conn.timeout>0:
151            self.command_timeout = GLib.timeout_add((conn.timeout + self.COMMAND_TIMEOUT) * 1000, self.timeout)
152        return protocol
153
154    def timeout(self, *_args):
155        log.warn("timeout!")    # pragma: no cover
156
157    def cancel_command_timeout(self):
158        ct = self.command_timeout
159        if ct:
160            self.command_timeout = None
161            GLib.source_remove(ct)
162
163    def _process_connection_lost(self, packet):
164        log("_process_connection_lost%s", packet)
165        #override so we don't log a warning
166        #"command clients" are meant to exit quickly by losing the connection
167        p = self._protocol
168        if p and p.input_packetcount==0:
169            #never got any data back so we have never connected,
170            #try to give feedback to the user as to why that is:
171            details = ""
172            if len(packet)>1:
173                details = ": %s" % csv(packet[1:])
174            log.warn("Connection failed%s", details)
175            self.quit(EXIT_CONNECTION_LOST)
176        else:
177            self.quit(EXIT_OK)
178
179    def server_connection_established(self, caps : typedict):
180        #don't bother parsing the network caps:
181        #* it could cause errors if the caps are missing
182        #* we don't care about sending anything back after hello
183        log("server_capabilities: %s", ellipsizer(caps))
184        log("protocol state: %s", self._protocol.save_state())
185        self.cancel_command_timeout()
186        self.do_command(caps)
187        return True
188
189    def do_command(self, caps : typedict):
190        raise NotImplementedError()
191
192
193class SendCommandConnectClient(CommandConnectClient):
194    """
195        Utility superclass for clients that only send at least one more packet
196        after the hello packet.
197        So unlike CommandConnectClient, we do need the network and encryption to be setup.
198    """
199
200    def server_connection_established(self, caps):
201        assert self.parse_encryption_capabilities(caps), "encryption failure"
202        assert self.parse_network_capabilities(caps), "network capabilities failure"
203        return super().server_connection_established(caps)
204
205    def do_command(self, caps : typedict):
206        raise NotImplementedError()
207
208
209class HelloRequestClient(SendCommandConnectClient):
210    """
211        Utility superclass for clients that send a server request
212        as part of the hello packet.
213    """
214
215    def make_hello_base(self):
216        caps = super().make_hello_base()
217        caps.update(self.hello_request())
218        return caps
219
220    def timeout(self, *_args):
221        self.warn_and_quit(EXIT_TIMEOUT, "timeout: server did not disconnect us")
222
223    def hello_request(self):        # pragma: no cover
224        raise NotImplementedError()
225
226    def do_command(self, caps : typedict):
227        self.quit(EXIT_OK)
228
229    def _process_disconnect(self, packet):
230        #overriden method so we can avoid printing a warning,
231        #we haven't received the hello back from the server
232        #but that's fine for a request client
233        info = tuple(nonl(bytestostr(x)) for x in packet[1:])
234        reason = info[0]
235        if disconnect_is_an_error(reason):
236            self.server_disconnect_warning(*info)
237        elif self.exit_code is None:
238            #we're not in the process of exiting already,
239            #tell the user why the server is disconnecting us
240            self.server_disconnect(*info)
241
242
243class ScreenshotXpraClient(CommandConnectClient):
244    """ This client does one thing only:
245        it sends the hello packet with a screenshot request
246        and exits when the resulting image is received (or timedout)
247    """
248
249    def __init__(self, opts, screenshot_filename):
250        self.screenshot_filename = screenshot_filename
251        super().__init__(opts)
252        self.hello_extra["screenshot_request"] = True
253        self.hello_extra["request"] = "screenshot"
254
255    def timeout(self, *_args):
256        self.warn_and_quit(EXIT_TIMEOUT, "timeout: did not receive the screenshot")
257
258    def _process_screenshot(self, packet):
259        (w, h, encoding, _, img_data) = packet[1:6]
260        assert bytestostr(encoding)=="png", "expected png screenshot data but got %s" % bytestostr(encoding)
261        if not img_data:
262            self.warn_and_quit(EXIT_OK,
263                               "screenshot is empty and has not been saved (maybe there are no windows or they are not currently shown)")
264            return
265        if self.screenshot_filename=="-":
266            output = os.fdopen(sys.stdout.fileno(), "wb", closefd=False)
267        else:
268            output = open(self.screenshot_filename, "wb")
269        with output:
270            output.write(img_data)
271            output.flush()
272        self.warn_and_quit(EXIT_OK, "screenshot %sx%s saved to: %s" % (w, h, self.screenshot_filename))
273
274    def init_packet_handlers(self):
275        super().init_packet_handlers()
276        self._ui_packet_handlers["screenshot"] = self._process_screenshot
277
278
279class InfoXpraClient(CommandConnectClient):
280    """ This client does one thing only:
281        it queries the server with an 'info' request
282    """
283
284    def __init__(self, opts):
285        super().__init__(opts)
286        self.hello_extra["info_request"] = True
287        self.hello_extra["request"] = "info"
288        if FLATTEN_INFO>=1:
289            self.hello_extra["info-namespace"] = True
290
291    def timeout(self, *_args):
292        self.warn_and_quit(EXIT_TIMEOUT, "timeout: did not receive the info")
293
294    def do_command(self, caps : typedict):
295        def print_fn(s):
296            sys.stdout.write("%s\n" % (s,))
297        if not caps:
298            self.quit(EXIT_NO_DATA)
299            return
300        exit_code = EXIT_OK
301        try:
302            if FLATTEN_INFO<2:
303                #compatibility mode:
304                c = flatten_dict(caps)
305                for k in sorted_nicely(c.keys()):
306                    v = c.get(k)
307                    #FIXME: this is a nasty and horrible python3 workaround (yet again)
308                    #we want to print bytes as strings without the ugly 'b' prefix..
309                    #it assumes that all the strings are raw or in (possibly nested) lists or tuples only
310                    #we assume that all strings we get are utf-8,
311                    #and fallback to the bytestostr hack if that fails
312                    def fixvalue(w):
313                        if isinstance(w, bytes):
314                            if k.endswith(".data"):
315                                return hexstr(w)
316                            return u(w)
317                        elif isinstance(w, (tuple,list)):
318                            return type(w)([fixvalue(x) for x in w])
319                        return w
320                    v = fixvalue(v)
321                    k = fixvalue(k)
322                    print_fn("%s=%s" % (k, nonl(v)))
323            else:
324                print_nested_dict(caps, print_fn=print_fn)
325        except OSError:
326            exit_code = EXIT_IO_ERROR
327        self.quit(exit_code)
328
329class IDXpraClient(InfoXpraClient):
330
331    def __init__(self, *args):
332        super().__init__(*args)
333        self.hello_extra["request"] = "id"
334
335
336class RequestXpraClient(CommandConnectClient):
337
338    def __init__(self, request, opts):
339        super().__init__(opts)
340        self.hello_extra["request"] = request
341
342    def do_command(self, caps : typedict):
343        self.quit(EXIT_OK)
344
345
346class ConnectTestXpraClient(CommandConnectClient):
347    """ This client does one thing only:
348        it queries the server with an 'info' request
349    """
350
351    def __init__(self, opts, **kwargs):
352        super().__init__(opts)
353        self.value = get_hex_uuid()
354        self.hello_extra.update({
355            "connect_test_request"      : self.value,
356            "request"                   : "connect_test",
357            #tells proxy servers we don't want to connect to the real / new instance:
358            "connect"                   : False,
359            #older servers don't know about connect-test,
360            #pretend that we're interested in info:
361            "info_request"              : True,
362            "info-namespace"            : True,
363            })
364        self.hello_extra.update(kwargs)
365
366    def timeout(self, *_args):
367        self.warn_and_quit(EXIT_TIMEOUT, "timeout: no server response")
368
369    def _process_connection_lost(self, _packet):
370        #we should always receive a hello back and call do_command,
371        #which sets the correct exit code, landing here is an error:
372        self.quit(EXIT_FAILURE)
373
374    def do_command(self, caps : typedict):
375        if caps:
376            ctr = caps.strget("connect_test_response")
377            log("do_command(..) expected connect test response='%s', got '%s'", self.value, ctr)
378            if ctr==self.value:
379                self.quit(EXIT_OK)
380            else:
381                self.quit(EXIT_INTERNAL_ERROR)
382        else:
383            self.quit(EXIT_FAILURE)
384
385
386class MonitorXpraClient(SendCommandConnectClient):
387    """ This client does one thing only:
388        it prints out events received from the server.
389        If the server does not support this feature it exits with an error.
390    """
391
392    def __init__(self, opts):
393        super().__init__(opts)
394        for x in ("wants_features", "wants_events", "event_request"):
395            self.hello_extra[x] = True
396        self.hello_extra["request"] = "event"
397        self.hello_extra["info-namespace"] = True
398
399    def timeout(self, *args):
400        pass
401        #self.warn_and_quit(EXIT_TIMEOUT, "timeout: did not receive the info")
402
403    def do_command(self, caps : typedict):
404        log.info("waiting for server events")
405
406    def _process_server_event(self, packet):
407        log.info(": ".join(bytestostr(x) for x in packet[1:]))
408
409    def init_packet_handlers(self):
410        super().init_packet_handlers()
411        self._packet_handlers["server-event"] = self._process_server_event
412        self._packet_handlers["ping"] = self._process_ping
413
414    def _process_ping(self, packet):
415        echotime = packet[1]
416        self.send("ping_echo", echotime, 0, 0, 0, -1)
417
418
419class InfoTimerClient(MonitorXpraClient):
420    """
421        This client keeps monitoring the server
422        and requesting info data
423    """
424    REFRESH_RATE = envint("XPRA_REFRESH_RATE", 1)
425
426    def __init__(self, *args):
427        super().__init__(*args)
428        self.info_request_pending = False
429        self.server_last_info = typedict()
430        self.server_last_info_time = 0
431        self.info_timer = 0
432
433    def run(self):
434        from xpra.gtk_common.gobject_compat import register_os_signals
435        register_os_signals(self.signal_handler, None)
436        v = super().run()
437        self.log("run()=%s" % v)
438        self.cleanup()
439        return v
440
441    def signal_handler(self, signum, *args):
442        self.log("exit_code=%s" % self.exit_code)
443        self.log("signal_handler(%s, %s)" % (signum, args,))
444        self.quit(128+signum)
445        self.log("exit_code=%s" % self.exit_code)
446
447    def log(self, message):
448        #this method is overriden in top client to use a log file
449        log(message)
450
451    def err(self, e):
452        log.error(str(e))
453
454    def cleanup(self):
455        self.cancel_info_timer()
456        MonitorXpraClient.cleanup(self)
457
458    def do_command(self, caps : typedict):
459        self.send_info_request()
460        self.timeout_add(self.REFRESH_RATE*1000, self.send_info_request)
461
462    def send_info_request(self, *categories):
463        self.log("send_info_request%s" % (categories,))
464        if not self.info_request_pending:
465            self.info_request_pending = True
466            window_ids = ()    #no longer used or supported by servers
467            self.send("info-request", [self.uuid], window_ids, categories)
468        if not self.info_timer:
469            self.info_timer = self.timeout_add((self.REFRESH_RATE+2)*1000, self.info_timeout)
470        return True
471
472    def init_packet_handlers(self):
473        MonitorXpraClient.init_packet_handlers(self)
474        self.add_packet_handler("info-response", self._process_info_response, False)
475
476    def _process_server_event(self, packet):
477        self.log("server event: %s" % (packet,))
478        self.last_server_event = packet[1:]
479        self.update_screen()
480
481    def _process_info_response(self, packet):
482        self.log("info response: %s" % repr_ellipsized(packet))
483        self.cancel_info_timer()
484        self.info_request_pending = False
485        self.server_last_info = typedict(packet[1])
486        self.server_last_info_time = monotonic()
487        #log.info("server_last_info=%s", self.server_last_info)
488        self.update_screen()
489
490    def cancel_info_timer(self):
491        it = self.info_timer
492        if it:
493            self.info_timer = None
494            self.source_remove(it)
495
496    def info_timeout(self):
497        self.log("info timeout")
498        self.update_screen()
499        return True
500
501    def update_screen(self):
502        raise NotImplementedError()
503
504
505class ShellXpraClient(SendCommandConnectClient):
506    """
507        Provides an interactive shell with the socket it connects to
508    """
509
510    def __init__(self, opts):
511        super().__init__(opts)
512        self.stdin_io_watch = None
513        self.stdin_buffer = ""
514        self.hello_extra["shell"] = "True"
515
516    def timeout(self, *args):
517        """
518        The shell client never times out,
519        but the superclass calls this method automatically,
520        just ignore it.
521        """
522
523    def cleanup(self):
524        siw = self.stdin_io_watch
525        if siw:
526            self.stdin_io_watch = None
527            self.source_remove(siw)
528        super().cleanup()
529
530    def do_command(self, caps : typedict):
531        if not caps.boolget("shell"):
532            msg = "this server does not support the 'shell' subcommand"
533            log.error(msg)
534            self.disconnect_and_quit(EXIT_UNSUPPORTED, msg)
535            return
536        #start reading from stdin:
537        self.install_signal_handlers()
538        stdin = sys.stdin
539        fileno = stdin.fileno()
540        import fcntl
541        fl = fcntl.fcntl(fileno, fcntl.F_GETFL)
542        fcntl.fcntl(fileno, fcntl.F_SETFL, fl | os.O_NONBLOCK)
543        self.stdin_io_watch = GLib.io_add_watch(sys.stdin,
544                                                GLib.PRIORITY_DEFAULT, GLib.IO_IN,
545                                                self.stdin_ready)
546        self.print_prompt()
547
548    def stdin_ready(self, *_args):
549        data = sys.stdin.read()
550        #log.warn("stdin=%r", data)
551        self.stdin_buffer += data
552        sent = 0
553        if self.stdin_buffer.endswith("\n"):
554            for line in self.stdin_buffer.splitlines():
555                if line:
556                    if line.rstrip("\n\r") in ("quit", "exit"):
557                        self.disconnect_and_quit(EXIT_OK, "user requested %s" % line)
558                        self.stdin_io_watch = None
559                        return False
560                    self.send("shell-exec", line.encode())
561                    sent += 1
562        self.stdin_buffer = ""
563        if not sent:
564            self.print_prompt()
565        return True
566
567    def init_packet_handlers(self):
568        super().init_packet_handlers()
569        self._packet_handlers["shell-reply"] = self._process_shell_reply
570        self._packet_handlers["ping"] = self._process_ping
571
572    def _process_ping(self, packet):
573        echotime = packet[1]
574        self.send("ping_echo", echotime, 0, 0, 0, -1)
575
576    def _process_shell_reply(self, packet):
577        fd = packet[1]
578        message = packet[2]
579        if fd==1:
580            stream = sys.stdout
581        elif fd==2:
582            stream = sys.stderr
583        else:
584            raise Exception("invalid file descriptor %i" % fd)
585        s = net_utf8(message)
586        if s.endswith("\n"):
587            s = s[:-1]
588        stream.write("%s" % s)
589        stream.flush()
590        if fd==2:
591            stream.write("\n")
592            self.print_prompt()
593
594    def print_prompt(self):
595        sys.stdout.write("> ")
596        sys.stdout.flush()
597
598
599class VersionXpraClient(HelloRequestClient):
600    """ This client does one thing only:
601        it queries the server for version information and prints it out
602    """
603
604    def hello_request(self):
605        return {
606            "version_request"       : True,
607            "request"               : "version",
608            "full-version-request"  : True,
609            }
610
611    def parse_network_capabilities(self, *_args):
612        #don't bother checking anything - this could generate warnings
613        return True
614
615    def do_command(self, caps : typedict):
616        v = caps.strget("version")
617        if not v:
618            self.warn_and_quit(EXIT_FAILURE, "server did not provide the version information")
619        else:
620            sys.stdout.write("%s\n" % (v,))
621            sys.stdout.flush()
622            self.quit(EXIT_OK)
623
624
625class ControlXpraClient(CommandConnectClient):
626    """ Allows us to send commands to a server.
627    """
628    def set_command_args(self, command):
629        self.command = command
630
631    def timeout(self, *_args):
632        self.warn_and_quit(EXIT_TIMEOUT, "timeout: server did not respond")
633
634    def do_command(self, caps : typedict):
635        cr = caps.tupleget("command_response")
636        if cr is None:
637            self.warn_and_quit(EXIT_UNSUPPORTED, "server does not support control command")
638            return
639        code, text = cr
640        text = bytestostr(text)
641        if code!=0:
642            log.warn("server returned error code %s", code)
643            self.warn_and_quit(EXIT_REMOTE_ERROR, " %s" % text)
644            return
645        self.warn_and_quit(EXIT_OK, text)
646
647    def make_hello(self):
648        capabilities = super().make_hello()
649        log("make_hello() adding command request '%s' to %s", self.command, capabilities)
650        capabilities["command_request"] = tuple(self.command)
651        capabilities["request"] = "command"
652        return capabilities
653
654
655class PrintClient(SendCommandConnectClient):
656    """ Allows us to send a file to the server for printing.
657    """
658    def set_command_args(self, command):
659        log("set_command_args(%s)", command)
660        self.filename = command[0]
661        #print command arguments:
662        #filename, file_data, mimetype, source_uuid, title, printer, no_copies, print_options_str = packet[1:9]
663        self.command = command[1:]
664        #TODO: load as needed...
665        def sizeerr(size):
666            self.warn_and_quit(EXIT_FILE_TOO_BIG,
667                               "the file is too large: %sB (the file size limit is %sB)" % (
668                                   std_unit(size), std_unit(self.file_size_limit)))
669            return
670        if self.filename=="-":
671            #replace with filename proposed
672            self.filename = command[2]
673            #read file from stdin
674            with open(sys.stdin.fileno(), mode='rb', closefd=False) as stdin_binary:
675                self.file_data = stdin_binary.read()
676            log("read %i bytes from stdin", len(self.file_data))
677        else:
678            size = os.path.getsize(self.filename)
679            if size>self.file_size_limit:
680                sizeerr(size)
681                return
682            from xpra.os_util import load_binary_file
683            self.file_data = load_binary_file(self.filename)
684            log("read %i bytes from %s", len(self.file_data), self.filename)
685        size = len(self.file_data)
686        if size>self.file_size_limit:
687            sizeerr(size)
688            return
689        assert self.file_data, "no data found for '%s'" % self.filename
690
691    def client_type(self):
692        return "Python/GObject/Print"
693
694    def timeout(self, *_args):
695        self.warn_and_quit(EXIT_TIMEOUT, "timeout: server did not respond")
696
697    def do_command(self, caps : typedict):
698        printing = caps.boolget("printing")
699        if not printing:
700            self.warn_and_quit(EXIT_UNSUPPORTED, "server does not support printing")
701            return
702        #we don't compress file data
703        #(this should run locally most of the time anyway)
704        from xpra.net.compression import Compressed  #pylint: disable=import-outside-toplevel
705        blob = Compressed("print", self.file_data)
706        self.send("print", self.filename, blob, *self.command)
707        log("print: sending %s as %s for printing", self.filename, blob)
708        self.idle_add(self.send, "disconnect", DONE, "detaching")
709
710    def make_hello(self):
711        capabilities = super().make_hello()
712        capabilities["wants_features"] = True   #so we know if printing is supported or not
713        capabilities["print_request"] = True    #marker to skip full setup
714        capabilities["request"] = "print"
715        return capabilities
716
717
718class ExitXpraClient(HelloRequestClient):
719    """ This client does one thing only:
720        it asks the server to terminate (like stop),
721        but without killing the Xvfb or clients.
722    """
723
724    def hello_request(self):
725        return {
726            "exit_request"  : True,
727            "request"       : "exit",
728            }
729
730    def do_command(self, caps : typedict):
731        self.idle_add(self.send, "exit-server", os.environ.get("XPRA_EXIT_MESSAGE", SERVER_EXIT))
732
733
734class StopXpraClient(HelloRequestClient):
735    """ stop a server """
736
737    def hello_request(self):
738        return {
739            "stop_request"  : True,
740            "request"       : "stop",
741            }
742
743    def do_command(self, caps : typedict):
744        if not self.server_client_shutdown:
745            log.error("Error: cannot shutdown this server")
746            log.error(" the feature is disable on the server")
747            self.quit(EXIT_FAILURE)
748            return
749        self.timeout_add(1000, self.send_shutdown_server)
750        #self.idle_add(self.send_shutdown_server)
751        #not exiting the client here,
752        #the server should send us the shutdown disconnection message anyway
753        #and if not, we will then hit the timeout to tell us something went wrong
754
755
756class DetachXpraClient(HelloRequestClient):
757    """ run the detach subcommand """
758
759    def hello_request(self):
760        return {
761            "detach_request"    : True,
762            "request"           : "detach",
763            }
764
765    def do_command(self, caps : typedict):
766        self.idle_add(self.send, "disconnect", DONE, "detaching")
767        #not exiting the client here,
768        #the server should disconnect us with the response
769
770class WaitForDisconnectXpraClient(DetachXpraClient):
771    """ we just want the connection to close """
772
773    def _process_disconnect(self, _packet):
774        self.quit(EXIT_OK)
775
776
777class RequestStartClient(HelloRequestClient):
778    """ request the system proxy server to start a new session for us """
779    #wait longer for this command to return:
780    from xpra.scripts.main import WAIT_SERVER_TIMEOUT
781    COMMAND_TIMEOUT = EXTRA_TIMEOUT+WAIT_SERVER_TIMEOUT
782
783    def dots(self):
784        errwrite(".")
785        return not self.connection_established
786
787    def _process_connection_lost(self, packet):
788        errwrite("\n")
789        super()._process_connection_lost(packet)
790
791    def hello_request(self):
792        if first_time("hello-request"):
793            #this can be called again if we receive a challenge,
794            #but only print this message once:
795            errwrite("requesting new session, please wait")
796        self.timeout_add(1*1000, self.dots)
797        return {
798            "start-new-session" : self.start_new_session,
799            #tells proxy servers we don't want to connect to the real / new instance:
800            "connect"                   : False,
801            }
802
803    def server_connection_established(self, caps : typedict):
804        #the server should respond with the display chosen
805        log("server_connection_established() exit_code=%s", self.exit_code)
806        display = caps.strget("display")
807        if display:
808            mode = caps.strget("mode")
809            session_type = {
810                "start"         : "seamless ",
811                "start-desktop" : "desktop ",
812                "shadow"        : "shadow ",
813                }.get(mode, "")
814            try:
815                errwrite("\n%ssession now available on display %s\n" % (session_type, display))
816                if POSIX and not OSX and self.displayfd>0 and display and display.startswith(":"):
817                    from xpra.platform.displayfd import write_displayfd
818                    log("writing display %s to displayfd=%s", display, self.displayfd)
819                    write_displayfd(self.displayfd, display[1:])
820            except OSError:
821                log("server_connection_established(..)", exc_info=True)
822        if not self.exit_code:
823            self.quit(0)
824        return True
825
826    def __init__(self, opts):
827        super().__init__(opts)
828        try:
829            self.displayfd = int(opts.displayfd)
830        except (ValueError, TypeError):
831            self.displayfd = 0
832