1

I'm in a bit of a bind... I'm trying to use HTA as a GUI to run some command line utilities in a more modern way...except it's not quite going as I'd hoped.

So far I've tried a few tricks to get what I want done, but each one is falling a bit short of what I'm aiming for.

My goal is to have my script run an exe that executes as a command line tool and capture the output in realtime, parse each line as it happens, and show something in the HTA window to indicate what the tool is doing behind the scenes, such as a progress bar or some kind of fancy formatted log output, rather than the console text of the cmd window.

The trouble is, the only way I've seen to get HTA to run another program is either the .Run or .Exec functions of WScript.Shell. Each has its own headache attached that keeps my GUI from doing its job. .Exec seemed to be what I wanted at first, since my intention is to capture the stdout from the command line and parse each line as it prints, but when I tried to use it, I noticed a big blank CMD window appearing and blocking most of the screen until the exe finished running. Yes, the HTA captured the output and responded as I was hoping, but that blank CMD window kinda defeats the purpose of making a GUI. I tried .Run, but while that lets me run the exe in a hidden window, I can't capture the stdout in realtime and at best I can only dump it into a txt file and try to read it after the fact.

Needless to say, neither is quite what I'm looking for...

As a real world(ish) example of what I'd be looking to create, imagine running an HTA that runs ping with like 10 pings. I want to show a progress bar to show how many pings have completed, with some text showing an average total of each ping as they're happening, like how many pings were successful vs timed out, what the average ms per hit was, and so on. Then when the ping tool finishes running, the HTA would remove the progress bar and show the final totals for the ping test. With .Exec, this technically works, but the entire time I have a command window sitting somewhere on the screen, which is annoying and usually blocks the HTA. With .Run, the HTA just appears to hang while the 10 pings run, and only gets the returned error code of 0, and then I have to load up a text file to see what happened before finally showing the results in the HTA...definitely not the ideal result.

I feel like I'm missing something important... how can I get an HTA to run a command line executable in a hidden window while still being able to capture the output line by line in realtime? (I don't mind semi-realtime with JScript setTimeout or similar) I want to avoid having the command prompt window appearing at all, and I want to be able to show the output in the GUI as it's happening, rather than having to wait for it to complete and waste time writing a file, reading it, then deleting it.

