ShellAndWait & Listening to an app?

Banana

split with a cherry atop.
Local time
, 19:58
Joined
Sep 1, 2005
Messages
6,318
I want to run a utility that can be executed within a command line, but the utility will want some extra prompts. My objective is to make this totally transparent to the users, but I need to be able to somehow to listen to the application and tell Access what next prompt it needs to send in order to successfully complete the command.

ShellAndWait function looks like a great start, but I can't see anything apropos for giving me information whether the utility is waiting for prompt, and if so, what kind of prompt it wants.
 
The Shelll function is great for launching an app that you can forget about, if you want to interact with an object then Shell is not the best solution for you. If this command line utility is inside a console window look at using Console API's (such as AllocConsole()), they are a pain to setup but worth it for the speed and reliability IMO.
 
All right. I'll go and read up on the documentation. Thanks for the guidance. :)
 
One thing...

I was able to get the API for AllocConsole, but other APIs such as ReadConsole() or WriteConsole() is not within Win32 or Win16 list for some reasons. I don't know where I would go to look for where it would be located in so I can declare it...

Advice?
 
One thing...

I was able to get the API for AllocConsole, but other APIs such as ReadConsole() or WriteConsole() is not within Win32 or Win16 list for some reasons. I don't know where I would go to look for where it would be located in so I can declare it...

Advice?

Code:
Private Declare Function ReadConsole Lib "kernel32.dll" Alias "ReadConsoleA" (ByVal hConsoleInput As Long, ByRef lpBuffer As Any, ByVal nNumberOfCharsToRead As Long, ByRef lpNumberOfCharsRead As Long, ByRef lpReserved As Any) As Long

Private Declare Function WriteConsole Lib "kernel32.dll" Alias "WriteConsoleA" (ByVal hConsoleOutput As Long, ByRef lpBuffer As Any, ByVal nNumberOfCharsToWrite As Long, ByRef lpNumberOfCharsWritten As Long, ByRef lpReserved As Any) As Long

quick search online netted this
 
It's weird because MSDN had documentations on those APIs but didn't bother to give that information on their website. I found a website that lists all API. Thanks for the pointer.

That said, my initial experimentation with console doesn't indicate to me that it acts like cmd.exe; I'd issue a command, but the console seems to treat it as a string to be printed on the screen... Will have to dig deeper.
 
It's weird because MSDN had documentations on those APIs but didn't bother to give that information on their website. I found a website that lists all API. Thanks for the pointer.

That said, my initial experimentation with console doesn't indicate to me that it acts like cmd.exe; I'd issue a command, but the console seems to treat it as a string to be printed on the screen... Will have to dig deeper.

That's because it doesn't unless you tell it to, this creates an empty console window, if you want it to act like the cmd.exe then you need to essentially open an instance of cmd.exe inside the console (SetParent / GetParent APIs). As I said initially this method is time consuming to setup but the end result you being able to send and read information from the window using the input buffers, no more sendkeys, no more worrying about if the object has focus, and it's much faster, I can read the screen about 1000 times a second to check for changes.
 
Great- thanks! Will look into Set/Get Parent APIs.

Yes, it seems complicated, but this is probably the way to go-

I'm trying to automate a SSH connection, for which there are plenty of free clients, but no free library, which would have made everything easier. One company was asking $1000 for a library which is based on a set of code that's been around for decades and available free. :eek: :eek: Talk about highway robbery!

The closest thing I can get is plink, a CLI version of Putty, but it will need some big brother watching over it if it is to be completely automated.

So I vote for time over money here.
 
Just one more thing I couldn't quite grasp-

I need to have a handle for Set/Get Parent, and there's a function, GetStdHandle(), but I don't think they are the same thing as the handle needed for parent function presumably refers to a pointer to a process whereas getstdhandle tells me whether a process is write/read-able or not... AllocConsole function doesn't return or provide a handle...
 
For those interested:

