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) 2010-2020 Antoine Martin <antoine@xpra.org> 

4# Copyright (C) 2008 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 

9from io import BytesIO 

10 

11from xpra.server.source.stub_source_mixin import StubSourceMixin 

12from xpra.server.window.metadata import make_window_metadata 

13from xpra.net.compression import Compressed 

14from xpra.os_util import monotonic_time, strtobytes, bytestostr 

15from xpra.util import typedict, envint, envbool, DEFAULT_METADATA_SUPPORTED, XPRA_BANDWIDTH_NOTIFICATION_ID 

16from xpra.log import Logger 

17 

18log = Logger("server") 

19focuslog = Logger("focus") 

20cursorlog = Logger("cursor") 

21metalog = Logger("metadata") 

22bandwidthlog = Logger("bandwidth") 

23eventslog = Logger("events") 

24filterslog = Logger("filters") 

25 

26CONGESTION_WARNING_EVENT_COUNT = envint("XPRA_CONGESTION_WARNING_EVENT_COUNT", 10) 

27CONGESTION_REPEAT_DELAY = envint("XPRA_CONGESTION_REPEAT_DELAY", 60) 

28SAVE_CURSORS = envbool("XPRA_SAVE_CURSORS", False) 

29MIN_BANDWIDTH = envint("XPRA_MIN_BANDWIDTH", 5*1024*1024) 

30 

31PROPERTIES_DEBUG = [x.strip() for x in os.environ.get("XPRA_WINDOW_PROPERTIES_DEBUG", "").split(",")] 

32 

33 

34""" 

35Handle window forwarding: 

36- damage 

37- geometry 

38- events 

39etc 

40""" 

41class WindowsMixin(StubSourceMixin): 

42 

43 @classmethod 

44 def is_needed(cls, caps : typedict) -> bool: 

45 return caps.boolget("windows") 

46 

47 

48 def __init__(self): 

49 self.get_transient_for = None 

50 self.get_focus = None 

51 self.get_cursor_data_cb = None 

52 self.get_window_id = None 

53 self.window_filters = [] 

54 self.readonly = False 

55 #duplicated from encodings: 

56 self.global_batch_config = None 

57 #duplicated from clientconnection: 

58 self.statistics = None 

59 

60 def init_from(self, _protocol, server): 

61 self.get_transient_for = server.get_transient_for 

62 self.get_focus = server.get_focus 

63 self.get_cursor_data_cb = server.get_cursor_data 

64 self.get_window_id = server.get_window_id 

65 self.window_filters = server.window_filters 

66 self.readonly = server.readonly 

67 

68 def init_state(self): 

69 #WindowSource for each Window ID 

70 self.window_sources = {} 

71 

72 self.window_frame_sizes = {} 

73 self.suspended = False 

74 self.send_cursors = False 

75 self.cursor_encodings = () 

76 self.send_bell = False 

77 self.send_windows = True 

78 self.pointer_grabs = False 

79 self.window_min_size = 0, 0 

80 self.window_max_size = 0, 0 

81 self.window_restack = False 

82 self.system_tray = False 

83 self.metadata_supported = () 

84 

85 self.cursor_timer = None 

86 self.last_cursor_sent = None 

87 

88 def cleanup(self): 

89 for window_source in self.all_window_sources(): 

90 window_source.cleanup() 

91 self.window_sources = {} 

92 self.cancel_cursor_timer() 

93 

94 def all_window_sources(self): 

95 return tuple(self.window_sources.values()) 

96 

97 

98 def suspend(self, ui, wd): 

99 eventslog("suspend(%s, %s) suspended=%s", ui, wd, self.suspended) 

100 if ui: 

101 self.suspended = True 

102 for wid in wd.keys(): 

103 ws = self.window_sources.get(wid) 

104 if ws: 

105 ws.suspend() 

106 

107 def resume(self, ui, wd): 

108 eventslog("resume(%s, %s) suspended=%s", ui, wd, self.suspended) 

109 if ui: 

110 self.suspended = False 

111 for wid in wd.keys(): 

