#!/usr/bin/env ruby -w
# encoding: UTF-8
#
# = ReportServer.rb -- The TaskJuggler III Project Management Software
#
# Copyright (c) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014
#               by Chris Schlaeger <cs@taskjuggler.org>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of version 2 of the GNU General Public License as
# published by the Free Software Foundation.
#

require 'taskjuggler/daemon/ProcessIntercom'
require 'taskjuggler/TjException'
require 'taskjuggler/TjTime'

class TaskJuggler

  class ReportServer

    include ProcessIntercom

    attr_reader :uri, :authKey

    def initialize(tj, logConsole = false)
      initIntercom

      @pid = nil
      @uri = nil

      # A reference to the TaskJuggler object that holds the project data.
      @tj = tj
      # This is set to the ID(s) of the reports that should be generated.
      @reportId = 'unknown'

      @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('rs_fork_failed', 'ReportServer fork failed')
      elsif @pid.nil?
        if logConsole
          # If the Broker wasn't daemonized, log stdout and stderr to PID
          # specific files.
          $stderr.reopen("tj3d.rs.#{$$}.stderr", 'w')
          $stdout.reopen("tj3d.rs.#{$$}.stdout", 'w')
        end
        begin
          # This is the child
          $SAFE = 1
          DRb.install_acl(ACL.new(%w[ deny all
                                      allow 127.0.0.1 ]))
          iFace = ReportServerIface.new(self)
          begin
            uri = DRb.start_service('druby://127.0.0.1:0', iFace).uri
            debug('', "Report server is listening on #{uri}")
          rescue
            error('rs_cannot_start_drb',
                  "ReportServer 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
          startWatchDog

          # Cleanup the DRb threads
          DRb.thread.join
          debug('', 'Report server terminated')
          exit 0
        rescue => exception
          # TjRuntimeError exceptions are simply passed through.
          if exception.is_a?(TjRuntimeError)
            raise TjRuntimeError, $!
          end

          error('rs_unexp_excp',
                "ReportServer caught unexpected exception: #{$!}")
        end
      else
        Process.detach(@pid)
        # This is the parent
        wr.close
        @uri = rd.read
        rd.close
      end
    end

    def ping
      @lastPing = TjTime.new
    end

    def addFile(file)
      begin
        @tj.parseFile(file, :reportPropertiesFile)
      rescue TjRuntimeError
        return false
      end
      restartTimer
      true
    end

    def generateReport(id, regExpMode, formats, dynamicAttributes)
      info('generating_report', "Generating report #{id}")
      startTime = Time.now
      @reportId = id
      begin
        if (ok = @tj.generateReport(id, regExpMode, formats, dynamicAttributes))
          info('report_id_generated',
               "Report #{id} generated in #{Time.now - startTime} seconds")
        else
          error('report_generation_failed', "Report generation of #{id} failed")
        end
      rescue TjRuntimeError
        return false
      end
      restartTimer
      ok
    end

    def listReports(id, regExpMode)
      info('listing_report_id', "Listing report #{id}")
      begin
        if (ok = @tj.listReports(id, regExpMode))
          debug('', "Report list for #{id} generated")
        else
          error('repor_list_comp_failed',
                "Report list compilation of #{id} failed")
        end
      rescue TjRuntimeError
        return false
      end
      restartTimer
      ok
    end

    def checkTimeSheet(sheet)
      info('check_time_sheet', "Checking time sheet #{sheet}")
      @reportId = 'timesheet'
      begin
        ok = @tj.checkTimeSheet(sheet)
        debug('', "Time sheet #{sheet} is #{ok ? '' : 'not '}ok")
      rescue TjRuntimeError
        return false
      end
      restartTimer
      ok
    end

    def checkStatusSheet(sheet)
      info('check_status_sheet', "Checking status sheet #{sheet}")
      @reportId = 'statussheet'
      begin
        ok = @tj.checkStatusSheet(sheet)
        debug('', "Status sheet #{sheet} is #{ok ? '' : 'not '}ok")
      rescue TjRuntimeError
        return false
      end
      restartTimer
      ok
    end

    private

    def startWatchDog
      Thread.new do
        loop do
          if TjTime.new - @lastPing > 120
            # Since the abort via error() is not thread safe, we issue a
            # warning and abort manually.
            warning('ps_heartbeat_lost',
                    "Report server (Project #{@tj.project['projectid']} " +
                    "report #{@reportId}) lost heartbeat " +
                    'from ProjectServer. Terminating.')
            exit 1
          end
          sleep 30
        end
      end
    end

  end

  class ReportServerIface

    include ProcessIntercomIface

    def initialize(server)
      @server = server
    end

    def ping(authKey)
      return false unless @server.checkKey(authKey, 'addFile')

      trap { @server.ping }
    end

    def addFile(authKey, file)
      return false unless @server.checkKey(authKey, 'addFile')

      trap { @server.addFile(file) }
    end

    def generateReport(authKey, reportId, regExpMode, formats,
                       dynamicAttributes)
      return false unless @server.checkKey(authKey, 'generateReport')

      trap do
        @server.generateReport(reportId, regExpMode, formats, dynamicAttributes)
      end
    end


    def listReports(authKey, reportId, regExpMode)
      return false unless @server.checkKey(authKey, 'generateReport')

      trap { @server.listReports(reportId, regExpMode) }
    end

    def checkTimeSheet(authKey, sheet)
      return false unless @server.checkKey(authKey, 'checkTimeSheet')

      trap { @server.checkTimeSheet(sheet) }
    end

    def checkStatusSheet(authKey, sheet)
      return false unless @server.checkKey(authKey, 'checkStatusSheet')

      trap { @server.checkStatusSheet(sheet) }
    end

  end

end

