#Requires -Version 5.1

<#PSScriptInfo
#Requires -RunAsAdministrator

    .VERSION 1.1
    .GUID frdepo13-10b0-481d-a26c-389f4cbaa31z
    .AUTHOR Sage X3 R&D
    .COMPANYNAME Sage
    .COPYRIGHT (c) Copyright SAGE 2006-2025. All Rights Reserved.
    .TAGS MSWindows, Linux
    .LICENSEURI
    .PROJECTURI
    .ICONURI
    .EXTERNALMODULEDEPENDENCIES
    .REQUIREDSCRIPTS beforepacks.ps1
    .EXTERNALSCRIPTDEPENDENCIES Powershell-5.1
    .RELEASENOTES
    .PRIVATEDATA
    .SYNOPSIS
       Prepare Runtime Update
    .DESCRIPTION      
      beforepacks.ps1: PowerShell Script to prepare Runtime Update
    Manages processes and logs activities, likely in the context of updating or installing Sage X3 AdxAdmin/Runtime. 
    If a process does not stop within a certain timeout, the script logs a warning message indicating that the process may still be running. 
    If an error occurs during the attempt to stop the process, it is caught and logged as well. 
    Regardless of the outcome, the script logs that the process has been stopped, with an error level of 0 to indicate a normal event.

     .PARAMETER InstallPath
     -InstallPath [ex: "C:\Sage\SafeX3\ADXADMIN"]
     -InstallPath [ex: "C:\Sage\LOCAL96\runtime"]
     .PARAMETER IsDebug
     -IsDebug 0|1 (default is 0)
     0: No debug information
     1: Debug information

    .EXAMPLE
     Powershell.exe -executionpolicy remotesigned -File  .\beforepacks.ps1 -InstallPath "C:\Sage\SafeX3\ADXADMIN"

     .NOTES
    Author: Franck DEPOORTERE
    Date:   Nov 01, 2024
    Version: 1.1
    Changelog:
    - (X3-346733): Adxadmin setup blocked by 96.3.133 update - Use Stop-RuntimeProcess function
    #>

param( 
    [Parameter(Mandatory = $true)] [string] $InstallPath,
    [Parameter(Mandatory = $false)] [int] $IsDebug = 0
)

function Get-Formatted-Date() {
    <#
        .SYNOPSIS
            Date formating for Log trace
        .INPUTS
            None
        .OUTPUTS
            Date in a specific Format, such as  2020_09_28_08_36_59_123
    #>
    return Get-Date -Format yyyy_MM_dd_HH_mm_ss_fff
}

function Show-Usage() {
    Add-To-Log -line "Usage: beforepacks.ps1 -InstallPath c:\\Sage\\XXX"
}


function Add-To-Log {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)] [string] $line,
        [Parameter(Mandatory = $false)] [int] $errorLevel
    )
    $prefix = "[INFO]"
    # Error
    if ($errorLevel -eq 1) {
        $prefix = "[ERROR]"
        Write-Error $line
    }
    else {
        Write-Host $line
    }

    if (-Not ([string]::IsNullOrEmpty($SCRIPT_LOGDAT)) ) {
        # Add-Content -Value $line -Path $SCRIPT_LOGDAT
        $timeStampString = (Get-Date).ToString("yyyy-MM-dd HH:mm:ss")
        [System.IO.File]::AppendAllText($SCRIPT_LOGDAT, ($timeStampString + "`t" + $prefix + "`t" + $line + [Environment]::NewLine))
    }
}