112 ws = self.window_sources.get(wid) 

113 if ws: 

114 ws.resume() 

115 self.send_cursor() 

116 

117 

118 def go_idle(self): 

119 #usually fires from the server's idle_grace_timeout_cb 

120 if self.idle: 

121 return 

122 self.idle = True 

123 for window_source in self.all_window_sources(): 

124 window_source.go_idle() 

125 

126 def no_idle(self): 

127 #on user event, we stop being idle 

128 if not self.idle: 

129 return 

130 self.idle = False 

131 for window_source in self.all_window_sources(): 

132 window_source.no_idle() 

133 

134 

135 def parse_client_caps(self, c): 

136 self.send_windows = c.boolget("ui_client", True) and c.boolget("windows", True) 

137 self.pointer_grabs = c.boolget("pointer.grabs") 

138 self.send_cursors = self.send_windows and c.boolget("cursors") 

139 self.cursor_encodings = c.strtupleget("encodings.cursor") 

140 self.send_bell = c.boolget("bell") 

141 self.system_tray = c.boolget("system_tray") 

142 self.metadata_supported = c.strtupleget("metadata.supported", DEFAULT_METADATA_SUPPORTED) 

143 self.window_frame_sizes = typedict(c.dictget("window.frame_sizes", {})) 

144 self.window_min_size = c.inttupleget("window.min-size", (0, 0)) 

145 self.window_max_size = c.inttupleget("window.max-size", (0, 0)) 

146 self.window_restack = c.boolget("window.restack", False) 

147 log("cursors=%s (encodings=%s), bell=%s", 

148 self.send_cursors, self.cursor_encodings, self.send_bell) 

149 #window filters: 

150 try: 

151 for object_name, property_name, operator, value in c.tupleget("window-filters"): 

152 self.add_window_filter(object_name, property_name, operator, value) 

153 except Exception as e: 

154 filterslog.error("Error parsing window-filters: %s", e) 

155 

156 

157 def get_caps(self) -> dict: 

158 return {} 

159 

160 

161 ###################################################################### 

162 # info: 

163 def get_info(self) -> dict: 

164 info = { 

165 "windows" : self.send_windows, 

166 "cursors" : self.send_cursors, 

167 "bell" : self.send_bell, 

168 "system-tray" : self.system_tray, 

169 "suspended" : self.suspended, 

170 } 

171 wsize = info.setdefault("window-size", {}) 

172 wsize.update({ 

173 "min" : self.window_min_size, 

174 "max" : self.window_max_size, 

175 }) 

176 if self.window_frame_sizes: 

177 wsize.update({"frame-sizes" : self.window_frame_sizes}) 

178 info.update(self.get_window_info()) 

179 return info 

180 

181 def get_window_info(self) -> dict: 

182 """ 

183 Adds encoding and window specific information 

184 """ 

185 from xpra.simple_stats import get_list_stats 

186 pqpixels = [x[2] for x in tuple(self.packet_queue)] 

187 pqpi = get_list_stats(pqpixels) 

188 if pqpixels: 

189 pqpi["current"] = pqpixels[-1] 

190 info = {"damage" : { 

191 "compression_queue" : {"size" : {"current" : self.encode_queue_size()}}, 

192 "packet_queue" : {"size" : {"current" : len(self.packet_queue)}}, 

193 "packet_queue_pixels" : pqpi, 

194 }, 

195 } 

196 gbc = self.global_batch_config 

197 if gbc: 

198 info["batch"] = self.global_batch_config.get_info() 

199 s = self.statistics 

200 if s: 

201 info.update(s.get_info()) 

202 if self.window_sources: 

203 total_pixels = 0 

204 total_time = 0.0 

205 in_latencies, out_latencies = [], [] 

206 winfo = {} 

207 for wid, ws in list(self.window_sources.items()): 

208 #per-window source stats: 

209 winfo[wid] = ws.get_info() 

210 #collect stats for global averages: 

211 for _, _, pixels, _, _, encoding_time in tuple(ws.statistics.encoding_stats): 

212 total_pixels += pixels 

