PageRenderTime 74ms CodeModel.GetById 18ms app.highlight 50ms RepoModel.GetById 2ms app.codeStats 0ms

/neatx/lib/agent.py

http://neatx.googlecode.com/
Python | 658 lines | 467 code | 73 blank | 118 comment | 31 complexity | b10ed428054b28d98e84215df445e941 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 for nxagent interaction and session runtime
 23
 24
 25"""
 26
 27
 28import errno
 29import gobject
 30import logging
 31import os
 32import re
 33import signal
 34
 35from cStringIO import StringIO
 36
 37from neatx import constants
 38from neatx import daemon
 39from neatx import errors
 40from neatx import protocol
 41from neatx import utils
 42
 43
 44_STATUS_MAP = {
 45  constants.SESS_STATE_STARTING:
 46    re.compile(r"^Session:\s+Starting\s+session\s+at\s+"),
 47  constants.SESS_STATE_WAITING:
 48    re.compile(r"Info:\s+Waiting\s+for\s+connection\s+from\s+"
 49               r"'(?P<host>.*)'\s+on\s+port\s+'(?P<port>\d+)'\."),
 50  constants.SESS_STATE_RUNNING:
 51    re.compile(r"^Session:\s+Session\s+(started|resumed)\s+at\s+"),
 52  constants.SESS_STATE_SUSPENDING:
 53    re.compile(r"^Session:\s+Suspending\s+session\s+at\s+"),
 54  constants.SESS_STATE_SUSPENDED:
 55    re.compile(r"^Session:\s+Session\s+suspended\s+at\s+"),
 56  constants.SESS_STATE_TERMINATING:
 57    re.compile(r"^Session:\s+(Terminat|Abort)ing\s+session\s+at\s+"),
 58  constants.SESS_STATE_TERMINATED:
 59    re.compile(r"^Session:\s+Session\s+(terminat|abort)ed\s+at\s+"),
 60  }
 61
 62_WATCHDOG_PID_RE = re.compile(r"^Info:\s+Watchdog\s+running\s+with\s+pid\s+"
 63                              r"'(?P<pid>\d+)'\.")
 64_WAIT_WATCHDOG_RE = re.compile(r"^Info:\s+Waiting\s+the\s+watchdog\s+"
 65                               r"process\s+to\s+complete\.")
 66_AGENT_PID_RE = re.compile(r"^Info:\s+Agent\s+running\s+with\s+pid\s+"
 67                           r"'(?P<pid>\d+)'\.")
 68_GENERAL_ERROR_RE = re.compile(r"^Error:\s+(?P<error>.*)$")
 69_GENERAL_WARNING_RE = re.compile(r"^Warning:\s+(?P<warning>.*)$")
 70_GEOMETRY_RE = re.compile(r"^Info:\s+Screen\s+\[0\]\s+resized\s+to\s+"
 71                          r"geometry\s+\[(?P<geometry>[^\]]+)\]"
 72                          r"( fullscreen \[(?P<fullscreen>\d)\])?\.$")
 73
 74
 75class UserApplication(daemon.Program):
 76  """Wraps the user-defined application.
 77
 78  """
 79  def __init__(self, env, cwd, args, logfile, login=False):
 80    """Initializes this class.
 81
 82    @type env: dict
 83    @param env: Environment variables
 84    @type cwd: str
 85    @param cwd: Working directory
 86    @type args: list
 87    @param args: Command and arguments
 88    @type logfile: str
 89    @param logfile: Path to application logfile
 90    @type login: boolean
 91    @param login: Run the command as a login shell
 92
 93    """
 94    if login:
 95      executable = args[0]
 96      args = args[:]
 97      args[0] = "-%s" % os.path.basename(args[0])
 98    else:
 99      executable = None
100
101    lang = self._GetLangEnv(env)
102    if lang:
103      env['LANG'] = lang
104
105    # TODO: logfile
106
107    daemon.Program.__init__(self, args, env=env, cwd=cwd,
108                            executable=executable,
109                            umask=constants.DEFAULT_APP_UMASK)
110
111  def _GetLangEnv(self, env):
112    lang_rx = re.compile("^\s*Language\s*=\s*(?P<lang>\S+)\s*$", re.M)
113    dmrc_path = os.path.expanduser("~/.dmrc")
114
115    if not os.path.exists(dmrc_path):
116      logging.debug("Dmrc doesn't exist")
117      return
118    logging.debug("Dmrc exists")
119
120    try:
121      contents = open(dmrc_path).read()
122    except IOError, err:
123      logging.warning("Error reading %r: %r", dmrc_path, err.strerror)
124      return
125
126    m = lang_rx.match(contents)
127    if not m:
128      logging.debug("Dmrc doesn't contain Language setting")
129      return
130
131    lang = m.group("lang")
132    logging.debug("Dmrc language setting %r", lang)
133    return lang
134
135
136class XAuthProgram(daemon.Program):
137  """Wrapper for xauth.
138
139  Quoting xauth(1): "The xauth program is used to edit and display the
140  authorization information used in connecting to the X server."
141
142  """
143  _MIT_MAGIC_COOKIE_1 = "MIT-MAGIC-COOKIE-1"
144
145  def __init__(self, env, filename, cookies, cfg):
146    """Initializes this class.
147
148    @type env: dict
149    @param env: Environment variables
150    @type cookies: list of tuples
151    @param cookies: Cookies as [(display, cookie), ...]
152    @type cfg: L{config.Config}
153    @param cfg: Configuration object
154
155    """
156    args = [cfg.xauth, "-f", filename]
157    daemon.Program.__init__(self, args, env=env,
158                            stdin_data=self.__BuildInput(cookies))
159
160  @classmethod
161  def __BuildInput(cls, cookies):
162    """Builds the input for xauth.
163
164    @type cookies: list of tuples
165    @param cookies: Cookies as [(display, cookie), ...]
166
167    """
168    buf = StringIO()
169
170    for (display, cookie) in cookies:
171      buf.write("add %s %s %s\n" % (display, cls._MIT_MAGIC_COOKIE_1, cookie))
172
173    buf.write("exit\n")
174
175    return buf.getvalue()
176
177
178class XRdbProgram(daemon.Program):
179  """Wrapper for xrdb.
180
181  Quoting xrdb(1): "X server resource database utility. Xrdb is used to get or
182  set the contents of the RESOURCE_MANAGER property [...] Most X clients use
183  the RESOURCE_MANAGER and SCREEN_RESOURCES properties to get user preferences
184  about color, fonts, and so on for applications."
185
186  """
187  def __init__(self, env, settings, cfg):
188    args = [cfg.xrdb, "-merge"]
189
190    if not settings.endswith(os.linesep):
191      settings += os.linesep
192
193    xrdbenv = env.copy()
194    xrdbenv["LC_ALL"] = "C"
195
196    daemon.Program.__init__(self, args, env=xrdbenv, stdin_data=settings)
197
198
199class NxDialogProgram(daemon.Program):
200  """Wrapper for nxdialog program.
201
202  """
203  def __init__(self, env, dlgtype, caption, message):
204    args = [constants.NXDIALOG, "--dialog", dlgtype,
205            "--caption", caption, "--message", message]
206    daemon.Program.__init__(self, args, env=env)
207
208
209class NxAgentProgram(daemon.Program):
210  """Wraps nxagent and acts on its output.
211
212  """
213  DISPLAY_READY_SIGNAL = "display-ready"
214
215  __gsignals__ = {
216    DISPLAY_READY_SIGNAL:
217      (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE,
218       ()),
219    }
220
221  def __init__(self, ctx):
222    """Initializes this class.
223
224    @type ctx: NxNodeContext
225    @param ctx: Nxnode context object
226
227    """
228    self._ctx = ctx
229
230    args = [self._ctx.cfg.nxagent] + self._GetNxAgentArgs()
231
232    display = self._GetDisplayWithOptions()
233    logging.debug("Display for nxagent: %r", display)
234
235    env = ctx.session.GetSessionEnvVars().copy()
236    env["DISPLAY"] = display
237    env["NX_CLIENT"] = constants.NXDIALOG
238
239    self._agent_pid = None
240    self._watchdog_pid = None
241    self._want_restore = False
242
243    daemon.Program.__init__(self, args, env=env)
244
245    signal_name = daemon.ChopReader.SLICE_COMPLETE_SIGNAL
246    self._stderr_line_reg = \
247      daemon.SignalRegistration(self,
248                                self.stderr_line.connect(signal_name,
249                                                         self._HandleStderrLine))
250
251  def Start(self):
252    """Starts nxagent.
253
254    See L{daemon.Program.Start} for more details.
255
256    """
257    # Ensure options file exists
258    self._UpdateOptionsFile()
259
260    pid = daemon.Program.Start(self)
261    self._agent_pid = pid
262    return pid
263
264  def Restore(self):
265    """Prepare session restore.
266
267    Depending on the current status, different things need to be done. If the
268    session is already suspended, SIGHUP can be sent right away. If it's still
269    running, sending SIGHUP will suspend the session and can be restored after
270    sending another SIGHUP.
271
272    """
273    sess = self._ctx.session
274
275    if sess.state == constants.SESS_STATE_SUSPENDING:
276      # Restore once in suspended status
277      self._want_restore = True
278
279    elif sess.state == constants.SESS_STATE_SUSPENDED:
280      # Restore directly
281      self._PrepareSessionRestore()
282
283    elif sess.state == constants.SESS_STATE_RUNNING:
284      # Send SIGHUP to terminate session
285      self._SendSighup()
286
287      # Send SIGHUP again on status change
288      self._want_restore = True
289
290    else:
291      raise errors.GenericError("Cannot restore session in %r state" %
292                                sess.state)
293
294  def Terminate(self):
295    """Terminates nxagent by sending SIGTERM.
296
297    """
298    return self._SendSignal(signal.SIGTERM)
299
300  def _SendSighup(self):
301    """Sends a SIGHUP signal to nxagent.
302
303    """
304    return self._SendSignal(signal.SIGHUP)
305
306  def _SendSignal(self, signum):
307    """Sends a signal to nxagent.
308
309    @type signum: int
310    @param signum: Signal number
311
312    """
313    # Get signal name as string
314    signame = utils.GetSignalName(signum)
315
316    logging.info("Sending %s to nxagent", signame)
317    try:
318      os.kill(self._agent_pid, signum)
319    except OSError, err:
320      # kill(2) on ESRCH: The pid or process group does not exist. Note that
321      # an existing process might be a zombie, a process which already
322      # committed termination, but has not yet been wait(2)ed for.
323      if err.errno not in (errno.ESRCH, ):
324        raise
325      logging.exception("Failed to send %s to nxagent", signame)
326
327  def _HandleStderrLine(self, _, line):
328    """Handle a line on nxagent's stderr output.
329
330    @type line: string
331    @param line: Line without newline
332
333    """
334    if self._CheckStatus(line):
335      return
336
337    m = _WATCHDOG_PID_RE.match(line)
338    if m:
339      self._watchdog_pid = int(m.group("pid"))
340      logging.info("Matched info watchdog, PID %r", self._watchdog_pid)
341      return
342
343    m = _AGENT_PID_RE.match(line)
344    if m:
345      real_agent_pid = int(m.group("pid"))
346      logging.info("Matched info agent_pid, PID %r", real_agent_pid)
347
348      if self._agent_pid != real_agent_pid:
349        # Probably caused by nxagent being a shell script
350        logging.warning("Agent pid (%r) doesn't match spawned PID (%r)",
351                        self._agent_pid, real_agent_pid)
352        self._agent_pid = real_agent_pid
353
354      return
355
356    m = _WAIT_WATCHDOG_RE.match(line)
357    if m:
358      if self._watchdog_pid is None:
359        logging.error("Matched info kill_watchdog, but no known watchdog pid")
360      else:
361        # Before terminating, nxagent starts a separate process, called
362        # watchdog here, which must be sent SIGTERM. Otherwise it wouldn't
363        # terminate.
364        try:
365          os.kill(self._watchdog_pid, signal.SIGTERM)
366        except OSError, err:
367          logging.warning(("Matched info kill_watchdog, got error from "
368                           "killing PID %r: %r"), self._watchdog_pid, err)
369        else:
370          logging.info("Matched info kill_watchdog, sent SIGTERM.")
371
372      return
373
374    m = _GENERAL_ERROR_RE.match(line)
375    if m:
376      logging.error("Agent error: %s", m.group("error"))
377      return
378
379    m = _GENERAL_WARNING_RE.match(line)
380    if m:
381      logging.warning("Agent warning: %s", m.group("warning"))
382      return
383
384    m = _GEOMETRY_RE.match(line)
385    if m:
386      geometry = m.group("geometry")
387      fullscreen = (m.group("fullscreen") == "1")
388      self._ChangeGeometry(geometry, fullscreen)
389      logging.info("Matched info geometry change, new is %r, fullscreen %r",
390                   geometry, fullscreen)
391      return
392
393  def _CheckStatus(self, line, _status_map=None):
394    """Check whether the line indicates a session status change.
395
396    @type line: string
397    @param line: Line without newline
398
399    """
400    sess = self._ctx.session
401
402    if _status_map is None:
403      _status_map = _STATUS_MAP
404
405    for status, rx in _status_map.iteritems():
406      m = rx.match(line)
407      if m:
408        logging.info("Nxagent changed status from %r to %r",
409                     sess.state, status)
410        self._ChangeStatus(m, sess.state, status)
411        return True
412
413    return False
414
415  def _ChangeStatus(self, m, old, new):
416    """Called when session status changed.
417
418    @type m: x
419    @param m: Regex match object
420    @type old: str
421    @param old: Previous session status
422    @type new: str
423    @param new: New session status
424
425    """
426    sess = self._ctx.session
427
428    if new == old:
429      pass
430
431    elif new == constants.SESS_STATE_WAITING:
432      if old == constants.SESS_STATE_STARTING:
433        self.__EmitDisplayReady()
434
435      port = m.group("port")
436
437      try:
438        portnum = int(port)
439      except ValueError:
440        logging.warning("Port number for nxagent (%r) is not numeric",
441                        port)
442        portnum = None
443
444      logging.debug("Setting session port to %r", portnum)
445
446      sess.port = portnum
447
448    elif (old == constants.SESS_STATE_SUSPENDING and
449          new == constants.SESS_STATE_SUSPENDED and
450          self._want_restore):
451      self._want_restore = False
452
453      self._PrepareSessionRestore()
454
455    elif (old == constants.SESS_STATE_TERMINATING and
456          new == constants.SESS_STATE_TERMINATED):
457      logging.info("Nxagent terminated")
458
459    sess.state = new
460    sess.Save()
461
462  def _PrepareSessionRestore(self):
463    """Prepare session restore by telling nxagent to reopen its port.
464
465    """
466    # Write options file with new options
467    self._UpdateOptionsFile()
468
469    # Send SIGHUP to reopen port
470    self._SendSighup()
471
472  def _ChangeGeometry(self, geometry, fullscreen):
473    """Called when geometry changed.
474
475    @type geometry: str
476    @param geometry: Geometry information
477    @type fullscreen: boolean
478    @param fullscreen: Fullscreen state
479
480    """
481    sess = self._ctx.session
482    sess.geometry = geometry
483    sess.fullscreen = fullscreen
484    sess.Save()
485
486  def _FormatNxAgentOptions(self, opts):
487    """Formats options for nxagent.
488
489    @type opts: dict
490    @param opts: Options
491
492    """
493    sess = self._ctx.session
494
495    formatted = ",".join(["%s=%s" % (name, value)
496                          for name, value in opts.iteritems()])
497
498    return "nx/nx,%s:%d\n" % (formatted, sess.display)
499
500  def _GetDisplayWithOptions(self):
501    """Returns the value for the DISPLAY variable for nxagent.
502
503    """
504    sess = self._ctx.session
505
506    self.__CheckStrChars(sess.optionsfile, "Session options file")
507
508    return "nx/nx,options=%s:%d" % (sess.optionsfile, sess.display)
509
510  def _GetOptions(self):
511    """Returns session options for nxagent.
512
513    @rtype: dict
514    @return: Options
515
516    """
517    sess = self._ctx.session
518
519    # We need to write the type without the "unix-" prefix for nxagent
520    if sess.type.startswith(constants.SESS_TYPE_UNIX_PREFIX):
521      shorttype = sess.type[len(constants.SESS_TYPE_UNIX_PREFIX):]
522    else:
523      shorttype = sess.type
524
525    opts = {
526      # This limits what IPs nxagent will accept connections from. When using
527      # encrypted sessions, connections are always from localhost. Unencrypted
528      # connections come directly from nxclient.
529      # Note: Unencrypted connections are not supported.
530      "accept": "127.0.0.1",
531      "backingstore": "1",
532      "cache": protocol.FormatNxSize(sess.cache),
533      "cleanup": "0",
534      "client": sess.client,
535      "clipboard": "both",
536      "composite": "1",
537      "cookie": sess.cookie,
538      "id": sess.full_id,
539      "images": protocol.FormatNxSize(sess.images),
540      "keyboard": sess.keyboard,
541      "link": sess.link,
542      # TODO: What is this used for in nxagent?
543      "product": "Neatx-%s" % constants.DEFAULT_SUBSCRIPTION,
544      "render": "1",
545      "resize": protocol.FormatNxBoolean(sess.resize),
546      "shmem": "1",
547      "shpix": "1",
548      "strict": "0",
549      }
550
551    if sess.type == constants.SESS_TYPE_SHADOW:
552      # TODO: Make shadowmode configurable and/or controllable by the shadowed
553      # user.  Be aware, though, that this flag is under the control of the
554      # shadowing user.
555      # 0 = view only, 1 = interactive
556      opts["shadowmode"] = "1"
557      # TODO: Check which UID we should pass here.
558      opts["shadowuid"] = self._ctx.uid
559      opts["shadow"] = ":%s" % sess.shadow_display
560
561    if sess.rootless:
562      opts["menu"] = "1"
563    else:
564      opts["geometry"] = sess.geometry
565      opts["fullscreen"] = protocol.FormatNxBoolean(sess.fullscreen)
566
567    if sess.rootless and sess.type == constants.SESS_TYPE_CONSOLE:
568      opts["type"] = "rootless"
569    else:
570      opts["type"] = shorttype
571
572    return opts
573
574  def _GetNxAgentArgs(self):
575    """Returns command line arguments for nxagent.
576
577    """
578    sess = self._ctx.session
579
580    if sess.type == constants.SESS_TYPE_SHADOW:
581      # Run nxagent in shadow mode
582      mode = "-S"
583
584    elif sess.rootless:
585      # Run nxagent in rootless mode
586      mode = "-R"
587
588    else:
589      # Run nxagent in desktop mode
590      mode = "-D"
591
592    args = [
593      mode,
594      "-name", sess.windowname,
595      "-options", self._ctx.session.optionsfile,
596
597      # Disable permanently-open TCP port for X protocol (doesn't affect
598      # nxagent port).
599      "-nolisten", "tcp",
600
601      ":%d" % sess.display,
602      ]
603
604    if sess.type == constants.SESS_TYPE_SHADOW:
605      args.append("-nopersistent")
606
607    return args
608
609  def _UpdateOptionsFile(self):
610    """Update session options file.
611
612    """
613    self._WriteOptionsFile(self._GetOptions())
614
615  def _WriteOptionsFile(self, opts):
616    """Writes session options to the session-specific options file.
617
618    @type opts: dict
619    @param opts: Options
620
621    """
622    sess = self._ctx.session
623    filename = sess.optionsfile
624    self.__CheckOptsChars(opts)
625    formatted = self._FormatNxAgentOptions(opts)
626
627    logging.debug("Writing session options %r to %s", formatted, filename)
628    utils.WriteFile(filename, data=formatted, mode=0600)
629
630  def __EmitDisplayReady(self):
631    self.emit(self.DISPLAY_READY_SIGNAL)
632
633  def __CheckOptsChars(self, opts):
634    """Checks to make sure option name/values don't contain illegal characters.
635
636    @type opts: dict
637    @param opts: Options
638
639    """
640
641    for name, value in opts.iteritems():
642      self.__CheckStrChars(name, "Name of option %r" % name)
643      self.__CheckStrChars(value, "Value of option %r (%r)" % (name, value))
644
645  def __CheckStrChars(self, s, description):
646    """Checks to make sure string don't contain illegal characters.
647
648    @type s: string
649    @param s: text to test
650    @type description: string
651    @param description: description of text
652
653    """
654    illegal_chars = [","]
655    for c in illegal_chars:
656      if c in s:
657        raise errors.IllegalCharacterError("%s contains illegal character %r" %
658                                           (description, c))