Problems with GenerateConsoleCtrlEvent and SendInput

M

mike_cole

I am running a Perl script and within that script, I create a process
which starts up a 3rd party console app. At a later point I want to
send a ctrl-c to that console app. I tried using Win32::API to send a
ctrl-c via GenerateConsoleCtrlEvent(), but it keeps sending ctrl-c to
*my* application, not to the app I want to close. I also tried using
SendInput, but the return value indicates that it did not work because
one or more of the parameters was incorrect. Has anybody used either of
these two functions successfully to send a ctrl-c to a console app?
BTW, I am using ActiveState Perl 5.8. I tried adding in snippets of
code, but there is not enough space or else I don't know to access more
space :-(

Thanks,
Mike
 
A

A. Sinan Unur

(e-mail address removed) wrote in
I am running a Perl script and within that script, I create a process
which starts up a 3rd party console app. At a later point I want to
send a ctrl-c to that console app. I tried using Win32::API to send a
ctrl-c via GenerateConsoleCtrlEvent(), but it keeps sending ctrl-c to
*my* application, not to the app I want to close. I also tried using
SendInput, but the return value indicates that it did not work because
one or more of the parameters was incorrect.

Please post a short but complete script illustrating the problem. For
suggestions on how to prepare such a post, please read the posting
guidelines for this group.
of these two functions successfully to send a ctrl-c to a console app?
BTW, I am using ActiveState Perl 5.8. I tried adding in snippets of
code, but there is not enough space or else I don't know to access
more space :-(

That makes no sense.

Sinan
 
M

mike_cole

The code that I was using is shown below. The API calls were declared
using Win32::API, Win32::API::prototype and Win32::process

A portion of the creation routine:

Win32::process->Create ($ProcessObj, "E:\Tools\Hdeperf.exe","", 0,
CREATE_DEFAULT_ERROR_MODE |
CREATE_NEW_CONSOLE |
CREATE_NEW_PROCESS_GROUP, "." );
return($ProcessObj);


Attempting to use GenerateConsoleCtrlEvent():

sub HaltHdPerf
{
my ($Pid) = @_;
my $CTRL_C_EVENT = pack('L',0);
# my $CTRL_C_EVENT = pack('L',1); # ctrl break
my $ProcessID = pack('L',$Pid->GetProcessID());

LogPrint("pid for hdperf = ".$Pid->GetProcessID()."\n");
LogPrint("pid for perl = $$ \n");
LogPrint("Halting HdPerf on pid ".unpack('L',$ProcessID)."\n");
my $Status = GenerateConsoleCtrlEvent($CTRL_C_EVENT, $ProcessID);
my $lasterror = GetLastError();
LogPrint("Status = $Status and last error = $lasterror\n");
}

Output from above code using a ctrl-c event:

pid for hdperf = 3424
pid for perl = 3436
Halting HdPerf on pid 3424
Terminating on signal SIGINT(2)
Status = 1 and last error = 183

Unfortunately it is my program that was terminated.
If I use ctrl break instead of ctrl c I get:

pid for hdperf = 3244
pid for perl = 4064
Halting HdPerf on pid 3244
Terminating on signal SIGINT(2)

And, again, it is my program that terminates. After failing at this, I
tried using SendInput(). This would be my preferred method (as if I had
a choice) since I need to send a ctrl-c to stop the program and then
follow that with a "Y" to actually close the window. The code for using
SendInput() is as follows. It is messier and there are better ways to
do it, but I went for the most simplistic since this was new to me.

sub HaltHdPerf2
{
my ($Pid) = @_;
my $ProcessID = pack('L',$Pid->GetProcessID());
my $CTRL_C_EVENT = pack('L',0);
my $INPUT_KEYBOARD = 1;
my $KEYEVENTF_KEYUP = 2;
my $KEYEVENTF_KEYDOWN = 0;
my $VK_CONTROL = 17;
my $StructSize = 20;

# This is an INPUT struct
Win32::API::Struct->typedef( KBD_INPUT => qw {
DWORD inputType; # 4 bytes
WORD wVK; # 2 bytes start of KEYBDINPUT struct
WORD wScan; # 2 bytes
DWORD dwFlags; # 4 bytes
DWORD dwTime; # 4 bytes
DWORD dwExtraInfo; # 4 bytes end of KEYBDINPUT
struct
} ); # total = 20 bytes

LogPrint("size of DWORD = ".Win32::API::Type->sizeof('DWORD')." and
size of WORD = ".Win32::API::Type->sizeof('WORD')."\n");

# Create a structure for SendInput
my $kbdInput = Win32::API::Struct->new ( 'KBD_INPUT' );
$kbdInput->{inputType} = $INPUT_KEYBOARD;
$kbdInput->{wVK} = $VK_CONTROL;
$kbdInput->{wScan} = 0;
$kbdInput->{dwFlags} = $KEYEVENTF_KEYDOWN;
$kbdInput->{dwTime} = time();
$kbdInput->{dwExtraInfo} = 0;

# Use the topmost window as a starting point
my $hWnd = GetTopWindow( 0 );
my $CharSize = 1 + Win32::API::IsUnicode();

# Cycle thru each window and find the one with the desired string in
the title
do {
# Allocate a buffer for the window title and set it to all null
characters
my $Size = ( GetWindowTextLength( $hWnd ) + 1 ) * $CharSize;
my $Buff = "\x0" x $Size;

# Get the window's title and get rid of any remaining null characters
from above
GetWindowText( $hWnd, $Buff, $Size );
$Buff =~ s/\x0//g;

# If the title matches then send crtl-c to the window
if( $Buff =~ m/WINDOWS\\system32\\cmd.exe/i ) {
LogPrint("HdPerf window found\n");

# Bring this window into focus
SetForegroundWindow($hWnd);

LogPrint("Sending ctrl-c command to HdPerf\n");

# Press ctrl key
my $InsertedEvents = SendInput(pack('I',1), $kbdInput,
pack('i',$StructSize));
LogPrint("InsertedEvents = $InsertedEvents\n");

# Press 'c' key
$kbdInput->{wVK} = 'c';
$InsertedEvents = SendInput(pack('I',1), $kbdInput,
pack('i',$StructSize));
LogPrint("InsertedEvents = $InsertedEvents\n");

# Release 'c' key
$kbdInput->{dwFlags} = $KEYEVENTF_KEYUP;
$InsertedEvents = SendInput(pack('I',1), $kbdInput,
pack('i',$StructSize));
LogPrint("InsertedEvents = $InsertedEvents\n");

# Release ctrl key
$kbdInput->{wVK} = $VK_CONTROL;
$InsertedEvents = SendInput(pack('I',1), $kbdInput,
pack('i',$StructSize));
LogPrint("InsertedEvents = $InsertedEvents\n");
}
}
while( $hWnd = GetWindow( $hWnd, 0x02 ) ); # get the next window
LogPrint("All done\n");
}
}


Output from the above code:

size of DWORD = 4 and size of WORD = 2
HdPerf window found
Sending ctrl-c command to HdPerf
InsertedEvents = 0
InsertedEvents = 0
InsertedEvents = 0
InsertedEvents = 0
All done
 
A

A. Sinan Unur

(e-mail address removed) wrote in

[ Please get in the habit of quoting an appropriate amount of context. ]
The code that I was using is shown below.

You did not post a short but complete script that others can run simply by
copying and pasting.
The API calls were declared
using Win32::API, Win32::API::prototype and Win32::process

A portion of the creation routine:

Win32::process->Create ($ProcessObj, "E:\Tools\Hdeperf.exe","", 0,
CREATE_DEFAULT_ERROR_MODE |
CREATE_NEW_CONSOLE |
CREATE_NEW_PROCESS_GROUP, "." );

It looks like you do not have:

use strict;
use warnings;

in your code. In addition, you do not check the return value of
Win32::process::Create. Did you try printing the string you are passing to
Create?

Now, given that you haven't posted a short but complete script I can run
without a lot of effort, and your call to Create is likely failing (unless
E:ToolsHdeperf.exe really exists), I am not inclined to spend much time on
your question at this point.

Please read the posting guidelines for this group. They contain invaluable
information on how to make it easier for other people to help you.

Sinan
 
M

mike_cole

Sinan,

"You did not post a short but complete script that others can run
simply by copying and pasting."

The program we are using constitutes several thousand lines. The
portion I posted needs several hundred lines of setup code involving
our libraries just to get to that point. The program HdPerf is a load
generator which has the potential to wipe out any disks you may have on
your system. Therefore, I cannot post some code which you can just cut
and paste and run.

"Now, given that you haven't posted a short but complete script I can
run without a lot of effort, and your call to Create is likely failing
(unless E:ToolsHdeperf.exe really exists), I am not inclined to spend
much time on your question at this point."

My call to Create works just fine. The program is executed and runs.
The problem is in getting the program to stop by sending it a ctrl-c. I
have tried using SendInput() and GenerateConsoleCtlrEvent() both within
Perl and as a call to a VC++ program. Neither works. From the posts
that I have read, it seems that this has befuddled many other people. I
was just trying to find out if anybody recently has had success using
either of these.
 
A

A. Sinan Unur

(e-mail address removed) wrote in @f14g2000cwb.googlegroups.com:
"You did not post a short but complete script that others can run
simply by copying and pasting."

The program we are using constitutes several thousand lines. The
portion I posted needs several hundred lines of setup code involving
our libraries just to get to that point. The program HdPerf is a load
generator which has the potential to wipe out any disks you may have
on your system. Therefore, I cannot post some code which you can just
cut and paste and run.

Put your question is about a very specific aspect that need not involve
any of that stuff. You should make an effort to reduce the script to the
bare minimum required to exhibit the problem.
"Now, given that you haven't posted a short but complete script I can
run without a lot of effort, and your call to Create is likely failing
(unless E:ToolsHdeperf.exe really exists), I am not inclined to spend
much time on your question at this point."

My call to Create works just fine.

The call to Create that you posted:
(e-mail address removed) wrote in

cannot work because you are passing "E:ToolsHdeperf.exe" to it.
Perl and as a call to a VC++ program. Neither works. From the posts
that I have read, it seems that this has befuddled many other people.
I was just trying to find out if anybody recently has had success
using either of these.

Have you had success in the simplest case?

Sinan
 
A

A. Sinan Unur

(e-mail address removed) wrote in @f14g2000cwb.googlegroups.com:
....


The call to Create that you posted:


cannot work because you are passing "E:ToolsHdeperf.exe" to it.

In addition, you are calling the way you are calling Create, the first
argument passed to the sub is *not$ $ProcessObj.

Sinan
 
A

A. Sinan Unur

A. Sinan Unur said:
(e-mail address removed) wrote in @f14g2000cwb.googlegroups.com:
....

....

Have you had success in the simplest case?

So, I tried to come up with the simplest case. First, note that using
my $CTRL_C_EVENT = pack('L',0);

is wrong. pack returns a string.

Anyway, I wrote a short script:

#!/usr/bin/perl

use strict;
use warnings;

use Win32;
use Win32::API;
use Win32::API::Type;
use Win32::process;

my $GenerateConsoleCtrlEvent = Win32::API->new(
'kernel32',
'BOOL GenerateConsoleCtrlEvent(DWORD dwCtrlEvent, DWORD dwProcessGroupId)'
);

my $process;

unless( Win32::process::Create($process, 'program.exe', q{}, 0,
CREATE_DEFAULT_ERROR_MODE
| CREATE_NEW_CONSOLE
| CREATE_NEW_PROCESS_GROUP,
".")
) {
die Win32::FormatMessage( Win32::GetLastError() );
}

warn $process->GetProcessID, "\n";

sleep 1;

my $result = $GenerateConsoleCtrlEvent->Call(
0, $process->GetProcessID
);

unless ($result) {
die Win32::FormatMessage( Win32::GetLastError() );
}

my $t = <STDIN>;

__END__

program.exe is just a simple C program that is waiting for
input on stdin.

When I run the program, a new cmd window is created with
program.exe running in it. Here is the output I get from
the Perl script:

D:\Home\asu1\UseNet\clpmisc\ctrlc> ctrlc.pl
2876
The parameter is incorrect.

Hmmm ... Let's check Microsoft's documentation
for GenerateConsoleCtrlEvent available at

<URL: http://msdn.microsoft.com/library/d...-us/dllproc/base/generateconsolectrlevent.asp>

Identifier of the process group to receive the signal.
A process group is created when the CREATE_NEW_PROCESS_GROUP
flag is specified in a call to the CreateProcess function.
The process identifier of the new process is also the process
group identifier of a new process group. The process group
includes all processes that are descendants of the root process.
Only those processes in the group that share the same console as
the calling process receive the signal. In other words,
if a process in the group creates a new console, that process
does not receive the signal, nor do its descendants.

So, I do not see any hope of using this API call to send a CTRL-C
or CTRL-BREAK to a process in a different process group.

You could have figured out up to this point by actually enabling
warnings, and checking return values of calls.

On the other hand:

#!/usr/bin/perl

use strict;
use warnings;

use Win32;
use Win32::API;
use Win32::API::Type;
use Win32::GuiTest qw(FindWindowLike SendKeys SetForegroundWindow);
use Win32::process;

my $GenerateConsoleCtrlEvent = Win32::API->new(
'kernel32',
'BOOL GenerateConsoleCtrlEvent(DWORD dwCtrlEvent, DWORD dwProcessGroupId)'
);

my $process;

unless( Win32::process::Create($process, 'program.exe', q{}, 0,
CREATE_DEFAULT_ERROR_MODE
| CREATE_NEW_CONSOLE
| CREATE_NEW_PROCESS_GROUP,
".")
) {
die Win32::FormatMessage( Win32::GetLastError() );
}

sleep 1;

my ($window) = FindWindowLike(0, '^program\.exe');
SetForegroundWindow($window);
SendKeys '{BREAK}';

my $t = <STDIN>;

__END__

I hope this demonstrates that one can write short but
complete scripts without the danger of crashing the hard drive.

Sinan
 
M

mike_cole

Sinan,

In your previous post, you asked about the simplest case. This prompted
me to put together the simplest script I could. I always use strict and
use warnings, but this did not help me. Below is the program I wrote
this morning before I read your latest reply (which, BTW, is one of the
most complete replies to a post I have seen in a long time) and my
results did not indicate an error from GetLastError(). I had read the
Windows documentation before, but I guess I had interpreted wrongly. I
don't quite understand about using the pack operation though. When I
did not use it, I got an "invalid param" error in my call to
GenerateConsoleCtrlEvent(). Thank you for taking the time to help me
out.

======================================================================

#!/usr/bin/perl

use strict;
use warnings;

use Win32;
use Win32::API;
use Win32::API::prototype;
use Win32::process;

my $ProcessObj;
my $FullPath = "E:\\callmon.bat";

Win32::API::prototype::ApiLink( 'kernel32.dll', 'BOOL
GenerateConsoleCtrlEvent( DWORD dwCtrlEvent, DWORD wProcessGroupId )'
);
Win32::API::prototype::ApiLink( 'kernel32.dll', 'DWORD GetLastError( )'
);

print("Process ID for this app = $$ \n");

#Win32::process::Create($obj,$appname,$cmdline,$iflags,$cflags,$curdir)

#Creates a new process.
# Args:
# $obj container for process object
# $appname full path name of executable module
# $cmdline command line args
# $iflags flag: inherit calling processes handles or not
# $cflags flags for creation (see exported vars below)
# $curdir working dir of new process
#Returns non-zero on success, 0 on failure.

print("Creating console app\n");
Win32::process::Create( $ProcessObj, $FullPath, "", 0,

CREATE_DEFAULT_ERROR_MODE|CREATE_NEW_CONSOLE|CREATE_NEW_PROCESS_GROUP,
"." ) || die "Could not create process\n";

my $Pid = pack('L', $ProcessObj->GetProcessID);
my $CTRL_C_EVENT = pack('L',0);

print("Process ID for console app = ".unpack('L',$Pid)."\n");
print("Halting console app\n");
print("Delay for a bit\n");
sleep(5);
my $Status = GenerateConsoleCtrlEvent($CTRL_C_EVENT, $Pid);
my $lasterror = GetLastError();
print("Status = $Status and last error = $lasterror\n");
sleep(5);
print("Notice that this line does not execute and the other app is
still running\n");

========================================================================
Here is the batch file (callmon.bat) that is called by the above
program:

start perl mon.pl

========================================================================
Here is the code for the program (mon.pl) started by the batch file:

#!/usr/bin/perl
use strict;
use warnings;

my ($ddir, $results);

print "Running monitor daemon - do not close window!\n";

while (1){
# Check for tokens
$_=`dir /b`;

if(m/kill.dat/) {
print "Found program exit token\n";
system "del kill.dat";
last;
}
sleep(5);
}
=============================================================
The output from above was:

E:\>perl closeapp.pl
Process ID for this app = 300
Creating console app
Process ID for console app = 3232
Halting console app
Delay for a bit
Terminating on signal SIGINT(2)
Status = 1 and last error = 0
 
A

A. Sinan Unur

(e-mail address removed) wrote in @o13g2000cwo.googlegroups.com:
I don't quite understand about using the pack operation though. When I
did not use it, I got an "invalid param" error in my call to
GenerateConsoleCtrlEvent().

I think the invalid parameter error is due to the second argument to
GenerateConsoleCtrlEvent, not the first. If you run the first example I
gave with the packed argument, you will still get an invalid parameter
error. That is because, as the GenerateConsoleCtrlEvent does not allow
you to send the event to a process group outside of the one in which the
controlling process is running.
#!/usr/bin/perl

use strict;
use warnings;

use Win32;
use Win32::API;
use Win32::API::prototype;

I do not have Win32::API::prototype installed, so I'll keep using just
the Win32::API.
use Win32::process;

my $ProcessObj;
my $FullPath = "E:\\callmon.bat";

my $FullPath = 'E:/callmon.bat';

would also work.
my $Pid = pack('L', $ProcessObj->GetProcessID);
my $CTRL_C_EVENT = pack('L',0);

I really think this is a red-herring. You do not need any of the packs.

Just to illustrate my point about the packs, here is a cut down version
of your script (using Win32::API and without the comments):

#!/usr/bin/perl

use strict;
use warnings;

use Win32;
use Win32::API;
use Win32::API::Type;
use Win32::process;

my $GenerateConsoleCtrlEvent = Win32::API->new(
'kernel32',
'BOOL GenerateConsoleCtrlEvent(DWORD dwCtrlEvent, DWORD
dwProcessGroupId)'
);

print("Process ID for this app = $$ \n");
print("Creating console app\n");

my $ProcessObj;
my $FullPath = q{callmon.bat};

Win32::process::Create($ProcessObj, $FullPath, q{}, 0,
CREATE_DEFAULT_ERROR_MODE
| CREATE_NEW_CONSOLE
| CREATE_NEW_PROCESS_GROUP, q{.})
or die Win32::FormatMessage(Win32::GetLastError());

my $Pid = pack('L', $ProcessObj->GetProcessID);
my $CTRL_C_EVENT = pack('L',0);

print("Halting console app\n");
print("Delay for a bit\n");

sleep(5);

my $Status = $GenerateConsoleCtrlEvent->Call($CTRL_C_EVENT, $Pid);
warn Win32::FormatMessage(Win32::GetLastError());

sleep(5);

print("Notice that this line does not execute and the other app is
still running\n");

Notice the warnings I get:

D:\Home\asu1\UseNet\clpmisc\proc> proc.pl
Process ID for this app = 2612
Creating console app
Halting console app
Delay for a bit
Argument "\0\0\0\0" isn't numeric in subroutine entry at D:\Home\asu1
\UseNet\clpmisc\proc\proc.pl line 36, <DATA> line 164.
Argument "¿^B\0\0" isn't numeric in subroutine entry at D:\Home\asu1
print("Process ID for console app = ".unpack('L',$Pid)."\n"); ....
my $Status = GenerateConsoleCtrlEvent($CTRL_C_EVENT, $Pid);

I suspect that $Pid, being non-numeric, is being passed as 0 which means
"send the CTRL-C even to the *this* process group".
Here is the code for the program (mon.pl) started by the batch file:

I think this code could be written in a better way, but let's not worry
about that right now. Basically, this app will not terminate properly on
a SIGINT.

The only solution I can think of, and my Win32 knowledge is fairly
limited, is to use SendKeys, along with a properly written version of
mon.pl.

This will require that you supply a title to start when you call mon.pl:

start "My monitor application" perl.exe mon.pl

#!/usr/bin/perl

use strict;
use warnings;

use Win32;
use Win32::GuiTest qw(FindWindowLike SendKeys SetForegroundWindow);
use Win32::process;

print("Process ID for this app = $$ \n");
print("Creating console app\n");

my $ProcessObj;
my $FullPath = q{callmon.bat};

Win32::process::Create($ProcessObj, $FullPath, q{}, 0,
CREATE_DEFAULT_ERROR_MODE
| CREATE_NEW_CONSOLE
| CREATE_NEW_PROCESS_GROUP, q{.})
or die Win32::FormatMessage(Win32::GetLastError());

print("Halting console app\n");
print("Delay for a bit\n");

sleep(5);

my ($window) = FindWindowLike(0, '^My monitor application');
SetForegroundWindow($window);
SendKeys '^{BREAK}';

__END__

You will notice that mon.pl, as you have it now, will not terminate
properly:

Running monitor daemon - do not close window!
Terminating on signal SIGBREAK(21)
The process tried to write to a nonexistent pipe.
The process tried to write to a nonexistent pipe.
The process tried to write to a nonexistent pipe.

If you want to force it terminate, you can just close the window using
SendKeys:

SendKeys q{% c};

Hope this helps.

Sinan
 
M

mike_cole

A. Sinan Unur said:
The only solution I can think of, and my Win32 knowledge is fairly
limited, is to use SendKeys, along with a properly written version of
mon.pl.
Hope this helps.

Yes, it helped a lot! I ran the code you provided using SendKeys(). By
sending '^C' and then following it with 'Y~', I was able to close the
HdPerf application. Also, the code you provided gave me some tips on
coding style and some ideas on how to simplify things. Thanks.

Mike
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Members online

Forum statistics

Threads
473,744
Messages
2,569,484
Members
44,904
Latest member
HealthyVisionsCBDPrice

Latest Threads

Top