213 total_time += encoding_time 

214 in_latencies += [x*1000 for _, _, _, x in tuple(ws.statistics.damage_in_latency)] 

215 out_latencies += [x*1000 for _, _, _, x in tuple(ws.statistics.damage_out_latency)] 

216 info["window"] = winfo 

217 v = 0 

218 if total_time>0: 

219 v = int(total_pixels / total_time) 

220 info.setdefault("encoding", {})["pixels_encoded_per_second"] = v 

221 dinfo = info.setdefault("damage", {}) 

222 dinfo["in_latency"] = get_list_stats(in_latencies, show_percentile=[9]) 

223 dinfo["out_latency"] = get_list_stats(out_latencies, show_percentile=[9]) 

224 return info 

225 

226 

227 ###################################################################### 

228 # grabs: 

229 def pointer_grab(self, wid): 

230 if self.pointer_grabs and self.hello_sent: 

231 self.send("pointer-grab", wid) 

232 

233 def pointer_ungrab(self, wid): 

234 if self.pointer_grabs and self.hello_sent: 

235 self.send("pointer-ungrab", wid) 

236 

237 

238 ###################################################################### 

239 # cursors: 

240 def send_cursor(self): 

241 if not self.send_cursors or self.suspended or not self.hello_sent: 

242 return 

243 #if not pending already, schedule it: 

244 gbc = self.global_batch_config 

245 if not self.cursor_timer and gbc: 

246 delay = max(10, int(gbc.delay/4)) 

247 self.cursor_timer = self.timeout_add(delay, self.do_send_cursor, delay) 

248 

249 def cancel_cursor_timer(self): 

250 ct = self.cursor_timer 

251 if ct: 

252 self.cursor_timer = None 

253 self.source_remove(ct) 

254 

255 def do_send_cursor(self, delay): 

256 self.cursor_timer = None 

257 cd = self.get_cursor_data_cb() 

258 if not cd or not cd[0]: 

259 self.send_empty_cursor() 

260 return 

261 cursor_data = list(cd[0]) 

262 cursor_sizes = cd[1] 

263 #skip first two fields (if present) as those are coordinates: 

264 if self.last_cursor_sent and self.last_cursor_sent[2:9]==cursor_data[2:9]: 

265 cursorlog("do_send_cursor(..) cursor identical to the last one we sent, nothing to do") 

266 return 

267 self.last_cursor_sent = cursor_data[:9] 

268 w, h, _xhot, _yhot, serial, pixels, name = cursor_data[2:9] 

269 #compress pixels if needed: 

270 encoding = "raw" 

271 if pixels is not None: 

272 #convert bytearray to string: 

273 cpixels = strtobytes(pixels) 

274 if "png" in self.cursor_encodings: 

275 from PIL import Image 

276 cursorlog("do_send_cursor() loading %i bytes of cursor pixel data for %ix%i cursor named '%s'", 

277 len(cpixels), w, h, bytestostr(name)) 

278 img = Image.frombytes("RGBA", (w, h), cpixels, "raw", "BGRA", w*4, 1) 

279 buf = BytesIO() 

280 img.save(buf, "PNG") 

281 pngdata = buf.getvalue() 

282 buf.close() 

283 cpixels = Compressed("png cursor", pngdata, can_inline=True) 

284 encoding = "png" 

285 if SAVE_CURSORS: 

286 filename = "raw-cursor-%#x.png" % serial 

287 with open(filename, "wb") as f: 

288 f.write(pngdata) 

289 cursorlog("cursor saved to %s", filename) 

290 elif len(cpixels)>=256 and ("raw" in self.cursor_encodings or not self.cursor_encodings): 

291 cpixels = self.compressed_wrapper("cursor", pixels) 

292 cursorlog("do_send_cursor(..) pixels=%s ", cpixels) 

293 encoding = "raw" 

294 cursor_data[7] = cpixels 

295 cursorlog("do_send_cursor(..) %sx%s %s cursor name='%s', serial=%#x with delay=%s (cursor_encodings=%s)", 

296 w, h, (encoding or "empty"), bytestostr(name), serial, delay, self.cursor_encodings) 

