/neatx/lib/node.py

http://neatx.googlecode.com/ · Python · 616 lines · 353 code · 140 blank · 123 comment · 57 complexity · f4b7371bf0d5d6e38c86048324f25145 MD5 · raw file

  1. #
  2. #
  3. # Copyright (C) 2009 Google Inc.
  4. #
  5. # This program is free software; you can redistribute it and/or modify
  6. # it under the terms of the GNU General Public License as published by
  7. # the Free Software Foundation; either version 2 of the License, or
  8. # (at your option) any later version.
  9. #
  10. # This program is distributed in the hope that it will be useful, but
  11. # WITHOUT ANY WARRANTY; without even the implied warranty of
  12. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
  13. # General Public License for more details.
  14. #
  15. # You should have received a copy of the GNU General Public License
  16. # along with this program; if not, write to the Free Software
  17. # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
  18. # 02110-1301, USA.
  19. """Module containing code used by nxnode
  20. """
  21. import collections
  22. import errno
  23. import logging
  24. import os
  25. import pwd
  26. import random
  27. import socket
  28. import sys
  29. from cStringIO import StringIO
  30. from neatx import agent
  31. from neatx import constants
  32. from neatx import daemon
  33. from neatx import errors
  34. from neatx import protocol
  35. from neatx import serializer
  36. from neatx import session
  37. from neatx import utils
  38. REQ_FIELD_CMD = "cmd"
  39. REQ_FIELD_ARGS = "args"
  40. RESP_FIELD_SUCCESS = "success"
  41. RESP_FIELD_RESULT = "result"
  42. CMD_STARTSESSION = "start"
  43. CMD_ATTACHSESSION = "attach"
  44. CMD_RESTORESESSION = "restore"
  45. CMD_TERMINATESESSION = "terminate"
  46. CMD_GET_SHADOW_COOKIE = "getshadowcookie"
  47. PROTO_SEPARATOR = "\0"
  48. def GetHostname():
  49. return socket.getfqdn()
  50. def _GetUserShell(username):
  51. return pwd.getpwnam(username).pw_shell
  52. def _GetUserHomedir(username):
  53. return pwd.getpwnam(username).pw_dir
  54. def FindUnusedDisplay(_pool=None, _check_paths=None):
  55. """Return an unused display number (corresponding to an unused port)
  56. FIXME: This should also be checking for open ports.
  57. """
  58. if _pool is None:
  59. # Choosing display numbers from a random pool to alleviate a potential race
  60. # condition between multiple clients connecting at the same time. If the
  61. # checked paths are not created fast enough, they could all detect the same
  62. # display number as free. By choosing random numbers this shouldn't happen
  63. # as often, but it's very hard to fix properly.
  64. # TODO: Find a better way to reserve display numbers in an atomic way.
  65. # FIXME: Potential DoS: any user could create all checked paths and thereby
  66. # lock other users out.
  67. _pool = random.sample(xrange(20, 1000), 10)
  68. if _check_paths is None:
  69. _check_paths = constants.DISPLAY_CHECK_PATHS
  70. for i in _pool:
  71. logging.debug("Trying display number %s", i)
  72. ok = True
  73. for path in _check_paths:
  74. if os.path.exists(path % i):
  75. ok = False
  76. break
  77. if ok:
  78. logging.debug("Display number %s appears to be unused", i)
  79. return i
  80. raise errors.NoFreeDisplayNumberFound()
  81. class NodeSession(session.SessionBase):
  82. """Keeps runtime properties of a session.
  83. """
  84. def __init__(self, ctx, clientargs, _env=None):
  85. self._ctx = ctx
  86. hostname = GetHostname()
  87. display = FindUnusedDisplay()
  88. session.SessionBase.__init__(self, ctx.sessid, hostname, display,
  89. ctx.username)
  90. self.name = clientargs.get("session")
  91. if not self.name:
  92. raise errors.SessionParameterError("Session name missing")
  93. self.type = clientargs.get("type")
  94. if not self.type:
  95. raise errors.SessionParameterError("Session type missing")
  96. if not protocol.ParseNxBoolean(clientargs.get("encryption")):
  97. raise errors.SessionParameterError("Unencrypted connections not "
  98. "supported")
  99. self.sessdir = self._ctx.sessmgr.GetSessionDir(self.id)
  100. self.authorityfile = os.path.join(self.sessdir, "authority")
  101. self.applogfile = os.path.join(self.sessdir, "app.log")
  102. self.optionsfile = os.path.join(self.sessdir, "options")
  103. # Default values
  104. self.cache = 16
  105. self.client = "unknown"
  106. self.fullscreen = False
  107. self.geometry = "640x480"
  108. self.images = 64
  109. self.keyboard = "pc105/gb"
  110. self.link = "isdn"
  111. self.rootless = False
  112. self.screeninfo = None
  113. self.ssl = True
  114. self.virtualdesktop = False
  115. self.resize = False
  116. self.shadow_cookie = None
  117. self.shadow_display = None
  118. self._ParseClientargs(clientargs)
  119. if _env is None:
  120. env = os.environ.copy()
  121. else:
  122. env = _env.copy()
  123. env["NX_ROOT"] = self.sessdir
  124. env["XAUTHORITY"] = self.authorityfile
  125. env["SHELL"] = _GetUserShell(self._ctx.username)
  126. self._env = env
  127. self.command = self._GetCommand(clientargs)
  128. def _ParseClientargs(self, clientargs):
  129. self.client = clientargs.get("client", self.client)
  130. self.geometry = clientargs.get("geometry", self.geometry)
  131. self.keyboard = clientargs.get("keyboard", self.keyboard)
  132. self.link = clientargs.get("link", self.link)
  133. self.screeninfo = clientargs.get("screeninfo", self.screeninfo)
  134. if self.type == constants.SESS_TYPE_SHADOW:
  135. if "display" not in clientargs:
  136. raise errors.SessionParameterError("Missing 'display' parameter")
  137. self.shadow_display = clientargs["display"]
  138. if "images" in clientargs:
  139. self.images = protocol.ParseNxSize(clientargs["images"])
  140. if "cache" in clientargs:
  141. self.cache = protocol.ParseNxSize(clientargs["cache"])
  142. if "resize" in clientargs:
  143. self.resize = protocol.ParseNxBoolean(clientargs["resize"])
  144. else:
  145. self.resize = False
  146. if "fullscreen" in clientargs:
  147. self.fullscreen = protocol.ParseNxBoolean(clientargs["fullscreen"])
  148. else:
  149. self.fullscreen = False
  150. if "rootless" in clientargs:
  151. self.rootless = protocol.ParseNxBoolean(clientargs["rootless"])
  152. else:
  153. self.rootless = False
  154. if "virtualdesktop" in clientargs:
  155. self.virtualdesktop = \
  156. protocol.ParseNxBoolean(clientargs["virtualdesktop"])
  157. else:
  158. self.virtualdesktop = True
  159. def _GetCommand(self, clientargs):
  160. """Returns the command requested by the client.
  161. """
  162. cfg = self._ctx.cfg
  163. sesstype = self.type
  164. args = [_GetUserShell(self._ctx.username), "-c"]
  165. if sesstype == constants.SESS_TYPE_SHADOW:
  166. return None
  167. elif sesstype == constants.SESS_TYPE_KDE:
  168. return args + [cfg.start_kde_command]
  169. elif sesstype == constants.SESS_TYPE_GNOME:
  170. return args + [cfg.start_gnome_command]
  171. elif sesstype == constants.SESS_TYPE_CONSOLE:
  172. return args + [cfg.start_console_command]
  173. elif sesstype == constants.SESS_TYPE_APPLICATION:
  174. # Get client-specified application
  175. app = clientargs.get("application", "")
  176. if not app.strip():
  177. raise errors.SessionParameterError(("Session type %s, but missing "
  178. "application") % sesstype)
  179. return args + [protocol.UnquoteParameterValue(app)]
  180. raise errors.SessionParameterError("Unsupported session type: %s" %
  181. sesstype)
  182. def PrepareRestore(self, clientargs):
  183. """Update session with new settings from client.
  184. """
  185. self._ParseClientargs(clientargs)
  186. def SetShadowCookie(self, cookie):
  187. """Sets the shadow cookie for this session.
  188. @type cookie: str
  189. @param cookie: Shadow cookie
  190. """
  191. self.shadow_cookie = cookie
  192. def GetSessionEnvVars(self):
  193. return self._env
  194. def Save(self):
  195. self._ctx.sessmgr.SaveSession(self)
  196. class SessionRunner(object):
  197. """Manages the various parts of a session lifetime.
  198. """
  199. def __init__(self, ctx):
  200. self.__ctx = ctx
  201. self.__nxagent = None
  202. self.__nxagent_exited_reg = None
  203. self.__nxagent_display_ready_reg = None
  204. def Start(self):
  205. sess = self.__ctx.session
  206. cookies = []
  207. cookies.extend(map(lambda display: (display, sess.cookie),
  208. self.__GetHostDisplays(sess.display)))
  209. if sess.shadow_cookie:
  210. # Add special shadow cookie
  211. cookies.extend(map(lambda display: (display, sess.shadow_cookie),
  212. self.__GetHostDisplays(sess.shadow_display)))
  213. logging.info("Starting xauth for %r", cookies)
  214. xauth = agent.XAuthProgram(sess.GetSessionEnvVars(), sess.authorityfile,
  215. cookies, self.__ctx.cfg)
  216. xauth.connect(agent.XAuthProgram.EXITED_SIGNAL, self.__XAuthDone)
  217. xauth.Start()
  218. def Restore(self):
  219. if not self.__nxagent:
  220. raise errors.GenericError("nxagent not yet started")
  221. self.__nxagent.Restore()
  222. def __XAuthDone(self, _, exitstatus, signum):
  223. """Called when xauth exits.
  224. """
  225. if exitstatus != 0 or signum is not None:
  226. self.__Quit()
  227. return
  228. self.__StartNxAgent()
  229. def __GetHostDisplays(self, display):
  230. return [":%s" % display,
  231. "localhost:%s" % display]
  232. def __GetXProgramEnv(self):
  233. sess = self.__ctx.session
  234. env = sess.GetSessionEnvVars().copy()
  235. env["DISPLAY"] = ":%s.0" % sess.display
  236. return env
  237. def __StartNxAgent(self):
  238. """Starts the nxagent program.
  239. """
  240. logging.info("Starting nxagent")
  241. self.__nxagent = agent.NxAgentProgram(self.__ctx)
  242. signal_name = agent.NxAgentProgram.EXITED_SIGNAL
  243. self.__nxagent_exited_reg = \
  244. daemon.SignalRegistration(self.__nxagent,
  245. self.__nxagent.connect(signal_name,
  246. self.__NxAgentDone))
  247. signal_name = agent.NxAgentProgram.DISPLAY_READY_SIGNAL
  248. self.__nxagent_display_ready_reg = \
  249. daemon.SignalRegistration(self.__nxagent,
  250. self.__nxagent.connect(signal_name,
  251. self.__DisplayReady))
  252. self.__nxagent.Start()
  253. def __NxAgentDone(self, prog, exitstatus, signum):
  254. assert prog == self.__nxagent
  255. logging.info("nxagent terminated")
  256. if self.__nxagent_exited_reg:
  257. self.__nxagent_exited_reg.Disconnect()
  258. self.__nxagent_exited_reg = None
  259. if self.__nxagent_display_ready_reg:
  260. self.__nxagent_display_ready_reg.Disconnect()
  261. self.__nxagent_display_ready_reg = None
  262. self.__Quit()
  263. def __DisplayReady(self, prog):
  264. assert prog == self.__nxagent
  265. self.__StartXRdb()
  266. def __StartXRdb(self):
  267. """Starts the xrdb program.
  268. """
  269. logging.info("Starting xrdb")
  270. settings = "Xft.dpi: 96"
  271. xrdb = agent.XRdbProgram(self.__GetXProgramEnv(), settings, self.__ctx.cfg)
  272. xrdb.connect(agent.XRdbProgram.EXITED_SIGNAL, self.__XRdbDone)
  273. xrdb.Start()
  274. def __XRdbDone(self, _, exitstatus, signum):
  275. # Ignoring xrdb errors
  276. self.__StartUserApp()
  277. def __StartUserApp(self):
  278. """Starts the user-defined or user-requested application.
  279. """
  280. sess = self.__ctx.session
  281. logging.info("Starting user application (%r)", sess.command)
  282. # Shadow sessions have no command
  283. if sess.command is None:
  284. return
  285. cwd = _GetUserHomedir(self.__ctx.username)
  286. userapp = agent.UserApplication(self.__GetXProgramEnv(), cwd, sess.command,
  287. sess.applogfile, login=True)
  288. userapp.connect(agent.UserApplication.EXITED_SIGNAL,
  289. self.__UserAppDone)
  290. userapp.Start()
  291. def __UserAppDone(self, _, exitstatus, signum):
  292. """Called when user application terminated.
  293. """
  294. sess = self.__ctx.session
  295. logging.info("User application terminated")
  296. usable_session = (sess.state in
  297. (constants.SESS_STATE_STARTING,
  298. constants.SESS_STATE_WAITING,
  299. constants.SESS_STATE_RUNNING))
  300. if usable_session and (exitstatus != 0 or signum is not None):
  301. msg = StringIO()
  302. msg.write("Application failed.\n\n")
  303. msg.write("Command: %s\n" % utils.ShellQuoteArgs(sess.command))
  304. if exitstatus is not None:
  305. msg.write("Exit code: %s\n" % exitstatus)
  306. if signum is not None:
  307. msg.write("Signal number: %s (%s)\n" %
  308. (signum, utils.GetSignalName(signum)))
  309. self.__StartNxDialog(constants.DLG_TYPE_ERROR,
  310. "Error", msg.getvalue())
  311. return
  312. self.__TerminateNxAgent()
  313. def __StartNxDialog(self, dlgtype, caption, message):
  314. dlg = agent.NxDialogProgram(self.__GetXProgramEnv(), dlgtype,
  315. caption, message)
  316. dlg.connect(agent.NxDialogProgram.EXITED_SIGNAL,
  317. self.__NxDialogDone)
  318. dlg.Start()
  319. def __NxDialogDone(self, _, exitstatus, signum):
  320. self.__TerminateNxAgent()
  321. def __TerminateNxAgent(self):
  322. """Tell nxagent to quit.
  323. """
  324. if self.__nxagent:
  325. self.__nxagent.Terminate()
  326. def __Quit(self):
  327. """Called when nxagent terminated.
  328. """
  329. self.__nxagent = None
  330. # Quit nxnode
  331. sys.exit(0)
  332. def StartNodeDaemon(username, sessid):
  333. def _StartNxNode():
  334. os.execl(constants.NXNODE_WRAPPER, "--", username, sessid)
  335. utils.StartDaemon(_StartNxNode)
  336. # TODO: Move this class somewhere else. It is not used by the node daemon, but
  337. # only by clients connecting to the daemon.
  338. class NodeClient(object):
  339. """Node RPC client implementation.
  340. Connects to an nxnode socket and provides methods to execute remote procedure
  341. calls.
  342. """
  343. _RETRY_TIMEOUT = 10
  344. _CONNECT_TIMEOUT = 10
  345. _RW_TIMEOUT = 20
  346. def __init__(self, address):
  347. """Initializes this class.
  348. @type address: str
  349. @param address: Unix socket path
  350. """
  351. self._address = address
  352. self._sock = None
  353. self._inbuf = ""
  354. self._inmsg = collections.deque()
  355. def _InnerConnect(self, sock, retry):
  356. sock.settimeout(self._CONNECT_TIMEOUT)
  357. try:
  358. sock.connect(self._address)
  359. except socket.timeout, err:
  360. raise errors.GenericError("Connection timed out: %s" % str(err))
  361. except socket.error, err:
  362. if retry and err.args[0] in (errno.ENOENT, errno.ECONNREFUSED):
  363. # Try again
  364. raise utils.RetryAgain()
  365. raise
  366. sock.settimeout(self._RW_TIMEOUT)
  367. return sock
  368. def Connect(self, retry):
  369. """Connects to Unix socket.
  370. @type retry: bool
  371. @param retry: Whether to retry connection for a while
  372. """
  373. logging.info("Connecting to %r", self._address)
  374. sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
  375. sock.setblocking(1)
  376. if retry:
  377. try:
  378. utils.Retry(lambda: self._InnerConnect(sock, True),
  379. 0.1, 1.1, 1.0, self._RETRY_TIMEOUT)
  380. except utils.RetryTimeout:
  381. logging.error("Socket didn't become ready in %s seconds",
  382. self._RETRY_TIMEOUT)
  383. raise errors.GenericError("Socket didn't become ready in time")
  384. else:
  385. self._InnerConnect(sock, False)
  386. self._sock = sock
  387. def Close(self):
  388. self._sock.close()
  389. def _SendRequest(self, cmd, args):
  390. """Sends a request and handles the response.
  391. @type cmd: str
  392. @param cmd: Procedure name
  393. @type args: built-in type
  394. @param args: Arguments
  395. @return: Value returned by the procedure call
  396. """
  397. # Build request
  398. req = {
  399. REQ_FIELD_CMD: cmd,
  400. REQ_FIELD_ARGS: args,
  401. }
  402. logging.debug("Sending request: %r", req)
  403. # TODO: sendall doesn't report errors properly
  404. self._sock.sendall(serializer.DumpJson(req))
  405. self._sock.sendall(PROTO_SEPARATOR)
  406. resp = serializer.LoadJson(self._ReadResponse())
  407. logging.debug("Received response: %r", resp)
  408. # Check whether we received a valid response
  409. if (not isinstance(resp, dict) or
  410. RESP_FIELD_SUCCESS not in resp or
  411. RESP_FIELD_RESULT not in resp):
  412. raise errors.GenericError("Invalid response from daemon: %r", resp)
  413. result = resp[RESP_FIELD_RESULT]
  414. if resp[RESP_FIELD_SUCCESS]:
  415. return result
  416. # Is it a serialized exception? They must have the following format (both
  417. # lists and tuples are accepted):
  418. # ("ExceptionClassName", (arg1, arg2, arg3))
  419. if (isinstance(result, (tuple, list)) and
  420. len(result) == 2 and
  421. isinstance(result[1], (tuple, list))):
  422. errcls = errors.GetErrorClass(result[0])
  423. if errcls is not None:
  424. raise errcls(*result[1])
  425. # Fallback
  426. raise errors.GenericError(resp[RESP_FIELD_RESULT])
  427. def _ReadResponse(self):
  428. """Reads a response from the socket.
  429. @rtype: str
  430. @return: Response message
  431. """
  432. # Read from socket while there are no messages in the buffer
  433. while not self._inmsg:
  434. data = self._sock.recv(4096)
  435. if not data:
  436. raise errors.GenericError("Connection closed while reading")
  437. parts = (self._inbuf + data).split(PROTO_SEPARATOR)
  438. self._inbuf = parts.pop()
  439. self._inmsg.extend(parts)
  440. return self._inmsg.popleft()
  441. def StartSession(self, args):
  442. return self._SendRequest(CMD_STARTSESSION, args)
  443. def AttachSession(self, args, shadowcookie):
  444. return self._SendRequest(CMD_ATTACHSESSION, [args, shadowcookie])
  445. def RestoreSession(self, args):
  446. return self._SendRequest(CMD_RESTORESESSION, args)
  447. def TerminateSession(self, args):
  448. return self._SendRequest(CMD_TERMINATESESSION, args)
  449. def GetShadowCookie(self, args):
  450. return self._SendRequest(CMD_GET_SHADOW_COOKIE, args)