#
# Copyright (c) 2015-2018, Inversoft Inc., All Rights Reserved
#
# General Purpose PowerShell script to read in properties and run something as a service (RSaaS).
#
# @author Daniel DeGroff
# @version 1.0.0
# @version 1.1.0 - Removed configuration file processing, pushed back into *.bat files
#                  such that running catalina.bat, or elasticsearch.bat works properly.
#                - Create logging directories as needed
# @version 1.1.1 - Fix Service type detection
# @version 1.1.2 - Updated instructions only. Functionally the same as 1.1.1.
# @version 1.1.3 - Updated instructions only. Functionally the same as 1.1.2.
# @version 1.1.4 - Updated instructions only. Functionally the same as 1.1.3.
# @version 1.1.5 - Updated instructions only. Functionally the same as 1.1.4.
# @version 1.1.6 - The name is now pulled from the service script name.
# @version 1.1.7 - Assume the correct child process is the first Java process.
# -------------------------------------------------------------------------------------------------
# Instruction for use:
#
# 1. Copy this part into your project as part of your build
#    Most likely into the service bin directory
#
# 2. By convention, the version you copy must be named the same as the corresponding batch file
#    Example: If you wish to run the 'GatherForum.bat' as a service, name this 'GatherForum.ps1'
#
#    Except for Tomcat and ElasticSearch, we detect Tomcat by looking for catalina.bat, and ElasticSearch by
#    looking for elasticsearch.bat. If one of those two types of services, we'll call those bat files and the
#    PowerShell script may be named whatever you'd like.
#
# 3. Create the *.exe file
#
#    Gather Forum Example:
#    > New-SelfHostedPS -SourceFile .\GatherForum.ps1 -Service -ServiceName GatherForum
#                                       -ServiceDisplayName "Gather Forum"
#                                       -ServiceDescription "Gather Forum"
#                                       -Version 1.1.5
#
# 4. Register the service, replace with the appropriate *.exe file
#   > ./GatherForum.exe /install
#
# 5. Unregister the service
#  > ./GatherForum.exe /uninstall
#
# -------------------------------------------------------------------------------------------------

# Global Variables
$global:command = $null
$global:commandHome = $null
$global:startBatchCommand = $null
$global:stopBatchCommand = $null
$global:isTomcat = $null
$global:isElastic = $null
$global:serviceName = $null

# Default Window Style
$global:windowStyle = "Hidden"

# Main function - Required by self-hosted PowerShell exe generated by New-SelfHostedPS provided by Daniel Sörlöv
function Main() {

  setupGlobalVariables
  $process = $null

  try {

    while($true) {

      # Get Application Name
      $appName = $global:serviceName.ToLower()

      # Exit if the expected start command is not found.
      If (-Not (Test-Path $global:startBatchCommand)) {
        log "Error. Expected command not found. [$($global:startBatchCommand)]"
        Exit 1
      }

      # Run the bat file in a new process.
      If ($global:isTomcat) {
        log "  Service type [Tomcat]"
        # Set CATALINA_HOME, this needs to be set here because catalina.bat won't detect it properly when running as a service.
        [Environment]::SetEnvironmentVariable("CATALINA_HOME", $global:commandHome, "Process")
        log "  Set Process-Level Environment Variable : CATALINA_HOME=$([Environment]::GetEnvironmentVariable('CATALINA_HOME', 'Process'))"
        $process = startProcess $global:startBatchCommand "run" $appName
      } Else {
        If ($global:isElastic) {
          log " Service type [Elastic]"
        } Else {
          log " Service type [$($global:serviceName)]"
        }
        # Do not pass a $null or empty string as the argument, it will not end well for you.
        $process = startProcess $global:startBatchCommand " " $appName
      }

      log "  Process Path [$($process.path)], Process Id [$($process.Id)]"

      # Sleep long enough so we can reliably get the child process
      Start-Sleep -m 2000
      $indent = " "

      # Look for the last child process, this is somewhat naive, we are just looking for the first java process.
      #  - If the process has children, continue to recurse.
      While ($childProcesses = Get-WmiObject Win32_Process -Filter "ParentProcessId = $($process.Id)" | Select ProcessId) {
        # The initial process may have launched more than one child process.
        ForEach ($child In $childProcesses) {
          $childProcess = Get-Process -Id $child.ProcessId
          log "  $($indent)Child Process [$($childProcess.path)], Process Id [$($childProcess.Id)]"
          If ($childProcess.ProcessName -eq "java") {
            $process = $childProcess
            break
          }
        }
        $indent += " "
      }

      log "Process started. Main process id [$($process.Id)]"
      Start-Sleep -m 2000

      # Once we've started the process, hang out here until we're done.
      while ($process -And !$process.HasExited) {
        $process = Get-Process -Id $process.Id -ErrorAction Ignore
        Start-Sleep -m 7000
      }
      log "Process [$($process.Id)] has ended. Exit."
      Exit
    }

  } finally {
    # Stop service
    log "Service is stopping, stop our process [$($serviceName)] using process id [$($process.Id)]"
    try {
      If ($process -And !$process.HasExited) {
        # If a stop command has been defined use, otherwise kill the process using Stop-Process
        If ($global:stopBatchCommand) {
          If ($global.isTomcat) {
            $stopProcess = startProcess $global:stopBatchCommand "stop" $appName
          } Else {
            # Do not pass a $null or empty string as the argument, it will not end well for you.
            $stopProcess = startProcess $global:stopBatchCommand " " $appName
          }
          Wait-Process -Id $stopProcess.Id -Timeout 10
          # If we still have not exited the main process, call Stop-Process
          If (!$process.HasExited) {
            Stop-Process $process.Id
          }
        } Else {
          Stop-Process $process.Id
        }
        log "Successfully stopped"
      } Else {
        log "Process found to be already stopped."
      }
    } catch {
      log "Failed to stop. Exiting. You may need to manually kill the process."
    }
  }
}