297 args = [encoding] + list(cursor_data[:9]) + [cursor_sizes[0]] + list(cursor_sizes[1]) 

298 self.send_more("cursor", *args) 

299 

300 def send_empty_cursor(self): 

301 cursorlog("send_empty_cursor(..)") 

302 self.last_cursor_sent = None 

303 self.send_more("cursor", "") 

304 

305 

306 def bell(self, wid, device, percent, pitch, duration, bell_class, bell_id, bell_name): 

307 if not self.send_bell or self.suspended or not self.hello_sent: 

308 return 

309 self.send_async("bell", wid, device, percent, pitch, duration, bell_class, bell_id, bell_name) 

310 

311 

312 ###################################################################### 

313 # window filters: 

314 def reset_window_filters(self): 

315 self.window_filters = [(uuid, f) for uuid, f in self.window_filters if uuid!=self.uuid] 

316 

317 def get_all_window_filters(self): 

318 return [f for uuid, f in self.window_filters if uuid==self.uuid] 

319 

320 def add_window_filter(self, object_name, property_name, operator, value): 

321 from xpra.server.window.filters import get_window_filter 

322 window_filter = get_window_filter(object_name, property_name, operator, value) 

323 assert window_filter 

324 self.do_add_window_filter(window_filter) 

325 

326 def do_add_window_filter(self, window_filter): 

327 #(reminder: filters are shared between all sources) 

328 self.window_filters.append((self.uuid, window_filter)) 

329 

330 def can_send_window(self, window): 

331 if not self.hello_sent or not (self.send_windows or self.system_tray): 

332 return False 

333 #we could also allow filtering for system tray windows? 

334 if self.window_filters and self.send_windows and not window.is_tray(): 

335 for uuid, window_filter in self.window_filters: 

336 filterslog("can_send_window(%s) checking %s for uuid=%s (client uuid=%s)", 

337 window, window_filter, uuid, self.uuid) 

338 if window_filter.matches(window): 

339 v = uuid=="*" or uuid==self.uuid 

340 filterslog("can_send_window(%s)=%s", window, v) 

341 return v 

342 if self.send_windows and self.system_tray: 

343 #common case shortcut 

344 v = True 

345 elif window.is_tray(): 

346 v = self.system_tray 

347 else: 

348 v = self.send_windows 

349 filterslog("can_send_window(%s)=%s", window, v) 

350 return v 

351 

352 

353 ###################################################################### 

354 # windows: 

355 def initiate_moveresize(self, wid, window, x_root, y_root, direction, button, source_indication): 

356 if not self.can_send_window(window): 

357 return 

358 log("initiate_moveresize sending to %s", self) 

359 self.send("initiate-moveresize", wid, x_root, y_root, direction, button, source_indication) 

360 

361 def or_window_geometry(self, wid, window, x, y, w, h): 

362 if not self.can_send_window(window): 

363 return 

364 self.send("configure-override-redirect", wid, x, y, w, h) 

365 

366 def window_metadata(self, wid, window, prop): 

367 if not self.can_send_window(window): 

368 return 

369 if prop=="icons": 

370 self.send_window_icon(wid, window) 

371 else: 

372 metadata = self._make_metadata(window, prop) 

373 if prop in PROPERTIES_DEBUG: 

374 metalog.info("make_metadata(%s, %s, %s)=%s", wid, window, prop, metadata) 

375 else: 

376 metalog("make_metadata(%s, %s, %s)=%s", wid, window, prop, metadata) 

377 if metadata: 

378 self.send("window-metadata", wid, metadata) 

379 

380 

381 # Takes the name of a WindowModel property, and returns a dictionary of 

382 # xpra window metadata values that depend on that property 

383 def _make_metadata(self, window, propname, skip_defaults=False): 

384 if propname not in self.metadata_supported: 

385 metalog("make_metadata: client does not support '%s'", propname) 

386 return {} 

