smalltalk.ps1
author Jan Vrany <jan.vrany@fit.cvut.cz>
Mon, 24 Sep 2018 20:52:34 +0100
branchjv
changeset 1579 e6c2667b4692
parent 1554 43b993b853cf
permissions -rw-r--r--
Fix bug when install `smalltalkx.svg` and `smalltalkx.desktop` when running from toy archive

# This file inherits the licencing from the batch file (smalltalk.bat). (MIT)

# $executable        ... stx.com or .exe
# $log_file          ... log file name with path
# $log_file_encoding ... user defined encoding
# $append_to_log     ... should the log file be appended? (TRUE/FALSE)
# $cmd_close         ... sends either cmd /u /c (close cmd) or /u /k (cmd remains open)
# $PowerShellVersion ... detected powershell version
# $cmd_in_powershell ... should cmd.exe be used for stdout redirection? (TRUE/FALSE)
param($executable, $log_file, $log_file_encoding, $append_to_log, $cmd_close, $PowerShellVersion, $cmd_in_powershell, $stx_manual_switch_detected)


# ===========================================================================
# Reading directly from environment - due to the issues with passing quotes =
# ===========================================================================
# StX switches which are build during batch file execution $env:stx_switch
$stx_switch = [environment]::GetEnvironmentVariable("stx_switch")

# PowerShell version detected during batch file execution  $env:powershell_version_all_functionality
If (![String]::IsNullOrEmpty([environment]::GetEnvironmentVariable("powershell_version_all_functionality"))) {
    $stx_powershell_version = [environment]::GetEnvironmentVariable("powershell_version_all_functionality")
} Else {
    $host.ui.WriteErrorLine('[ERROR] Missing powershell detection variable -> powershell_version_all_functionality <-.')
    EXIT 1
}

# getting line width for the log file
If (![String]::IsNullOrEmpty([environment]::GetEnvironmentVariable("stx.__numeric.log_file_width"))) {
    $log_file_width = [environment]::GetEnvironmentVariable("stx.__numeric.log_file_width")
} Else {
    $host.ui.WriteErrorLine('[ERROR] Missing -> stx.__numeric.log_file_width <- from smalltalk.cfg file.')
    EXIT 1
}

# Defines start-sleep periods while running custom switches
If (![String]::IsNullOrEmpty([environment]::GetEnvironmentVariable("stx.__numeric.start_sleep_in_debug"))) {
    $start_sleep_period = [environment]::GetEnvironmentVariable("stx.__numeric.start_sleep_in_debug")
} Else {
    $host.ui.WriteErrorLine('[ERROR] Missing -> stx.__numeric.start_sleep_in_debug <- from smalltalk.cfg file.')
    EXIT 1
}


# =======================================================
# Adjust all variables to PowerShell style $true/$false =
# =======================================================

If ([environment]::GetEnvironmentVariable("stx.__binary.colored_stdout") -eq 'TRUE') {
    $use_color = $true
} Else {
    $use_color = $false
}

If ($cmd_in_powershell -eq 'TRUE') {
    $cmd_in_powershell = $true
} Else {
    $cmd_in_powershell = $false
}

If ($append_to_log -eq 'TRUE') {
    $append_to_log = $true
} Else {
    $append_to_log = $false
}

If ($stx_manual_switch_detected -eq 'TRUE') {
    $stx_manual_switch_detected = $true
} Else {
    $stx_manual_switch_detected = $false
}

