PageRenderTime 614ms CodeModel.GetById 151ms app.highlight 212ms RepoModel.GetById 150ms app.codeStats 0ms

/neatx/lib/node.py

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