387 metadata = make_window_metadata(window, propname, 

388 get_transient_for=self.get_transient_for, 

389 get_window_id=self.get_window_id, 

390 skip_defaults=skip_defaults) 

391 if self.readonly: 

392 metalog("overriding size-constraints for readonly mode") 

393 size = window.get_dimensions() 

394 metadata["size-constraints"] = { 

395 "maximum-size" : size, 

396 "minimum-size" : size, 

397 "base-size" : size, 

398 } 

399 return metadata 

400 

401 def new_tray(self, wid, window, w, h): 

402 assert window.is_tray() 

403 if not self.can_send_window(window): 

404 return 

405 metadata = {} 

406 for propname in list(window.get_property_names()): 

407 metadata.update(self._make_metadata(window, propname, skip_defaults=True)) 

408 self.send_async("new-tray", wid, w, h, metadata) 

409 

410 def new_window(self, ptype, wid, window, x, y, w, h, client_properties): 

411 if not self.can_send_window(window): 

412 return 

413 send_props = list(window.get_property_names()) 

414 send_raw_icon = "icons" in send_props 

415 if send_raw_icon: 

416 send_props.remove("icons") 

417 metadata = {} 

418 for prop in send_props: 

419 v = self._make_metadata(window, prop, skip_defaults=True) 

420 if prop in PROPERTIES_DEBUG: 

421 metalog.info("make_metadata(%s, %s, %s)=%s", wid, window, prop, v) 

422 else: 

423 metalog("make_metadata(%s, %s, %s)=%s", wid, window, prop, v) 

424 metadata.update(v) 

425 log("new_window(%s, %s, %s, %s, %s, %s, %s, %s) metadata(%s)=%s", 

426 ptype, window, wid, x, y, w, h, client_properties, send_props, metadata) 

427 self.send_async(ptype, wid, x, y, w, h, metadata, client_properties or {}) 

428 if send_raw_icon: 

429 self.send_window_icon(wid, window) 

430 

431 def send_window_icon(self, wid, window): 

432 if not self.can_send_window(window): 

433 return 

434 #we may need to make a new source at this point: 

435 ws = self.make_window_source(wid, window) 

436 if ws: 

437 ws.send_window_icon() 

438 

439 

440 def lost_window(self, wid, _window): 

441 self.send("lost-window", wid) 

442 

443 def move_resize_window(self, wid, window, x, y, ww, wh, resize_counter=0): 

444 """ 

445 The server detected that the application window has been moved and/or resized, 

446 we forward it if the client supports this type of event. 

447 """ 

448 if not self.can_send_window(window): 

449 return 

450 self.send("window-move-resize", wid, x, y, ww, wh, resize_counter) 

451 

452 def resize_window(self, wid, window, ww, wh, resize_counter=0): 

453 if not self.can_send_window(window): 

454 return 

455 self.send("window-resized", wid, ww, wh, resize_counter) 

456 

457 

458 def cancel_damage(self, wid): 

459 """ 

460 Use this method to cancel all currently pending and ongoing 

461 damage requests for a window. 

462 """ 

463 ws = self.window_sources.get(wid) 

464 if ws: 

465 ws.cancel_damage() 

466 

467 

468 def map_window(self, wid, window, coords=None): 

469 ws = self.make_window_source(wid, window) 

470 ws.map(coords) 

471 

472 def unmap_window(self, wid, _window): 

473 ws = self.window_sources.get(wid) 

474 if ws: 

475 ws.unmap() 

476 

477 def restack_window(self, wid, window, detail, sibling): 

478 focuslog("restack_window%s", (wid, window, detail, sibling)) 

479 if not self.can_send_window(window): 

480 return 

481 if not self.window_restack: 

482 #older clients can only handle "raise-window" 

483 if detail!=0: 

484 return 

485 self.send_async("raise-window", wid) 

486 return 

487 sibling_wid = 0 

488 if sibling: 

489 sibling_wid = self.get_window_id(sibling) 

490 self.send_async("restack-window", wid, detail, sibling_wid) 

491 

492 def raise_window(self, wid, window): 

493 if not self.can_send_window(window): 

494 return 