function Invoke-CommandProcess {
    <#
    .SYNOPSIS
    Launch a command with Linux Shell binary (/bin/sh)
        
    .PARAMETER command
    Command to execute. Ex: "cp file1.txt file2.txt"
    
    .PARAMETER sudoer
    Execute the command with root user.
    #>

    [CmdletBinding()]

    param(
        [Parameter(Mandatory = $true, HelpMessage = "Command to execute")][string]$command,
        [Parameter(Mandatory = $false)] [string]$hideOccurence,
        [Parameter(Mandatory = $false)] [int]$timeoutSec = 7200,
        [Parameter(Mandatory = $false)] [bool]$sudoer
    )

    $proc = New-Object System.Diagnostics.Process
    $stdout = New-Object System.Text.StringBuilder
    $stderr = New-Object System.Text.StringBuilder
    $stdoutEvent = $null
    $stderrEvent = $null
    [string]$errorMessage = $null

    try {
        $pinfo = New-Object System.Diagnostics.ProcessStartInfo        
        if ($IsLinux) {
            $pinfo.FileName = "/bin/sh"
            if ($sudoer -eq $true) {
                $pinfo.UserName = "root"
            }
            $command = $command.Replace("""", "'")
            $pinfo.Arguments = "-c ""$command"" "
        }
        else {
            $pinfo.FileName = "cmd.exe"
            $command = $command.Replace("'", """")
            $pinfo.Arguments = "/c $command ";
        }
        $pinfo.RedirectStandardError = $true
        $pinfo.RedirectStandardOutput = $true
        $pinfo.UseShellExecute = $false
        $pinfo.WorkingDirectory = $PSScriptRoot 
        $pinfo.CreateNoWindow = $true

        $proc.StartInfo = $pinfo
        # deadlocks due to the synchronous call to ReadToEnd(),
        #https://stackoverflow.com/questions/8761888/capturing-standard-out-and-error-with-start-process
        $stdoutEvent = Register-ObjectEvent $proc -EventName OutputDataReceived -MessageData $stdout -Action {
            $Event.MessageData.AppendLine($Event.SourceEventArgs.Data)
        }
          
        $stderrEvent = Register-ObjectEvent $proc -EventName ErrorDataReceived -MessageData $stderr -Action {
            $Event.MessageData.AppendLine($Event.SourceEventArgs.Data)
        }

        $proc.Start() | Out-Null  #  workaround to avoid $null value
        $proc.BeginOutputReadLine()
        $proc.BeginErrorReadLine()
        Wait-Process -Id $proc.Id -TimeoutSec $timeoutSec # default timeout is 2 hours

        if ($proc.HasExited) {
            $exitCode = $proc.ExitCode
        }
        else {
            Stop-Process -Force -Id $proc.Id
            $exitCode = -1
        }

        # Be sure to unregister.  You have been warned.
        Unregister-Event $stdoutEvent.Id
        Unregister-Event $stderrEvent.Id        
        $errorMessage = ($stderr.ToString()).Trim()
        $output = ($stdout.ToString()).Trim()
        # Check if the command succeeded
        if (($null -ne $exitCode) -and ($exitCode -eq 0)) {
            if (-Not [string]::IsNullOrEmpty($hideOccurence)) {
                $command = $command.Replace($hideOccurence, "<hidden>")
            }

            Add-To-Log -Message "Command succeeded: $command"  -LogMessageType Debug -WriteToOutput $false
            Add-To-Log -Message "Output: $output" -LogMessageType Debug -WriteToOutput $false
            # Sometimes, the output is located on the $errorMessage  
            Add-To-Log -Message "Message: $errorMessage" -LogMessageType Debug -WriteToOutput $false
        }
        else {
            # Write-Host "Command failed with exit code $($proc.ExitCode)  Command: $command"
            if (-not [string]::IsNullOrEmpty($output)) {
                $errorMessage = "$errorMessage `n $($output)"
            }

            if (-not ([string]::IsNullOrWhiteSpace( $errorMessage) ) ) {
                switch ($ErrorAction) {
                    Stop { 
                        throw $errorMessage
                    }
                    Default {}
                }
            } 
        }
    }
    catch {
        if (-Not [string]::IsNullOrEmpty($hideOccurence)) {
            $command = $command.Replace($hideOccurence, "<hidden>")
        }

        Add-To-Log -Message "Error while running cmd $command Error:$($_.Exception.Message)" -LogMessageType Error -WriteToOutput $true

        if ($null -ne $stdoutEvent) {
            Unregister-Event $stdoutEvent.Id
        }
        if ($null -ne $stderrEvent) {
            Unregister-Event $stderrEvent.Id        
        }
        $errorMessage = ($stderr.ToString()).Trim()
        $output = ($stdout.ToString()).Trim()

        if ((-not [string]::IsNullOrEmpty($errorMessage)) -or (-not [string]::IsNullOrEmpty($output))  ) {
            Add-To-Log -Message "$($output) `n $errorMessage" -LogMessageType Error -WriteToOutput $true
        }
        Add-To-Log -Message $_.ScriptStackTrace  -LogMessageType Error -WriteToOutput $false
        Add-To-Log -Message $_.ErrorDetails -LogMessageType Error -WriteToOutput $false
        
        switch ($ErrorAction) {
            Stop { 
                throw
            }
            Default {}
        }
    }

    return   [pscustomobject]@{
        ExitCode = $exitCode
        stdout   = $stdout.ToString()
        stderr   = $errorMessage
    }
}



function Scan-AdxProcesses {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)] [string] $ProcessName,
        [Parameter(Mandatory = $true)] [string] $StartWithPath
    )

    Add-To-Log -line "Check running process(es) '$ProcessName' ..." -errorLevel 0
    
    $isRuningPowerShell7 = ($PSVersionTable.PSVersion.Major -ge 7)
    $runningWinPowerShell5x = -not $IsLinux -and -not $isRuningPowerShell7
    $pList = $null
    # MS Windows + PowerShell 5.x
    if ($runningWinPowerShell5x) {
        $ProcessName = "$($ProcessName).exe"        
        $pList = Get-CimInstance Win32_Process -Filter "name = '$ProcessName'"  -ErrorAction SilentlyContinue | Select-Object Name, ProcessId, CommandLine    
    }
    else {
        $pList = Get-Process -Name $ProcessName -ErrorAction SilentlyContinue |  Select-Object Name, Id, CommandLine
    }
  
    if ($null -eq $pList) {
        Add-To-Log -line "No running process(es) '$ProcessName' found" -errorLevel 0
    }
    elseif ( $pList.GetType().Name -eq "Object[]") {
        $i = 0
        foreach ($pp in $pList) {
            
            if ($null -eq $pp) {
                continue
            }
            $processId = -1
            if ($runningWinPowerShell5x) {
                $processId = $pp.ProcessId
            }
            else {
                $processId = $pp.Id
            }

            Check-AdxProcessItem  -processName $ProcessName -commandLine $pp.CommandLine -processId $processId -startWithPath $StartWithPath -index $i           
            $i++    
        }
    }
    elseif ($pList) {
        Add-To-Log -line "Checking pList process '$ProcessName': $pList" -errorLevel 0
        if ($runningWinPowerShell5x) {
            $processId = $pList.ProcessId
        }
        else {
            $processId = $pList.Id
        }
        Check-AdxProcessItem  -processName $ProcessName -commandLine $pList.CommandLine -processId $processId -startWithPath $StartWithPath

    }
   
} 

