Hide keyboard shortcuts

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) 2011 Serviware (Arthur Huillet, <ahuillet@serviware.com>) 

3# Copyright (C) 2010-2020 Antoine Martin <antoine@xpra.org> 

4# Copyright (C) 2008, 2010 Nathaniel Smith <njs@pobox.com> 

5# Xpra is released under the terms of the GNU GPL v2, or, at your option, any 

6# later version. See the file COPYING for details. 

7 

8import os 

9import sys 

10 

11from xpra.client.client_base import XpraClientBase 

12from xpra.client.keyboard_helper import KeyboardHelper 

13from xpra.platform import set_name 

14from xpra.platform.gui import ready as gui_ready, get_wm_name, get_session_type, ClientExtras 

15from xpra.version_util import full_version_str 

16from xpra.net import compression, packet_encoding 

17from xpra.net.net_util import get_info as get_net_info 

18from xpra.child_reaper import reaper_cleanup 

19from xpra.platform.info import get_sys_info 

20from xpra.os_util import ( 

21 platform_name, bytestostr, strtobytes, 

22 BITS, POSIX, is_Wayland, 

23 get_frame_info, get_info_env, get_sysconfig_info, 

24 ) 

25from xpra.util import ( 

26 std, envbool, envint, typedict, updict, repr_ellipsized, ellipsizer, log_screen_sizes, engs, csv, 

27 merge_dicts, 

28 XPRA_AUDIO_NOTIFICATION_ID, XPRA_DISCONNECT_NOTIFICATION_ID, 

29 ) 

30from xpra.exit_codes import EXIT_CONNECTION_FAILED, EXIT_OK, EXIT_CONNECTION_LOST 

31from xpra.version_util import get_version_info_full, get_platform_info 

32from xpra.client import mixin_features 

33from xpra.log import Logger 

34 

35 

36CLIENT_BASES = [XpraClientBase] 

37if mixin_features.display: 

38 from xpra.client.mixins.display import DisplayClient 

39 CLIENT_BASES.append(DisplayClient) 

40if mixin_features.windows: 

41 from xpra.client.mixins.window_manager import WindowClient 

42 CLIENT_BASES.append(WindowClient) 

43if mixin_features.webcam: 

44 from xpra.client.mixins.webcam import WebcamForwarder 

45 CLIENT_BASES.append(WebcamForwarder) 

46if mixin_features.audio: 

47 from xpra.client.mixins.audio import AudioClient 

48 CLIENT_BASES.append(AudioClient) 

49if mixin_features.clipboard: 

50 from xpra.client.mixins.clipboard import ClipboardClient 

51 CLIENT_BASES.append(ClipboardClient) 

52if mixin_features.notifications: 

53 from xpra.client.mixins.notifications import NotificationClient 

54 CLIENT_BASES.append(NotificationClient) 

55if mixin_features.dbus: 

56 from xpra.client.mixins.rpc import RPCClient 

57 CLIENT_BASES.append(RPCClient) 

58if mixin_features.mmap: 

59 from xpra.client.mixins.mmap import MmapClient 

60 CLIENT_BASES.append(MmapClient) 

61if mixin_features.logging: 

62 from xpra.client.mixins.remote_logging import RemoteLogging 

63 CLIENT_BASES.append(RemoteLogging) 

64if mixin_features.network_state: 

65 from xpra.client.mixins.network_state import NetworkState 

66 CLIENT_BASES.append(NetworkState) 

67if mixin_features.network_listener: 

68 from xpra.client.mixins.network_listener import NetworkListener 

69 CLIENT_BASES.append(NetworkListener) 

70if mixin_features.encoding: 

71 from xpra.client.mixins.encodings import Encodings 

72 CLIENT_BASES.append(Encodings) 

73if mixin_features.tray: 

74 from xpra.client.mixins.tray import TrayClient 

75 CLIENT_BASES.append(TrayClient) 

76 

77CLIENT_BASES = tuple(CLIENT_BASES) 

78ClientBaseClass = type('ClientBaseClass', CLIENT_BASES, {}) 

79 

80log = Logger("client") 

81keylog = Logger("client", "keyboard") 

82log("UIXpraClient%s: %s", ClientBaseClass, CLIENT_BASES) 

83 

