Index: umc/python/online/__init__.py =================================================================== --- umc/python/online/__init__.py (revision 3620) +++ umc/python/online/__init__.py (working copy) @@ -44,6 +44,7 @@ from string import join from subprocess import Popen from hashlib import md5 +from copy import deepcopy from univention.management.console.log import MODULE from univention.management.console.protocol.definitions import * @@ -104,8 +105,8 @@ 'logfile': '/var/log/univention/updater.log', 'statusfile': '/var/lib/univention-updater/univention-updater.status' }, - # no API available, and no wrapper-wrapper too (or at least, didn't find one). - # Should I write a wrapper, especially to get consistent behaviour in terms of 'logfile' and 'statusfile'? + # *** IMPORTANT! *** the arg list from our request contains the COMPONENT name but the command + # here must contain the list of DEFAULTPACKAGES! # cmd = '/usr/share/univention-updater/univention-updater-umc-univention-install %s' % (' '.join(pkglist)) 'component': { 'purpose': _("Install component '%s'"), @@ -843,22 +844,30 @@ MODULE.info(" << %s" % s) # ----------------------------------- result = None - job = 'unknown' + job = '' if self._current_job and 'job' in self._current_job: job = self._current_job['job'] else: - job = request.options.get('job','none') + job = request.options.get('job','') count = request.options.get('count',0) result = 0 if count < 0 else [] if not job in INSTALLERS: - MODULE.warn(" ?? Don't know a '%s' job" % job) + # job empty: this is the first call I can't avoid + if job != '': + MODULE.warn(" ?? Don't know a '%s' job" % job) else: if not 'logfile' in INSTALLERS[job]: MODULE.warn(" ?? Job '%s' has no associated log file" % job) else: fname = INSTALLERS[job]['logfile'] - result = self._logview(fname, count) + if count < 0: + result = self._logstamp(fname) + else: + # don't read complete file if we have an 'ignore' count + if (count == 0) and (self._current_job['lines']): + count = -self._current_job['lines'] + result = self._logview(fname, count) # again debug, shortened if isinstance(result,int): @@ -900,7 +909,12 @@ job = request.options.get('job','') result = {} if job in INSTALLERS: - result = INSTALLERS[job] + # make a copy, not a reference! +# result = {} +# for arg in INSTALLERS[job]: +# result[arg] = INSTALLERS[job][arg] + result = deepcopy(INSTALLERS[job]) + if 'statusfile' in INSTALLERS[job]: try: for line in open(INSTALLERS[job]['statusfile']): @@ -944,6 +958,10 @@ result['label'] = result['purpose'] % result['detail'] else: result['label'] = result['purpose'] + # Affordance to reboot... hopefully this gets set before + # we stop polling on this job status + self.ucr.load() # make it as current as possible + result['reboot'] = self.ucr.is_true('update/reboot/required',False) # ----------- DEBUG ----------------- MODULE.info("online/installer/status returns:") @@ -979,6 +997,9 @@ MODULE.info(" << %s" % s) # ----------------------------------- + # Clean up any stored job details ... they're now obsolete. + self._current_job = {} + result = {} result['status'] = 0 # successful. If not: set result['message'] too. @@ -999,12 +1020,50 @@ self.finished(request.id,result) return - cmd = INSTALLERS[subject]['command'] - if cmd.find('%') != -1: - cmd = cmd % request.options.get('detail','') - MODULE.info(" ++ Creating job: '%s'" % cmd) - self.__create_at_job(cmd,detail) + # We want to limit the amount of logfile data being transferred + # to the frontend. So we remember the line count of the associated + # log file. + if 'logfile' in INSTALLERS[subject]: + fname = INSTALLERS[subject]['logfile'] + count = 0 + try: + file = open(fname,'r') + count = 0 + for line in file: + count += 1 + finally: + if file != None: + file.close() + self._current_job['lines'] = count + try: + # Assemble the command line, now somewhat complicated: + # + # (1) take the 'command' entry from the INSTALLERS entry of this subject + # (2) if it doesn't contain a percent sign -> ready. + # (3) if it contains a percent sign: we must format something: + # (4) if the subject is about 'component' we must get the 'defaultpackages' + # entry from the UCR tuple named by 'detail' and use that. + # (5) if not, we can format the 'detail' field into the command. + # cmd = '%s' % INSTALLERS[subject]['command'] # I need a copy of this string! + cmd = INSTALLERS[subject]['command'] + if cmd.find('%') != -1: + if subject == 'component': + # Strictly spoken, we can't arrive here if 'defaultpackages' is not set + ucrs = '%s/%s/defaultpackages' % (COMPONENT_BASE,detail) + pkgs = self.ucr.get(ucrs,'') + cmd = cmd % pkgs + MODULE.info(" Resolution of default packages of the '%s' component:" % detail) + MODULE.info(" UCRS = '%s'" % ucrs) + MODULE.info(" PKGS = '%s'" % pkgs) + MODULE.info(" CMD = '%s'" % cmd) + else: + cmd = cmd % request.options.get('detail','') + MODULE.info(" ++ Creating job: '%s'" % cmd) + self.__create_at_job(cmd,detail) + except Exception,ex: + MODULE.warn(" ERROR: %s" % str(ex)) + # ----------- DEBUG ----------------- MODULE.info("online/installer/execute returns:") pp = pprint.PrettyPrinter(indent=4) @@ -1243,29 +1302,36 @@ # # ------------------------------------------------------------------------------ + def _logstamp(self,fname): + """ Logfile timestamp. Now a seperate function. + """ + try: + st = stat(fname) + if st: + MODULE.info(" >> log file stamp = '%s'" % st[9]) + return st[9] + return 0 + except: + return 0 + def _logview(self,fname,count): - """Contains all functions needed to view or 'tail' an arbitrary text file. - Argument 'count' can have different values: - < 0 ... return Unix timestamp of log file, to avoid fetching unchanged file. - 0 ..... return the whole file, splitted into lines. - > 0 ... return the last 'count' lines of the file. (a.k.a. tail -n )""" + """ Contains all functions needed to view or 'tail' an arbitrary text file. + Argument 'count' can have different values: + < 0 ... ignore this many lines, return the rest of the file + 0 ..... return the whole file, splitted into lines. + > 0 ... return the last 'count' lines of the file. (a.k.a. tail -n ) + """ lines = [] - if count < 0: - try: - st = stat(fname) - if st: - MODULE.info(" >> log file stamp = '%s'" % st[9]) - return st[9] - return 0 - except: - return 0 try: file = open(fname,'r') for line in file: - l = line.rstrip() - lines.append(l) - if (count) and (len(lines) > count): - lines.pop(0) + if (count < 0): + count += 1 + else: + l = line.rstrip() + lines.append(l) + if (count > 0) and (len(lines) > count): + lines.pop(0) finally: if file != None: file.close() @@ -1289,6 +1355,7 @@ script = ''' #:started: %s #:detail: %s +#:command: %s dpkg-statoverride --add root root 0644 /usr/sbin/univention-management-console-web-server dpkg-statoverride --add root root 0644 /usr/sbin/univention-management-console-server dpkg-statoverride --add root root 0644 /usr/sbin/apache2 @@ -1301,7 +1368,7 @@ dpkg-statoverride --remove /usr/sbin/univention-management-console-server dpkg-statoverride --remove /usr/sbin/apache2 chmod +x /usr/sbin/univention-management-console-server /usr/sbin/univention-management-console-web-server /usr/sbin/apache2 -''' % (started,detail,command) +''' % (started,detail,command,command) p1 = subprocess.Popen( [ 'LC_ALL=C at now', ], stdin = subprocess.PIPE, stdout = subprocess.PIPE, stderr = subprocess.PIPE, shell = True ) (stdout,stderr) = p1.communicate( script ) @@ -1330,7 +1397,8 @@ cmd = INSTALLERS[inst]['command'].split('%')[0] MODULE.info(" ++ Checking for '%s'" % cmd) if cmd in atout: - self._current_job = {} +# cleaning up is done in 'run_installer()' +# self._current_job = {} self._current_job['job'] = inst # job key self._current_job['running'] = True # currently running: we have found it per 'at' job self._current_job['time'] = int(time()) # record the last time we've seen this job Index: umc/js/online.js =================================================================== --- umc/js/online.js (revision 3620) +++ umc/js/online.js (working copy) @@ -93,14 +93,27 @@ // waits for the Progress Page to be closed (automatically or by a close button) dojo.connect(this._progress,'stopWatching',dojo.hitch(this, function(tab) { - this._updates.refreshPage(true); this.hideChild(this._progress); this.showChild(this._updates); this.showChild(this._components); this.showChild(this._settings); + + // Revert to the 'Updates' page if the installer action encountered + // the 'reboot' affordance. + if (! tab) + { + tab = this._updates; + } this.selectChild(tab); })); + // waits for the Progress Page to notify us that a job is finished. This + // should immediately refresh the 'Updates' and 'Components' pages. + dojo.connect(this._progress,'jobFinished',dojo.hitch(this, function() { + this._updates.refreshPage(true); + this._components.refreshPage(); + })); + // waits for the Progress Page to notify us that a job is running dojo.connect(this._progress,'jobStarted',dojo.hitch(this, function() { this._switch_to_progress_page(); @@ -210,6 +223,7 @@ this.hideChild(this._details); this.hideChild(this._progress); + }, // Seperate function that can be called the same way as _call_installer: @@ -238,7 +252,7 @@ { txt += "\n"; txt += "" + upd[i][0] + "\n"; - txt += "" + upd[i][1] + "\n"; + txt += "" + upd[i][1] + "\n"; txt += "\n"; } } @@ -249,7 +263,7 @@ { txt += "\n"; txt += "" + ins[i][0] + "\n"; - txt += "" + ins[i][1] + "\n"; + txt += "" + ins[i][1] + "\n"; txt += "\n"; } } Index: umc/js/_online/ProgressPage.js =================================================================== --- umc/js/_online/ProgressPage.js (revision 3620) +++ umc/js/_online/ProgressPage.js (working copy) @@ -75,6 +75,16 @@ region: 'bottom', onClick: dojo.hitch(this, function() { var tab = ''; + // Especially for the 'install a component' functionality: if we encounter + // that the 'reboot required' flag has been set we don't want to switch + // back to the 'Components' grid but rather to the 'Updates' page. We will + // request this by unsetting the _last_tab property, so the switching + // logic will revert to the first tab of our tab set. + if (this._reboot_required) + { + //alert("Resetting return tab to 'Updates'"); + this.last_tab = null; + } if (this.last_tab) { tab = this.last_tab; @@ -152,13 +162,21 @@ msg += this._statusfile_data(data.result); + // -------------- DEBUG ------------------ +// for (var v in this._last_job) +// { +// msg += ("
   " + v + " = '" + this._last_job[v] + "'"); +// } + // --------------------------------------- + this._head.set('content',msg); - this._pane.layout(); if (! data.result['running']) { this._allow_close(true); // does the rest. } + + this._pane.layout(); } } @@ -178,7 +196,9 @@ // takes a status structure as returned from the 'online/installer/status' call, // extracts the fields that came directly from the status file (if any) and - // formats them in a readable HTML fashion. + // formats them into a one-liner. Returns an empty string if nothing matches. + // + // FOR TEST: now includes the 'reboot' flag if it is present. _statusfile_data: function(data) { var txt = ''; @@ -193,6 +213,16 @@ } } + if (data['reboot']) + { + txt += ' [REBOOT] '; + if (! this._reboot_required) // not set or not true + { + //alert("Setting REBOOT from _statusfile_data"); + this._reboot_required = true; + } + } + if (txt != '') { txt = "(" + this._("Current status file content") + ":" + txt + ")