function Check-AdxProcessItem {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)] [string] $processName,
        [Parameter(Mandatory = $false)] [AllowNull()] [string] $commandLine,
        [Parameter(Mandatory = $true)] [int] $processId,
        [Parameter(Mandatory = $true)] [string] $StartWithPath,
        [Parameter(Mandatory = $false)] [string] $index
    )
    if ($IsDebug -eq 1) {
        Add-To-Log -line "$index - Checking process '$processName'  Id=$processId  commandLine=$commandLine" -errorLevel 0
    }
    if ( -Not ([string]::IsNullOrEmpty($commandLine)) -and ($commandLine.StartsWith($StartWithPath, [System.StringComparison]::InvariantCultureIgnoreCase))) {
        Add-To-Log -line "$index - Killing process '$processName'  Id=$processId  CommandLine=$commandLine" -errorLevel 0
        Stop-RuntimeProcess -processId $processId -processName $processName
        Add-To-Log -line "$index - process '$processName' Id=$processId killed" -errorLevel 0
    }
    else {
        if ($IsDebug -eq 1) {
            Add-To-Log -line "$index - Skip process '$processName'  Id=$processId  commandLine=$commandLine" -errorLevel 0
        }
    }
}
 

function Stop-RuntimeProcess {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)] [int] $processId,
        [Parameter(Mandatory = $true)] [string] $processName
    )
    Add-To-Log -line "Stopping '$processName' process $processId ..." -errorLevel 0
    $timeoutSeconds = 5 
    try {
        # Attempt to stop process
        Stop-Process -Id $processId -Force -ErrorAction SilentlyContinue

        # Wait up to $timeoutSeconds for the process to exit
        if (Wait-Process -Id $processId -Timeout $timeoutSeconds -ErrorAction SilentlyContinue) {
            Add-To-Log -line "Process $processId ($processName) stopped within $timeoutSeconds seconds."
        }
        else {
            Add-To-Log -line "Timeout reached. Process $processId ($processName) may still be running."
        }
    }
    catch {
        Add-To-Log -line "Error stopping process: $_"
    }
    Add-To-Log -line "'$processName' process $processId stopped." -errorLevel 0
}



