Class: TaskJuggler::ProjectServer

Inherits:
Object
  • Object
show all
Includes:
ProcessIntercom
Defined in:
lib/taskjuggler/daemon/ProjectServer.rb

Overview

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.

Instance Attribute Summary collapse

Instance Method Summary collapse

Methods included from ProcessIntercom

#checkKey, #connect, #disconnect, #generateAuthKey, #initIntercom, #restartTimer, #startTerminator, #terminate, #timerExpired?

Methods included from MessageHandler

#critical, #debug, #error, #fatal, #info, #warning

Constructor Details

#initialize(daemonAuthKey, projectData = nil, logConsole = false) ⇒ ProjectServer

Returns a new instance of ProjectServer.



40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
# File 'lib/taskjuggler/daemon/ProjectServer.rb', line 40

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

Instance Attribute Details

#authKeyObject (readonly)

Returns the value of attribute authKey.



38
39
40
# File 'lib/taskjuggler/daemon/ProjectServer.rb', line 38

def authKey
  @authKey
end

#uriObject (readonly)

Returns the value of attribute uri.



38
39
40
# File 'lib/taskjuggler/daemon/ProjectServer.rb', line 38

def uri
  @uri
end

Instance Method Details

#getProjectNameObject

Return the name of the loaded project or nil.



184
185
186
187
188
# File 'lib/taskjuggler/daemon/ProjectServer.rb', line 184

def getProjectName
  return nil unless @tj
  restartTimer
  @tj.projectName
end

#getReportListObject

Return a list of the HTML reports defined for the project.



191
192
193
194
195
196
197
198
199
200
201
# File 'lib/taskjuggler/daemon/ProjectServer.rb', line 191

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

#getReportServerObject

This function triggers the creation of a new ReportServer process. It will return the URI and the authentication key of this new server.



205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
# File 'lib/taskjuggler/daemon/ProjectServer.rb', line 205

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

#loadProject(args) ⇒ Object

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.



139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
# File 'lib/taskjuggler/daemon/ProjectServer.rb', line 139

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

#pingObject

This function is called regularly by the ProjectBroker process to check that the ProjectServer is still operating properly.



237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
# File 'lib/taskjuggler/daemon/ProjectServer.rb', line 237

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