If ($use_color) {
    # fastest way to init empty Hastable
    $saved_color = [System.Collections.Hashtable]@{}
    # save colors for later restore
    $window_private_data = (Get-Host).PrivateData
    $saved_color.Add('VerboseBackgroundColor', "$window_private_data.VerboseBackgroundColor") | Out-null; # Out-null for supressing the natural output
    $saved_color.Add('VerboseForegroundColor', "$window_private_data.VerboseForegroundColor") | Out-null
    $saved_color.Add('WarningBackgroundColor', "$window_private_data.WarningBackgroundColor") | Out-null
    $saved_color.Add('WarningForegroundColor', "$window_private_data.WarningForegroundColor") | Out-null
    $saved_color.Add('ErrorBackgroundColor', "$window_private_data.ErrorBackgroundColor") | Out-null
    $saved_color.Add('ErrorForegroundColor', "$window_private_data.ErrorForegroundColor") | Out-null
    #setting the user specified colors
    $window_private_data.VerboseBackgroundColor = [environment]::GetEnvironmentVariable("stx.stdout_VerboseBackgroundColor")
    $window_private_data.VerboseForegroundColor = [environment]::GetEnvironmentVariable("stx.stdout_VerboseForegroundColor")
    $window_private_data.WarningBackgroundColor = [environment]::GetEnvironmentVariable("stx.stdout_WarningBackgroundColor")
    $window_private_data.WarningForegroundColor = [environment]::GetEnvironmentVariable("stx.stdout_WarningForegroundColor")
    $window_private_data.ErrorBackgroundColor = [environment]::GetEnvironmentVariable("stx.stdout_ErrorBackgroundColor")
    $window_private_data.ErrorForegroundColor = [environment]::GetEnvironmentVariable("stx.stdout_ErrorForegroundColor")
} ElseIf ($cmd_in_powershell) {
    $window_private_data = (Get-Host).PrivateData
    $window_private_data.VerboseBackgroundColor = 'Black'
    $window_private_data.VerboseForegroundColor = 'Gray'
}


# ===========
# Functions =
# ===========

# Function for correct $LASTEXITCODE to ERRORLEVEL passing
function ExitWithCode {
    param (
        $exitcode
    )
    $host.SetShouldExit($exitcode)
    EXIT
} # end ExitWithCode

# To correctly write to stderr when launching from cmd.exe
# more at: https://stackoverflow.com/questions/4998173/how-do-i-write-to-standard-error-in-powershell/15669365#15669365

<#
 .SYNOPSIS
 Writes text to stderr when running in a regular console window,
 to the host''s error stream otherwise.
 
 .DESCRIPTION
 Writing to true stderr allows you to write a well-behaved CLI
 as a PS script that can be invoked from a batch file, for instance.
 
 Note that PS by default sends ALL its streams to *stdout* when invoked from 
 cmd.exe.
 
 This function acts similarly to Write-Host in that it simply calls
 .ToString() on its input; to get the default output format, invoke
 it via a pipeline and precede with Out-String.

#> 
# function Write-StdErr {
#     param (
#         [PSObject] $input_object
#     )
#     $out_function = If ($Host.Name -eq 'ConsoleHost') { 
#         [Console]::Error.WriteLine
#     } Else {
#         $host.ui.WriteErrorLine
#     }
#     If ($input_object) {
#         [void] $out_function.Invoke($input_object.ToString())
#     } Else {
#         [string[]] $lines = @()
#         $Input | % { $lines += $_.ToString() }
#         [void] $out_function.Invoke($lines -join "`r`n")
#     }
# }

# Print User message using String Array $message
function PrintMessage {
    param(
        [Parameter( `
            Mandatory=$True, `
            Valuefrompipeline = $true)]
        [String]$message
    )
    begin {}
    process {
        foreach ($Message in $Message) {
            # Write-Host Considered Harmful - see http://www.jsnover.com/blog/2013/12/07/write-host-considered-harmful/
            # first way how to correctly write it
            #Write-host $message
            # highlights warning and error messages from StX VM!
            If ($line -match '\[error\]' -or $line -match '\[sigsegv\]') {
                $host.ui.WriteErrorLine("$message")
            } ElseIf ($message -match '\[warn\]') {
                Write-Warning $message
            } Else {
                Write-Verbose -Message $message -Verbose
            } 
        }
    }
    end {}
} # end PrintMessage


# To correctly simultinously write to stdout and log file when using out-file!
# Colors the output based on string match
function Tee-Host {
    Param (
        [Parameter(Mandatory=$true,
        ValueFromPipeline=$true,
        Position=0)]
        $message
    )
    begin {}
    Process {
        # first way how to correctly write it
        # -EV is short (an alias) for -ErrorVariable
        If ($line -match '\[error\]' -or $line -match '\[sigsegv\]') {
            # this is printing error compatible with all PS versions
            $host.ui.WriteErrorLine("$message")
        } ElseIf ($message -match '\[warning\]') {
            Write-Warning "$message"
        } Else {
            Write-Verbose -Message "$message" -Verbose
        }
        # second correct way how to write it
        #$VerbosePreference = "Continue"
        #Write-Verbose $input_object; 
        return $message
    }
    end {}
} # end Tee-Host