<###########################################################
#   MAIN SCRIPT ENTRY POINT                               #
###########################################################>
try {

    $LOGDAT = Get-Formatted-Date
    $SCRIPT_LOG_BASE = "Beforepacks-Installation" 
    $SCRIPT_LOGDAT = $InstallPath | Join-Path -ChildPath "logs" | Join-Path -ChildPath "$SCRIPT_LOG_BASE-$LOGDAT.log"
    Write-Host "Log file generated will be: $SCRIPT_LOGDAT - Please analyze content after process"
    Set-Content -Value "Start of $SCRIPT_LOG_BASE - $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')" -Path $SCRIPT_LOGDAT 

    Add-To-Log -line "Initialization update Sage X3 AdxAdmin/Runtime ... "   -errorLevel 0
    Add-To-Log -line "Platform: $([System.Environment]::OSVersion.Platform)"  -errorLevel 0    
    Add-To-Log -line "PowerShell: $($PSVersionTable.PSVersion)" -errorLevel 0 # Get the PowerShell version

    Add-To-Log -line "Check Sage X3 processes"  -errorLevel 0

    # Linux:
    #
    # pkill -f sadfsq
    # pkill -f adxdsrv
    # pkill -f adxd

    # MS Windows:
    #
    # taskkill /im sadfsq.exe /f /t
    # taskkill /im adxdsrv.exe /f /t
    # taskkill /im adxd.exe /f /t

    Scan-AdxProcesses -ProcessName "sadfsq"  -StartWithPath $InstallPath
    Scan-AdxProcesses -ProcessName "adxdsrv"  -StartWithPath $InstallPath
    Scan-AdxProcesses -ProcessName "adxd"  -StartWithPath $InstallPath

    Add-To-Log -line  "Done - $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')" -errorLevel 0
}
catch {

    if ($null -ne $SCRIPT_LOGDAT) {
        Add-To-Log -line ($Error[0] | Out-String)  -errorLevel 1
        Add-To-Log -line "Stack Trace: " -file $SCRIPT_LOGDAT -errorLevel 0
        Add-To-Log -line ($PSItem.ScriptStackTrace | Out-String) -file $SCRIPT_LOGDAT -errorLevel 0
        Add-To-Log -line " " -file $SCRIPT_LOGDAT -errorLevel 2
    }
    else {
        throw $Error[0]
    }
    $Error.Clear()
}