function startProcess {
  Param([string]$command, [string]$arguments, [string]$appName)
  If ($global:windowStyle.equals("Hidden")) {
    return Start-Process $command $arguments `
                 -PassThru `
                 -RedirectStandardError "$([Environment]::GetEnvironmentVariable('INVERSOFT_LOG_DIR', 'Process'))\$($appName)_stderr.log" `
                 -RedirectStandardOutput "$([Environment]::GetEnvironmentVariable('INVERSOFT_LOG_DIR', 'Process'))\$($appName)_stdout.log" `
                 -WindowStyle $global:windowStyle
  } Else {
    return Start-Process $command $arguments `
                 -PassThru `
                 -WindowStyle $global:windowStyle
  }
}

function setupGlobalVariables {

  $global:command = [Environment]::GetCommandLineArgs()
  # If running interactive in PS, use $PSCommandPath
  If ($global:command -like "*PowerShell*") {
    $global:command = $PSCommandPath
  }

  $global:commandHome = (Get-Item $command).Directory.Parent.FullName
  $global:serviceName = (Get-Item $command).Basename

  $global:isTomcat = isTomcatService $global:commandHome
  $global:isElastic = isElasticService $global:commandHome

  # Use specific known commands for Tomcat and Elastic, else assume we'll find a .bat file
  # by the same name as this PowerShell script. i.e. FooService.ps1 --> FooService.bat
  If ($global:isTomcat) {
    $global:startBatchCommand = "$($commandHome)\bin\catalina.bat"
    $global:stopBatchCommand = "$($commandHome)\bin\catalina.bat"
  } ElseIf ($global:isElastic) {
    $global:startBatchCommand = "$($commandHome)\bin\elasticsearch.bat"
  } Else {
    $global:startBatchCommand = "$($commandHome)\bin\$($global:serviceName).bat"
  }

  # Create the service logs directory if it does not exist
  If (-Not (Test-Path "$($global:commandHome)\logs")) {
    New-Item -ItemType Directory -Path "$($global:commandHome)\logs"
    log "  Creating log directory : $($global:commandHome)\logs"
  } else {
    log "  Log directory : $($global:commandHome)\logs exists."
  }

  # Create Inversoft Log directory if it does not exist and set INVERSOFT_LOG_DIR
  #  This may also be set in a *.bat file, but it is required here so that we can log the PowerShell output
  If (-Not (Test-Path "$($global:commandHome)\..\..\logs")) {
    New-Item -ItemType Directory -Path "$($global:commandHome)\..\..\logs"
    log "  Creating log directory : $($global:commandHome)\..\..\logs"
  } else {
    log "  Log directory : $($global:commandHome)\..\..\logs exists"
  }
  [Environment]::SetEnvironmentVariable("INVERSOFT_LOG_DIR", "$($global:commandHome)\..\..\logs", "Process")
  log "  Set Process-Level Environment Variable : INVERSOFT_LOG_DIR=$([Environment]::GetEnvironmentVariable('INVERSOFT_LOG_DIR', 'Process'))"

  log "Service parameters:"
  log "  Command Home [$($global:commandHome)]"
  log "  Service Name [$($global:serviceName)]"
  log "  Start Batch Command [$($global:startBatchCommand)]"
  log "  Stop Batch Command [$($global:stopBatchCommand)]"
}

# Is this Apache Tomcat
function isTomcatService {
  Param([string]$path)

  return Test-Path "$($path)\bin\catalina.bat"
}

# Is this ElasticSearch
function isElasticService {
  Param([string]$path)

  return Test-Path "$($path)\bin\elasticsearch.bat"
}

# Log messages to an out file
function log {
  Param([string]$message)

  $(Get-Date -f o) + " $($message)" | Out-File "$($global:commandHome)\logs\$($global:serviceName).log" -Append
  Write-Verbose -Message $message
}

# Take arguments when run interactively in PowerShell
#
#  run - Manually run this w/out running the service. Probably just for testing.
#  start - Start the service, assumes the service has been installed already
#  stop - Stop the service, assumes the service has been installed already
#  status - Retrieve the current status of the service, assumes the service has been installed already
#  startup
#    manual - Change Service Startup Type to Manual
#    automatic - Change Service Startup Type to Automatic
If ($args.Length -gt 0) {
  If ($args[0].equals("run")) {
    $global:windowStyle = "Normal"
    Main
  } ElseIf ($args[0].equals("start")) {
    setupGlobalVariables
    log "Start requested for service $($global:serviceName)"
    Start-Service -Name $global:serviceName
  } ElseIf ($args[0].equals("stop")) {
    setupGlobalVariables
    log "Stop requested for service $($global:serviceName)"
    Stop-Service $global:serviceName
  } ElseIf ($args[0].equals("status")) {
    setupGlobalVariables
    $status = (Get-Service -Name $global:serviceName).Status
    log "Status of $($global:serviceName) : $($status)"
  } ElseIf ($args[0].equals("startup")) {
    setupGlobalVariables
    If ($args[1].equals("manual")) {
      log "Change startup mode for $($global:serviceName) to Manual"
      Set-Service -Name $global:serviceName -StartupType Manual
    } ElseIf ($args[1].equals("automatic")) {
      log "Change startup mode for $($global:serviceName) to Automatic"
      Set-Service -Name $global:serviceName -StartupType Automatic
    }
  }
}
