class TaskJuggler::ProjectServer
The ProjectServer objects are created from the ProjectBroker to handle the data of a particular project. Each ProjectServer runs in a separate process that is forked-off in the constructor. Any action such as adding more files or generating a report will cause the process to fork again, creating a ReportServer object. This way the initially loaded project can be modified but the original version is always preserved for subsequent calls. Each ProjectServer process has a unique secret authentication key that only the ProjectBroker knows. It will pass it with the URI of the ProjectServer to the client to permit direct access to the ProjectServer.
Attributes
Public Class Methods
# File lib/taskjuggler/daemon/ProjectServer.rb, line 39 def initialize(daemonAuthKey, projectData = nil, logConsole = false) @daemonAuthKey = daemonAuthKey @projectData = projectData # Since we are still in the ProjectBroker process, the current DRb # server is still the ProjectBroker DRb server. @daemonURI = DRb.current_server.uri # Used later to store the DRbObject of the ProjectBroker. @daemon = nil initIntercom @logConsole = logConsole @pid = nil @uri = nil # A reference to the TaskJuggler object that holds the project data. @tj = nil # The current state of the project. @state = :new # A time stamp when the last @state update happened. @stateUpdated = TjTime.new # A lock to protect access to @state @stateLock = Monitor.new # A Queue to asynchronously generate new ReportServer objects. @reportServerRequests = Queue.new # A list of active ReportServer objects @reportServers = [] @reportServers.extend(MonitorMixin) @lastPing = TjTime.new # We've started a DRb server before. This will continue to live somewhat # in the child. All attempts to create a DRb connection from the child # to the parent will end up in the child again. So we use a Pipe to # communicate the URI of the child DRb server to the parent. The # communication from the parent to the child is not affected by the # zombie DRb server in the child process. rd, wr = IO.pipe if (@pid = fork) == -1 fatal('ps_fork_failed', 'ProjectServer fork failed') elsif @pid.nil? # This is the child if @logConsole # If the Broker wasn't daemonized, log stdout and stderr to PID # specific files. $stderr.reopen("tj3d.ps.#{$$}.stderr", 'w') $stdout.reopen("tj3d.ps.#{$$}.stdout", 'w') end begin $SAFE = 1 DRb.install_acl(ACL.new(%w[ deny all allow 127.0.0.1 ])) iFace = ProjectServerIface.new(self) begin @uri = DRb.start_service('druby://127.0.0.1:0', iFace).uri debug('', "Project server is listening on #{@uri}") rescue error('ps_cannot_start_drb', "ProjectServer can't start DRb: #{$!}") end # Send the URI of the newly started DRb server to the parent process. rd.close wr.write @uri wr.close # Start a Thread that waits for the @terminate flag to be set and does # other background tasks. startTerminator # Start another Thread that will be used to fork-off ReportServer # processes. startHousekeeping # Cleanup the DRb threads DRb.thread.join debug('', 'Project server terminated') exit 0 rescue => exception # TjRuntimeError exceptions are simply passed through. if exception.is_a?(TjRuntimeError) raise TjRuntimeError, $! end error('ps_cannot_start_drb', "ProjectServer can't start DRb: #{$!}") end else # This is the parent Process.detach(@pid) wr.close @uri = rd.read rd.close end end
Public Instance Methods
Return the name of the loaded project or nil.
# File lib/taskjuggler/daemon/ProjectServer.rb, line 183 def getProjectName return nil unless @tj restartTimer @tj.projectName end
Return a list of the HTML reports defined for the project.
# File lib/taskjuggler/daemon/ProjectServer.rb, line 190 def getReportList return [] unless @tj && (project = @tj.project) list = [] project.reports.each do |report| unless report.get('formats').empty? list << [ report.fullId, report.name ] end end restartTimer list end
This function triggers the creation of a new ReportServer process. It will return the URI and the authentication key of this new server.
# File lib/taskjuggler/daemon/ProjectServer.rb, line 204 def getReportServer # ReportServer objects only make sense for successfully scheduled # projects. return [ nil, nil ] unless @state == :ready # The ReportServer will be created asynchronously in another Thread. To # find it in the @reportServers list, we create a unique tag to identify # it. tag = rand(99999999999999) debug('', "Pushing #{tag} onto report server request queue") @reportServerRequests.push(tag) # Now wait until the new ReportServer shows up in the list. reportServer = nil while reportServer.nil? @reportServers.synchronize do @reportServers.each do |rs| reportServer = rs if rs.tag == tag end end # It should not take that long, so we use a short idle time here. sleep 0.1 if reportServer.nil? end debug('', "Got report server with URI #{reportServer.uri} for " + "tag #{tag}") restartTimer [ reportServer.uri, reportServer.authKey ] end
Wait until the project load has been finished. The result is true if the project scheduled without errors. Otherwise the result is false. args is an Array of Strings. The first element is the working directory. The second one is the master project file (.tjp file). Additionally a list of optional .tji files can be provided.
# File lib/taskjuggler/daemon/ProjectServer.rb, line 138 def loadProject(args) dirAndFiles = args.dup.untaint # The first argument is the working directory Dir.chdir(args.shift.untaint) # Save a time stamp of when the project file loading started. @modifiedCheck = TjTime.new updateState(:loading, dirAndFiles, false) begin @tj = TaskJuggler.new # Make sure that trace reports get CSV formats included so there # reports can be generated on request. @tj.generateTraces = true # Parse all project files unless @tj.parse(args, true) warning('parse_failed', "Parsing of #{args.join(' ')} failed") updateState(:failed, nil, false) @terminate = true return false end # Then schedule the project unless @tj.schedule warning('schedule_failed', "Scheduling of project #{@tj.projectId} failed") updateState(:failed, @tj.projectId, false) @terminate = true return false end rescue TjRuntimeError updateState(:failed, nil, false) @terminate = true return false end # Great, everything went fine. We've got a project to work with. updateState(:ready, @tj.projectId, false) debug('', "Project #{@tj.projectId} loaded") restartTimer true end
This function is called regularly by the ProjectBroker process to check that the ProjectServer is still operating properly.
# File lib/taskjuggler/daemon/ProjectServer.rb, line 236 def ping # Store the time stamp. If we don't get the ping for some time, we # assume the ProjectBroker has died. @lastPing = TjTime.new # Now also check our ReportServers if they are still there. If not, we # can remove them from the @reportServers list. @reportServers.synchronize do deadServers = [] @reportServers.each do |rs| unless rs.ping deadServers << rs end end @reportServers.delete_if { |rs| deadServers.include?(rs) } end end
Private Instance Methods
# File lib/taskjuggler/daemon/ProjectServer.rb, line 280 def startHousekeeping Thread.new do begin loop do # Exit this thread if the @terminate flag is set. break if @terminate # Was the project data provided during object creation? # Then we load the data here. if @projectData loadProject(@projectData) @projectData = nil end # Check every 60 seconds if the input files have been modified. # Don't check if we already know it has been modified. if @stateLock.synchronize { @state == :ready && !@modified && @modifiedCheck + 60 < TjTime.new } # Reset the timer @stateLock.synchronize { @modifiedCheck = TjTime.new } if @tj.project.inputFiles.modified? debug('', "Project #{@tj.projectId} has been modified") updateState(:ready, @tj.projectId, true) end end # Check for pending requests for new ReportServers. unless @reportServerRequests.empty? tag = @reportServerRequests.pop debug('', "Popped #{tag}") # Create an new entry for the @reportServers list. rsr = ReportServerRecord.new(tag) debug('', "RSR created") # Create a new ReportServer object that runs as a separate # process. The constructor will tell us the URI and authentication # key of the new ReportServer. rs = ReportServer.new(@tj, @logConsole) rsr.uri = rs.uri rsr.authKey = rs.authKey debug('', "Adding ReportServer with URI #{rsr.uri} to list") # Add the new ReportServer to our list. @reportServers.synchronize do @reportServers << rsr end end # Some state changing operations are not atomic. Since the client # can die during the transaction, the server might hang in some # states. Here we define timeout for each state. If the timeout is # not 0 and exceeded, we immediately terminate the process. timeouts = { :new => 30, :loading => 15 * 60, :failed => 60, :ready => 0 } if timeouts[@state] > 0 && TjTime.new - @stateUpdated > timeouts[@state] error('state_timeout', "Reached timeout for state #{@state}. Terminating.") end # If we have not received a ping from the ProjectBroker for 2 # minutes, we assume it has died and terminate as well. if TjTime.new - @lastPing > 180 # Since the abort via error() is not thread safe, we issue a # warning and abort manually. warning('daemon_heartbeat_lost', 'Heartbeat from daemon lost. Terminating.') exit 1 end sleep 1 end rescue => exception # TjRuntimeError exceptions are simply passed through. if exception.is_a?(TjRuntimeError) raise TjRuntimeError, $! end # Make sure we get a backtrace for this thread. fatal('ps_housekeeping_error', "ProjectServer housekeeping error: #{$!}") end end end
Update the state, id and modified state of the project locally and remotely.
# File lib/taskjuggler/daemon/ProjectServer.rb, line 258 def updateState(state, filesOrId, modified) begin @daemon = DRbObject.new(nil, @daemonURI) unless @daemon @daemon.updateState(@daemonAuthKey, @authKey, filesOrId, state, modified) rescue => exception # TjRuntimeError exceptions are simply passed through. if exception.is_a?(TjRuntimeError) raise TjRuntimeError, $! end error('cannot_update_daemon_state', "Can't update state with daemon: #{$!}") end @stateLock.synchronize do @state = state @stateUpdated = TjTime.new @modified = modified @modifiedCheck = TjTime.new end end