84NOTIFICATION_EXIT_DELAY = envint("XPRA_NOTIFICATION_EXIT_DELAY", 2) 

85MOUSE_DELAY_AUTO = envbool("XPRA_MOUSE_DELAY_AUTO", True) 

86SYSCONFIG = envbool("XPRA_SYSCONFIG", False) 

87 

88 

89""" 

90Utility superclass for client classes which have a UI. 

91See gtk_client_base and its subclasses. 

92""" 

93class UIXpraClient(ClientBaseClass): 

94 #NOTE: these signals aren't registered here because this class 

95 #does not extend GObject, 

96 #the gtk client subclasses will take care of it. 

97 #these are all "no-arg" signals 

98 __signals__ = ["first-ui-received",] 

99 for c in CLIENT_BASES: 

100 if c!=XpraClientBase: 

101 __signals__ += c.__signals__ 

102 

103 def __init__(self): 

104 log.info("Xpra %s client version %s %i-bit", self.client_toolkit(), full_version_str(), BITS) 

105 #mmap_enabled belongs in the MmapClient mixin, 

106 #but it is used outside it, so make sure we define it: 

107 self.mmap_enabled = False 

108 #same for tray: 

109 self.tray = None 

110 for c in CLIENT_BASES: 

111 log("calling %s.__init__()", c) 

112 c.__init__(self) 

113 try: 

114 pinfo = get_platform_info() 

115 osinfo = "%s" % platform_name(sys.platform, pinfo.get("linux_distribution") or pinfo.get("sysrelease", "")) 

116 log.info(" running on %s", osinfo) 

117 except Exception: 

118 log("platform name error:", exc_info=True) 

119 wm = get_wm_name() #pylint: disable=assignment-from-none 

120 if wm: 

121 log.info(" window manager is '%s'", wm) 

122 

123 self._ui_events = 0 

124 self.title = "" 

125 self.session_name = "" 

126 

127 self.server_platform = "" 

128 self.server_session_name = None 

129 

130 #features: 

131 self.opengl_enabled = False 

132 self.opengl_props = {} 

133 self.readonly = False 

134 self.xsettings_enabled = False 

135 self.server_start_new_commands = False 

136 self.server_xdg_menu = None 

137 self.start_new_commands = [] 

138 self.start_child_new_commands = [] 

139 self.headerbar = None 

140 

141 #in WindowClient - should it be? 

142 #self.server_is_desktop = False 

143 self.server_sharing = False 

144 self.server_sharing_toggle = False 

145 self.server_lock = False 

146 self.server_lock_toggle = False 

147 self.server_keyboard = True 

148 self.server_pointer = True 

149 

150 self.client_supports_opengl = False 

151 self.client_supports_sharing = False 

152 self.client_lock = False 

153 

154 #helpers and associated flags: 

155 self.client_extras = None 

156 self.keyboard_helper_class = KeyboardHelper 

157 self.keyboard_helper = None 

158 self.keyboard_grabbed = False 

159 self.keyboard_sync = False 

160 self.pointer_grabbed = False 

161 self.kh_warning = False 

162 self.menu_helper = None 

163 

164 #state: 

165 self._on_handshake = [] 

166 self._on_server_setting_changed = {} 

167 

168 

169 def init(self, opts): 

170 """ initialize variables from configuration """ 

171 for c in CLIENT_BASES: 

172 log("init: %s", c) 

173 c.init(self, opts) 

174 

175 self.title = opts.title 

176 self.session_name = bytestostr(opts.session_name) 

177 self.xsettings_enabled = opts.xsettings 

178 self.readonly = opts.readonly 

179 self.client_supports_sharing = opts.sharing is True 

180 self.client_lock = opts.lock is True 

181 self.headerbar = opts.headerbar 

182 

183 

184 def init_ui(self, opts): 

185 """ initialize user interface """ 

186 if not self.readonly: 

187 def noauto(v): 

188 if not v: 

189 return None 

190 if str(v).lower()=="auto": 

191 return None 

192 return v 

193 overrides = [noauto(getattr(opts, "keyboard_%s" % x)) for x in ( 

194 "layout", "layouts", "variant", "variants", "options", 

195 )] 

196 def send_keyboard(*parts): 

197 self.after_handshake(self.send, *parts) 

198 try: 

