The Tj3Client class provides the primary interface to the TaskJuggler daemon. It exposes a rich commandline interface that supports key operations like add/removing a project, generating a report or checking a time or status sheet. All connections are made via DRb and tj3client requires a properly configured tj3d to work.
# File lib/taskjuggler/apps/Tj3Client.rb, line 31 31: def initialize 32: super 33: 34: # For security reasons, this will probably not change. All DRb 35: # operations are limited to localhost only. The client and the sever 36: # must have access to the identical file system. 37: @host = '127.0.0.1' 38: # The default port. 'T' and 'J' in ASCII decimal 39: @port = 8474 40: # The file with the server URI in case port is 0. 41: @uriFile = File.join(Dir.getwd, '.tj3d.uri') 42: # This must must be changed for the communication to work. 43: @authKey = nil 44: # Determines whether report IDs are fix IDs or regular expressions that 45: # match a set of reports. 46: @regExpMode = false 47: # Prevents usage of protective sandbox if set to true. 48: @unsafeMode = false 49: # List of requested output formats for reports. 50: @formats = nil 51: 52: @mandatoryArgs = '<command> [arg1 arg2 ...]' 53: 54: # This list describes the supported command line commands and their 55: # parameter. 56: # :label : The command name 57: # :args : A list of parameters. If the first character is a '+' the 58: # parameter must be provided 1 or more times. If the first character is 59: # a '*' the parameter must be provided 0 or more times. Repeatable and 60: # optional paramters must follow the mandatory ones. 61: # :descr : A short description of the command used for the help text. 62: @commands = [ 63: { :label => 'status', 64: :args => [], 65: :descr => 'Display the status of the available projects' }, 66: { :label => 'terminate', 67: :args => [], 68: :descr => 'Terminate the TaskJuggler daemon' }, 69: { :label => 'add', 70: :args => [ 'tjp file', '*tji file'], 71: :descr => 'Add a new project or update and existing one' }, 72: { :label => 'update', 73: :args => [], 74: :descr => 'Reload all projects that have modified files and '+ 75: 'are not being reloaded already' }, 76: { :label => 'remove', 77: :args => [ '+project ID' ], 78: :descr => 'Remove the project with the specified ID from the ' + 79: 'daemon' }, 80: { :label => 'report', 81: :args => [ 'project ID', '+report ID', '!=', '*tji file'], 82: :descr => 'Generate the report with the provided ID for ' + 83: 'the project with the given ID'}, 84: { :label => 'list-reports', 85: :args => [ 'project ID', '!report ID' ], 86: :descr => 'List all available reports of the project or those ' + 87: 'that match the provided report ID' }, 88: { :label => 'check-ts', 89: :args => [ 'project ID', 'time sheet' ], 90: :descr => 'Check the provided time sheet for correctness ' + 91: 'against the project with the given ID'}, 92: { :label => 'check-ss', 93: :args => [ 'project ID', 'status sheet' ], 94: :descr => 'Check the provided status sheet for correctness ' + 95: 'against the project with the given ID'} 96: ] 97: end
# File lib/taskjuggler/apps/Tj3Client.rb, line 162 162: def appMain(args) 163: begin 164: # Run a first check of the non-optional command line arguments. 165: checkCommand(args) 166: # Read some configuration variables. Except for the authKey, they are 167: # all optional. 168: @rc.configure(self, 'global') 169: 170: connectDaemon 171: retVal = executeCommand(args[0], args[1..1]) 172: disconnectDaemon 173: 174: return retVal 175: rescue TjRuntimeError 176: return 1 177: end 178: end
# File lib/taskjuggler/apps/Tj3Client.rb, line 99 99: def processArguments(argv) 100: super do 101: @opts.banner += The TaskJuggler client is used to send commands and data to the TaskJugglerdaemon. The communication is done via TCP/IP.The following commands are supported: 102: 103: # Convert the command list into a help text. 104: @commands.each do |cmd| 105: tail = '' 106: args = cmd[:args].dup 107: args.map! do |c| 108: if c[0] == '*' 109: "[<#{c[1..-1]}> ...]" 110: elsif c[0] == '+' 111: "<#{c[1..-1]}> [<#{c[1..-1]}> ...]" 112: elsif c[0] == '!' 113: tail += ']' 114: "[#{c[1..-1]} " 115: else 116: "<#{c}>" 117: end 118: end 119: args = args.join(' ') 120: @opts.banner += " #{cmd[:label] + ' ' + args + tail}" + 121: "\n\n#{' ' * 10 + format(cmd[:descr], 10)}\n" 122: end 123: @opts.on('-p', '--port <NUMBER>', Integer, 124: format('Use the specified TCP/IP port')) do |arg| 125: @port = arg 126: end 127: @opts.on('--urifile <FILE>', String, 128: format('If the port is 0, use this file to get the URI ' + 129: 'of the server.')) do |arg| 130: @uriFile = arg 131: end 132: @opts.on('-r', '--regexp', 133: format('The report IDs are not fixed but regular expressions ' + 134: 'that match a set of reports')) do |arg| 135: @regExpMode = true 136: end 137: @opts.on('--unsafe', 138: format('Run the program without sandbox protection. This ' + 139: 'is not recommended for normal operation! It may ' + 140: 'only be used for debugging or testing ' + 141: 'purposes.')) do |arg| 142: @unsafeMode = true 143: end 144: @opts.on('--format [FORMAT]', [ :csv, :html, :niku, :tjp ], 145: format('Request the report to be generated in the specified' + 146: 'format. Use multiple options to request multiple ' + 147: 'formats. Supported formats are csv, html, niku and ' + 148: 'tjp. By default, the formats specified in the report ' + 149: 'definition are used.')) do |arg| 150: @formats = [] unless @formats 151: @formats << arg 152: end 153: end 154: end
Transfer the tjiFiles to the reportServer.
# File lib/taskjuggler/apps/Tj3Client.rb, line 441 441: def addFiles(tjiFiles) 442: tjiFiles.each do |file| 443: begin 444: unless @reportServer.addFile(@rs_authKey, file) 445: return false 446: end 447: rescue 448: error("Cannot add file #{file} to ReportServer") 449: end 450: end 451: true 452: end
Call the TaskJuggler daemon (ProjectBroker) and execute the provided command with the provided arguments.
# File lib/taskjuggler/apps/Tj3Client.rb, line 410 410: def callDaemon(command, args) 411: begin 412: return @broker.command(@authKey, command, args) 413: rescue 414: error("Call to TaskJuggler server on host '#{@host}' " + 415: "port #{@port} failed: #{$!}") 416: end 417: end
# File lib/taskjuggler/apps/Tj3Client.rb, line 182 182: def checkCommand(args) 183: if args.empty? 184: errorMessage = 'You must specify a command!' 185: else 186: errorMessage = "Unknown command #{args[0]}" 187: @commands.each do |cmd| 188: # The first value of args is the command name. 189: if cmd[:label] == args[0] 190: # Find out how many arguments we need to have and if that's a 191: # lower limit or a fixed value. 192: minArgs = 0 193: varArgs = false 194: cmd[:args].each do |arg| 195: # Arguments starting with '+' must have 1 or more showings. 196: # Arguments starting with '*' may show up 0 or more times. 197: minArgs += 1 unless '!*'.include?(arg[0]) 198: varArgs = true if '!*+'.include?(arg[0]) 199: end 200: return true if args.length - 1 >= minArgs 201: errorMessage = "Command #{args[0]} must have " + 202: "#{varArgs ? 'at least ' : ''}#{minArgs} " + 203: 'arguments' 204: end 205: end 206: end 207: 208: error(errorMessage) 209: end
# File lib/taskjuggler/apps/Tj3Client.rb, line 211 211: def connectDaemon 212: unless @authKey 213: $stderr.puts You must set an authentication key in the configuration file. Create a filenamed .taskjugglerrc or taskjuggler.rc that contains at least the followinglines. Replace 'your_secret_key' with some random character sequence._global: authKey: your_secret_key 214: end 215: 216: uri = "druby://#{@host}:#{@port}" 217: if @port == 0 218: # If the @port is configured to 0, we need to read the URI to connect 219: # to the server from the .tj3d.uri file that has been generated by the 220: # server. 221: begin 222: uri = File.read(@uriFile).chomp 223: rescue 224: error('The server port is configured to be 0, but no ' + 225: ".tj3d.uri file can be found: #{$!}") 226: end 227: end 228: 229: # We try to play it safe here. The client also starts a DRb server, so 230: # we need to make sure it's constricted to localhost only. We require 231: # the DRb server for the standard IO redirection to work. 232: $SAFE = 1 unless @unsafeMode 233: DRb.install_acl(ACL.new(] deny all 234: allow 127.0.0.1 ])) 235: DRb.start_service('druby://127.0.0.1:0') 236: 237: begin 238: # Get the ProjectBroker object from the tj3d. 239: @broker = DRbObject.new(nil, uri) 240: # Client and server should always come from the same Gem. Since we 241: # restict communication to localhost, that's probably not a problem. 242: if (check = @broker.apiVersion(@authKey, 1)) < 0 243: error('This client is too old for the server. Please ' + 244: 'upgrade to a more recent version of the software.') 245: elsif check == 0 246: error('Authentication failed. Please check your authentication ' + 247: 'key to match the server key.') 248: end 249: rescue 250: error("TaskJuggler server on host '#{@host}' port " + 251: "#{@port} is not responding") 252: end 253: end
# File lib/taskjuggler/apps/Tj3Client.rb, line 370 370: def connectToReportServer(projectId) 371: @ps_uri, @ps_authKey = callDaemon(:getProject, projectId) 372: if @ps_uri.nil? 373: error("No project with ID #{projectId} loaded") 374: end 375: begin 376: @projectServer = DRbObject.new(nil, @ps_uri) 377: @rs_uri, @rs_authKey = @projectServer.getReportServer(@ps_authKey) 378: @reportServer = DRbObject.new(nil, @rs_uri) 379: rescue 380: error("Cannot get report server") 381: end 382: begin 383: @reportServer.connect(@rs_authKey, $stdout, $stderr, $stdin, @silent) 384: rescue 385: error("Can't connect IO: #{$!}") 386: end 387: end
# File lib/taskjuggler/apps/Tj3Client.rb, line 262 262: def disconnectDaemon 263: @broker = nil 264: 265: DRb.stop_service 266: end
# File lib/taskjuggler/apps/Tj3Client.rb, line 389 389: def disconnectReportServer 390: begin 391: @reportServer.disconnect(@rs_authKey) 392: rescue 393: error("Can't disconnect IO: #{$!}") 394: end 395: begin 396: @reportServer.terminate(@rs_authKey) 397: rescue 398: error("Report server termination failed: #{$!}") 399: end 400: @reportServer = nil 401: @rs_uri = nil 402: @rs_authKey = nil 403: @projectServer = nil 404: @ps_uri = nil 405: @ps_authKey = nil 406: end
# File lib/taskjuggler/apps/Tj3Client.rb, line 459 459: def error(message, exitVal = 1) 460: $stderr.puts "ERROR: #{message}" 461: # Don't call exit in unsafe mode. Raise a StandardError instead. 462: raise TjRuntimeError 463: end
# File lib/taskjuggler/apps/Tj3Client.rb, line 268 268: def executeCommand(command, args) 269: case command 270: when 'status' 271: $stdout.puts callDaemon(:status, []) 272: when 'terminate' 273: callDaemon(:stop, []) 274: info('Daemon terminated') 275: when 'add' 276: res = callDaemon(:addProject, [ Dir.getwd, args, 277: $stdout, $stderr, $stdin, @silent ]) 278: info("Project(s) #{args.join(', ')} added") 279: return res ? 0 : 1 280: when 'remove' 281: args.each do |arg| 282: unless callDaemon(:removeProject, arg) 283: error("Project '#{arg}' not found in list") 284: end 285: end 286: info('Project removed') 287: when 'update' 288: callDaemon(:update, []) 289: info('Reload requested') 290: when 'report' 291: # The first value of args is the project ID. The following values 292: # could be either report IDs or TJI file # names ('.' or '*.tji'). 293: projectId = args.shift 294: # Ask the ProjectServer to launch a new ReportServer process and 295: # provide a DRbObject reference to it. 296: connectToReportServer(projectId) 297: 298: reportIds, tjiFiles = splitIdsAndFiles(args) 299: if reportIds.empty? 300: disconnectReportServer 301: error('You must provide at least one report ID') 302: end 303: # Send the provided .tji files to the ReportServer. 304: failed = !addFiles(tjiFiles) 305: # Ask the ReportServer to generate the reports with the provided IDs. 306: unless failed 307: reportIds.each do |reportId| 308: unless @reportServer.generateReport(@rs_authKey, reportId, 309: @regExpMode, @formats, nil) 310: failed = true 311: break 312: end 313: end 314: end 315: # Terminate the ReportServer 316: disconnectReportServer 317: return failed ? 1 : 0 318: when 'list-reports' 319: # The first value of args is the project ID. The following values 320: # could be either report IDs or TJI file # names ('.' or '*.tji'). 321: projectId = args.shift 322: # Ask the ProjectServer to launch a new ReportServer process and 323: # provide a DRbObject reference to it. 324: connectToReportServer(projectId) 325: 326: reportIds, tjiFiles = splitIdsAndFiles(args) 327: if reportIds.empty? 328: # If the user did not provide a report ID we generate a full list. 329: reportIds = [ '.*' ] 330: @regExpMode = true 331: end 332: # Send the provided .tji files to the ReportServer. 333: failed = !addFiles(tjiFiles) 334: # Ask the ReportServer to generate the reports with the provided IDs. 335: unless failed 336: reportIds.each do |reportId| 337: unless @reportServer.listReports(@rs_authKey, reportId, @regExpMode) 338: failed = true 339: break 340: end 341: end 342: end 343: # Terminate the ReportServer 344: disconnectReportServer 345: return failed ? 1 : 0 346: when 'check-ts' 347: connectToReportServer(args[0]) 348: begin 349: res = @reportServer.checkTimeSheet(@rs_authKey, args[1]) 350: rescue 351: error("Time sheet check failed: #{$!}") 352: end 353: disconnectReportServer 354: return res ? 0 : 1 355: when 'check-ss' 356: connectToReportServer(args[0]) 357: begin 358: res = @reportServer.checkStatusSheet(@rs_authKey, args[1]) 359: rescue 360: error("Status sheet check failed: #{$!}") 361: end 362: disconnectReportServer 363: return res ? 0 : 1 364: else 365: raise "Unknown command #{command}" 366: end 367: 0 368: end
# File lib/taskjuggler/apps/Tj3Client.rb, line 454 454: def info(message) 455: return if @silent 456: $stdout.puts "#{message}" 457: end
Sort the remaining arguments into a report ID and a TJI file list. If .tji files are present, they must be separated from the report ID list by a ’=’.
# File lib/taskjuggler/apps/Tj3Client.rb, line 422 422: def splitIdsAndFiles(args) 423: reportIds = [] 424: tjiFiles = [] 425: addToReports = true 426: args.each do |arg| 427: if arg == '=' 428: # Switch to tji file list. 429: addToReports = false 430: elsif addToReports 431: reportIds << arg 432: else 433: tjiFiles << arg 434: end 435: end 436: 437: [ reportIds, tjiFiles ] 438: end
Disabled; run with --debug to generate this.
Generated with the Darkfish Rdoc Generator 1.1.6.