I'm attempting to call some code remotely using Window 10 WMIC.
rem command prompt, machine 1 (main computer)
WMIC /node:"MACHINE_2" process call create "cmd.exe /k "%USERPROFILE%\test.vbs" "
This call seems to go through correctly. It produces partial output from test.vbs (shown below), which means the file is called and executing.
' test.vbs, machine 2 (MACHINE_2, in above code)
Set objFSO=CreateObject("Scripting.FileSystemObject")
outFile="%USERPROFILE%\out.txt"
Set objFile = objFSO.CreateTextFile(outFile,True)
objFile.Write "test1" & vbCrLf
objFile.Close
MsgBox("hi")
' CreateObject("WScript.Shell").Run "cmd /c ""nircmd.exe sendkey 0xB3 press"" ", 0
' Set WshShell = Nothing
outFile="%USERPROFILE%\out.txt"
Set objFile = objFSO.CreateTextFile(outFile,True)
objFile.Write "test2" & vbCrLf
objFile.Close
Expected behaviour: The script should write "test1" to out.txt. Then it should open a MsgBox. And after the MsgBox is closed on Machine_2, it should override the contents of out.txt with "test2."
The two lines of commented code below MsgBox can be substituted for the MsgBox code, and also have the same behaviour.
When executing the vbs file locally, the expected behaviour happens. However, when using the WMIC call, "test1" is printed, and then execution seems to stop. The MsgBox is never shown, and "test2" never overrides the content of out.txt.
I'm quite lost as to why this happens, and the steps I might take to make it work. At this point I've exhausted my google-fu.
You CANNOT display a user interface on remote scripts.
You may not mess with the logged on user.
I'm trying to execute this simple test script, but a command shell window is appearing after I execute the script.:
Set objShell = WScript.CreateObject("WScript.Shell")
strCommand = "cmd /C tasklist"
Set objExecObject = objShell.Exec(strCommand)
wscript.echo "Test"
How can I prevent it from showing up?
Update
I was able to improve it with this code change:
strCommand = "cmd /C /Q tasklist"
Now the window only shows up for a split second. But I don't want it to show up at all.
You're always going to get a window flash with Exec(). You can use Run() instead to execute the command in a hidden window. But you can't directly capture the command's output with Run(). You'd have to redirect the output to a temporary file that your VBScript could then open, read, and delete.
For example:
With CreateObject("WScript.Shell")
' Pass 0 as the second parameter to hide the window...
.Run "cmd /c tasklist.exe > c:\out.txt", 0, True
End With
' Read the output and remove the file when done...
Dim strOutput
With CreateObject("Scripting.FileSystemObject")
strOutput = .OpenTextFile("c:\out.txt").ReadAll()
.DeleteFile "c:\out.txt"
End With
The FileSystemObject class has methods like GetSpecialFolder() to retrieve the path of Windows temp folder and GetTempName() to generate a temporary filename that you can use instead of hardcoding an output filename as I've done above.
Also note that you can use the /FO CSV argument with tasklist.exe to create a CSV file which should make parsing it much easier.
Finally, there are VBScript "native" ways to retrieve the list of running processes. WMI's Win32_Process class, for example, can do this without the need for Run/Exec.
Edit:
For the sake of completeness, I should mention that your script can relaunch itself in a hidden console window where you can run Exec() silently. Unfortunately, this hidden console window will also hide your output from functions like WScript.Echo(). Aside from that, however, you probably won't notice any differences running your script under cscript vs wscript. Here's an example of this method:
' If running under wscript.exe, relaunch under cscript.exe in a hidden window...
If InStr(1, WScript.FullName, "wscript.exe", vbTextCompare) > 0 Then
With CreateObject("WScript.Shell")
WScript.Quit .Run("cscript.exe """ & WScript.ScriptFullName & """", 0, True)
End With
End If
' "Real" start of script. We can run Exec() hidden now...
Dim strOutput
strOutput = CreateObject("WScript.Shell").Exec("tasklist.exe").StdOut.ReadAll()
' Need to use MsgBox() since WScript.Echo() is sent to hidden console window...
MsgBox strOutput
Of course, if your script expects command-line parameters, those would need to be forwarded when relaunching your script as well.
Edit 2:
Yet another possibility is to use the Windows clipboard. You can pipe the output of your command to the clip.exe utility. Then, retrieve the text via any number of available COM objects that can access the contents of the clipboard. For example:
' Using a hidden window, pipe the output of the command to the CLIP.EXE utility...
CreateObject("WScript.Shell").Run "cmd /c tasklist.exe | clip", 0, True
' Now read the clipboard text...
Dim strOutput
strOutput = CreateObject("htmlfile").ParentWindow.ClipboardData.GetData("text")
You can use .Exec() method, without console window flash, temp files and unexpected WScript.Echo output muting. The method is slightly tricky, and requires to launch secondary linked script, so I added the comments:
Option Explicit
Dim objDummy, strSignature, objPrimary, objSecondary, objContainer, objWshShell, objWshShellExec, strResult
' this block is executed only in the secondary script flow, after primary script runs cscript
If WScript.Arguments.Named.Exists("signature") Then
' retrieve signature string from argument
strSignature = WScript.Arguments.Named("signature")
Do
' loop through all explorer windows
For Each objContainer In CreateObject("Shell.Application").Windows
' check if the explorer's property with signature name contains the reference to the live script
If ChkVBScriptTypeInfo(objContainer.getProperty(strSignature)) Then
Exit Do
End If
Next
WScript.Sleep 10
Loop
' create shell object within secondary script
Set objWshShell = CreateObject("WScript.Shell")
' retrieve the primary script me object reference from explorer's property with signature name
Set objPrimary = objContainer.getProperty(strSignature)
' quit explorer window to release memory as it's no longer needed
objContainer.Quit
' assign the secondary script me object to the primary script's variable
Set objPrimary.objSecondary = Me
' emtpy loop while primary script is working
Do While ChkVBScriptTypeInfo(objPrimary)
WScript.Sleep 10
Loop
' terminate secondary
WScript.Quit
End If
' the code below is executed first in the primary script flow
' create signature string
strSignature = Left(CreateObject("Scriptlet.TypeLib").Guid, 38)
' create new hidden explorer window as container to transfer a reference between script processes
Set objContainer = GetObject("new:{C08AFD90-F2A1-11D1-8455-00A0C91F3880}")
' put this script's me object reference into explorer's property
objContainer.putProperty strSignature, Me
' launch new secondary process of the same script file via cscript.exe with hidden console window, providing signature string in named argument to identify host script
CreateObject("WScript.Shell").Run ("""" & Replace(LCase(WScript.FullName), "wscript", "cscript") & """ //nologo """ & WScript.ScriptFullName & """ ""/signature:" & strSignature & """"), 0
' wait until secondary script has been initialized and put his me object into this script variable
Do Until ChkVBScriptTypeInfo(objSecondary)
WScript.Sleep 10
Loop
' here is your code starts...
' create exec object within hidden console window of secondary script, execute cmd instruction
Set objWshShellExec = objSecondary.objWshShell.Exec("%comspec% /c tasklist")
' read cmd output
strResult = objWshShellExec.StdOut.ReadAll()
WScript.Echo strResult
' ...
' utility check if me object is live
Function ChkVBScriptTypeInfo(objSample)
On Error Resume Next
If TypeName(objSample) <> "VBScriptTypeInfo" Then
ChkVBScriptTypeInfo = False
Exit Function
End If
ChkVBScriptTypeInfo = True
End Function
UPDATE
I've slightly reworked the code to make it more straightforward:
Option Explicit
Dim strCmd, strRes, objWnd, objParent, strSignature
If WScript.Arguments.Named.Exists("signature") Then WshShellExecCmd
strCmd = "%comspec% /c tasklist"
RunCScriptHidden
WScript.Echo strRes
Sub RunCScriptHidden()
strSignature = Left(CreateObject("Scriptlet.TypeLib").Guid, 38)
GetObject("new:{C08AFD90-F2A1-11D1-8455-00A0C91F3880}").putProperty strSignature, Me
CreateObject("WScript.Shell").Run ("""" & Replace(LCase(WScript.FullName), "wscript", "cscript") & """ //nologo """ & WScript.ScriptFullName & """ ""/signature:" & strSignature & """"), 0, True
End Sub
Sub WshShellExecCmd()
For Each objWnd In CreateObject("Shell.Application").Windows
If IsObject(objWnd.getProperty(WScript.Arguments.Named("signature"))) Then Exit For
Next
Set objParent = objWnd.getProperty(WScript.Arguments.Named("signature"))
objWnd.Quit
objParent.strRes = CreateObject("WScript.Shell").Exec(objParent.strCmd).StdOut.ReadAll()
WScript.Quit
End Sub
BTW, here is VBScript "multithreading" implementation that uses the same container approach.
Some great suggestions are listed above. I'd like to make one more suggestion which is more of a workaround. You can use Sysinternals Desktops (a free program) to run your macro on another desktop on your same machine. That way the flashing can all happen on its own desktop and won't interrupt your work.
I use Sysinternals PSEXEC
https://learn.microsoft.com/sv-se/sysinternals/downloads/psexec
Created a Batch-file (in the same folder as the vbs and exe-file) that runs the script as system user.
I can not Access the user profile and I need to be local Admin but when i run the script without interaction with the desktop it will hide all annoying popups.
Run Script as system without interaction with desktop
"%~dp0PsExec.exe" -s wscript.exe "%~dp0MyScript.vbs"
Run Script as system with interaction with desktop
"%~dp0PsExec.exe" -s -i wscript.exe "%~dp0MyScript.vbs"
To hide the command line windows in VBscipt is use Run in WshShell Object
Then to get the result you can send this result to text file in %temp%
Then read this result with FileSystemObject
Set Sh = CreateObject("WScript.Shell")
tFile=Sh.ExpandEnvironmentStrings("%Temp%")&"\t.txt"
Sh.Run "cmd.exe /C tasklist > """&tFile&""" ",0,False
Wscript.echo CreateObject("Scripting.FileSystemObject").openTextFile(tFile).readAll()
OR
If StrComp(right(WScript.FullName,11),"wscript.exe",1) = 0 Then '' get only wscript.exe from "c:\windows\system32\wscript.exe" to compere with wscript.exe
WScript.Quit CreateObject("WScript.Shell").Run("cscript.exe """ & WScript.ScriptFullName & """", 0, False)
End If
MsgBox CreateObject("WScript.Shell").Exec("cmd.exe /c tasklist /v /fi ""imagename EQ explorer*"" /FO LIST | FIND ""PID:""").StdOut.ReadAll()
An alternative to using to windows scripting host is here: Run a batch program(.bat) through a Visual Basic 6.0
It runs a program and captures its screen output. It works for me in VB6, but not in VBA (hangs at WaitForSingleObject, don't know why).
After trying the main solutions without success, I was able to solve my problem with the following code:
With CreateObject("WScript.Shell")
.Run "cmd /c start /b tasklist.exe > c:\out.txt", 0, True
End With
The real deal was the "/b" as the console help display:
START ["title"] [/D path] [/I] [/MIN] [/MAX] [/SEPARATE | /SHARED]
[/LOW | /NORMAL | /HIGH | /REALTIME | /ABOVENORMAL | /BELOWNORMAL]
[/NODE <NUMA node>] [/AFFINITY <hex affinity mask>] [/WAIT] [/B]
[command/program] [parameters]
"title" Title to display in window title bar.
path Starting directory.
B Start application without creating a new window. The
application has ^C handling ignored. Unless the application
enables ^C processing, ^Break is the only way to interrupt
the application.
I'm trying to run a run the following command line for each specific file type (as example for each .txt file) in the current directory:
"C:\Program Files (x86)\some program\someprogram.exe" "file.txt" "file.txt.mod" -someparameter
When I run this exact command from an open Windows command prompt (including all the quotation marks), it works.
But when I run it through this VB, it does nothing/closes right away.
What am I doing wrong? I have a feeling it has to do with the quotes, but my head can't sort it out.
Set objFSO = CreateObject("Scripting.FileSystemObject")
objStartFolder = left(WScript.ScriptFullName,(Len(WScript.ScriptFullName))-(len(WScript.ScriptName)))
Set objFolder = objFSO.GetFolder(objStartFolder)
Set colFiles = objFolder.Files
For Each objFile in colFiles
strFileName = objFile.Name
If objFSO.GetExtensionName(strFileName) = "txt" Then
RunCommand()
End If
Next
Sub RunCommand
Set oShell = WScript.CreateObject ("WScript.Shell")
oShell.run "cmd.exe /C ""C:\Program Files (x86)\some program\someprogram.exe"" """ & objFile.Path & """ """ & objFile.Path & ".mod"" -someparameter"
Set oShell = Nothing
End Sub
You should
Reduce risk of failures
by
using "Option Explicit"
avoiding clever "roll your own" hacks by using standard methods (.GetParentFolderName) instead
using type prefixes correctly (objStartFolder)
avoiding variables just used once (objFolder, colFiles)
not using globals to pass parameters into Subs/Functions (objFile)
avoiding (unnecessary) stress (.Run without wait, new WScript.Shell for each file, "cmd" instead of "%comspec%")
using cscript in a 'dos box' instead of double click/wscript
and
check your assumptions
by
diagnostic output (.Echo objFile.Name immediately before calling RunCommand, use a variable to store and .Echo the command send to .Run)
Check return values of functions that provide diagnostics (.Run)
sanity checks like:
(just to tame the formatter)
>> WScript.Echo goFS.GetExtensionName("A.TXT")
>>
TXT
I have a couple of applications that I would like to execute one following the other.
how do I do this?
I tried this but the second task never executed.
on error resume next
Set WshShell = CreateObject("WScript.Shell")
WshShell.Run """C:\Program Files\my folder\do task1.exe.vbs"""
WshShell.Run """C:\Program Files\my folder\do task2.exe.vbs"""
msgbox "Finished tasks"
update:
notes found on WshShell.Run click here
What you are missing (per ISDi's answer) is the third parameter of Run, which tells it to not wait for the program to quit (false), before continuing code exection.
Try (If you want to put your code in a subroutine, which if good coding practice for repeat activities):
'Place all of the following in a .vbs file
Sub RunApplication(ByVal sFile)
Dim WShell : Set WShell = CreateObject("WScript.Shell")
WShell.Run Chr(34) & sFile & Chr(34), 8, false
End Sub
'Executing apps.
RunApplication "C:\Program Files\my folder\task1.exe"
RunApplication "C:\Program Files\my folder\task2.exe"
The Run method of WScript.shell has an optional parameter that can halt execution of the script until the Run method returns.
Try:
WshShell.Run("""C:\YourPathTo\task1.exe""", 1, true)
The third parameter, true in the line above tells the interpreter to wait until this task exits before continuing to the next line of the script.
-isdi-
I have a scenario setup where I need to test to see if the results of a .bat file execution returned results. The .bat file calls up another .exe and the .bat file has a CMD DOS window that outputs critical error info to that DOS box from the .exe. If the .exe does not start correctly, I am able to check the results in our SQL DB. I need to close the current .bat file and re-launch it.
This seems fairly simple, I can get the ProcessID using the WMI call. I can terminate the process with a Terminate() command. This works for any .exe I use for testing: notepad, calc, iexplorer, etc. When I run the VBScript to kill the .bat file, it says it terminates the PID (which it does, since I cannot see the PID anymore), however, the DOS box is still open and the .exe called from the .bat file is still running. If I click on the "X" on the DOS box or right-click on the title bar and select "Close", the DOS box and the .exe are both killed. How do I get this script to work correctly. The server is running Windows Server 2003 (some are x32 and others are x64). Any ideas? Here is a version of my code, there have been several revs:
If colFiles.Count = 0 Then
Wscript.Echo "The file does not exist on the remote computer."
Else
Wscript.Echo "The file exists on the remote computer."
Set objWMIService = GetObject("winmgmts:" & "{impersonationLevel=impersonate}!\\" & strComputer & "\root\cimv2")
Set colProcessList = objWMIService.ExecQuery("Select * from Win32_Process")
For Each objItem in colProcessList
If InStr(objItem.CommandLine, "r15_A.bat") Then
Wscript.Echo "Batch file: " & objItem.CommandLine & " Process ID: " & objItem.ProcessID & " Parent ID: " & objItem.ParentProcessID
Call TerminateProcess(objItem.ParentProcessID)
Call TerminateProcess(objItem.ProcessID)
End If
Next
dim shell
set shell=createobject("wscript.shell")
shell.run BATDIR & BATFILE
'set shell=nothing
End If
Function TerminateProcess(PID)
Set objWMIService = GetObject("winmgmts:\\.\root\CIMV2")
Set colProcesses = objWMIService.ExecQuery("SELECT * FROM Win32_Process WHERE Handle = '" & PID & "'")
For Each objProcess in colProcesses
On Error Resume Next
return = objProcess.Terminate()
Set colProcesses = objWMIService.ExecQuery("SELECT * FROM Win32_Process WHERE Handle = '" & PID & "'")
If Error Then
TerminateProcess = FALSE
Else
TerminateProcess = TRUE
msgBox colProcesses.Count
End If
Next
End Function
I have even tried to use the SendKeys to send ALt-F4, X, C, etc. AppActivate does bring the correct DOS box to the front.
Set WshShell = CreateObject("WScript.Shell")
WshShell.AppActivate "r15_A"
'WshShell.SendKeys("+{F10}")
WshShell.SendKeys("C")
Thanks in advance
EDIT--------------
Yes, I am going to have to kill the process that this .bat file calls. Only down side is this DOS box stays open. Annoying but I can live with it. In the end I wrote a script similar to my first one but only checks for the instance I want to kill by checking the CommandLine parameter: (The first bit of code with "-FAIL.txt" test to see if a FAIL file is present, if it is, then the rest of the scipts executes)
Set colFiles = objWMIService. _
ExecQuery("Select * From CIM_DataFile Where Name = 'S:\\servers\\Logs\\" & LOGFILE & "-FAIL.txt'")
If colFiles.Count = 0 Then
'Wscript.Echo "The file does not exist on the remote computer."
Else
'Wscript.Echo "The file exists on the remote computer."
strComputer = "."
Set objWMIService = GetObject("winmgmts:" _
& "{impersonationLevel=impersonate}!\\" & strComputer & "\root\cimv2")
Set colProcessList = objWMIService.ExecQuery _
("Select * from Win32_Process Where Name = 'AppStart.exe'")
For Each objProcess in colProcessList
IF instr(objProcess.CommandLine, "robo05-A") > 0 THEN
'msgBox "Found text"
objProcess.Terminate()
'This part restarts the failed .bat file.
wscript.sleep (200)
dim shell
set shell=createobject("wscript.shell")
shell.run BATDIR & BATFILE
'set shell=nothing
ELSE
'msgBox "Not found"
END IF
Next
End If
The problem your having is the the executable that is being called from within your batch file is still running. I tried to recreated your setup with a bat the just did a ping -t %computername%, and sure enough the vbscript killed the cmd.exe but not the ping (Please note the Parent PID was explorer.exe, so you probably don't want to attempt killing that). Now I could easily kill ping.exe from task manager which then closed the window...but I only knew to do that because I knew what the bat was doing and what was left open. You could do a WMI search for the process that contains "ping.exe", grab the PID and then close that and that would basically be doing the same thing. Of course in your situation "ping.exe" is probably not the same executable as mine. If the exe changes every now and then then you could grab the commandline from your process find the parameter which contains the filepath to the r15_A.bat file, then use the filesystemobject to open this bat file, look for any executables which it calls which could possibly be still running and then kill those....but really that may be overkill for what your trying to accomplish.