199 self.keyboard_helper = self.keyboard_helper_class(send_keyboard, opts.keyboard_sync, 

200 opts.shortcut_modifiers, 

201 opts.key_shortcut, 

202 opts.keyboard_raw, *overrides) 

203 except ImportError as e: 

204 keylog("error instantiating %s", self.keyboard_helper_class, exc_info=True) 

205 keylog.warn("Warning: no keyboard support, %s", e) 

206 

207 if mixin_features.windows: 

208 self.init_opengl(opts.opengl) 

209 

210 if ClientExtras is not None: 

211 self.client_extras = ClientExtras(self, opts) #pylint: disable=not-callable 

212 

213 if opts.start or opts.start_child: 

214 from xpra.scripts.main import strip_defaults_start_child 

215 from xpra.scripts.config import make_defaults_struct 

216 defaults = make_defaults_struct() 

217 self.start_new_commands = strip_defaults_start_child(opts.start, defaults.start) #pylint: disable=no-member 

218 self.start_child_new_commands = strip_defaults_start_child(opts.start_child, defaults.start_child) #pylint: disable=no-member 

219 

220 if MOUSE_DELAY_AUTO: 

221 try: 

222 from xpra.platform.gui import get_vrefresh 

223 v = get_vrefresh() 

224 if v<=0: 

225 #some platforms don't detect the vrefresh correctly 

226 #(ie: macos in virtualbox?), so use a sane default: 

227 v = 60 

228 self._mouse_position_delay = 1000//v//2 

229 log("mouse delay: %s", self._mouse_position_delay) 

230 except Exception: 

231 log("failed to calculate automatic delay", exc_info=True) 

232 

233 

234 def run(self): 

235 if self.client_extras: 

236 self.idle_add(self.client_extras.ready) 

237 for c in CLIENT_BASES: 

238 c.run(self) 

239 

240 

241 def quit(self, _exit_code=0): 

242 raise NotImplementedError() 

243 

244 def cleanup(self): 

245 log("UIXpraClient.cleanup()") 

246 for c in CLIENT_BASES: 

247 c.cleanup(self) 

248 for x in (self.keyboard_helper, self.tray, self.menu_helper, self.client_extras): 

249 if x is None: 

250 continue 

251 log("UIXpraClient.cleanup() calling %s.cleanup()", type(x)) 

252 try: 

253 x.cleanup() 

254 except Exception: 

255 log.error("error on %s cleanup", type(x), exc_info=True) 

256 #the protocol has been closed, it is now safe to close all the windows: 

257 #(cleaner and needed when we run embedded in the client launcher) 

258 reaper_cleanup() 

259 log("UIXpraClient.cleanup() done") 

260 

261 

262 def signal_cleanup(self): 

263 log("UIXpraClient.signal_cleanup()") 

264 XpraClientBase.signal_cleanup(self) 

265 reaper_cleanup() 

266 log("UIXpraClient.signal_cleanup() done") 

267 

268 

269 def get_info(self): 

270 info = { 

271 "pid" : os.getpid(), 

272 "threads" : get_frame_info(), 

273 "env" : get_info_env(), 

274 "sys" : get_sys_info(), 

275 "network" : get_net_info(), 

276 } 

277 if SYSCONFIG: 

278 info["sysconfig"] = get_sysconfig_info() 

279 for c in CLIENT_BASES: 

280 try: 

281 i = c.get_info(self) 

282 info = merge_dicts(info, i) 

283 except Exception: 

284 log.error("Error collection information from %s", c, exc_info=True) 

285 return info 

286 

287 

288 def show_about(self, *_args): 

289 log.warn("show_about() is not implemented in %s", self) 

290 

291 def show_session_info(self, *_args): 

292 log.warn("show_session_info() is not implemented in %s", self) 

293 

294 def show_bug_report(self, *_args): 

295 log.warn("show_bug_report() is not implemented in %s", self) 

296 

297 

298 def init_opengl(self, _enable_opengl): 

299 self.opengl_enabled = False 

300 self.client_supports_opengl = False 

301 self.opengl_props = {"info" : "not supported"} 

302 

303 

304 def _ui_event(self): 

305 if self._ui_events==0: 

306 self.emit("first-ui-received") 

307 self._ui_events += 1 

308 

309 

310 def get_mouse_position(self): 

311 raise NotImplementedError() 

312 

313 def get_current_modifiers(self): 

