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# -*- coding: utf-8 -*- 

2# This file is part of Xpra. 

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

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 

8 

9from xpra.server.window.batch_config import DamageBatchConfig 

10from xpra.server.shadow.root_window_model import RootWindowModel 

11from xpra.notifications.common import parse_image_path 

12from xpra.platform.gui import get_native_notifier_classes, get_wm_name 

13from xpra.platform.paths import get_icon_dir 

14from xpra.server import server_features 

15from xpra.util import envint, envbool, DONE, XPRA_STARTUP_NOTIFICATION_ID, XPRA_NEW_USER_NOTIFICATION_ID 

16from xpra.log import Logger 

17 

18log = Logger("shadow") 

19notifylog = Logger("notify") 

20mouselog = Logger("mouse") 

21cursorlog = Logger("cursor") 

22 

23REFRESH_DELAY = envint("XPRA_SHADOW_REFRESH_DELAY", 50) 

24NATIVE_NOTIFIER = envbool("XPRA_NATIVE_NOTIFIER", True) 

25POLL_POINTER = envint("XPRA_POLL_POINTER", 20) 

26CURSORS = envbool("XPRA_CURSORS", True) 

27SAVE_CURSORS = envbool("XPRA_SAVE_CURSORS", False) 

28NOTIFY_STARTUP = envbool("XPRA_SHADOW_NOTIFY_STARTUP", True) 

29 

30 

31SHADOWSERVER_BASE_CLASS = object 

32if server_features.rfb: 

33 from xpra.server.rfb.rfb_server import RFBServer 

34 SHADOWSERVER_BASE_CLASS = RFBServer 

35 

36 

37class ShadowServerBase(SHADOWSERVER_BASE_CLASS): 

38 

39 def __init__(self, root_window, capture=None): 

40 SHADOWSERVER_BASE_CLASS.__init__(self) 

41 self.capture = capture 

42 self.root = root_window 

43 self.mapped = [] 

44 self.pulseaudio = False 

45 self.sharing = True 

46 self.refresh_delay = REFRESH_DELAY 

47 self.refresh_timer = None 

48 self.notifications = False 

49 self.notifier = None 

50 self.pointer_last_position = None 

51 self.pointer_poll_timer = None 

52 self.last_cursor_data = None 

53 DamageBatchConfig.ALWAYS = True #always batch 

54 DamageBatchConfig.MIN_DELAY = 50 #never lower than 50ms 

55 

56 def init(self, opts): 

57 if SHADOWSERVER_BASE_CLASS!=object: 

58 #RFBServer: 

59 SHADOWSERVER_BASE_CLASS.init(self, opts) 

60 self.notifications = bool(opts.notifications) 

61 if self.notifications: 

62 self.make_notifier() 

63 log("init(..) session_name=%s", opts.session_name) 

64 if opts.session_name: 

65 self.session_name = opts.session_name 

66 else: 

67 self.guess_session_name() 

68 

69 def run(self): 

70 if NOTIFY_STARTUP: 

71 from gi.repository import GLib 

72 GLib.timeout_add(1000, self.notify_startup_complete) 

73 return super().run() 

74 

75 def cleanup(self): 

76 for wid in self.mapped: 

77 self.stop_refresh(wid) 

78 self.cleanup_notifier() 

79 self.cleanup_capture() 

80 

81 def cleanup_capture(self): 

82 capture = self.capture 

83 if capture: 

84 self.capture = None 

85 capture.clean() 

86 

87 

88 def guess_session_name(self, procs=None): 

89 log("guess_session_name(%s)", procs) 

90 self.session_name = get_wm_name() # pylint: disable=assignment-from-none 

91 log("get_wm_name()=%s", self.session_name) 

92 

93 def get_server_mode(self): 

94 return "GTK3 shadow" 

95 

96 def print_screen_info(self): 

97 if not server_features.display: 

98 return 

99 w, h = self.root.get_geometry()[2:4] 

100 display = os.environ.get("DISPLAY") 

101 self.do_print_screen_info(display, w, h) 

102 

103 def do_print_screen_info(self, display, w, h): 

104 if display: 

105 log.info(" on display '%s' of size %ix%i", display, w, h) 

106 else: 

107 log.info(" on display of size %ix%i", w, h) 

108 try: 

109 l = len(self._id_to_window) 

110 except AttributeError as e: 

111 log("no screen info: %s", e) 

112 return 

113 if l>1: 

114 log.info(" with %i monitors:", l) 

115 for window in self._id_to_window.values(): 

116 title = window.get_property("title") 

117 x, y, w, h = window.geometry 

118 log.info(" %-16s %4ix%-4i at %4i,%-4i", title, w, h, x, y) 

119 

