Friday, September 09, 2011

Electric Commander PERL system call limition

If you ever wanted to execute a UI process on Windows using an Electric Commander step written in perl you will realize that you can only execute binary using STD IO in case your agent is running as a service.
One reason is that Electric commander will pipe all STD IO to its internal logging mecanism associated to each JOB you are running for each STEP.

Complete reason has been posted here
https://electriccloud.zendesk.com/entries/64647-kbec-00037-running-steps-that-require-an-interactive-windows-desktop-on-the-agent-machine

The only workaround I found is to implement an IOCP WIN32 service that you can use to send message using SOCKET. At this point the IOCP WIN32 service can execute the native application including complex application which may require OLE AUTOMATION. EG Itunes :), Microsoft Excel, etc...

Will not work

my $exePath = 'C:/Program Files/Notepad++/Notepad++.exe';
print "Execute $exePath \n";
$exitCode = system($exePath );

The process is spawn. I can see it using the Task Manager under Process tab but the Process doesn't receive ACTIVATION.

Here is the script that I use!

use IO::Socket;
use ElectricCommander;
use Cwd;
use Time::Local;

use Win32::Process;
use Win32::TieRegistry;
use Win32::TieRegistry qw(:KEY_);

my $regKey;
my $UserShellFoldersKey;
my $valueString;
my $exitCode = -1;
my $process;

# if you need to request current user folder
$regKey = new Win32::TieRegistry "CUser",
{ Access=>KEY_READ(), Delimiter=>"/" };
$UserShellFoldersKey = $regKey->Open( "Software/Microsoft/Windows/CurrentVersion/Explorer/User Shell Folders" );

# Get value data:
$valueString= $UserShellFoldersKey->GetValue("Personal");


my $launchedByUser = '$[/myJob/launchedByUser]';
my $jobName = '$[/myJob/jobName]';

my $build_test_dir = 'C:\\ec_workspace\\'.$jobName.'\\';


my $msg = '';

#the caller
my $sock_call = new IO::Socket::INET (
PeerAddr => 'localhost',
PeerPort => '4000',
Proto => 'tcp',
);
if ($sock_call)
{
print $sock_call "LaunchNotepad++\n";
my $sock_addr = recv($sock_call,$msg,190,0);
print $msg;

close($sock_call);
}
else
{
print "ERROR Could not create client socket to IOCP Broadcom Automation Service!\n";
die "Could not create client socket to IOCP Broadcom Automation Service $!\n";
}


}

The IOCP server (WIN32 service) is very small and powerful because it is using asynchronous IO so it can handle a lot of simultaneous request comming from multiple Electric commander JOBS. In my case the IOCP server will take care of enumerating the target process before executing it because the target process was not single instance and in my case if two JOBS were running at the same time I wanted to make sure not to end up with two running instances. Off course you can limit this inside Electric Commander itself by preventing job concurrency but then the UI can be left open at the end of its execution. Maybe you want to Remote Desktop the machine the next day to look at results. During this time you want to prevent others automation to be executed. To perform synchronisation the script can wait on idle for the same time used by the automation process to complete. You know this only after monitoring the automation process. Once you know you'll have to tune your step.

Electric Commander is very powerful. I wish I could afford the license to use it to perform recuring video security monitoring and reporting on a server running at home because once you can send message using local socket between EC apache instance and your process, you are entitled to implement a million of creative ideas!

Automation server and bot rules the world! Aren't they ?

IOCP Server

int CALLBACK WinMain(HINSTANCE hInstance,HINSTANCE ,LPSTR ,int ){
....
init();
run();
}

static void init(void)
{
init_winsock();
create_io_completion_port();
create_listening_socket();
bind_listening_socket();
start_listening();
load_accept_ex();
start_accepting();
}

static void run(void)
{
DWORD length;
BOOL resultOk;
WSAOVERLAPPED* ovl_res;
SocketState* socketState;

for (;;)
{
resultOk = get_completion_status(&length, &socketState, &ovl_res);

switch (socketState->operation)
{
case OP_ACCEPT:
OutputDebugString(L"* operation ACCEPT completed\n");
accept_completed(resultOk, length, socketState, ovl_res);
break;

case OP_READ:
OutputDebugString(L"* operation READ completed\n");
read_completed(resultOk, length, socketState, ovl_res);
break;

case OP_WRITE:
OutputDebugString(L"* operation WRITE completed\n");
write_completed(resultOk, length, socketState, ovl_res);
break;

default:
OutputDebugString(L"* error, unknown operation!!!\n");
destroy_connection(socketState, ovl_res); // hope for the best!
break;
} // switch
} // for
}

static void read_completed(BOOL resultOk, DWORD length,
SocketState* socketState, WSAOVERLAPPED* ovl)
{

if (resultOk)
{
if (length > 0)
{
_TCHAR message[512];
_sntprintf(message,512,L"* read operation completed, %d bytes read\n", length);
OutputDebugString(message);


wchar_t *pwText = NULL;
pwText = new wchar_t[length];
if(!pwText)
{
delete []pwText;
}
memset(pwText,0,length*sizeof(wchar_t));
MultiByteToWideChar (CP_ACP, 0, socketState->buf, -1, pwText, length-1 );

_sntprintf(message,512,L"* buffer: %s\n", pwText);
OutputDebugString(message);


HINSTANCE hInstanceMobCat = NULL;
HWND hWndMobCat = NULL;
DWORD dwProcessIdMobcat = 0;

if (_tcsicmp(pwText,L"LaunchNotepad++")==0)
{
std::string szExecutable = "C:\\Program Files\\Notepad++\\Notepad++.exe";

bstrExecutable = utf8toUTF16(szExecutable);
// Can't call ShellExecuteEx on main thread since it runs a message loop!
// That would let windows messages sneak in before they are expected (Bug 2507120)
HANDLE const hShellExecThread = CreateThread(NULL, 0, callShellExecThreadProc, bstrExecutable, 0, NULL);
if (!hShellExecThread)
{
}
DWORD result = WaitForSingleObject( hShellExecThread, INFINITE );
if( result == WAIT_FAILED )
{
}
DWORD exitCode = 5;
BOOL success = ::GetExitCodeProcess( hShellExecThread, &exitCode );
if( !success ) exitCode = 5;
CloseHandle(hShellExecThread);
}

delete []pwText;

// starts another write back to caller
socketState->length = length;
start_writing(socketState, ovl);
}
else // length == 0
{
OutputDebugString(L"* connection closed by client\n");
destroy_connection(socketState, ovl);
}
}
else // !resultOk, assumes connection was reset
{ int err = GetLastError();

_TCHAR message[512];

_sntprintf(message,512,L"* error %d in recv, assuming connection was reset by client\n", err);

OutputDebugString(message);
destroy_connection(socketState, ovl);
}
}

1 comment:

Anonymous said...

Where's the definition of callShellExecThreadProc?