314 raise NotImplementedError() 

315 

316 

317 def send_start_new_commands(self): 

318 log("send_start_new_commands() start_new_commands=%s, start_child_new_commands=%s", 

319 self.start_new_commands, self.start_child_new_commands) 

320 import shlex 

321 for cmd in self.start_new_commands: 

322 cmd_parts = shlex.split(cmd) 

323 self.send_start_command(cmd_parts[0], cmd, True) 

324 for cmd in self.start_child_new_commands: 

325 cmd_parts = shlex.split(cmd) 

326 self.send_start_command(cmd_parts[0], cmd, False) 

327 

328 def send_start_command(self, name, command, ignore, sharing=True): 

329 log("send_start_command(%s, %s, %s, %s)", name, command, ignore, sharing) 

330 assert name is not None and command is not None and ignore is not None 

331 self.send("start-command", name, command, ignore, sharing) 

332 

333 def get_version_info(self) -> dict: 

334 return get_version_info_full() 

335 

336 

337 ###################################################################### 

338 # trigger notifications on disconnection, 

339 # and wait before actually exiting so the notification has a chance of being seen 

340 def server_disconnect_warning(self, reason, *info): 

341 if self.exit_code is None: 

342 body = "\n".join(info) 

343 if self.connection_established: 

344 title = "Xpra Session Disconnected: %s" % reason 

345 self.exit_code = EXIT_CONNECTION_LOST 

346 else: 

347 title = "Connection Failed: %s" % reason 

348 self.exit_code = EXIT_CONNECTION_FAILED 

349 self.may_notify(XPRA_DISCONNECT_NOTIFICATION_ID, 

350 title, body, icon_name="disconnected") 

351 #show text notification then quit: 

352 delay = NOTIFICATION_EXIT_DELAY*mixin_features.notifications 

353 self.timeout_add(delay*1000, XpraClientBase.server_disconnect_warning, self, reason, *info) 

354 self.cleanup() 

355 

356 def server_disconnect(self, reason, *info): 

357 body = "\n".join(info) 

358 self.may_notify(XPRA_DISCONNECT_NOTIFICATION_ID, 

359 "Xpra Session Disconnected: %s" % reason, body, icon_name="disconnected") 

360 self.exit_code = EXIT_OK 

361 delay = NOTIFICATION_EXIT_DELAY*mixin_features.notifications 

362 self.timeout_add(delay*1000, XpraClientBase.server_disconnect, self, reason, *info) 

363 self.cleanup() 

364 

365 

366 ###################################################################### 

367 # hello: 

368 def make_hello(self): 

369 caps = XpraClientBase.make_hello(self) 

370 caps["session-type"] = get_session_type() 

371 #don't try to find the server uuid if this platform cannot run servers.. 

372 #(doing so causes lockups on win32 and startup errors on osx) 

373 if POSIX and not is_Wayland(): 

374 #we may be running inside another server! 

375 try: 

376 from xpra.server.server_uuid import get_uuid 

377 caps["server_uuid"] = get_uuid() or "" 

378 except ImportError: 

379 pass 

380 for x in (#generic feature flags: 

381 "wants_events", "setting-change", 

382 "xdg-menu-update", 

383 ): 

384 caps[x] = True 

385 caps.update({ 

386 #generic server flags: 

387 "share" : self.client_supports_sharing, 

388 "lock" : self.client_lock, 

389 }) 

390 caps.update({"mouse" : True}) 

391 caps.update(self.get_keyboard_caps()) 

392 for c in CLIENT_BASES: 

393 caps.update(c.get_caps(self)) 

394 def u(prefix, c): 

395 updict(caps, prefix, c, flatten_dicts=False) 

396 u("control_commands", self.get_control_commands_caps()) 

397 u("platform", get_platform_info()) 

398 u("opengl", self.opengl_props) 

399 return caps 

400 

401 

402 

403 ###################################################################### 

404 # connection setup: 

405 def setup_connection(self, conn): 

406 protocol = super().setup_connection(conn) 

407 for c in CLIENT_BASES: 

408 if c!=XpraClientBase: 

409 c.setup_connection(self, conn) 

410 return protocol 

411 

412 def server_connection_established(self, caps : typedict): 

413 if not XpraClientBase.server_connection_established(self, caps): 

414 return False 