495 self.send_async("raise-window", wid) 

496 

497 def remove_window(self, wid, window): 

498 """ The given window is gone, ensure we free all the related resources """ 

499 if not self.can_send_window(window): 

500 return 

501 ws = self.window_sources.pop(wid, None) 

502 if ws: 

503 ws.cleanup() 

504 self.calculate_window_pixels.pop(wid, None) 

505 

506 

507 def refresh(self, wid, window, opts): 

508 if not self.can_send_window(window): 

509 return 

510 self.cancel_damage(wid) 

511 w, h = window.get_dimensions() 

512 self.damage(wid, window, 0, 0, w, h, opts) 

513 

514 def update_batch(self, wid, window, batch_props): 

515 ws = self.window_sources.get(wid) 

516 if ws: 

517 if "reset" in batch_props: 

518 ws.batch_config = self.make_batch_config(wid, window) 

519 for x in ("always", "locked"): 

520 if x in batch_props: 

521 setattr(ws.batch_config, x, batch_props.boolget(x)) 

522 for x in ("min_delay", "max_delay", "timeout_delay", "delay"): 

523 if x in batch_props: 

524 setattr(ws.batch_config, x, batch_props.intget(x)) 

525 log("batch config updated for window %s: %s", wid, ws.batch_config) 

526 

527 def set_client_properties(self, wid, window, new_client_properties): 

528 assert self.send_windows 

529 ws = self.make_window_source(wid, window) 

530 ws.set_client_properties(new_client_properties) 

531 

532 

533 def get_window_source(self, wid): 

534 return self.window_sources.get(wid) 

535 

536 def make_window_source(self, wid, window): 

537 ws = self.window_sources.get(wid) 

538 if ws is None: 

539 batch_config = self.make_batch_config(wid, window) 

540 ww, wh = window.get_dimensions() 

541 bandwidth_limit = self.bandwidth_limit 

542 mmap = getattr(self, "mmap", None) 

543 mmap_size = getattr(self, "mmap_size", 0) 

544 av_sync = getattr(self, "av_sync", False) 

545 av_sync_delay = getattr(self, "av_sync_delay", 0) 

546 if mmap_size>0: 

547 bandwidth_limit = 0 

548 from xpra.server.window.window_video_source import WindowVideoSource 

549 ws = WindowVideoSource( 

550 self.idle_add, self.timeout_add, self.source_remove, 

551 ww, wh, 

552 self.record_congestion_event, self.encode_queue_size, 

553 self.call_in_encode_thread, self.queue_packet, 

554 self.statistics, 

555 wid, window, batch_config, self.auto_refresh_delay, 

556 av_sync, av_sync_delay, 

557 self.video_helper, 

558 self.server_core_encodings, self.server_encodings, 

559 self.encoding, self.encodings, self.core_encodings, 

560 self.window_icon_encodings, self.encoding_options, self.icons_encoding_options, 

561 self.rgb_formats, 

562 self.default_encoding_options, 

563 mmap, mmap_size, bandwidth_limit, self.jitter) 

564 self.window_sources[wid] = ws 

565 if len(self.window_sources)>1: 

566 #re-distribute bandwidth: 

567 self.update_bandwidth_limits() 

568 return ws 

569 

570 

571 def damage(self, wid, window, x, y, w, h, options=None): 

572 """ 

573 Main entry point from the window manager, 

574 we dispatch to the WindowSource for this window id 

575 (creating a new one if needed) 

576 """ 

577 if not self.can_send_window(window): 

578 return 

579 assert window is not None 

580 if options: 

581 damage_options = options.copy() 

582 else: 

583 damage_options = {} 

584 s = self.statistics 

585 if s: 

586 s.damage_last_events.append((wid, monotonic_time(), w*h)) 

587 ws = self.make_window_source(wid, window) 

588 ws.damage(x, y, w, h, damage_options) 

589 

590 def client_ack_damage(self, damage_packet_sequence, wid, width, height, decode_time, message): 

