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# 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 

7from xpra.os_util import strtobytes 

8from xpra.util import engs, iround, log_screen_sizes 

9from xpra.os_util import bytestostr 

10from xpra.scripts.config import FALSE_OPTIONS 

11from xpra.server.mixins.stub_server_mixin import StubServerMixin 

12from xpra.log import Logger 

13 

14log = Logger("screen") 

15gllog = Logger("opengl") 

16 

17""" 

18Mixin for servers that handle displays. 

19""" 

20class DisplayManager(StubServerMixin): 

21 

22 def __init__(self): 

23 self.randr = False 

24 self.bell = False 

25 self.cursors = False 

26 self.default_dpi = 96 

27 self.dpi = 0 

28 self.xdpi = 0 

29 self.ydpi = 0 

30 self.antialias = {} 

31 self.cursor_size = 0 

32 self.double_click_time = -1 

33 self.double_click_distance = -1, -1 

34 self.opengl = False 

35 self.opengl_props = {} 

36 

37 def init(self, opts): 

38 self.opengl = opts.opengl 

39 self.bell = opts.bell 

40 self.cursors = opts.cursors 

41 self.default_dpi = int(opts.dpi) 

42 

43 

44 def parse_hello(self, ss, caps, send_ui): 

45 if send_ui: 

46 self.parse_screen_info(ss) 

47 

48 

49 def last_client_exited(self): 

50 self.reset_icc_profile() 

51 

52 

53 def threaded_setup(self): 

54 self.opengl_props = self.query_opengl() 

55 

56 

57 def query_opengl(self): 

58 props = {} 

59 if self.opengl.lower()=="noprobe" or self.opengl.lower() in FALSE_OPTIONS: 

60 gllog("query_opengl() skipped because opengl=%s", self.opengl) 

61 return props 

62 try: 

63 from subprocess import Popen, PIPE 

64 from xpra.platform.paths import get_xpra_command 

65 cmd = self.get_full_child_command(get_xpra_command()+["opengl", "--opengl=yes"]) 

66 env = self.get_child_env() 

67 #we want the output so we can parse it: 

68 env["XPRA_REDIRECT_OUTPUT"] = "0" 

69 proc = Popen(cmd, stdin=PIPE, stdout=PIPE, stderr=PIPE, env=env) 

70 out,err = proc.communicate() 

71 gllog("out(%s)=%s", cmd, out) 

72 gllog("err(%s)=%s", cmd, err) 

73 if proc.returncode==0: 

74 #parse output: 

75 for line in out.splitlines(): 

76 parts = line.split(b"=") 

77 if len(parts)!=2: 

78 continue 

79 k = bytestostr(parts[0].strip()) 

80 v = bytestostr(parts[1].strip()) 

81 props[k] = v 

82 gllog("opengl props=%s", props) 

83 if props: 

84 gllog.info("OpenGL is supported on display '%s'", self.display_name) 

85 renderer = props.get("renderer") 

86 if renderer: 

87 gllog.info(" using '%s' renderer", renderer) 

88 else: 

89 gllog.info("No OpenGL information available") 

90 else: 

91 props["error-details"] = str(err).strip("\n\r") 

92 error = "unknown error" 

93 for x in str(err).splitlines(): 

94 if x.startswith("RuntimeError: "): 

95 error = x[len("RuntimeError: "):] 

96 break 

97 if x.startswith("ImportError: "): 

98 error = x[len("ImportError: "):] 

99 break 

100 props["error"] = error 

101 log.warn("Warning: OpenGL support check failed:") 

102 log.warn(" %s", error) 

103 except Exception as e: 

104 gllog("query_opengl()", exc_info=True) 

105 gllog.error("Error: OpenGL support check failed") 

106 gllog.error(" '%s'", e) 

107 props["error"] = str(e) 

108 gllog("OpenGL: %s", props) 

109 return props 

110 

111 

112 def get_caps(self, source) -> dict: 

113 root_w, root_h = self.get_root_window_size() 

114 caps = { 

115 "bell" : self.bell, 

116 "cursors" : self.cursors, 

117 "desktop_size" : self._get_desktop_size_capability(source, root_w, root_h), 

118 } 

119 if self.opengl_props: 

120 caps["opengl"] = self.opengl_props 

121 return caps 

122 

123 def get_info(self, _proto) -> dict: 

124 i = { 

125 "randr" : self.randr, 

126 "bell" : self.bell, 

127 "cursors" : { 

128 "" : self.cursors, 

129 "size" : self.cursor_size, 

130 }, 

131 "double-click" : { 

132 "time" : self.double_click_time, 

133 "distance" : self.double_click_distance, 

134 }, 

135 "dpi" : { 

136 "default" : self.default_dpi, 

137 "value" : self.dpi, 

138 "x" : self.xdpi, 

139 "y" : self.ydpi, 

140 }, 

141 "antialias" : self.antialias, 

142 } 