415 #process the rest from the UI thread: 

416 self.idle_add(self.process_ui_capabilities, caps) 

417 return True 

418 

419 

420 def parse_server_capabilities(self, c : typedict) -> bool: 

421 for cb in CLIENT_BASES: 

422 if not cb.parse_server_capabilities(self, c): 

423 log.info("failed to parse server capabilities in %s", cb) 

424 return False 

425 self.server_session_name = strtobytes(c.rawget("session_name", b"")).decode("utf-8") 

426 set_name("Xpra", self.session_name or self.server_session_name or "Xpra") 

427 self.server_platform = c.strget("platform") 

428 self.server_sharing = c.boolget("sharing") 

429 self.server_sharing_toggle = c.boolget("sharing-toggle") 

430 self.server_lock = c.boolget("lock") 

431 self.server_lock_toggle = c.boolget("lock-toggle") 

432 self.server_keyboard = c.boolget("keyboard", True) 

433 self.server_pointer = c.boolget("pointer", True) 

434 self.server_start_new_commands = c.boolget("start-new-commands") 

435 if self.server_start_new_commands: 

436 self.server_xdg_menu = c.dictget("xdg-menu", None) 

437 if self.start_new_commands or self.start_child_new_commands: 

438 if self.server_start_new_commands: 

439 self.after_handshake(self.send_start_new_commands) 

440 else: 

441 log.warn("Warning: cannot start new commands") 

442 log.warn(" the feature is currently disabled on the server") 

443 self.server_commands_info = c.boolget("server-commands-info") 

444 self.server_commands_signals = c.strtupleget("server-commands-signals") 

445 self.server_readonly = c.boolget("readonly") 

446 if self.server_readonly and not self.readonly: 

447 log.info("server is read only") 

448 self.readonly = True 

449 if not self.server_keyboard and self.keyboard_helper: 

450 #swallow packets: 

451 def nosend(*_args): 

452 pass 

453 self.keyboard_helper.send = nosend 

454 

455 i = platform_name(self._remote_platform, 

456 c.strtupleget("platform.linux_distribution") or c.strget("platform.release", "")) 

457 r = self._remote_version 

458 if self._remote_revision: 

459 r += "-r%s" % self._remote_revision 

460 mode = c.strget("server.mode", "server") 

461 bits = c.intget("python.bits", 32) 

462 log.info("Xpra %s server version %s %i-bit", mode, std(r), bits) 

463 if i: 

464 log.info(" running on %s", std(i)) 

465 if c.boolget("desktop") or c.boolget("shadow"): 

466 v = c.intpair("actual_desktop_size") 

467 if v: 

468 w, h = v 

469 ss = c.tupleget("screen_sizes") 

470 if ss: 

471 log.info(" remote desktop size is %sx%s with %s screen%s:", w, h, len(ss), engs(ss)) 

472 log_screen_sizes(w, h, ss) 

473 else: 

474 log.info(" remote desktop size is %sx%s", w, h) 

475 if c.boolget("proxy"): 

476 proxy_hostname = c.strget("proxy.hostname") 

477 proxy_platform = c.strget("proxy.platform") 

478 proxy_release = c.strget("proxy.platform.release") 

479 proxy_version = c.strget("proxy.version") 

480 proxy_version = c.strget("proxy.build.version", proxy_version) 

481 proxy_distro = c.strget("proxy.linux_distribution") 

482 msg = "via: %s proxy version %s" % ( 

483 platform_name(proxy_platform, proxy_distro or proxy_release), 

484 std(proxy_version or "unknown") 

485 ) 

486 if proxy_hostname: 

487 msg += " on '%s'" % std(proxy_hostname) 

488 log.info(msg) 

489 return True 

490 

491 def process_ui_capabilities(self, caps : typedict): 

492 for c in CLIENT_BASES: 

493 if c!=XpraClientBase: 

494 c.process_ui_capabilities(self, caps) 

495 #keyboard: 

496 if self.keyboard_helper: 

497 modifier_keycodes = caps.dictget("modifier_keycodes", {}) 

498 if modifier_keycodes: 

499 self.keyboard_helper.set_modifier_mappings(modifier_keycodes) 

500 self.key_repeat_delay, self.key_repeat_interval = caps.intpair("key_repeat", (-1,-1)) 

501 self.handshake_complete() 