# Synchronously (stdout & stderr) executes Smalltalk/X executable 
# stdout & stderr are redirected together via 2>&1 redirect
function InvokeCommandExecute {
    param(
       $execute_command
    )
    try {
       "`n", "[INFO] Executing command: $execute_command" | PrintMessage
       Invoke-Expression -Command:$execute_command
       If ($lastexitcode -ne 0) {
           $result = $result -join "`n"
           throw "$result `n"
       }
    }
    catch {
        $window_private_data = (Get-Host).PrivateData
        # saving the original colors
        $saved_background_color = $window_private_data.ErrorBackgroundColor
        $saved_foreground_color = $window_private_data.ErrorForegroundColor
        # setting the new colors
        $window_private_data.ErrorBackgroundColor = 'White'
        $window_private_data.ErrorForegroundColor = 'Red'

        $host.ui.WriteErrorLine("[ERROR] happned in stx.com or PowerShell script - See log file: $log_file for more information.")
        Write-Error "`n`n[ERROR] Error from PowerShell:`n`n $_" 2>&1 | Tee-Host | Out-File -Append -Encoding $log_file_encoding -FilePath $log_file -Width $log_file_width

        $window_private_data.ErrorBackgroundColor = $saved_background_color
        $window_private_data.ErrorForegroundColor = $saved_foreground_color
    }
} # end InvokeCommandExecute


# Asynchronously output stdout & stderr while executing Smalltalk/X executable
# Note: 
# 1) for now used only while having manual switches to smalltalk.bat
# 2) Is seems to be little bit slower than the Invoke-Expression, howerver Smalltalk/X GUI
#    appears sooner than all the messages are processed so the end speed should be comparable
# 3) for now used only when manual switches (inputed by user) are detected
# 4) Will output everything from stdout/stderr even when stx executable crashes or powershell encounters an error
#    (of course not closing the powershell window)