120 def make_hello(self, _source): 

121 return {"shadow" : True} 

122 

123 def get_info(self, _proto=None): 

124 return { 

125 "sharing" : self.sharing, 

126 "refresh-delay" : self.refresh_delay, 

127 "pointer-last-position" : self.pointer_last_position, 

128 } 

129 

130 

131 def get_window_position(self, _window): 

132 #we export the whole desktop as a window: 

133 return 0, 0 

134 

135 def watch_keymap_changes(self): 

136 pass 

137 

138 def timeout_add(self, *args): 

139 #usually done via gobject 

140 raise NotImplementedError("subclasses should define this method!") 

141 

142 def source_remove(self, *args): 

143 #usually done via gobject 

144 raise NotImplementedError("subclasses should define this method!") 

145 

146 

147 ############################################################################ 

148 # notifications 

149 def cleanup_notifier(self): 

150 n = self.notifier 

151 if n: 

152 self.notifier = None 

153 n.cleanup() 

154 

155 def notify_setup_error(self, exception): 

156 notifylog("notify_setup_error(%s)", exception) 

157 notifylog.info("notification forwarding is not available") 

158 if str(exception).endswith("is already claimed on the session bus"): 

159 log.info(" the interface is already claimed") 

160 

161 def make_notifier(self): 

162 nc = self.get_notifier_classes() 

163 notifylog("make_notifier() notifier classes: %s", nc) 

164 for x in nc: 

165 try: 

166 self.notifier = x() 

167 notifylog("notifier=%s", self.notifier) 

168 break 

169 except Exception: 

170 notifylog("failed to instantiate %s", x, exc_info=True) 

171 

172 def get_notifier_classes(self): 

173 #subclasses will generally add their toolkit specific variants 

174 #by overriding this method 

175 #use the native ones first: 

176 if not NATIVE_NOTIFIER: 

177 return [] 

178 return get_native_notifier_classes() 

179 

180 def notify_new_user(self, ss): 

181 #overriden here so we can show the notification 

182 #directly on the screen we shadow 

183 notifylog("notify_new_user(%s) notifier=%s", ss, self.notifier) 

184 if self.notifier: 

185 tray = self.get_notification_tray() #pylint: disable=assignment-from-none 

186 nid = XPRA_NEW_USER_NOTIFICATION_ID 

187 title = "User '%s' connected to the session" % (ss.name or ss.username or ss.uuid) 

188 body = "\n".join(ss.get_connect_info()) 

189 actions = [] 

190 hints = {} 

191 icon = None 

192 icon_filename = os.path.join(get_icon_dir(), "user.png") 

193 if os.path.exists(icon_filename): 

194 icon = parse_image_path(icon_filename) 

195 self.notifier.show_notify("", tray, nid, "Xpra", 0, "", title, body, actions, hints, 10*1000, icon) 

196 

197 def get_notification_tray(self): 

198 return None 

199 

200 def notify_startup_complete(self): 

201 self.do_notify_startup("Xpra shadow server is ready", replaces_nid=XPRA_STARTUP_NOTIFICATION_ID) 

202 

203 def do_notify_startup(self, title, body="", replaces_nid=0): 

204 #overriden here so we can show the notification 

205 #directly on the screen we shadow 

206 notifylog("do_notify_startup%s", (title, body, replaces_nid)) 

207 if self.notifier: 

208 tray = self.get_notification_tray() #pylint: disable=assignment-from-none 

209 nid = XPRA_STARTUP_NOTIFICATION_ID 

210 actions = [] 

211 hints = {} 

212 icon = None 

213 icon_filename = os.path.join(get_icon_dir(), "server-connected.png") 

214 if os.path.exists(icon_filename): 

215 icon = parse_image_path(icon_filename) 

216 self.notifier.show_notify("", tray, nid, "Xpra", replaces_nid, "", 

217 title, body, actions, hints, 10*1000, icon) 

218 

219 

220 ############################################################################ 

221 # refresh 

222 

223 def start_refresh(self, wid): 

224 log("start_refresh(%i) mapped=%s, timer=%s", wid, self.mapped, self.refresh_timer) 

225 if wid not in self.mapped: 

226 self.mapped.append(wid) 

227 if not self.refresh_timer: 

228 self.refresh_timer = self.timeout_add(self.refresh_delay, self.refresh) 

229 self.start_poll_pointer() 

230 

231 def set_refresh_delay(self, v): 

232 assert 0<v<10000 

233 self.refresh_delay = v 

234 if self.mapped: 

235 self.cancel_refresh_timer() 

236 for wid in self.mapped: 

237 self.start_refresh(wid) 

238 

239 

240 def stop_refresh(self, wid): 