502 

503 

504 def _process_startup_complete(self, packet): 

505 log("all the existing windows and system trays have been received") 

506 XpraClientBase._process_startup_complete(self, packet) 

507 gui_ready() 

508 if self.tray: 

509 self.tray.ready() 

510 self.send_info_request() 

511 msg = "running" 

512 try: 

513 windows = tuple(self._id_to_window.values()) 

514 except AttributeError: 

515 pass 

516 else: 

517 trays = sum(1 for w in windows if w.is_tray()) 

518 wins = sum(1 for w in windows if not w.is_tray()) 

519 if wins: 

520 msg += ", %i window%s" % (wins, engs(wins)) 

521 if trays: 

522 msg += ", %i tray%s" % (trays, engs(trays)) 

523 log.info(msg) 

524 

525 def handshake_complete(self): 

526 oh = self._on_handshake 

527 self._on_handshake = None 

528 for cb, args in oh: 

529 try: 

530 cb(*args) 

531 except Exception: 

532 log.error("Error processing handshake callback %s", cb, exc_info=True) 

533 

534 def after_handshake(self, cb, *args): 

535 log("after_handshake(%s, %s) on_handshake=%s", cb, args, ellipsizer(self._on_handshake)) 

536 if self._on_handshake is None: 

537 #handshake has already occurred, just call it: 

538 self.idle_add(cb, *args) 

539 else: 

540 self._on_handshake.append((cb, args)) 

541 

542 

543 ###################################################################### 

544 # server messages: 

545 def _process_server_event(self, packet): 

546 log(": ".join((str(x) for x in packet[1:]))) 

547 

548 def on_server_setting_changed(self, setting, cb): 

549 self._on_server_setting_changed.setdefault(setting, []).append(cb) 

550 

551 def _process_setting_change(self, packet): 

552 setting, value = packet[1:3] 

553 setting = bytestostr(setting) 

554 #convert "hello" / "setting" variable names to client variables: 

555 if setting in ( 

556 "clipboard-limits", 

557 ): 

558 pass 

559 elif setting in ( 

560 "bell", "randr", "cursors", "notifications", "dbus-proxy", "clipboard", 

561 "clipboard-direction", "session_name", 

562 "sharing", "sharing-toggle", "lock", "lock-toggle", 

563 "start-new-commands", "client-shutdown", "webcam", 

564 "bandwidth-limit", "clipboard-limits", 

565 "xdg-menu", 

566 ): 

567 setattr(self, "server_%s" % setting.replace("-", "_"), value) 

568 else: 

569 log.info("unknown server setting changed: %s=%s", setting, repr_ellipsized(bytestostr(value))) 

570 return 

571 log("_process_setting_change: %s=%s", setting, value) 

572 #xdg-menu is too big to log, and we have to update our attribute: 

573 if setting=="xdg-menu": 

574 self.server_xdg_menu = value 

575 else: 

576 log.info("server setting changed: %s=%s", setting, repr_ellipsized(value)) 

577 self.server_setting_changed(setting, value) 

578 

579 def server_setting_changed(self, setting, value): 

580 log("setting_changed(%s, %s)", setting, value) 

581 cbs = self._on_server_setting_changed.get(setting) 

582 if cbs: 

583 for cb in cbs: 

584 log("setting_changed(%s, %s) calling %s", setting, value, cb) 

585 cb(setting, value) 

586 

587 

588 def get_control_commands_caps(self): 

589 caps = ["show_session_info", "show_bug_report", "debug"] 

590 for x in compression.get_enabled_compressors(): 

591 caps.append("enable_"+x) 

592 for x in packet_encoding.get_enabled_encoders(): 

593 caps.append("enable_"+x) 

594 log("get_control_commands_caps()=%s", caps) 

595 return {"" : caps} 

596 

597 def _process_control(self, packet): 

598 command = bytestostr(packet[1]) 

599 if command=="show_session_info": 

600 args = packet[2:] 

601 log("calling %s%s on server request", self.show_session_info, args) 

602 self.show_session_info(*args) 

603 elif command=="show_bug_report": 

604 self.show_bug_report() 

605 elif command in ("enable_%s" % x for x in compression.get_enabled_compressors()): 

606 compressor = command.split("_")[1] 

607 log.info("switching to %s on server request", compressor) 