function DebugProcessExecute {
    param(
        [Parameter(Mandatory=$true, Position = 0)]
        [ValidateNotNullOrEmpty()]
        [String]$executable,
        [Parameter(Mandatory=$true, Position = 1)]
        $stx_switch,
        [Parameter(Mandatory=$true, Position = 2)]
        [String]$logging_function,
        [Parameter(Mandatory=$true, Position = 3)]
        [Int]$start_sleep_period,
        [Parameter(Mandatory=$false, Position = 4)]
        [String]$verb,
        [Parameter(Mandatory=$false, Position = 5)]
        [System.Diagnostics.ProcessPriorityClass]$Priority = [System.Diagnostics.ProcessPriorityClass]::Normal
    )
    begin {
        If ($log_file_encoding -eq 'ASCII') {
            $output_encoding = New-Object -typename System.Text.ASCIIEncoding
        } ElseIf ($log_file_encoding -eq 'UTF8') {
            $output_encoding = New-Object -typename System.Text.UTF8Encoding
        } ElseIf ($log_file_encoding -eq 'UTF16') {
            $output_encoding = New-Object -typename System.Text.UnicodeEncoding
        } ElseIf ($log_file_encoding -eq 'UTF32') {
            $output_encoding = New-Object -typename System.Text.UTF32Encoding
        }
    }
    process {
        try {
           "`n", "[INFO] Executing asynchronously command: $executable $stx_switch | $logging_function" | PrintMessage

           # Setting process invocation parameters.
           $process_start_info = New-Object -TypeName System.Diagnostics.ProcessStartInfo
           $process_start_info.CreateNoWindow = $true
           $process_start_info.UseShellExecute = $false
           $process_start_info.RedirectStandardOutput = $true
           $process_start_info.StandardOutputEncoding = $output_encoding
           $process_start_info.RedirectStandardError = $true
           $process_start_info.StandardErrorEncoding = $output_encoding
           $process_start_info.FileName = $executable
           $process_start_info.Arguments = $stx_switch
        
           If (![String]::IsNullOrEmpty($verb)) {
               $process_start_info.Verb = $verb
           }
        

           # Creating process object.
           $process = New-Object -TypeName System.Diagnostics.Process
           $process.StartInfo = $process_start_info

           $passing_variable = new-object psobject -property @{file_logging = $logging_function}

           # Asynchronously listen for OutputDataReceived events on the process (stdout)
           # Note: To use all events without ForEach loop use: $event.SourceEventArgs.Data variable
           $outEvent = Register-ObjectEvent -InputObj $process `
                           -Event "OutputDataReceived" `
                           -Action `
            {
                param
                (
                    [System.Object] $sender,
                    [System.Diagnostics.DataReceivedEventArgs] $events 
                )
                # Where to log
                $file_log = $Event.MessageData.file_logging

                ForEach  ($line in $events.data) {
                    Write-Verbose "[stdout]: $line" -Verbose
                   
                    # write to the log file
                    $exec_with_logging = @"
Write-Output '[stdout]: $line' | $file_log
"@
                    Invoke-Expression -Command:$exec_with_logging
                    
                }
            } -MessageData $passing_variable
            # /Out Listener - stdout


            # # asynchronously listen for ErrorDataReceived events on the process (stderr)
            $errEvent = Register-ObjectEvent -InputObj $process `
            -Event "ErrorDataReceived" `
            -Action `
            {
                param
                (
                    [System.Object] $sender,
                    [System.Diagnostics.DataReceivedEventArgs] $events
                ) 

                $file_log = $Event.MessageData.file_logging

                ForEach  ($line in $events.data) {

                    # $line = ("[stderr]:" + $line)
                    If ($line -match '\[error\]' -or $line -match '\[sigsegv\]') {
                        $host.ui.WriteErrorLine("$line")
                    } ElseIf ($line -match '\[warn\]') {
                        Write-Warning $line
                    } Else {
                        Write-Verbose -Message $line -Verbose
                    } 
 
                    # write to the log file
                    $exec_with_logging = @"
Write-Output '$line' | $file_log
"@
                    Invoke-Expression -Command:$exec_with_logging
                }
                
            } -MessageData $passing_variable
            #  Error Listener - stderr

           # Starting process with no return value
           [Void]$process.Start()
           $process.PriorityClass = $Priority

           # Begin async read events
           $process.BeginOutputReadLine()
           $process.BeginErrorReadLine()

           # loop till application exited 
           # used for logging into the stdout/stderr
           # Note: the timeout has been tested for speed vs. process usage -> 50ms does not use CPU that much + the output is reasonably fast
           while (!$process.HasExited) { 
               [System.Console]::Out.Flush()
               [System.Console]::Error.Flush()
               Start-Sleep -Milliseconds $start_sleep_period
           }

           $exit_code = $process.ExitCode

           If ($process.ExitCode -ne 0) {
               $result = $result -join "`n"
               throw "$result `n"
           }
        }
        catch {
            # The only reliable way to flush stderr is to produce an error message
            Write-Error -Message 'An error has occurred...'

            # If an error occurrences than wait till exit - all buffers are emptied
            [System.Console]::Out.Flush()
            [System.Console]::Error.Flush()
            $process.WaitForExit()
            $process.CancelOutputRead()
            $process.CancelErrorRead()

            $window_private_data = (Get-Host).PrivateData
            # saving the original colors
            $saved_background_color = $window_private_data.ErrorBackgroundColor
            $saved_foreground_color = $window_private_data.ErrorForegroundColor
            # setting the new colors
            $window_private_data.ErrorBackgroundColor = 'White'
            $window_private_data.ErrorForegroundColor = 'Red'
        
            $host.ui.WriteErrorLine("[ERROR] happened in stx.com or PowerShell script - See log file: $log_file for more information.")
            Write-Error "`n`n[ERROR] Error from PowerShell:`n`n $_" 2>&1 | Tee-Host | Out-File -Append -Encoding $log_file_encoding -FilePath $log_file -Width $log_file_width
        
            $window_private_data.ErrorBackgroundColor = $saved_background_color
            $window_private_data.ErrorForegroundColor = $saved_foreground_color
           
            # =======================================================
            # Add Catch section Separator when adding into the file =
            # =======================================================
            If (!$is_logfile_locked){
                If ($append_to_log) {
                    Write-Output "`r`n!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!`r`n" | Out-File -Append -Encoding $log_file_encoding -FilePath $log_file -Width $log_file_width
                }
            }
        }
        finally {
           # Unregister events
           If (($Null -ne $outEvent) -or ($Null -ne $errEvent)) {
               $outEvent.Name, $errEvent.Name |
               ForEach-Object {Unregister-Event -SourceIdentifier $_}
           }
        }
    }
    end {
        return $exit_code
    } 
} # end DebugProcessExecute