241 log("stop_refresh(%i) mapped=%s", wid, self.mapped) 

242 try: 

243 self.mapped.remove(wid) 

244 except KeyError: 

245 pass 

246 if not self.mapped: 

247 self.cancel_refresh_timer() 

248 self.cancel_poll_pointer() 

249 

250 def cancel_refresh_timer(self): 

251 t = self.refresh_timer 

252 log("cancel_refresh_timer() timer=%s", t) 

253 if t: 

254 self.refresh_timer = None 

255 self.source_remove(t) 

256 

257 def refresh(self): 

258 raise NotImplementedError() 

259 

260 

261 ############################################################################ 

262 # pointer polling 

263 

264 def get_pointer_position(self): 

265 raise NotImplementedError() 

266 

267 def start_poll_pointer(self): 

268 log("start_poll_pointer() pointer_poll_timer=%s, input_devices=%s, POLL_POINTER=%s", 

269 self.pointer_poll_timer, server_features.input_devices, POLL_POINTER) 

270 if self.pointer_poll_timer: 

271 self.cancel_poll_pointer() 

272 if server_features.input_devices and POLL_POINTER>0: 

273 self.pointer_poll_timer = self.timeout_add(POLL_POINTER, self.poll_pointer) 

274 

275 def cancel_poll_pointer(self): 

276 ppt = self.pointer_poll_timer 

277 log("cancel_poll_pointer() pointer_poll_timer=%s", ppt) 

278 if ppt: 

279 self.pointer_poll_timer = None 

280 self.source_remove(ppt) 

281 

282 def poll_pointer(self): 

283 self.poll_pointer_position() 

284 if CURSORS: 

285 self.poll_cursor() 

286 return True 

287 

288 

289 def poll_pointer_position(self): 

290 x, y = self.get_pointer_position() 

291 #find the window model containing the pointer: 

292 if self.pointer_last_position!=(x, y): 

293 self.pointer_last_position = (x, y) 

294 rwm = None 

295 wid = None 

296 rx, ry = 0, 0 

297 for wid, window in self._id_to_window.items(): 

298 wx, wy, ww, wh = window.geometry 

299 if wx<=x<(wx+ww) and wy<=y<(wy+wh): 

300 rwm = window 

301 rx = x-wx 

302 ry = y-wy 

303 break 

304 if rwm: 

305 mouselog("poll_pointer_position() wid=%i, position=%s, relative=%s", wid, (x, y), (rx, ry)) 

306 for ss in self._server_sources.values(): 

307 um = getattr(ss, "update_mouse", None) 

308 if um: 

309 um(wid, x, y, rx, ry) 

310 else: 

311 mouselog("poll_pointer_position() model not found for position=%s", (x, y)) 

312 else: 

313 mouselog("poll_pointer_position() unchanged position=%s", (x, y)) 

314 

315 

316 def poll_cursor(self): 

317 prev = self.last_cursor_data 

318 curr = self.do_get_cursor_data() #pylint: disable=assignment-from-none 

319 self.last_cursor_data = curr 

320 def cmpv(lcd): 

321 if not lcd: 

322 return None 

323 v = lcd[0] 

324 if v and len(v)>2: 

325 return v[2:] 

326 return None 

327 if cmpv(prev)!=cmpv(curr): 

328 fields = ("x", "y", "width", "height", "xhot", "yhot", "serial", "pixels", "name") 

329 if len(prev or [])==len(curr or []) and len(prev or [])==len(fields): 

330 diff = [] 

331 for i, prev_value in enumerate(prev): 

332 if prev_value!=curr[i]: 

333 diff.append(fields[i]) 

334 cursorlog("poll_cursor() attributes changed: %s", diff) 

335 if SAVE_CURSORS and curr: 

336 ci = curr[0] 

337 if ci: 

338 w = ci[2] 

339 h = ci[3] 

340 serial = ci[6] 

341 pixels = ci[7] 

342 cursorlog("saving cursor %#x with size %ix%i, %i bytes", serial, w, h, len(pixels)) 

343 from PIL import Image 

344 img = Image.frombuffer("RGBA", (w, h), pixels, "raw", "BGRA", 0, 1) 

345 img.save("cursor-%#x.png" % serial, format="PNG") 

346 for ss in self._server_sources.values(): 

347 ss.send_cursor() 

348 

349 def do_get_cursor_data(self): 

350 #this method is overriden in subclasses with platform specific code 

351 return None 

352 

353 def get_cursor_data(self): 

354 #return cached value we get from polling: 

355 return self.last_cursor_data 

356 

357 

358 ############################################################################ 

359 

360 def sanity_checks(self, _proto, c): 

361 server_uuid = c.strget("server_uuid") 