608 self._protocol.enable_compressor(compressor) 

609 elif command in ("enable_%s" % x for x in packet_encoding.get_enabled_encoders()): 

610 pe = command.split("_")[1] 

611 log.info("switching to %s on server request", pe) 

612 self._protocol.enable_encoder(pe) 

613 elif command=="name": 

614 assert len(args)>=3 

615 self.server_session_name = args[2] 

616 log.info("session name updated from server: %s", self.server_session_name) 

617 #TODO: reset tray tooltip, session info title, etc.. 

618 elif command=="debug": 

619 args = packet[2:] 

620 if not args: 

621 log.warn("not enough arguments for debug control command") 

622 return 

623 from xpra.log import ( 

624 add_debug_category, add_disabled_category, 

625 enable_debug_for, disable_debug_for, 

626 get_all_loggers, 

627 ) 

628 log_cmd = bytestostr(args[0]) 

629 if log_cmd=="status": 

630 dloggers = [x for x in get_all_loggers() if x.is_debug_enabled()] 

631 if dloggers: 

632 log.info("logging is enabled for:") 

633 for l in dloggers: 

634 log.info(" - %s", l) 

635 else: 

636 log.info("logging is not enabled for any loggers") 

637 return 

638 log_cmd = bytestostr(args[0]) 

639 if log_cmd not in ("enable", "disable"): 

640 log.warn("invalid debug control mode: '%s' (must be 'enable' or 'disable')", log_cmd) 

641 return 

642 if len(args)<2: 

643 log.warn("not enough arguments for '%s' debug control command" % log_cmd) 

644 return 

645 loggers = [] 

646 #each argument is a group 

647 groups = [bytestostr(x) for x in args[1:]] 

648 for group in groups: 

649 #and each group is a list of categories 

650 #preferably separated by "+", 

651 #but we support "," for backwards compatibility: 

652 categories = [v.strip() for v in group.replace("+", ",").split(",")] 

653 if log_cmd=="enable": 

654 add_debug_category(*categories) 

655 loggers += enable_debug_for(*categories) 

656 else: 

657 assert log_cmd=="disable" 

658 add_disabled_category(*categories) 

659 loggers += disable_debug_for(*categories) 

660 if not loggers: 

661 log.info("%s debugging, no new loggers matching: %s", log_cmd, csv(groups)) 

662 else: 

663 log.info("%sd debugging for:", log_cmd) 

664 for l in loggers: 

665 log.info(" - %s", l) 

666 else: 

667 log.warn("received invalid control command from server: %s", command) 

668 

669 

670 def may_notify_audio(self, summary, body): 

671 self.may_notify(XPRA_AUDIO_NOTIFICATION_ID, summary, body, icon_name="audio") 

672 

673 

674 ###################################################################### 

675 # features: 

676 def send_sharing_enabled(self): 

677 assert self.server_sharing and self.server_sharing_toggle 

678 self.send("sharing-toggle", self.client_supports_sharing) 

679 

680 def send_lock_enabled(self): 

681 assert self.server_lock_toggle 

682 self.send("lock-toggle", self.client_lock) 

683 

684 def send_notify_enabled(self): 

685 assert self.client_supports_notifications, "cannot toggle notifications: the feature is disabled by the client" 

686 self.send("set-notify", self.notifications_enabled) 

687 

688 def send_bell_enabled(self): 

689 assert self.client_supports_bell, "cannot toggle bell: the feature is disabled by the client" 

690 assert self.server_bell, "cannot toggle bell: the feature is disabled by the server" 

691 self.send("set-bell", self.bell_enabled) 

692 

693 def send_cursors_enabled(self): 

694 assert self.client_supports_cursors, "cannot toggle cursors: the feature is disabled by the client" 

695 assert self.server_cursors, "cannot toggle cursors: the feature is disabled by the server" 

696 self.send("set-cursors", self.cursors_enabled) 

697 

698 def send_force_ungrab(self, wid): 

699 self.send("force-ungrab", wid) 

700 

701 def send_keyboard_sync_enabled_status(self, *_args): 

702 self.send("set-keyboard-sync-enabled", self.keyboard_sync) 

703 

704 

705 ###################################################################### 

706 # keyboard: 

707 def get_keyboard_caps(self): 

708 caps = {} 