# =============================
# check if log file is locked =
# =============================
function Test-FileLock {
    param (
      [parameter(Mandatory=$true)][string]$path
    )
    $log_file = New-Object System.IO.FileInfo $path
    If ((Test-Path -Path $path) -eq $false) {
        return $false
    }

    try {
      $log_file_stream = $log_file.Open([System.IO.FileMode]::Open, [System.IO.FileAccess]::ReadWrite, [System.IO.FileShare]::None)
      If ($log_file_stream) {
          $log_file_stream.Close()
      }
      $false
    } catch {
      # file is locked by a process.
      return $true
    }
}


# ========================================================================
# Check if user did not start the PowerShell file directly - exit if yes =
# ========================================================================
If ([string]::IsNullOrEmpty($executable)) {
    "`n", '[ERROR] You can not run this powershell script directly!', 'Execute batch file (.bat) instead.' | PrintMessage
    EXIT 1
}


# ====================================
# Checking the state of the log file =
# ====================================
# Must be done only once in the file in case 
# the file is unlocked before the second instance is closed
$is_logfile_locked = Test-FileLock($log_file)


# ================
# Stdout logging =
# ================
If ($is_logfile_locked){
    "`n", "[WARN] Log file $log_file in use.`n`n  !!NO LOGGING will be available for this Smalltalk/X instance!!" | PrintMessage
    '[INFO] Press any key to continue ...' | PrintMessage
    # pause - waits for pressing any key
    $host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown") | Out-null
    If (!$cmd_in_powershell -and $stx_manual_switch_detected) {
        $logging_function = 'Out-null'
    } Else {
        $logging_function = 'Tee-Host | Out-null'
    }
} Else {
    If (!$cmd_in_powershell -and $stx_manual_switch_detected) {
        If ($append_to_log) {
            $logging_function = "Out-File -Append -Encoding '$log_file_encoding' -FilePath '$log_file' -Width $log_file_width"
        } Else {
            "`n", "[INFO] Creating a blank log file." | PrintMessage
            Write-Output $null | Out-File -Encoding $log_file_encoding -FilePath $log_file -Width $log_file_width
            $logging_function = "Out-File -Append -Encoding '$log_file_encoding' -FilePath '$log_file' -Width $log_file_width"
        }
    } Else {  
        If ($append_to_log) {
            $logging_function = "Tee-Host | Out-File -Append -Encoding $log_file_encoding -FilePath $log_file -Width $log_file_width"
        } Else {
            $logging_function = "Tee-Host | Out-File -Encoding $log_file_encoding -FilePath $log_file -Width $log_file_width"
        }
    }
}