143 if self.opengl_props: 

144 i["opengl"] = self.opengl_props 

145 return { 

146 "display": i, 

147 } 

148 

149 

150 def _process_set_cursors(self, proto, packet): 

151 assert self.cursors, "cannot toggle send_cursors: the feature is disabled" 

152 ss = self.get_server_source(proto) 

153 if ss: 

154 ss.send_cursors = bool(packet[1]) 

155 

156 def _process_set_bell(self, proto, packet): 

157 assert self.bell, "cannot toggle send_bell: the feature is disabled" 

158 ss = self.get_server_source(proto) 

159 if ss: 

160 ss.send_bell = bool(packet[1]) 

161 

162 

163 ###################################################################### 

164 # display / screen / root window: 

165 def set_screen_geometry_attributes(self, w, h): 

166 #by default, use the screen as desktop area: 

167 self.set_desktop_geometry_attributes(w, h) 

168 

169 def set_desktop_geometry_attributes(self, w, h): 

170 self.calculate_desktops() 

171 self.calculate_workarea(w, h) 

172 self.set_desktop_geometry(w, h) 

173 

174 

175 def parse_screen_info(self, ss): 

176 return self.do_parse_screen_info(ss, ss.desktop_size) 

177 

178 def do_parse_screen_info(self, ss, desktop_size): 

179 log("do_parse_screen_info%s", (ss, desktop_size)) 

180 dw, dh = None, None 

181 if desktop_size: 

182 try: 

183 dw, dh = desktop_size 

184 if not ss.screen_sizes: 

185 log.info(" client root window size is %sx%s", dw, dh) 

186 else: 

187 log.info(" client root window size is %sx%s with %s display%s:", 

188 dw, dh, len(ss.screen_sizes), engs(ss.screen_sizes)) 

189 log_screen_sizes(dw, dh, ss.screen_sizes) 

190 except Exception: 

191 dw, dh = None, None 

192 sw, sh = self.configure_best_screen_size() 

193 log("configure_best_screen_size()=%s", (sw, sh)) 

194 #we will tell the client about the size chosen in the hello we send back, 

195 #so record this size as the current server desktop size to avoid change notifications: 

196 ss.desktop_size_server = sw, sh 

197 #prefer desktop size, fallback to screen size: 

198 w = dw or sw 

199 h = dh or sh 

200 #clamp to max supported: 

201 maxw, maxh = self.get_max_screen_size() 

202 w = min(w, maxw) 

203 h = min(h, maxh) 

204 self.set_desktop_geometry_attributes(w, h) 

205 self.set_icc_profile() 

206 return w, h 

207 

208 

209 def set_icc_profile(self): 

210 log("set_icc_profile() not implemented") 

211 

212 def reset_icc_profile(self): 

213 log("reset_icc_profile() not implemented") 

214 

215 

216 def _monitors_changed(self, screen): 

217 self.do_screen_changed(screen) 

218 

219 def _screen_size_changed(self, screen): 

220 self.do_screen_changed(screen) 

221 

222 def do_screen_changed(self, screen): 

223 log("do_screen_changed(%s)", screen) 

224 #randr has resized the screen, tell the client (if it supports it) 

225 w, h = screen.get_width(), screen.get_height() 

226 log("new screen dimensions: %ix%i", w, h) 

227 self.set_screen_geometry_attributes(w, h) 

228 self.idle_add(self.send_updated_screen_size) 

229 

230 def get_root_window_size(self): 

231 raise NotImplementedError() 

232 

233 def send_updated_screen_size(self): 

234 max_w, max_h = self.get_max_screen_size() 

235 root_w, root_h = self.get_root_window_size() 

236 root_w = min(root_w, max_w) 

237 root_h = min(root_h, max_h) 

238 count = 0 

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

240 if ss.updated_desktop_size(root_w, root_h, max_w, max_h): 

241 count +=1 

242 if count>0: 

243 log.info("sent updated screen size to %s client%s: %sx%s (max %sx%s)", 

244 count, engs(count), root_w, root_h, max_w, max_h) 

245 

246 def get_max_screen_size(self): 

247 max_w, max_h = self.get_root_window_size() 

248 return max_w, max_h 

249 

250 def _get_desktop_size_capability(self, server_source, root_w, root_h): 

251 client_size = server_source.desktop_size 

252 log("client resolution is %s, current server resolution is %sx%s", client_size, root_w, root_h) 

253 if not client_size: 

254 #client did not specify size, just return what we have 

255 return root_w, root_h 

256 client_w, client_h = client_size 

257 w = min(client_w, root_w) 