362 if server_uuid: 

363 if server_uuid==self.uuid: 

364 log.warn("Warning: shadowing your own display can be quite confusing") 

365 clipboard = self._clipboard_helper and c.boolget("clipboard", True) 

366 if clipboard: 

367 log.warn("clipboard sharing cannot be enabled! (consider using the --no-clipboard option)") 

368 c["clipboard"] = False 

369 else: 

370 log.warn("This client is running within the Xpra server %s", server_uuid) 

371 return True 

372 

373 def parse_screen_info(self, ss): 

374 try: 

375 log.info(" client root window size is %sx%s", *ss.desktop_size) 

376 except Exception: 

377 log.info(" unknown client desktop size") 

378 return self.get_root_window_size() 

379 

380 def _process_desktop_size(self, proto, packet): 

381 #just record the screen size info in the source 

382 ss = self.get_server_source(proto) 

383 if ss and len(packet)>=4: 

384 ss.set_screen_sizes(packet[3]) 

385 

386 

387 def set_keyboard_repeat(self, key_repeat): 

388 """ don't override the existing desktop """ 

389 pass #pylint: disable=unnecessary-pass 

390 

391 def set_keymap(self, server_source, force=False): 

392 log("set_keymap%s", (server_source, force)) 

393 log.info("shadow server: setting default keymap translation") 

394 self.keyboard_config = server_source.set_default_keymap() 

395 

396 def load_existing_windows(self): 

397 self.min_mmap_size = 1024*1024*4*2 

398 for i,model in enumerate(self.makeRootWindowModels()): 

399 log("load_existing_windows() root window model %i: %s", i, model) 

400 self._add_new_window(model) 

401 #at least big enough for 2 frames of BGRX pixel data: 

402 w, h = model.get_dimensions() 

403 self.min_mmap_size = max(self.min_mmap_size, w*h*4*2) 

404 

405 def makeRootWindowModels(self): 

406 return (RootWindowModel(self.root),) 

407 

408 def send_initial_windows(self, ss, sharing=False): 

409 log("send_initial_windows(%s, %s) will send: %s", ss, sharing, self._id_to_window) 

410 for wid in sorted(self._id_to_window.keys()): 

411 window = self._id_to_window[wid] 

412 w, h = window.get_dimensions() 

413 ss.new_window("new-window", wid, window, 0, 0, w, h, self.client_properties.get(wid, {}).get(ss.uuid)) 

414 

415 

416 def _add_new_window(self, window): 

417 self._add_new_window_common(window) 

418 self._send_new_window_packet(window) 

419 

420 def _send_new_window_packet(self, window): 

421 geometry = window.get_geometry() 

422 self._do_send_new_window_packet("new-window", window, geometry) 

423 

424 def _process_window_common(self, wid): 

425 window = self._id_to_window.get(wid) 

426 assert window is not None, "wid %s does not exist" % wid 

427 return window 

428 

429 def _process_map_window(self, proto, packet): 

430 wid, x, y, width, height = packet[1:6] 

431 window = self._process_window_common(wid) 

432 self._window_mapped_at(proto, wid, window, (x, y, width, height)) 

433 self.refresh_window_area(window, 0, 0, width, height) 

434 if len(packet)>=7: 

435 self._set_client_properties(proto, wid, window, packet[6]) 

436 self.start_refresh(wid) 

437 

438 def _process_unmap_window(self, proto, packet): 

439 wid = packet[1] 

440 window = self._process_window_common(wid) 

441 self._window_mapped_at(proto, wid, window) 

442 #TODO: deal with more than one window / more than one client 

443 #and stop refresh if all the windows are unmapped everywhere 

444 if len(self._server_sources)<=1 and len(self._id_to_window)<=1: 

445 self.stop_refresh(wid) 

446 

447 def _process_configure_window(self, proto, packet): 

448 wid, x, y, w, h = packet[1:6] 

449 window = self._process_window_common(wid) 

450 self._window_mapped_at(proto, wid, window, (x, y, w, h)) 

451 self.refresh_window_area(window, 0, 0, w, h) 

452 if len(packet)>=7: 

453 self._set_client_properties(proto, wid, window, packet[6]) 

454 

455 def _process_close_window(self, proto, packet): 

456 wid = packet[1] 

457 self._process_window_common(wid) 

458 self.disconnect_client(proto, DONE, "closed the only window") 

459 

460 

461 def do_make_screenshot_packet(self): 

462 raise NotImplementedError() 

463 

464 

465 def make_dbus_server(self): 

466 from xpra.server.shadow.shadow_dbus_server import Shadow_DBUS_Server 

467 return Shadow_DBUS_Server(self, os.environ.get("DISPLAY", "").lstrip(":"))