Ceetch
  • 177
  • 4
  • 17
  • Check [this WSH VBS GUI](https://stackoverflow.com/a/47111556/2165759) solution, and [this console window hiding](https://stackoverflow.com/a/32302212/2165759) method. – omegastripes Nov 09 '17 at 15:22

1 Answers1

1

If you start a Exec operation from a cscript running inside a hidden console, the started process will be also hidden. But this does not happen from .hta, the Exec method creates a console attached to the process executed and there is no way to hide it without third party code.

So, the usual way to deal with command output parsing is to redirect the output to a file and then read the file, of course losing the option to write into the StdIn stream of the started process.

To redirect the output of a process to a file, the first idea is to use something like

>"outputFile" command

but the problem is that trying to execute this directly will not work. This redirection is not part of the OS, but an operator of cmd.exe, so, we need to run something like

cmd /c" >"outputFile" command "

We will also need a way to know if the process has ended and no more data will be available. We can use the Wait argument of the Run method, but this will leave the .hta interface freezed, blocked by the Run method.

Without been able to use the Wait argument, we have to start the process, get its process id and wait for it to end and read all its data or periodically retrieve the output until the process has ended.

You have a basic class (ProcessOutputMonitor) that implements this approach in this test .hta

<html>
<head>
<title>pingMonitor</title>
<HTA:APPLICATION 
    ID="pingMonitor"
    APPLICATIONNAME="pingMonitorHTA"
    MINIMIZEBUTTON="no"
    MAXIMIZEBUTTON="no"
    SINGLEINSTANCE="no"
    SysMenu="no"
    BORDER="thin"
/>
<script type="text/vbscript" >

Class ProcessOutputMonitor

    Dim shell, fso, wmi
    Dim processID, retCode, processQuery
    Dim outputFile, inputFile

    Private Sub Class_Initialize
        Set fso     = CreateObject("Scripting.FileSystemObject")
        Set shell   = CreateObject("WScript.Shell")
        Set wmi     = GetObject("winmgmts:{impersonationLevel=impersonate}!\\.\root\cimv2")
        Set inputFile = Nothing 
    End Sub

    Private Sub Class_Terminate
        freeResources
    End Sub

    Public Default Function Start( ByVal commandLine )
    Const SW_HIDE = 0
    Const SW_NORMAL = 1
    Const TemporaryFolder = 2
    Dim startUp

        Start = False
        If Not IsEmpty( processID ) Then Exit Function

        outputFile  = fso.BuildPath( _
            fso.GetSpecialFolder(TemporaryFolder) _ 
            , Left(CreateObject("Scriptlet.TypeLib").GUID,38) & ".tmp" _ 
        )

        ' "%comspec%" /c">"outputFile" command arguments "
        commandLine = Join( _ 
            Array( _ 
                quote(shell.ExpandEnvironmentStrings("%comspec%")) _ 
                , "/c"">" & quote(outputFile) _ 
                , commandLine _ 
                , """" _ 
            ) _ 
            , " " _ 
        )

        ' https://msdn.microsoft.com/en-us/library/aa394375%28v=vs.85%29.aspx
        Set startUp = wmi.Get("Win32_ProcessStartup").SpawnInstance_
        startUp.ShowWindow = SW_HIDE

        retCode = wmi.Get("Win32_Process").Create( commandLine , Null, startUp, processID )
        If retCode <> 0 Then
            freeResources
            Exit Function 
        End If 

        processQuery = "SELECT ProcessID From Win32_Process WHERE ProcessID=" & processID 


        Start = True
    End Function 

    Public Property Get StartReturnCode
        StartReturnCode = retCode
    End Property 

    Public Property Get WasStarted
    End Property 

    Public Property Get PID
        PID = processID
    End Property 

    Public Property Get IsRunning()
        IsRunning = False 
        If Not IsEmpty( processID ) Then 
            If getWMIProcess() Is Nothing Then 
                processID = Empty
                freeResources
            Else 
                IsRunning = True
            End If 
        End If 
    End Property 

    Public Property Get NextLine
        NextLine = getFromInputFile("line")
    End Property 

    Public Property Get NextData
        NextData = getFromInputFile("all")
    End Property

    Private Function getFromInputFile( what )
    Const ForReading = 1
        getFromInputFile = Empty
        If Not IsEmpty( processID ) Then 
            If inputFile Is Nothing Then 
                If fso.FileExists( outputFile ) Then 
                    Set inputFile = fso.GetFile( outputFile ).OpenAsTextStream( ForReading )
                End If 
            End If 
            If Not ( inputFile Is Nothing ) Then 
                If Not inputFile.AtEndOfStream Then 
                    Select Case what
                        Case "line" :   getFromInputFile = inputFile.ReadLine()
                        Case "all"  :   getFromInputFile = inputFile.ReadAll()
                    End Select
                End If 
            End If 
        End If 
    End Function 

    Private Function quote( text )
        quote = """" & text & """"
    End Function 

    Private Function getWMIProcess()
    Const wbemFlagForwardOnly = 32
    Dim process
        Set getWMIProcess = Nothing
        If Not IsEmpty( processID ) Then 
            For Each process In wmi.ExecQuery( processQuery, "WQL", wbemFlagForwardOnly )
                Set getWMIProcess = process
            Next 
        End If 
    End Function 

    Private Sub freeResources()
    Dim process
        Set process = getWMIProcess()
        If Not ( process Is Nothing ) Then 
            process.Terminate
        End If 
        processID = Empty
        processQuery = Empty
        If Not ( inputFile Is Nothing ) Then 
            inputFile.Close
            Set inputFile = Nothing 
            fso.DeleteFile outputFile 
        End If 
    End Sub

End Class

</script>

<SCRIPT LANGUAGE="VBScript">

Dim timerID
Dim monitorGoogle, monitorMicrosoft

Sub Window_onLoad
    window.resizeTo 1024,400
    Set monitorGoogle = New ProcessOutputMonitor
        monitorGoogle.Start "ping -4 www.google.com"
    Set monitorMicrosoft = New ProcessOutputMonitor
        monitorMicrosoft.Start "ping -4 www.microsoft.com"

    timerID = window.setInterval(GetRef("monitorPings"), 500)
End Sub

Sub monitorPings
Dim buffer, keepRunning
    keepRunning = False

    buffer = monitorGoogle.NextData
    If Not IsEmpty( buffer ) Then 
        google.innerHTML = google.innerHTML & Replace( buffer, vbCrLf, "<br>" )
        keepRunning = True
    Else 
        keepRunning = CBool( keepRunning Or monitorGoogle.IsRunning )
    End If 

    buffer = monitorMicrosoft.NextData
    If Not IsEmpty( buffer ) Then 
        microsoft.innerHTML = microsoft.innerHTML & Replace( buffer, vbCrLf, "<br>" )
        keepRunning = True
    Else 
        keepRunning = CBool( keepRunning Or monitorMicrosoft.IsRunning )
    End If 

    If Not keepRunning Then 
        window.clearInterval( timerID )
        timerID = Empty
        alert("Done")
    End If 

End Sub 

Sub ExitProgram
    If Not IsEmpty(timerID) Then window.clearInterval(timerID)
    window.close()
End Sub

</SCRIPT>

</head>

<body>
    <input id="checkButton" type="button" value="EXIT" name="run_button" onClick="ExitProgram" align="right">
<br><br>
    <span id="CurrentTime"></span>
<br><br>
    <table style="width:100%">
        <tr><th style="width:50%;">microsoft</th><th style="width:50%">google</th></tr>
        <tr>
            <td id="microsoft" style="font-family=courier;font-size:0.6em;vertical-align:top;"></td>
            <td id="google" style="font-family=courier;font-size:0.6em;vertical-align:top;"></td>
        </tr>
    </table>

</body>
</html>

This will simply start two ping processes, and using window.setInterval will retrieve each 500ms the output of the two processes and append it (the bad way, just test code) to the output until both processes have ended.

MC ND
  • 69,615
  • 8
  • 84
  • 126
  • While this is helpful, I need to clarify a bit to make sure my own version will work. First, when I had tried sending the output to a file, it seemed like it sent the output all at once to the file, but that might be because the function usually completes in only a few ms. However, your example, if I'm reading it correctly, suggests that longer processes, like a ping, will output each line to the file as it happens, and it is possible to read the file repeatedly to see each new line shortly after it is appended to the file, giving the script a chance to see the running status of the process. – Ceetch Apr 10 '17 at 15:28
  • @Ceetch, Right. I included the `ping` case (by default 4 packets sent with a second between them) and the usage of the `.ReadAll` method to make it clear the `monitorPings` subroutine retrieves data in an incremental manner. If the process is fast it will write all the output before the interval has started and the first read operation will retrive all the data. – MC ND Apr 10 '17 at 16:14
  • That's helpful to know. The adb process depends on a background service to connect with devices. In these cases, running `adb devices` will first check to see if the service is running already and start it if not. In cases where it is not started, the process will print additional lines to show it is starting the service and will appear to hang while the service is loaded before continuing with listing connected devices. If I can read the output before the process is finished, I'll have the chance to catch the server start and show it in the hta's progress. – Ceetch Apr 10 '17 at 16:30
  • I've tried a set of code that uses .Run to run `cmd /c "adb devices" > c:\adb.tmp`, but the result has no file content until the entire adb process finishes, rather than the file populating each line as it would have printed to the console. – Ceetch Apr 10 '17 at 17:29
  • I might have my answer... it seems like ADB is a bit odd in that while it appears to run inside the same console window, CMD does not become aware of its output until ADB exits. Using the redirect to file _does_ work, but it dumps the entire output into the file all at once, rather than line by line. – Ceetch Apr 10 '17 at 18:18