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.
# File lib/taskjuggler/daemon/ProjectServer.rb, line 41 41: def initialize(daemonAuthKey, projectData = nil, logConsole = false) 42: @daemonAuthKey = daemonAuthKey 43: @projectData = projectData 44: # Since we are still in the ProjectBroker process, the current DRb 45: # server is still the ProjectBroker DRb server. 46: @daemonURI = DRb.current_server.uri 47: # Used later to store the DRbObject of the ProjectBroker. 48: @daemon = nil 49: initIntercom 50: 51: @logConsole = logConsole 52: @pid = nil 53: @uri = nil 54: 55: # A reference to the TaskJuggler object that holds the project data. 56: @tj = nil 57: # The current state of the project. 58: @state = :new 59: # A time stamp when the last @state update happened. 60: @stateUpdated = TjTime.new 61: # A lock to protect access to @state 62: @stateLock = Monitor.new 63: 64: # A Queue to asynchronously generate new ReportServer objects. 65: @reportServerRequests = Queue.new 66: 67: # A list of active ReportServer objects 68: @reportServers = [] 69: @reportServers.extend(MonitorMixin) 70: 71: @lastPing = TjTime.new 72: 73: # We've started a DRb server before. This will continue to live somewhat 74: # in the child. All attempts to create a DRb connection from the child 75: # to the parent will end up in the child again. So we use a Pipe to 76: # communicate the URI of the child DRb server to the parent. The 77: # communication from the parent to the child is not affected by the 78: # zombie DRb server in the child process. 79: rd, wr = IO.pipe 80: 81: if (@pid = fork) == 1 82: @log.fatal('ProjectServer fork failed') 83: elsif @pid.nil? 84: # This is the child 85: if @logConsole 86: # If the Broker wasn't daemonized, log stdout and stderr to PID 87: # specific files. 88: $stderr.reopen("tj3d.ps.#{$$}.stderr", 'w') 89: $stdout.reopen("tj3d.ps.#{$$}.stdout", 'w') 90: end 91: begin 92: $SAFE = 1 93: DRb.install_acl(ACL.new(] deny all allow 127.0.0.1 ])) 94: DRb.start_service 95: iFace = ProjectServerIface.new(self) 96: begin 97: @uri = DRb.start_service('druby://127.0.0.1:0', iFace).uri 98: @log.debug("Project server is listening on #{@uri}") 99: rescue 100: @log.fatal("ProjectServer can't start DRb: #{$!}") 101: end 102: 103: # Send the URI of the newly started DRb server to the parent process. 104: rd.close 105: wr.write @uri 106: wr.close 107: 108: # Start a Thread that waits for the @terminate flag to be set and does 109: # other background tasks. 110: startTerminator 111: # Start another Thread that will be used to fork-off ReportServer 112: # processes. 113: startHousekeeping 114: 115: # Cleanup the DRb threads 116: DRb.thread.join 117: @log.debug('Project server terminated') 118: exit 0 119: rescue 120: $stderr.print $!.to_s 121: $stderr.print $!.backtrace.join("\n") 122: @log.fatal("ProjectServer can't start DRb: #{$!}") 123: end 124: else 125: # This is the parent 126: Process.detach(@pid) 127: wr.close 128: @uri = rd.read 129: rd.close 130: end 131: end
Return the name of the loaded project or nil.
# File lib/taskjuggler/daemon/ProjectServer.rb, line 174 174: def getProjectName 175: return nil unless @tj 176: restartTimer 177: @tj.projectName 178: end
Return a list of the HTML reports defined for the project.
# File lib/taskjuggler/daemon/ProjectServer.rb, line 181 181: def getReportList 182: return [] unless @tj && (project = @tj.project) 183: list = [] 184: project.reports.each do |report| 185: unless report.get('formats').empty? 186: list << [ report.fullId, report.name ] 187: end 188: end 189: restartTimer 190: list 191: 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 195 195: def getReportServer 196: # ReportServer objects only make sense for successfully scheduled 197: # projects. 198: return [ nil, nil ] unless @state == :ready 199: 200: # The ReportServer will be created asynchronously in another Thread. To 201: # find it in the @reportServers list, we create a unique tag to identify 202: # it. 203: tag = rand(99999999999999) 204: @log.debug("Pushing #{tag} onto report server request queue") 205: @reportServerRequests.push(tag) 206: 207: # Now wait until the new ReportServer shows up in the list. 208: reportServer = nil 209: while reportServer.nil? 210: @reportServers.synchronize do 211: @reportServers.each do |rs| 212: reportServer = rs if rs.tag == tag 213: end 214: end 215: # It should not take that long, so we use a short idle time here. 216: sleep 0.1 if reportServer.nil? 217: end 218: 219: @log.debug("Got report server with URI #{reportServer.uri} for " + 220: "tag #{tag}") 221: restartTimer 222: [ reportServer.uri, reportServer.authKey ] 223: 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 138: def loadProject(args) 139: dirAndFiles = args.dup.untaint 140: # The first argument is the working directory 141: Dir.chdir(args.shift.untaint) 142: 143: # Save a time stamp of when the project file loading started. 144: @modifiedCheck = TjTime.new 145: 146: updateState(:loading, dirAndFiles, false) 147: @tj = TaskJuggler.new(true) 148: 149: # Parse all project files 150: unless @tj.parse(args, true) 151: @log.error("Parsing of #{args.join(' ')} failed") 152: updateState(:failed, nil, false) 153: @terminate = true 154: return false 155: end 156: 157: # Then schedule the project 158: unless @tj.schedule 159: @log.error("Scheduling of project #{@tj.projectId} failed") 160: updateState(:failed, @tj.projectId, false) 161: Log.exit('scheduler') 162: @terminate = true 163: return false 164: end 165: 166: # Great, everything went fine. We've got a project to work with. 167: updateState(:ready, @tj.projectId, false) 168: @log.info("Project #{@tj.projectId} loaded") 169: restartTimer 170: true 171: 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 227 227: def ping 228: # Store the time stamp. If we don't get the ping for some time, we 229: # assume the ProjectBroker has died. 230: @lastPing = TjTime.new 231: 232: # Now also check our ReportServers if they are still there. If not, we 233: # can remove them from the @reportServers list. 234: @reportServers.synchronize do 235: deadServers = [] 236: @reportServers.each do |rs| 237: unless rs.ping 238: deadServers << rs 239: end 240: end 241: @reportServers.delete_if { |rs| deadServers.include?(rs) } 242: end 243: end
# File lib/taskjuggler/daemon/ProjectServer.rb, line 264 264: def startHousekeeping 265: Thread.new do 266: begin 267: loop do 268: # Was the project data provided during object creation? 269: # Then we load the data here. 270: if @projectData 271: loadProject(@projectData) 272: @projectData = nil 273: end 274: 275: # Check every 60 seconds if the input files have been modified. 276: # Don't check if we already know it has been modified. 277: if @stateLock.synchronize { @state == :ready && !@modified && 278: @modifiedCheck + 60 < TjTime.new } 279: # Reset the timer 280: @stateLock.synchronize { @modifiedCheck = TjTime.new } 281: 282: if @tj.project.inputFiles.modified? 283: @log.info("Project #{@tj.projectId} has been modified") 284: updateState(:ready, @tj.projectId, true) 285: end 286: end 287: 288: # Check for pending requests for new ReportServers. 289: unless @reportServerRequests.empty? 290: tag = @reportServerRequests.pop 291: @log.debug("Popped #{tag}") 292: # Create an new entry for the @reportServers list. 293: rsr = ReportServerRecord.new(tag) 294: @log.debug("RSR created") 295: # Create a new ReportServer object that runs as a separate 296: # process. The constructor will tell us the URI and authentication 297: # key of the new ReportServer. 298: rs = ReportServer.new(@tj, @logConsole) 299: rsr.uri = rs.uri 300: rsr.authKey = rs.authKey 301: @log.debug("Adding ReportServer with URI #{rsr.uri} to list") 302: # Add the new ReportServer to our list. 303: @reportServers.synchronize do 304: @reportServers << rsr 305: end 306: end 307: 308: # Some state changing operations are not atomic. Since the client 309: # can die during the transaction, the server might hang in some 310: # states. Here we define timeout for each state. If the timeout is 311: # not 0 and exceeded, we immediately terminate the process. 312: timeouts = { :new => 10, :loading => 15 * 60, :failed => 60, 313: :ready => 0 } 314: if timeouts[@state] > 0 && 315: TjTime.new - @stateUpdated > timeouts[@state] 316: @log.fatal("Reached timeout for state #{@state}. Terminating.") 317: end 318: 319: # If we have not received a ping from the ProjectBroker for 2 320: # minutes, we assume it has died and terminate as well. 321: if TjTime.new - @lastPing > 180 322: @log.fatal('Heartbeat from daemon lost. Terminating.') 323: end 324: sleep 1 325: end 326: rescue 327: # Make sure we get a backtrace for this thread. 328: $stderr.print $!.to_s 329: $stderr.print $!.backtrace.join("\n") 330: @log.fatal("ProjectServer housekeeping error: #{$!}") 331: end 332: end 333: end
Update the state, id and modified state of the project locally and remotely.
# File lib/taskjuggler/daemon/ProjectServer.rb, line 249 249: def updateState(state, filesOrId, modified) 250: begin 251: @daemon = DRbObject.new(nil, @daemonURI) unless @daemon 252: @daemon.updateState(@daemonAuthKey, @authKey, filesOrId, state, modified) 253: rescue 254: @log.fatal("Can't update state with daemon: #{$!}") 255: end 256: @stateLock.synchronize do 257: @state = state 258: @stateUpdated = TjTime.new 259: @modified = modified 260: @modifiedCheck = TjTime.new 261: end 262: end
Disabled; run with --debug to generate this.
Generated with the Darkfish Rdoc Generator 1.1.6.