Shell and get a handle (Note that it does not declare APIs so add the required API declaration to make this work.

Just succeeded in attaching cmd.exe to console window. :D Now to work on automating the input (writeline doesn't seem to explicitly issue commands for some reasons)
 
Last edited:
Okay, am kind of stumped as WriteLine prints out the correct string and issues vbCrLf but somehow it doesn't "execute" the string given to the command prompt....

Any more guidance?
 
Sorry Banana but can you write out the code you specifically are using? I'm not following about the WriteLine or "execute."
 
Ooo. Doh. Really should have had copied my modules, as it's on another computer and won't be back until tomorrow.

Nonetheless, there's a sample code I used as a template. Also, all APIs are expalined by browsing the list

The only difference besides the strings themselves is that I've attached cmd.exe to the new console by calling Shell Function (cmd.exe) then getting the cmd's handle and setting console as cmd.exe's parent, so whatever I type in the console, it acts like a proper command prompt, but automated input just isn't same...

If that's not enough, I'll be back tomorrow with the complete source. :)

Edit: Added a link for complete API list
 
Last edited:
Bob, here's the complete source. This is a module behind the form, actually (as I want it executed when Access starts up and the form is set to be first to opened)

Code:
Option Compare Database
Option Explicit

Private Const ENABLE_LINE_INPUT = &H2&
Private Const ENABLE_ECHO_INPUT = &H4&
Private Const ENABLE_MOUSE_INPUT = &H10&
Private Const ENABLE_PROCESSED_INPUT = &H1&
Private Const ENABLE_WINDOW_INPUT = &H8&
Private Const ENABLE_PROCESSED_OUTPUT = &H1&
Private Const ENABLE_WRAP_AT_EOL_OUTPUT = &H2&
Private Const STD_OUTPUT_HANDLE = -11&
Private Const STD_INPUT_HANDLE = -10&
Private Const STD_ERROR_HANDLE = -12&
Private Const INVALID_HANDLE_VALUE = -1&
Private Const GW_HWNDNEXT = 2

Private Declare Function GetParent Lib "user32" _
        (ByVal hwnd As Long) As Long

Private Declare Function SetParent Lib "user32" _
        (ByVal hWndChild As Long, _
        ByVal hWndNewParent As Long) As Long

Private Declare Function AllocConsole Lib "kernel32" () As Long

Private Declare Function FreeConsole Lib "kernel32" () As Long

Private Declare Function CloseHandle Lib "kernel32" _
        (ByVal hObject As Long) As Long

Private Declare Function GetStdHandle Lib "kernel32" _
        (ByVal nStdHandle As Long) As Long

Private Declare Function WriteConsole Lib "kernel32" Alias "WriteConsoleA" _
        (ByVal hConsoleOutput As Long, _
        lpBuffer As Any, _
        ByVal nNumberOfCharsToWrite As Long, _
        lpNumberOfCharsWritten As Long, _
        lpReserved As Any) As Long

Private Declare Function ReadConsole Lib "kernel32" Alias "ReadConsoleA" _
        (ByVal hConsoleInput As Long, _
        ByVal lpBuffer As String, _
        ByVal nNumberOfCharsToRead As Long, _
        lpNumberOfCharsRead As Long, _
        lpReserved As Any) As Long

Private Declare Function SetConsoleTitle Lib "kernel32" Alias "SetConsoleTitleA" _
        (ByVal lpConsoleTitle As String) As Long

Private Declare Function FindWindow Lib "user32" Alias "FindWindowA" _
        (ByVal lpClassName As String, _
        ByVal lpWindowName As String) As Long

Private Declare Function GetWindowThreadProcessId Lib "user32" _
        (ByVal hwnd As Long, _
        lpdwProcessId As Long) As Long

Private Declare Function GetWindow Lib "user32" _
        (ByVal hwnd As Long, _
        ByVal wCmd As Long) As Long

Private Declare Function GetConsoleMode Lib "kernel32" _
        (ByVal hConsoleHandle As Integer, _
        ByRef lpMode As Integer) As Integer

Private Declare Function SetConsoleMode Lib "kernel32" _
        (ByVal hConsoleHandle As Integer, _
        ByVal dwMode As Integer) As Integer

Private Declare Sub ExitProcess Lib "kernel32" _
        (ByVal uExitCode As Long)
        
Private Declare Function GetExitCodeProcess Lib "kernel32" _
        (ByVal hProcess As Long, _
        lpExitCode As Long) As Long
        
Private hWConHandle As Long
Private hWCmdHandle As Long
Private hConsoleOut As Long
Private hConsoleIn As Long
Private hConsoleErr As Long
Private ShellID As Variant

Private Sub Form_Load()

Dim test As Long
Dim var As Variant
Dim str As String
Dim i As Integer
    'Create console
    If AllocConsole() Then
        hConsoleOut = GetStdHandle(STD_OUTPUT_HANDLE)
        If hConsoleOut = INVALID_HANDLE_VALUE Then MsgBox "Unable to get STDOUT"
        hConsoleIn = GetStdHandle(STD_INPUT_HANDLE)
        If hConsoleOut = INVALID_HANDLE_VALUE Then MsgBox "Unable to get STDIN"
    Else
        MsgBox "Couldn't allocate console"
    End If

    'Set the caption of the console window
    SetConsoleTitle "SSHConn"
    
    'Get the handle of console
    hWConHandle = FindWindow(vbNullString, "SSHConn")
    
    Call StartCmd
    
    If SetParent(hWConHandle, hWCmdHandle) Then
        Debug.Print "OK"
    End If
    
    If GetConsoleMode(hConsoleOut, i) Then
        Debug.Print i
    End If

[color=red]'This is the problem area[/color]    
    ConsoleWriteLine "cd C:\putty"
    ConsoleWriteLine "plink -v -ssh -load SecureConnection"
    
End Sub

Private Sub Form_Unload(Cancel As Integer)
    
    'Delete console
    CloseHandle hConsoleOut
    CloseHandle hConsoleIn
    FreeConsole
    ExitProcess GetExitCodeProcess(hWConHandle, 0)
    
    'Force the application to quit in order to destroy the cached connection.
    'Application.Quit
    
End Sub

Sub ConsoleWriteLine(sInput As String)
     
     ConsoleWrite sInput + vbCrLf
     
End Sub

Sub ConsoleWrite(sInput As String)
     
     Dim cWritten As Long
     WriteConsole hConsoleOut, ByVal sInput, Len(sInput), cWritten, ByVal 0&
     
End Sub

Function ConsoleReadLine() As String
    
    Dim ZeroPos As Long
    'Create a buffer
    ConsoleReadLine = String(10, 0)
    'Read the input
    ReadConsole hConsoleIn, ConsoleReadLine, Len(ConsoleReadLine), vbNull, vbNull
    'Strip off trailing vbCrLf and Chr$(0)'s
    ZeroPos = InStr(ConsoleReadLine, Chr$(0))
    If ZeroPos > 0 Then ConsoleReadLine = Left$(ConsoleReadLine, ZeroPos - 3)
    
End Function

Private Sub StartCmd()

Dim buf As String
Dim buf_len As Long

On Error GoTo ShellError

ShellID = Shell("C:\Windows\system32\cmd.exe", vbNormalFocus) 'vbHide

' Display the hWnd.
hWCmdHandle = InstanceToWnd(ShellID)

Exit Sub

ShellError:
    MsgBox "Error Shelling file." & vbCrLf & _
        Err.Description, vbOKOnly Or vbExclamation, _
        "Error"
    Exit Sub

End Sub

' Return the window handle for an instance handle.

Private Function InstanceToWnd(ByVal target_pid As Long) As _
    Long
Dim test_hwnd As Long
Dim test_pid As Long
Dim test_thread_id As Long

    ' Get the first window handle.
    test_hwnd = FindWindow(ByVal 0&, ByVal 0&)

    ' Loop until we find the target or we run out
    ' of windows.
    Do While test_hwnd <> 0
        ' See if this window has a parent. If not,
        ' it is a top-level window.
        If GetParent(test_hwnd) = 0 Then
            ' This is a top-level window. See if
            ' it has the target instance handle.
            test_thread_id = _
                GetWindowThreadProcessId(test_hwnd, _
                test_pid)

            If test_pid = target_pid Then
                ' This is the target.
                InstanceToWnd = test_hwnd
                Exit Do
            End If
        End If

        ' Examine the next window.
        test_hwnd = GetWindow(test_hwnd, GW_HWNDNEXT)
    Loop
    
End Function

I've already marked the point where I'm stumped at; everything else seems to work as expected.

Now, if I manually typed in the exact same string in the console I create, the commands issued will execute (e.g. the cmd will change the directory and start up the application "plink". However, automating the same string just places the command there but just doesn't execute. That is in despite that vbCrLf was already issued as well. (In that case, the cursor just go to a new line without command prompt doing anything).

I hope that helps makes thing clearer.
 
In short you need to send an Enter keystroke to the console, below is the method I use to send input

Code:
Private Declare Function WriteConsoleInput Lib "kernel32.dll" Alias "WriteConsoleInputA" (ByVal hConsoleInput As Long, ByRef lpBuffer As Any, ByVal nLength As Long, ByRef lpNumberOfEventsWritten As Long) As Long

The lpBuffer parameter needs to contain an INPUT_RECORD structure, which can be many things but in your case you'd want a KEY_EVENT_RECORD.
 
Great!!

It was quite a doozy reading the documentations as I had to do it recursively (e.g. read the documentation on WriteConsoleInput, then read INPUT_RECORD, then KEY_EVENT_RECORD), but I got it all together.

I'm happy to report that it is now executing commands as executed and was successful in logging to the remote server and exiting, and in end using fewer API calls than I originally planned for. (Apparently I didn't really need to set console's parent).

But there is one weird kink.... Sometime a character go missing. Sometime nothing happens at all. Mind you, we're talking about intermittent results. I can execute same code again and again, but with different results (e.g. flawless input one time, then flawed input next time.) Have you had that happen?

Edit: Some more testing, it seems to be consistent in that one character would go missing and I'm suspecting that it may be because too many events is being sent to the cmd.exe and too fast? Should I issue a DoEvents or wait a short time?
 
DoEvents might help some, the other thing I ended up doing is checking that my input made it by reading the screen and verifying that the output and input are the same. Takes a while to dial in the timing but once its done it's very nice.
 
For now, DoEvents seems to does the trick and there is no discernible slowdown... I hope. Will need to do some more tests to be positive.

Will keep your other solution in mind as well.

Also, would I assume you'd use ConsoleReadOutput to read what Console has outputted as a result of execution? ConsoleRead seems to be for a user input only.
 
No, it writes out what you input yourself. e.g. You tell it to print "Hello, World", and it does just that.

The question is when a command has been issued, I need to read what was outputted as a result of the command like thus:

Code:
ConsoleWrite "cd C:\"

The Console window would now output "C:\" on the new line; how would I read that?
 

Users who are viewing this thread

Back
Top Bottom