4

I am trying to run an ssh command, and capture the output to a variable in VBA (using OpenSSH). I can get this to work fine in the command line:

ssh user@ip python C:\Temp\Remote.py

The results are a list of values returned to the command line window. I would like to read this into a VBA variable.

I found this, this and this. The first two do not seem to send or recieve the command correctly, as the code gets hung up on oShell.Exec(...). The shell looks like it is executing, but it just gets hung. If I close the hanging command window, the results in VBA are blank.

Dim sCmd As String
sCmd = "ssh user@ip python C:\Temp\Remote.py"

Dim oShell As Object
Set oShell = CreateObject("WScript.Shell")
Dim oExec As Object
Dim oOutput As Object
Set oExec = oShell.Exec(sCmd)
Set oOutput = oExec.StdOut

Dim s As String
Dim sLine As String

While Not oOutput.AtEndOfStream
    sLine = oOutput.ReadLine
    If sLine <> "" Then s = s & sLine & vbCrLf
Wend

The third seems to partially work (Using 'retVal = Shell("ssh user@ip python C:\Temp\Remote.py", vbNormalFocus)' ), but I cannot get the value out. I can see the command window open, the values come back, but I get some integer back in retVal.

Any help?

Community
  • 1
  • 1
lukehawk
  • 1,183
  • 3
  • 17
  • 43
  • OK, thanks for the clarity on Shell(). There should be no input required, as there is no password, and the command works as is on its own at the command prompt. – lukehawk Dec 19 '16 at 20:56
  • It gets stuck on the oShell.Exec(). If I step through it using the VBA debugger, it never comes back from the subsequent command window. If I close that stuck window, it just comes back with an empty StdOut. – lukehawk Dec 19 '16 at 21:03
  • It is a console application, and it is in my PATH variable, meaning from a command prompt, on any folder on my drive, I can run exactly this: ssh user@ip python C:\Temp\Remote.py and it works fine. – lukehawk Dec 20 '16 at 13:29

2 Answers2

5

The WScript.Shell object's .Exec() method is the method that is designed to (a) run a console command / application, and (b) capture its output via properties of the WshScriptExec instance it returns.

At this point it is a mystery as to why your ssh-based command doesn't work - your code generally does work with console commands.

That said, there is another reason not to use .Exec(), when run from a GUI application, such as a VBA-hosting application, the console window is invariably made visible during execution, which can be visually disruptive.

The following alternatives (optionally) allow running a command hidden (without a visible window), but note that neither directly supports capturing output from the invoked command:

  • VBA's own Shell() function, which, however, invariably executes asynchronously, so it's cumbersome to determine when the command finishes.

  • The WScriptShell object's .Run() method, which optionally allows waiting for the command to finish.

To get both invisible execution and output capturing, you can:

  • combine the WScriptShell object's .Run() method
  • with sending the command's output to a temporary file,
  • and, upon command completion, read the temporary file into memory.
Dim cmd as String
Dim exitCode As Integer
Dim tempFile As String
Dim capturedOutput As String

' Construct the name of a temporary file to capture the command's stdout output in.
tempFile = Environ$("TEMP") & "\" & CreateObject("Scripting.FileSystemObject").GetTempName()

' Define the command to invoke.
cmd = "ssh user@ip python C:\Temp\Remote.py"

' Use .Run() to invoke the command.
'  - In order to use output redirection, the command must be prefixed with 'cmd /c '.
'  - Setting the last argument to `True` makes the invocation synchronous.
'  - Replace vbNormalFocus with vbHidden to run the command *invisibly*.
exitCode = CreateObject("WScript.Shell").Run("cmd /c " & cmd & " >" & tempFile, vbNormalFocus, True)
If exitCode = 0 Then ' Command succeeded.

    ' Read the output file, then delete the temporary file.
    capturedOutput = CreateObject("Scripting.FileSystemObject").OpenTextFile(tempFile).ReadAll()
    CreateObject("Scripting.FileSystemObject").DeleteFile(tempFile)

    ' Display the output.
    MsgBox "Captured Output:" & vbNewLine & capturedOutput

Else ' Command indicated failure.

    MsgBox "An unexpected error occurred.", vbExclamation

End If

Here's an alternative based on Shell() - inspired by this implementation.

As you can see, making Shell() synchronous requires a lot more effort and requires use of the Windows API:

Private Declare Function OpenProcess Lib "kernel32.dll" (ByVal dwDesiredAccessas As Long, ByVal bInheritHandle As Long, ByVal dwProcId As Long) As Long
Private Declare Function CloseHandle Lib "kernel32.dll" (ByVal hObject As Long) As Long
Private Declare Function WaitForSingleObject Lib "kernel32.dll" (ByVal hHandle As Long, ByVal dwMilliseconds As Long) As Long
Private Declare Function GetExitCodeProcess Lib "kernel32.dll" (ByVal hProcess As Long, ByRef lpExitCodeOut As Long) As Integer