"; @@ -243,6 +273,10 @@ { if ((this._job_key != '') && (this._last_job)) { + // First thing to do: notify the Module that the job is finished. So it can already + // refresh the 'Updates' and 'Components' pages before the user gets back there. + this.jobFinished(); + // FIXME Manually making empty lines before and after this text; should better be done // by a style or a style class. var msg = " 
"; @@ -256,18 +290,36 @@ msg += this._statusfile_data(this._last_job); + // -------------- DEBUG ------------------ +// for (var v in this._last_job) +// { +// msg += ("
   " + v + " = '" + this._last_job[v] + "'"); +// } + // --------------------------------------- + this._head.set('content',msg); - // for the last time: scroll log view to bottom and stop polling timer. - this._log.scrollToBottom(); - this._log.stopWatching(); + this._log.stopWatching(); // now log is freely scrollable manually //alert("Watching is finished, job is '" + this._job_key + "'"); + if ((this._last_job) && (this._last_job['reboot'])) + { + var reb = this._last_job['reboot']; + if (typeof(reb) == 'string') + { + reb = (reb == 'true'); + } + if ((! this._reboot_required) || (reb != this._reboot_required)) + { + //alert("setting REBOOT from _allow_close()"); + this._reboot_required = reb; + } + } + this._last_job = null; // can be deleted, but this._job_key should be retained! } } - this._pane.layout(); }, // gives a means to restart polling after reauthentication @@ -281,9 +333,18 @@ jobStarted: function() { }, + // This function will be called when the already opened ProgressPage encounters + // the end of the current job. The online Module listens here and will refresh + // the 'Updates' and 'Components' pages. + jobFinished: function() { + }, + // online Module calls this when the ProgressPage is to be opened. startWatching: function(args) { - //alert("ProgressPage::startWatching()"); + + // ensure a clean look (and not some stale text from last job) + this._head.set('content',this._("... loading job data ...")); + dojo.mixin(this,args); // as simple as possible. this._allow_close(false); // forbid closing this tab. this._log.startWatching(this._interval); // start logfile tail @@ -291,10 +352,12 @@ // online Module listens to this event to close the page // and reopen the named tab. + // + // This is a good place to reset the log viewer contents too. stopWatching: function(tab) { this._job_key = ''; this._last_job = null; - this._log.stopWatching(); + this._log.stopWatching(true); }, // lets the timer loop stop when the module is closed. Index: umc/js/_online/UpdatesPage.js =================================================================== --- umc/js/_online/UpdatesPage.js (revision 3620) +++ umc/js/_online/UpdatesPage.js (working copy) @@ -10,6 +10,7 @@ dojo.declare("umc.modules._online.UpdatesPage", umc.modules._online.Page, { i18nClass: 'umc.modules.online', + _last_reboot: false, postMixInProperties: function() { @@ -507,6 +508,13 @@ on = (on == 'true'); } + // pop a message up whenever the 'on' value changes + if (on != this._last_reboot) + { + //alert("Reboot affordance changed to " + on); + this._last_reboot = on; + } + var pane = this._form.getChildren()[0].getChildren()[0]; dojo.toggleClass(pane.domNode,'dijitHidden',! on); Index: umc/js/_online/_LogViewer.js =================================================================== --- umc/js/_online/_LogViewer.js (revision 3620) +++ umc/js/_online/_LogViewer.js (working copy) @@ -186,11 +186,10 @@ // job key can't be an argument here as we don't know it. startWatching: function(interval) { - //alert("LogViewer::StartWatching(" + interval + ")"); - this._check_interval = interval; // clean up any stale display and states from last run this.set('content',this._("... loading log file ...")); + this._first_call = 3; this._last_stamp = 0; @@ -207,8 +206,16 @@ // effectively stops the polling timer. Can be called from outside (if ProgressPage is being closed) // or from inside (as 'uninitialize' handler) - stopWatching: function() { + // + // Argument 'clean' = TRUE -> also clean up display buffer contents. + stopWatching: function(clean) { + this._check_interval = 0; + + if ((typeof(clean) != 'undefined') && (clean)) + { + this.set('content',this._("... loading log file ...")); + } }, uninitialize: function() {