709 if self.readonly or not self.keyboard_helper: 

710 #don't bother sending keyboard info, as it won't be used 

711 caps["keyboard"] = False 

712 else: 

713 caps.update(self.get_keymap_properties()) 

714 #show the user a summary of what we have detected: 

715 self.keyboard_helper.log_keyboard_info() 

716 

717 caps["modifiers"] = self.get_current_modifiers() 

718 delay_ms, interval_ms = self.keyboard_helper.key_repeat_delay, self.keyboard_helper.key_repeat_interval 

719 if delay_ms>0 and interval_ms>0: 

720 caps["key_repeat"] = (delay_ms,interval_ms) 

721 else: 

722 #cannot do keyboard_sync without a key repeat value! 

723 #(maybe we could just choose one?) 

724 self.keyboard_helper.keyboard_sync = False 

725 caps["keyboard_sync"] = self.keyboard_helper.keyboard_sync 

726 log("keyboard capabilities: %s", caps) 

727 return caps 

728 

729 def window_keyboard_layout_changed(self, window): 

730 #win32 can change the keyboard mapping per window... 

731 keylog("window_keyboard_layout_changed(%s)", window) 

732 if self.keyboard_helper: 

733 self.keyboard_helper.keymap_changed() 

734 

735 def get_keymap_properties(self): 

736 if not self.keyboard_helper: 

737 return {} 

738 props = self.keyboard_helper.get_keymap_properties() 

739 props["modifiers"] = self.get_current_modifiers() 

740 return props 

741 

742 def handle_key_action(self, window, key_event): 

743 if self.readonly or self.keyboard_helper is None: 

744 return False 

745 wid = self._window_to_id[window] 

746 keylog("handle_key_action(%s, %s) wid=%s", window, key_event, wid) 

747 return self.keyboard_helper.handle_key_action(window, wid, key_event) 

748 

749 def mask_to_names(self, mask): 

750 if self.keyboard_helper is None: 

751 return [] 

752 return self.keyboard_helper.mask_to_names(mask) 

753 

754 

755 ###################################################################### 

756 # windows overrides 

757 def cook_metadata(self, _new_window, metadata): 

758 #convert to a typedict and apply client-side overrides: 

759 metadata = typedict(metadata) 

760 if self.server_is_desktop and self.desktop_fullscreen: 

761 #force it fullscreen: 

762 metadata.pop("size-constraints", None) 

763 metadata["fullscreen"] = True 

764 #FIXME: try to figure out the monitors we go fullscreen on for X11: 

765 #if POSIX: 

766 # metadata["fullscreen-monitors"] = [0, 1, 0, 1] 

767 return metadata 

768 

769 ###################################################################### 

770 # network and status: 

771 def server_connection_state_change(self): 

772 if not self._server_ok: 

773 log.info("server is not responding, drawing spinners over the windows") 

774 def timer_redraw(): 

775 if self._protocol is None: 

776 #no longer connected! 

777 return False 

778 ok = self.server_ok() 

779 self.redraw_spinners() 

780 if ok: 

781 log.info("server is OK again") 

782 return not ok #repaint again until ok 

783 self.idle_add(self.redraw_spinners) 

784 self.timeout_add(250, timer_redraw) 

785 

786 def redraw_spinners(self): 

787 #draws spinner on top of the window, or not (plain repaint) 

788 #depending on whether the server is ok or not 

789 ok = self.server_ok() 

790 log("redraw_spinners() ok=%s", ok) 

791 for w in self._id_to_window.values(): 

792 if not w.is_tray(): 

793 w.spinner(ok) 

794 

795 

796 ###################################################################### 

797 # packets: 

798 def init_authenticated_packet_handlers(self): 

799 log("init_authenticated_packet_handlers()") 

800 for c in CLIENT_BASES: 

801 c.init_authenticated_packet_handlers(self) 

802 #run from the UI thread: 

803 self.add_packet_handlers({ 

804 "startup-complete": self._process_startup_complete, 

805 "setting-change": self._process_setting_change, 

806 "control" : self._process_control, 

807 }) 

808 #run directly from the network thread: 

809 self.add_packet_handler("server-event", self._process_server_event, False) 

810 

811 

812 def process_packet(self, proto, packet): 

813 self.check_server_echo(0) 

814 XpraClientBase.process_packet(self, proto, packet)