Sub Main()

    Dim cmd As String
    Dim taskId As Integer
    Dim exitCode As Integer
    Dim tempFile As String
    Dim capturedOutput As String

    ' Construct the name of a temporary file to capture the command's stdout output in.
    tempFile = Environ$("TEMP") & "\" & CreateObject("Scripting.FileSystemObject").GetTempName()

    ' Define the command to run.
    cmd = "ssh user@ip python C:\Temp\Remote.py"

    ' Use the SyncShell() helper function defined below to invoke the command
    ' synchronously and to obtain its exit code.
    '  - In order to use output redirection, the command must be prefixed with 'cmd /c '.
    '  - Add a 3rd argument with a timeout value in seconds if you don't want to wait
    '    indefinitely for the process to complete.
    '  - Replace vbNormalFocus with vbHidden to run the command *invisibly*.
    exitCode = SyncShell("cmd /c " & cmd & " >" & tempFile, vbNormalFocus)
    If exitCode = 0 Then ' Command succeeded.

        ' Read the output file and delete the temporary file.
        capturedOutput = CreateObject("Scripting.FileSystemObject").OpenTextFile(tempFile).ReadAll()
        CreateObject("Scripting.FileSystemObject").DeleteFile (tempFile)

        ' Display the output.
        MsgBox "Captured Output:" & vbNewLine & capturedOutput

    Else ' Command indicated failure.

        MsgBox "An unexpected error occurred.", vbExclamation

    End If

End Sub

' Helper function
Private Function SyncShell(ByVal cmd As String, Optional ByVal windowStyle As VbAppWinStyle = vbMinimizedFocus, Optional ByVal timeoutInSecs As Double = -1) As Long

    Dim pid As Long ' PID (Process ID) as returned by Shell().
    Dim h As Long ' Process handle
    Dim sts As Long ' WinAPI return value
    Dim timeoutMs As Long ' WINAPI timeout value
    Dim exitCode As Long

    ' Invoke the command (invariably asynchronously) and store the PID returned.
    ' Note that the invocation may fail.
    pid = Shell(cmd, windowStyle)

    ' Translate the PIP into a process *handle* with the SYNCHRONIZE and PROCESS_QUERY_LIMITED_INFORMATION access rights,
    ' so we can wait for the process to terminate and query its exit code.
    h = OpenProcess(&H100000 Or &H1000, 0, pid) ' &H100000 == SYNCHRONIZE, &H1000 == PROCESS_QUERY_LIMITED_INFORMATION
    If h = 0 Then Err.Raise vbObjectError + 1024, , "Failed to obtain process handle for process with ID " & pid & "."

    ' Now wait for the process to terminate.
    If timeoutInSecs = -1 Then
        timeoutMs = &HFFFF ' INFINITE
    Else
        timeoutMs = timeoutInSecs * 1000
    End If
    sts = WaitForSingleObject(h, timeoutMs)
    If sts <> 0 Then Err.Raise vbObjectError + 1025, , "Waiting for process with ID " & pid & " to terminate timed out, or an unexpected error occurred."

    ' Obtain the process's exit code.
    sts = GetExitCodeProcess(h, exitCode) ' Return value is a BOOL: 1 for true, 0 for false
    If sts <> 1 Then Err.Raise vbObjectError + 1026, , "Failed to obtain exit code for process ID " & pid & "."

    CloseHandle h

    ' Return the exit code.
    SyncShell = exitCode

End Function
Community
  • 1
  • 1
mklement0
  • 245,023
  • 45
  • 419
  • 492
0

I think you should try using the "command spec" at the start of the command. On your machine this can be obtained in VBA with VBA.Environ$("comspec") which on my my machine returns C:\WINDOWS\system32\cmd.exe

So try

 sCmd = VBA.Environ$("comspec") & " /C " & " ssh user@ip python C:\Temp\Remote.py"

The command spec needs switches in some instances cmd switches

I do not have Python installed so cannot test.

mklement0
  • 245,023
  • 45
  • 419
  • 492
S Meaden
  • 7,411
  • 3
  • 27
  • 53
  • Thanks! I will try this. (That link for cmd switches is broken, though. Can you provide it again?) I do not think i need the cmd part of the command, though. If ssh is in my %path%, which it is, it should recognize ssh as a command, correct? – lukehawk Dec 19 '16 at 20:59
  • I just tried this, and it does not change the behavior. – lukehawk Dec 19 '16 at 21:05
  • ok, googling yields some code here https://www.example-code.com/vb/ssh_exec.asp. vb6 is the same syntax as vba. or this http://www.mrexcel.com/forum/excel-questions/816494-visual-basic-applications-excel-2010-how-initiate-ssh-connection-list-ip-address-send-credential-write-result-connection-credential-2-cell.html or this http://stackoverflow.com/questions/18135551/how-to-call-python-script-on-excel-vba – S Meaden Dec 19 '16 at 21:15