258 h = min(client_h, root_h) 

259 return w, h 

260 

261 def configure_best_screen_size(self): 

262 root_w, root_h = self.get_root_window_size() 

263 return root_w, root_h 

264 

265 def _process_desktop_size(self, proto, packet): 

266 log("new desktop size from %s: %s", proto, packet) 

267 width, height = packet[1:3] 

268 ss = self.get_server_source(proto) 

269 if ss is None: 

270 return 

271 ss.desktop_size = (width, height) 

272 if len(packet)>=10: 

273 #added in 0.16 for scaled client displays: 

274 xdpi, ydpi = packet[8:10] 

275 if xdpi!=self.xdpi or ydpi!=self.ydpi: 

276 self.xdpi, self.ydpi = xdpi, ydpi 

277 log("new dpi: %ix%i", self.xdpi, self.ydpi) 

278 self.dpi = iround((self.xdpi + self.ydpi)/2.0) 

279 self.dpi_changed() 

280 if len(packet)>=8: 

281 #added in 0.16 for scaled client displays: 

282 ss.desktop_size_unscaled = packet[6:8] 

283 if len(packet)>=6: 

284 desktops, desktop_names = packet[4:6] 

285 ss.set_desktops(desktops, desktop_names) 

286 self.calculate_desktops() 

287 if len(packet)>=4: 

288 ss.set_screen_sizes(packet[3]) 

289 bigger = ss.screen_resize_bigger 

290 log("client requesting new size: %sx%s (bigger=%s)", width, height, bigger) 

291 self.set_screen_size(width, height, bigger) 

292 if len(packet)>=4: 

293 log.info("received updated display dimensions") 

294 log.info("client display size is %sx%s with %s screen%s:", 

295 width, height, len(ss.screen_sizes), engs(ss.screen_sizes)) 

296 log_screen_sizes(width, height, ss.screen_sizes) 

297 self.calculate_workarea(width, height) 

298 #ensures that DPI and antialias information gets reset: 

299 self.update_all_server_settings() 

300 

301 def dpi_changed(self): 

302 pass 

303 

304 def calculate_desktops(self): 

305 count = 1 

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

307 if ss.desktops: 

308 count = max(count, ss.desktops) 

309 count = max(1, min(20, count)) 

310 names = [] 

311 for i in range(count): 

312 if i==0: 

313 name = "Main" 

314 else: 

315 name = "Desktop %i" % (i+1) 

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

317 if ss.desktops and i<len(ss.desktop_names) and ss.desktop_names[i]: 

318 dn = ss.desktop_names[i] 

319 if isinstance(dn, str): 

320 #newer clients send unicode 

321 name = dn 

322 else: 

323 #older clients send byte strings: 

324 try : 

325 v = strtobytes(dn).decode("utf8") 

326 except (UnicodeEncodeError, UnicodeDecodeError): 

327 log.error("Error parsing '%s'", dn, exc_info=True) 

328 else: 

329 if v!="0" or i!=0: 

330 name = v 

331 names.append(name) 

332 self.set_desktops(names) 

333 

334 def set_desktops(self, names): 

335 pass 

336 

337 def calculate_workarea(self, w, h): 

338 raise NotImplementedError() 

339 

340 def set_workarea(self, workarea): 

341 pass 

342 

343 

344 ###################################################################### 

345 # screenshots: 

346 def _process_screenshot(self, proto, _packet): 

347 packet = self.make_screenshot_packet() 

348 ss = self.get_server_source(proto) 

349 if packet and ss: 

350 ss.send(*packet) 

351 

352 def make_screenshot_packet(self): 

353 try: 

354 return self.do_make_screenshot_packet() 

355 except Exception: 

356 log.error("make_screenshot_packet()", exc_info=True) 

357 return None 

358 

359 def do_make_screenshot_packet(self): 

360 raise NotImplementedError("no screenshot capability in %s" % type(self)) 

361 

362 def send_screenshot(self, proto): 

363 #this is a screenshot request, handle it and disconnect 

364 try: 

365 packet = self.make_screenshot_packet() 

366 if not packet: 

367 self.send_disconnect(proto, "screenshot failed") 

368 return 

369 proto.send_now(packet) 

370 self.timeout_add(5*1000, self.send_disconnect, proto, "screenshot sent") 

371 except Exception as e: 

372 log.error("failed to capture screenshot", exc_info=True) 

373 self.send_disconnect(proto, "screenshot failed: %s" % e) 

374 

375 

376 def init_packet_handlers(self): 

377 self.add_packet_handlers({ 

378 "set-cursors" : self._process_set_cursors, 

379 "set-bell" : self._process_set_bell, 

380 "desktop_size" : self._process_desktop_size, 

381 "screenshot" : self._process_screenshot, 

382 })