591 """ 

592 The client is acknowledging a damage packet, 

593 we record the 'client decode time' (which is provided by the client) 

594 and WindowSource will calculate and record the "client latency". 

595 (since it knows when the "draw" packet was sent) 

596 """ 

597 if not self.send_windows: 

598 log.error("client_ack_damage when we don't send any window data!?") 

599 return 

600 if decode_time>0: 

601 self.statistics.client_decode_time.append((wid, monotonic_time(), width*height, decode_time)) 

602 ws = self.window_sources.get(wid) 

603 if ws: 

604 ws.damage_packet_acked(damage_packet_sequence, width, height, decode_time, message) 

605 self.may_recalculate(wid, width*height) 

606 

607# 

608# Methods used by WindowSource: 

609# 

610 def record_congestion_event(self, source, late_pct=0, send_speed=0): 

611 if not self.bandwidth_detection: 

612 return 

613 gs = self.statistics 

614 if not gs: 

615 #window cleaned up? 

616 return 

617 now = monotonic_time() 

618 elapsed = now-self.bandwidth_warning_time 

619 bandwidthlog("record_congestion_event(%s, %i, %i) bandwidth_warnings=%s, elapsed time=%i", 

620 source, late_pct, send_speed, self.bandwidth_warnings, elapsed) 

621 gs.last_congestion_time = now 

622 gs.congestion_send_speed.append((now, late_pct, send_speed)) 

623 if self.bandwidth_warnings and elapsed>CONGESTION_REPEAT_DELAY: 

624 #enough congestion events? 

625 T = 10 

626 min_time = now-T 

627 count = len(tuple(True for x in gs.congestion_send_speed if x[0]>min_time)) 

628 bandwidthlog("record_congestion_event: %i events in the last %i seconds (warnings after %i)", 

629 count, T, CONGESTION_WARNING_EVENT_COUNT) 

630 if count>CONGESTION_WARNING_EVENT_COUNT: 

631 self.bandwidth_warning_time = now 

632 nid = XPRA_BANDWIDTH_NOTIFICATION_ID 

633 summary = "Network Performance Issue" 

634 body = "Your network connection is struggling to keep up,\n" + \ 

635 "consider lowering the bandwidth limit,\n" + \ 

636 "or turning off automatic network congestion management.\n" + \ 

637 "Choosing 'ignore' will silence all further warnings." 

638 actions = [] 

639 if self.bandwidth_limit==0 or self.bandwidth_limit>MIN_BANDWIDTH: 

640 actions += ["lower-bandwidth", "Lower bandwidth limit"] 

641 actions += ["bandwidth-off", "Turn off"] 

642 #if self.default_min_quality>10: 

643 # actions += ["lower-quality", "Lower quality"] 

644 actions += ["ignore", "Ignore"] 

645 hints = {} 

646 self.may_notify(nid, summary, body, actions, hints, 

647 icon_name="connect", user_callback=self.congestion_notification_callback) 

648 

649 def congestion_notification_callback(self, nid, action_id): 

650 bandwidthlog("congestion_notification_callback(%i, %s)", nid, action_id) 

651 if action_id=="lower-bandwidth": 

652 bandwidth_limit = 50*1024*1024 

653 if self.bandwidth_limit>256*1024: 

654 bandwidth_limit = self.bandwidth_limit//2 

655 css = 50*1024*1024 

656 if self.statistics.avg_congestion_send_speed>256*1024: 

657 #round up: 

658 css = int(self.statistics.avg_congestion_send_speed//16/1024)*16*1024 

659 self.bandwidth_limit = max(MIN_BANDWIDTH, min(bandwidth_limit, css)) 

660 self.setting_changed("bandwidth-limit", self.bandwidth_limit) 

661 #elif action_id=="lower-quality": 

662 # self.default_min_quality = max(1, self.default_min_quality-15) 

663 # self.set_min_quality(self.default_min_quality) 

664 # self.setting_changed("min-quality", self.default_min_quality) 

665 elif action_id=="bandwidth-off": 

666 self.bandwidth_detection = False 

667 elif action_id=="ignore": 

668 self.bandwidth_warnings = False