I have seen:
How to start an application without waiting in a batch file?
How can I run a program from a batch file without leaving the console open after the program starts?
How to get PID of process just started from within a batch file?
Is there any way to redirect stderr output from a command run with "start" in the Windows command line?
... but I still cannot really get what I want working, so here goes my question.
I have a program that basically loops forever (until interrupted with Ctrl-C), and outputs log messages to stderr; here is an example, testlogerr.c:
// based on https://stackoverflow.com/questions/26965508/infinite-while-loop-and-control-c
// can be compiled in MINGW64 with:
// gcc -g testlogerr.c -o testlogerr.exe
#include <unistd.h>
#include <stdio.h>
#include <signal.h>
#include <time.h>
volatile sig_atomic_t stop;
void inthand(int signum) {
stop = 1;
}
int main(int argc, char **argv) {
signal(SIGINT, inthand);
int counter = 0;
while(!stop) {
fprintf( stderr, "%d: Logging line %d\n", (int)time(NULL), counter++);
sleep(2);
}
printf("exiting safely\n");
//system("pause"); // does "Press any key to continue . . ."; skip here
return 0;
}
Now, having built this program (in MINGW64) as a Windows .exe, I would like to start it via a batch script in cmd.exe as a background process, having its stderr redirected to a log file, and obtaining its PID as a process. To do this, in Linux bash I'd simply do (see also https://unix.stackexchange.com/questions/74520/can-i-redirect-output-to-a-log-file-and-background-a-process-at-the-same-time , How to get process ID of background process?):
testlogerr > myfile.log 2>&1 &
TESTLOGERR_PID=$!
echo "testlogerr started, its PID is $TESTLOGERR_PID"
My question is: how can I do the same in a batch script, so I get only one cmd.exe window started, and I get a prompt in that window after the background process has started?
As far as I've seen from the links above, start /b would start a command in background - but then one cannot obtain the PID of the background process.
Furthermore, https://stackoverflow.com/a/59971707/6197439 recommends PowerShell, so I tried the following, e.g. as testlogerr.bat:
powershell -executionPolicy bypass -command ^
"& {$process = start-process $args[0] -passthru -argumentlist $args[1..($args.length-1)]; exit $process.id}" ^
testlogerr.exe 2>testlogerr.log
... however, the problem is - when I double-click this testlogerr.bat file in Windows Explorer:
First one cmd.exe terminal window gets started, then it closes, and the testlogerr.exe gets started in another cmd.exe window
The cmd.exe terminal window where testlogerr.exe runs, shows no cmd.exe prompt - instead, it shows stderr log messages; meaning it is running in foreground, not background
Another indication of foreground run, is that when I hit Ctrl-C, testlogerr.exe exits - and so does its terminal cmd.exe window
The testlogerr.log file gets created, but its empty
So - how can I start the program as a background process, redirecting its stderr to file, obtain and print its pid, and finally show a cmd.exe terminal prompt (while the started process runs in the background) - all in a single cmd.exe terminal window?
In Linux shell, $! stores the last executed PID. Powershell can achieve the same using $process.id The current PowerShell code only exists with the PID though and is never displayed. Therefore change the code in the batch file to use Write-Host to display the PID (Similar to echo in bash):
#echo off
powershell -executionPolicy bypass -command "& {$process = start-process $args[0] -passthru -argumentlist $args[1..($args.length-1)]; Write-Host testlogerr started, its PID is $process.id}" testlogerr.exe 2>testlogerr.log
pause>nul
PS!! If you want both stdout and stderr in the log file, then change from 2>testlogerr.log to >testlogerr.log 2>&1
I think I finally got this - after a ton of failed attempts ...
First, let me note that I expected a "line buffered" redirection; however, Windows has no support for that, it either has "character buffered" I/O (serial ports), or it buffers everything (until process exits, then its output is flushed to file). However, the buffering happens at the output of the program - so to make sure my program is unbuffered, I made these changes (if you do not have control over the program you want to run in that way, see one of the referred links where they suggest using winpty for "unbuffer"ing) in testlogerr.c:
// based on https://stackoverflow.com/questions/26965508/infinite-while-loop-and-control-c
// can be compiled in MINGW64 with:
// gcc -g testlogerr.c -o testlogerr.exe
#include <unistd.h>
#include <stdio.h>
#include <signal.h>
#include <time.h>
volatile sig_atomic_t stop;
void inthand(int signum) {
stop = 1;
}
int main(int argc, char **argv) {
// disable output/line buffering:
// https://stackoverflow.com/questions/8192318/why-does-delayed-expansion-fail-when-inside-a-piped-block-of-code#8194279
// https://stackoverflow.com/questions/40487671/is-out-host-buffering
// https://stackoverflow.com/questions/11516258/what-is-the-equivalent-of-unbuffer-program-on-windows
// https://stackoverflow.com/questions/7876660/how-to-turn-off-buffering-of-stdout-in-c
setvbuf(stdout, NULL, _IONBF, 0);
setvbuf(stderr, NULL, _IONBF, 0);
signal(SIGINT, inthand);
int counter = 0;
while(!stop) {
fprintf( stderr, "%d: Logging line %d\n", (int)time(NULL), counter++);
sleep(2);
}
printf("exiting safely\n");
//system("pause"); // does "Press any key to continue . . ."; skip here
return 0;
}
Right, so now that we have a program that writes "unbuffered" (i.e. "character buffered") to stderr, this is the batch file that works for me, testlogerr.bat - first the working portion, then for reference, everything else that did not work for me:
:: so, the only way to prevent Ctrl-C, AND run in the same cmd.exe window started by .bat, AND obtain the PID of the background process, is to use start /b - and then, retrieve the PID from the difference in started tasks ..
:: https://stackoverflow.com/questions/4677462/windows-batch-file-pid-of-last-process
#echo off
tasklist /FI "imagename eq testlogerr.exe" /NH /FO csv > task-before.txt
start /b testlogerr.exe > testlogerr.log 2>&1
tasklist /FI "imagename eq testlogerr.exe" /NH /FO csv > task-after.txt
for /f "delims=, tokens=2,*" %%A in ('"fc /L /LB1 task-before.txt task-after.txt | find /I "testlogerr.exe""') do set TESTPID=%%A
del task-before.txt
del task-after.txt
:: next command deletes double quotes (") from the string itself, so only the PID number remains:
SET TESTPID=%TESTPID:"=%
:: now, print the PID we obtained:
echo TESTPID is %TESTPID%
:: note that at this point, the started terminal actually blocks!
:: so here we run cmd.exe one more time, so we get the command prompt shell
cmd
:: to exit this cmd shell, first you have to do `taskkill /F /PID %TESTPID%`, and only then `exit` will work
:: (otherwise it blocks) - or, just close via the X button at upper right corner of the cmd.exe window
REM Failed approaches below:
REM :: there is -RedirectStandardError for powershell Start-Process;
REM :: https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.management/start-process?view=powershell-7.1
REM :: that means we do not have to start cmd so we have stream redirection; however,
REM :: -RedirectStandardError causes the reading via for..in..do to freeze (otherwise the below works)
REM #echo off
REM for /F "delims=" %%i IN ('powershell -command "$proc = Start-Process testlogerr.exe -RedirectStandardError testlogerr.log -NoNewWindow -passthru ; Write-Output $proc.id"') DO set i=%%i
REM echo i was %i%
REM pause
REM :: also "batch macro" https://stackoverflow.com/q/69539939/ does not work,
REM :: when RedirectStandardError is there (but works otherwise)
REM #echo off
REM call :initMacro
REM %$set% TESTPID="powershell -command "$proc = Start-Process testlogerr.exe 2>testlogerr.log -NoNewWindow -passthru ; Write-Output $proc.id""
REM echo TESTPID %TESTPID[0]%
REM ...
REM :: piping to SET https://stackoverflow.com/q/8192318 also does not work:
REM powershell -command "$proc = Start-Process testlogerr.exe -RedirectStandardError testlogerr.log -NoNewWindow -passthru ; Write-Output $proc.id" | set /p TESTPID=
REM echo TESTPID %TESTPID%
REM pause
REM :: must redirect to files, then https://stackoverflow.com/q/8192318 - this works:
REM :: openfiles /local on :: requires system reboot!
REM :: note: somehow test.pid here ends up being held/"used by" testlogerr.exe? yes, see https://superuser.com/q/986202 ; that means we cannot really delete it (ugh!)
REM #echo off
REM powershell -command "$proc = Start-Process testlogerr.exe -RedirectStandardError testlogerr.log -NoNewWindow -passthru ; Write-Output $proc.id" > test.pid
REM set /p TESTPID=<test.pid
REM :: openfiles /query /fo table | findstr test.pid :: access denied
REM :: del test.pid
REM echo TESTPID %TESTPID%
REM pause
REM :: like this, though, test.pid is not kept under ownership, so we can easily delete it
REM #echo off
REM powershell -command "$proc = Start-Process testlogerr.exe -RedirectStandardError testlogerr.log -NoNewWindow -passthru ; Write-Output $proc.id | Out-File -Encoding ASCII -FilePath .\test.pid"
REM set /p TESTPID=<test.pid
REM del test.pid
REM echo TESTPID %TESTPID%
REM :: pause :: not needed anymore, no need for "Press any key to continue . . ."
REM :: note that at this point, the started terminal actually blocks!
REM :: so here we run cmd.exe one more time, so we get the command prompt shell
REM :: (however, even there, if we hit Ctrl-C, it will break our background program!)
REM cmd
REM :: as per https://superuser.com/q/1479119
REM :: like this, Ctrl-C does not kill the process by accident anymore;
REM :: however, the PID returned is for the cmd.exe, not the testlogerr.exe
REM #echo off
REM powershell -command "$proc = Start-Process -FilePath 'CMD.EXE' -ArgumentList '/C START /B testlogerr.exe' -RedirectStandardError testlogerr.log -NoNewWindow -passthru ; Write-Output $proc.id | Out-File -Encoding ASCII -FilePath .\test.pid"
REM set /p TESTPID=<test.pid
REM del test.pid
REM echo TESTPID %TESTPID%
REM cmd
REM :: so, the only way to prevent Ctrl-C, AND run in the same cmd.exe window started by .bat, AND obtain the PID of the background process, is to use start /b - and then, retrieve the PID from the difference in started tasks ..
REM :: https://stackoverflow.com/questions/4677462/windows-batch-file-pid-of-last-process
REM ( ... here was the working code, which has now been moved at start/top of snippet)
So, what this .bat file now allows me, is that I can double click it, and I will get in the newly started cmd.exe terminal window:
TESTPID is 5532
Microsoft Windows [Version 10.0.19043.1266]
(c) Microsoft Corporation. All rights reserved.
C:\tmp>
So, the testlogerr.exe process started in the background - but if I hit Ctrl-C by accident in the newly started cmd.exe terminal, I will not shut testlogerr.exe down.
Furthermore, testlogerr.exe's log messages are piped to the testlogerr.log file (and I can confirm lines in that logfile appear one by one - if we had Linux tail, we could have seen realtime updates with tail -f testlogerr.log).
And finally, I get a cmd.exe shell prompt at end - which means, I can immediately inspect the situation:
C:\tmp>tasklist | findstr 5532
testlogerr.exe 5532 Console 1 3,148 K
Great, that works!
Unfortunately, I thought this would allow me to start two such terminals, with two separate instances of the background process - unfortunately, if I try to do that, I get in the second terminal:
The process cannot access the file because it is being used by another process.
TESTPID is "=
But, at least I got the behavior that I wanted in the OP/question ...
So I've got this pretty basic powershell script to backup files to our network drive:
Function Backup {
param ($backupSource)
#Define backup location
$backupTarget = '\\192.168.0.247\Public'
#Make sure we're targeting a folder
If (!(Test-Path $backupSource -pathtype container)) {
[System.Windows.Forms.MessageBox]::Show("Target must be a folder" , "Error", 0)
Exit
}
#Make sure we have access to the backup location
DO {
$test = Test-Path $backupTarget
If (!$test) {
$loop = [System.Windows.Forms.MessageBox]::Show("Is the WiFi on? I can't reach the public drive. Maybe try again in a second." , "Internet Connection Unavailable" , 5)
If ($loop -eq 'Cancel') {
Exit
}
}
} WHILE (!$test)
Copy-Item $backupSource $backupTarget -recurse
}
I'm trying to get it to work in a right click menu, making it show up is no problem, and it executes, but I can't figure out how to successfully feed it the $backupSource parameter.
I'm working out of HKEY_CLASSES_ROOT\Directory\shell\NASBackup\command with my default key. I've tried every combination of "C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe" -File "C:\Windows\System32\Backup.ps1" "%1" I can think of. Can someone please help me out with the syntax here?
The script as provided doesn't work with command line arguments. i.e. calling the script as is doesn't work.
Try adding the call to the function passing the command line arguments.
At the end of your script add:
Backup $args[0]
I do it like this.
First I start cmd.exe from inside the registry. CMD window will appear briefly while it sends the powershell command.
Second I call powershell with the hidden switch so the rest of it runs invisibly.
Below is a working .reg file.
Windows Registry Editor Version 5.00
[HKEY_CLASSES_ROOT\Directory\shell\NASBackup\command]
#="cmd /c start /b /wait powershell.exe -nologo -WindowStyle Hidden -file C:\\Windows\\System32\\Backup.ps1"
;
Here is a powershell snip to set the same bits in the registry.
If ( -Not ( Test-Path "Registry::HKEY_CLASSES_ROOT\Directory\shell\NASBackup\command")){New-Item -Path "Registry::HKEY_CLASSES_ROOT\Directory\shell\NASBackup\command" -ItemType RegistryKey -Force}
Set-ItemProperty -path "Registry::HKEY_CLASSES_ROOT\Directory\shell\NASBackup\command" -Name "(Default)" -Type "String" -Value "cmd /c start /b /wait powershell.exe -nologo -WindowStyle Hidden -file C:\Windows\System32\Backup.ps1"
#
I've hit a little bit of a brick wall here. I have written a rather large PS script that requires the SQLServerProviderSnapin100 and this all works happily.
To run the script I have written a BAT file for the users to run so that they won't have to fiddle around with execuptionpolicy, so it sets it unrestricted calls the script and once the script completes it sets the executionpolicy back to restricted.
The problem I have is that I need the BAT file to detect if the user has the SqlServerProvider100 snapin registered to the 32Bit Powershell or the 64Bit powershell.
I know I could run something like this in powershell
$prov = get-pssnapin -registered | where-object{$_.Name -eq "Sqlserverprovidersnapin100"}
if($prov -eq $null){ <call powershell x86>}else{<call powershell 64Bit>}
But how do I do this in the BAT, or is what I am trying to do even possible?
I hope I making sense to someone who can help me get over this bump! :)
Try these 2 commands:
C:\Windows\Syswow64\WindowsPowerShell\v1.0\powershell.exe "if(Get-PSSnapin -registered "SqlServerCmdletSnapin100" -ea 0){ write-host "sql snapin present in 32bit shell"}"
for 64bit:
C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe "if(Get-PSSnapin -registered "SqlServerCmdletSnapin100" -ea 0){ write-host "sql snapin present in 64bit shell"}"
They will return a pretty message if sql snapin is present.
Ok worked it out!
FOR /F "usebackq delims=" %%i IN (`powershell -command "get-pssnapin -registered | where-object { $_.Name -eq 'sqlserverprovidersnapin100' }"`) DO ( set snapin=%%i )
REM if /i {%snapin:~0,4%}=={%test:~0,4%} (goto :there)
if not defined snapin goto :notthere
(goto :there)
:there
echo %snapin%
pause
exit
:notthere
echo "Not loaded!"
pause
exit
This exercise has taught me two things now: apparently when I was testing against NULL within a BAT, if the value was NULL it would remove the variable and secondly how to pass a PS variable back to BAT.
I have a text file of the format:
computername1 uninstallkey1
computername2 uninstallkey2
...
computername200 uninstallkey200
I am trying to write a startup script (batch file or powershell?) that generates a msiexec command that looks up and implants the correct key for each computer it executes on e.g.:
msiexec /x install.msi key=uninstallkey
If I have not made anything clear, please ask and any help is much appreciated!
#ECHO OFF
SETLOCAL
FOR /f "tokens=1*" %%i IN (yourtextfilename.txt) DO (
IF /i %%i==%COMPUTERNAME% ECHO MSIEXEC /x install.msi key=%%j
)
This should do as you require - yourtextfilename.txt contains the data, presumably on a shared drive; finds the line where the computername in column 1 is the same as the computername returned by %computername% in the target computer's environment.
(all case-insensitive EXCEPT %%i and %%j which must match and be the same case)
Command simply ECHOed - remove the ECHO keyword after verification to activate.
In PowerShell,
$comp = Import-CSV -Delimiter " " -Path C:\comp.txt -Header computername,uninstallkey
$comp | ForEach-Object {
if ($env:COMPUTERNAME -eq $_.Computername) {
Start-Process -FilePath "msiexec.exe" -ArgumentList "/x install.msi key=$_.uninstallkey"
}
}
::Stop Windows service
sc query MyWinService | find "STOPPED" & if errorlevel 1 net stop MyWinService
::delete the dll
del /q E:\MyWinService\\*
for /d %%x in (E:\MyWinService\\*) do #rd /s /q "%%x"
But some of the dlls not get deleted and the output comes as ----"ACCESS DENIED"---- if I re-run the same command after 5 minutes it executes. I know because the dll is still associated with the windows service so error is coming but I want to delete the dlls without re-running the command again after 5 min. :(
Batch file way
:Repeat
del "del /q E:\MyWinService\*"
if exist "E:\MyWinService\sampledll.dll" goto Repeat
Powershell way:
do
{
$a = stop-service MyWinService -PassThru
}while ($a.status -ne "Stopped")
do
{
remove-item e:\MyWinService\* -recurse -force -ea silentlycontinue
} untill ( (dir e:\mywinservice).count -gt 0 )