# ===========
# Execution =
# ===========
# Decide which stdout use - either powershell.exe or cmd.exe
If ($PowerShellVersion -ge $stx_powershell_version) {
    # stdout output via powershell.exe
    If (!$cmd_in_powershell) {
        # Debug powershell mode started with stdout and stderr separation!
        If ($stx_manual_switch_detected) {
            # getting rid of all the double quotes around stx switches (were added in batch file)
            $stx_switch = $stx_switch -replace '"([-]+\w+)"', "`$1"

            $exit_code = DebugProcessExecute -executable $executable -stx_switch $stx_switch -logging_function $logging_function -start_sleep_period $start_sleep_period 

        } Else {
            $command = @"
$executable $stx_switch 2>&1 | $logging_function
"@
            # Due to the PowerShell bug produces FullyQualifiedErrorId : NativeCommandError in the log file -> you can ignore it.
            # actual execution
            InvokeCommandExecute -execute_command $command
       }
    # stdout output via cmd.exe
    # --% was introduced in PowerShell 3.0, forces PowerShell to ignore all code afterwards
    # redirection from 9 - combined (all output combined into a single - easy to redirect stream) from Powershell
    # if manual switch detected force powershell to ignore all code afterwards
    } Else {
        # check if manual switch detected
        If ($stx_manual_switch_detected) {
            # if manual switch detected force powershell to ignore all code afterwards
            $stx_switch = "--% $stx_switch"
            $command = @"
cmd.exe $cmd_close $executable $stx_switch '2^>^&1' ^| $logging_function
"@
        } Else {
            # must replace double quotes in order for the cmd.exe to work correctly - cmd.exe requirement
            $stx_switch = $stx_switch -replace '"','^"'
            $command = @"
cmd.exe $cmd_close $executable $stx_switch '2>&1' | $logging_function
"@
        }
        
        # Due to the PowerShell bug may produce an error: "FullyQualifiedErrorId : NativeCommandError" in the log file -> you can ignore it.
        # actual execution
        Write-verbose -message $command -verbose
        InvokeCommandExecute -execute_command $command
    } # end Else
} Else { # legacy powershell - manual switches available via Asynchronous mode
    # stdout output via powershell.exe
    If (!$cmd_in_powershell) {
        If ($stx_manual_switch_detected) {
            # getting rid of all the double quotes around stx switches (were added in batch file)
            $stx_switch = $stx_switch -replace '"([-]+\w+)"', "`$1"

            $exit_code = DebugProcessExecute -executable $executable -stx_switch $stx_switch -logging_function $logging_function -start_sleep_period $start_sleep_period 

        } Else {
            $command = @"
$executable $stx_switch 2>&1 | $logging_function
"@
            # Due to the PowerShell bug produces FullyQualifiedErrorId : NativeCommandError in the log file -> you can ignore it.
            # actual execution
            InvokeCommandExecute -execute_command $command
        }
    # stdout output via cmd.exe
    } Else {
        If ($stx_manual_switch_detected) {
            "`n", '[ERROR] You can not have -> __binary.cmd_in_powershell <- enabled and run manual switches with StX!' | PrintMessage
            EXIT 1
        } Else {
            # must replace double quotes in order for the cmd.exe to work correctly??
            $stx_switch = $stx_switch -replace '"','^"'
            $command = @"
cmd.exe $cmd_close $executable $stx_switch '2>&1' | $logging_function
"@
           # Due to the PowerShell bug produces FullyQualifiedErrorId : NativeCommandError in the log file -> you can ignore it.
           # actual execution
           InvokeCommandExecute -execute_command $command
        }
    }
} # end if


# ======
# Exit =
# ======
# Sending exit code to calling batch file
try {
   #If ($cmd_in_powershell -or !$stx_manual_switch_detected){
   If (![string]::IsNullOrEmpty($exit_code)){
      "`n", "[INFO] Exiting from PowerShell with code $exit_code " | PrintMessage
      ExitWithCode -exitcode $exit_code
   } Else {
      "`n", "[INFO] Exiting from PowerShell with code $LastExitCode " | PrintMessage
      ExitWithCode -exitcode $LastExitCode
   }
}
catch {
   $host.ui.WriteErrorLine("[ERROR] An error happend during exiting PowerShell.")
}
finally {
    # =========================================
    # Add Separator when adding into the file =
    # =========================================
    If (!$is_logfile_locked){
        If ($append_to_log) {
            Write-Output "`r`n=================================================================================================`r`n" | Out-File -Append -Encoding $log_file_encoding -FilePath $log_file -Width $log_file_width
        }
    }

    # ===============================
    # Restore original Shell colors =
    # ===============================
    If ($use_color) {
        $window_private_data = (Get-Host).PrivateData
        ForEach($item in $saved_color.GetEnumerator()) {
            Set-Variable -name "$window_private_data.$($item.Key)" -Value "$($item.Value)"
        }
    }
}