Mega Search
23.2 Million


Sign Up

Make a donation  
Running VBScripts through Winapi [Edit]  
News Group: embarcadero.public.delphi.nativeapi

Hi. I have successfully gotten my x64 application to launch a VBScript with parameters using the code below. I was pushed in this direction due to the TScriptControl object not being available in 64 bit applications. What I am looking for here is a suggestion to get better return information from the script e.g. if the script fails due to a syntax error, return that error like you would see it on the command line. TScriptControl had this functionality built in.

{code}
  // CommandLine format: 'cscript.exe "C:\Temp\ScriptName.vbs" "parm1" "parm2" .....
  // DefaultDir would match the path the VBS file is in

  If CreateProcess(Nil, PChar(CommandLine), Nil, Nil, False, 0, Nil, PChar(DefaultDir), si, pi) Then Begin
    WaitForSingleObject(pi.hProcess, INFINITE);
    GetExitCodeProcess(pi.hProcess,wResult);
    Result := (wResult > 0);
  End Else Begin
    Raise Exception.CreateFmt(''System error: %s', [SysErrorMessage(GetLastError)]);
  End;
{code}

This works fine but has no way to retrieve error messages should the script fail due to a programmer or like type of error which would cause the script to stop in a less than graceful manner. By default the value for wResult will be 0 in that case, but I have nothing else to return to the user other than the fact that the script failed. BTW: If the script fails due to a syntax or like type error, it does NOT go into the ELSE portion and raise the exception. That only occurs if the command line supplied is
 invalid for some reason (e.g. the script doesn't exist.)

 I look forward to community input on this.

Edited by: Rod Sherer on Sep 19, 2014 12:35 PM

Vote for best question.
Score: 0  # Vote:  0
Date Posted: 19-Sep-2014, at 12:35 PM EST
From: Rod Sherer
 
Re: Running VBScripts through Winapi [Edit] [Edit] [Edit]  
News Group: embarcadero.public.delphi.nativeapi
> {quote:title=Remy Lebeau (TeamB) wrote:}{quote}
> Wojciech wrote:
> 
> > Excuse me, what is TScriptControl anyway?
> 
> A Delphi-imported wrapper of Microsoft's "Windows Script Control" COM object.
> 
> > My help (XE4) does not say a word about it, DocWiki neither.
> 
> That is because it is not an Embarcadero-provided unit/component, it is one 
> that is auto-generated when importing the WSC object from a Type Library.
> 
> --
> Remy Lebeau (TeamB)

Thank you Remy for your explanation. I guess there is no sense in getting to know WSC as Microsoft abandoned it in favour of IActiveScript.
This topic is interesting and rarely covered.

Vote for best answer.
Score: 0  # Vote:  0
Date Posted: 25-Nov-2014, at 12:27 AM EST
From: Wojciech Pomianowski
 
Re: Running VBScripts through Winapi [Edit] [Edit] [Edit]  
News Group: embarcadero.public.delphi.nativeapi
Wojciech wrote:

> Excuse me, what is TScriptControl anyway?

A Delphi-imported wrapper of Microsoft's "Windows Script Control" COM object.

> My help (XE4) does not say a word about it, DocWiki neither.

That is because it is not an Embarcadero-provided unit/component, it is one 
that is auto-generated when importing the WSC object from a Type Library.

--
Remy Lebeau (TeamB)

Vote for best answer.
Score: 0  # Vote:  0
Date Posted: 24-Nov-2014, at 2:10 PM EST
From: Remy Lebeau (TeamB)
 
Re: Running VBScripts through Winapi [Edit] [Edit] [Edit]  
News Group: embarcadero.public.delphi.nativeapi
Excuse me, what is TScriptControl anyway? What unit?

My help (XE4) does not say a word about it, DocWiki neither.

Vote for best answer.
Score: 0  # Vote:  0
Date Posted: 24-Nov-2014, at 1:43 PM EST
From: Wojciech Pomianowski
 
Re: Running VBScripts through Winapi [Edit] [Edit] [Edit] [E  
News Group: embarcadero.public.delphi.nativeapi
{code}
unit ActiveScriptDemoUnit;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, AscrLib, ActiveX, ComObj, StdCtrls, Automation;

const
  CLSID_VBScript: TGUID = '{b54f3741-5b07-11cf-a4b0-00aa004a55e8}';
  CLSID_JScript: TGUID = '{f414c260-6ac0-11cf-b6d1-00aa00bbbb58}';
  SCRIPT_E_REPORTED = HRESULT($80020101);

type
  TForm3 = class(TForm, IActiveScriptSite, IActiveScriptSiteWindow)
    Memo1: TMemo;
    Button1: TButton;
    RadioButton1: TRadioButton;
    RadioButton2: TRadioButton;
    ListBox1: TListBox;
    ComboBox1: TComboBox;
    Edit1: TEdit;
    CheckBox1: TCheckBox;
    CheckBox2: TCheckBox;
    procedure FormCreate(Sender: TObject);
    procedure Button1Click(Sender: TObject);
  private
    { Private declarations }
    FScript: IActiveScript;
    FParse: IActiveScriptParse64;
    { IActiveScriptSite }
    function  GetLCID(out plcid: LongWord): HResult; stdcall;
    function  GetItemInfo(pstrName: PWideChar;
                          dwReturnMask: LongWord;
                          out ppiunkItem: IUnknown;
                          out ppti: IUnknown): HResult; stdcall;
    function  GetDocVersionString(out pbstrVersion: WideString): HResult; stdcall;
    function  OnScriptTerminate(var pvarResult: OleVariant;
                                var pexcepinfo: EXCEPINFO): HResult; stdcall;
    function  OnStateChange(ssScriptState: tagSCRIPTSTATE): HResult; stdcall;
    function  OnScriptError(const pscripterror: IActiveScriptError): HResult; stdcall;
    function  OnEnterScript: HResult; stdcall;
    function  OnLeaveScript: HResult; stdcall;
    { IActiveScriptSiteWindow }
    function  GetWindow(out phwnd: HWND): HResult; stdcall;
    function  EnableModeless(fEnable: Integer): HResult; stdcall;
  public
    { Public declarations }
  end;

var
  Form3: TForm3;

implementation

{$R *.dfm}

{ TForm3 }

procedure TForm3.Button1Click(Sender: TObject);
var
  Info: EXCEPINFO;
  Code: WideString;
begin
  Code := Memo1.Text;
  OleCheck(FParse.ParseScriptText(PWideChar(Code), nil, nil, nil, 0, 0,
    SCRIPTITEM_ISVISIBLE or SCRIPTITEM_ISPERSISTENT, nil, Info));
  OleCheck(FScript.SetScriptState(SCRIPTSTATE_CONNECTED));
  OleCheck(FScript.SetScriptState(SCRIPTSTATE_DISCONNECTED));
end;

function TForm3.EnableModeless(fEnable: Integer): HResult;
begin
  Result := S_OK;
end;

procedure TForm3.FormCreate(Sender: TObject);
begin
  CoInitializeEx(nil, COINIT_APARTMENTTHREADED);
  OleCheck(CoCreateInstance(CLSID_JScript, nil, CLSCTX_INPROC_SERVER, IID_IActiveScript, FScript));
  OleCheck(FScript.SetScriptSite(Self as IActiveScriptSite));
  OleCheck(FScript.AddNamedItem('Application', SCRIPTITEM_ISVISIBLE or SCRIPTITEM_ISSOURCE));
  if Supports(FScript, IActiveScriptParse64, FParse) then
    OleCheck(FParse.InitNew);
end;

function TForm3.GetDocVersionString(out pbstrVersion: WideString): HResult;
begin
  pbstrVersion := '1.0';
  Result := S_OK;
end;

function TForm3.GetItemInfo(pstrName: PWideChar; dwReturnMask: LongWord; out ppiunkItem,
  ppti: IInterface): HResult;
begin
  Result := S_FALSE;
  if SameText('Application', pstrName) then
  begin
    if dwReturnMask and SCRIPTINFO_IUNKNOWN <> 0 then
    begin
      ppiunkItem := TAutoObjectDispatch.Create(TApplicationWrapper.Connect(Application)) as IInterface;
      Result := S_OK;
      Exit;
    end;
    if dwReturnMask and SCRIPTINFO_ITYPEINFO <> 0 then
    begin
      Result := TYPE_E_ELEMENTNOTFOUND;
      Exit;
    end;
  end;
end;

function TForm3.GetLCID(out plcid: LongWord): HResult;
begin
  plcid := GetThreadLocale;
  Result := S_OK;
end;

function TForm3.GetWindow(out phwnd: HWND): HResult;
begin
  phwnd := Handle;
  Result := S_OK;
end;

function TForm3.OnEnterScript: HResult;
begin
  Result := S_OK;
end;

function TForm3.OnLeaveScript: HResult;
begin
  Result := S_OK;
end;

function TForm3.OnScriptError(const pscripterror: IActiveScriptError): HResult;
var
  Info: EXCEPINFO;
  Ctxt: DWORD;
  Line: Cardinal;
  Pos: Longint;
begin
  if Succeeded(pscripterror.GetExceptionInfo(Info)) and
    Succeeded(pscripterror.GetSourcePosition(Ctxt, Line, Pos)) then
    ShowMessage(Format('%s: Position: (%d, %d)', [Info.bstrDescription, Line, Pos]));
  Result := SCRIPT_E_REPORTED;
end;

function TForm3.OnScriptTerminate(var pvarResult: OleVariant; var pexcepinfo: EXCEPINFO): HResult;
begin
  Result := S_OK;
end;

function TForm3.OnStateChange(ssScriptState: tagSCRIPTSTATE): HResult;
begin
  Result := S_OK;
end;

end.
{code}

This is from the example project taken from the blog at http://blogs.embarcadero.com/abauer/2007/06/13/36013

Edited by: Rod Sherer on Sep 24, 2014 12:33 PM I found a second reference to IActiveScriptParse, but updating that to IActiveParse64 didn't help either. I have updated the code above.

Edited by: Rod Sherer on Sep 25, 2014 7:47 AM
Correction: The line it is failing on is 

OleCheck(FScript.SetScriptState(SCRIPTSTATE_CONNECTED));

This is in TForm3.Button1Click and it is the call to SetScriptState which is raising the error.

Edited by: Rod Sherer on Sep 25, 2014 8:26 AM

It is the state that is being set which causes the issue. Trying to set SCRIPTSTATE_CONNECTED or SCRIPTSTATE_DISCONNECTED raises an exception. Apparently these states are not compatible with x64, which is unfortunate because I believe they are required to share objects between the application and the script. This still gives me much more script control than I had though, so this is still very useful. I just need to use SCRIPTSTATE_STARTED and SCRIPTSTATE_CLOSED instead and I can run VBScript or JScript. N
ow I just need to make sure I can set parameter values for the script, since I can't share objects.

Edited by: Rod Sherer on Sep 25, 2014 12:26 PM
Once I got the sample project implemented in 64 bit, I tried to implement my own script executer object which would receive information about a script, parse it, run it, and return any feedback. I was disappointed to find out that the active script site and window must piggyback on another class, so I just piggybacked them on my "job" class, which was going to create and call the original object as needed. I don't even think I need the site window as there will be non visible feedback. All feedback is log
ged to a database. So with that thought in mind, I only piggybacked the IActiveScriptSite. It appears to be reliant upon the object it is riding on to have implemented the following functions though.

    function QueryInterface(const IID: TGUID; out Obj): HResult; stdcall;
    function _AddRef: Integer; stdcall;
    function _Release: Integer; stdcall;

I found QueryInterface on the TCustomForm object and mimicked it to always return 0. (I should mention that my application has no forms. It is strictly a command line executable with no feedback other than database entries (log records in the database). The other two I could not find anywhere. Can I just have them return 0 at all times as well? What is the purpose of these methods? They are declared on IInterface.

I am beginning to think that I am in way over my head.

Vote for best answer.
Score: 0  # Vote:  0
Date Posted: 25-Sep-2014, at 12:34 PM EST
From: Rod Sherer
 
Re: Running VBScripts through Winapi [Edit] [Edit] [Edit]  
News Group: embarcadero.public.delphi.nativeapi
Rod wrote:

> However, adding 64 bit to the Target Platforms and changing
> the FParse object from IActiveScriptParse to IActiveScriptParse64
> didn't seem to be enough. It is raising an error from the KERNELBASE.DLL

Please show your actual code, and indicate which line of code is actually 
crashing.

--
Remy Lebeau (TeamB)

Vote for best answer.
Score: 0  # Vote:  0
Date Posted: 24-Sep-2014, at 10:14 AM EST
From: Remy Lebeau (TeamB)
 
Re: Running VBScripts through Winapi [Edit] [Edit] [Edit]  
News Group: embarcadero.public.delphi.nativeapi
Let me switch gears now that I have had a chance to look at the IActiveScript example that Allen Bauer supplied in his blog. I really like the functionality that this promises. Being able to have access to Delphi objects is a huge plus for me. However, adding 64 bit to the Target Platforms and changing the FParse object from IActiveScriptParse to IActiveScriptParse64 didn't seem to be enough. It is raising an error from the KERNELBASE.DLL. What more do I need to do to get his example project to run as a 6
4 bit project?

Vote for best answer.
Score: 0  # Vote:  0
Date Posted: 24-Sep-2014, at 9:05 AM EST
From: Rod Sherer
 
Re: Running VBScripts through Winapi [Edit] [Edit] [Edit]  
News Group: embarcadero.public.delphi.nativeapi
Rod wrote:

> So I thought that if the script failed for any reason, syntax or
> otherwise, that I would get a return value of 0 from
> GetExitCodeProcess.

Don't make assumptions like that.  Test it and find out for sure.  For instance, 
you could have run cscript.exe in a .bat file and have it echo the %ERRORLEVEL% 
value after cscript exited.

> If I have a programmer/syntax error in my VBScript, I have a return
> value of 1. If I have another type of error, e.g. an insert fails returning
> a database error (Oracle in this case), then I get a 0. So in my code
> example, the check versus zero is invalid. This seems confusing to me
> as I would expect a failure to return the same value regardless of what
> stopped the script from running correctly

Not really.  There is a difference between a failure parsing the script so 
it can't run at all versus a failure due to a logic error while running the 
script.

> My script is not using On Error Resume Next, so any errors just stop the
> script from running.

But the script ran, thus cscript did its job, so it returned 0 for success. 
 If the script could not be parsed, cscript could not run the script, so 
it returned 1 for failure.

The result of the running script is irrelevant to the exit code.  cscript 
can only report whether it was able to run the script or not.

All the more reason why you need to switch to ActiveScript, so that you can 
differentiate between parser errors versus runtime errors.

> I'm a little confused as to why ReadPipe is being assigned to Input and 
not Output.

It should not be.  That is a bug in your code.

> I am guessing because it is input from the process/scripts perspective, not
> the applications? I'm also confused as to how I am supposed to assign two
> pipes to three possible elements (input, output, error).

You are calling CreatePipe() only once, so you only have one pipe, not two 
pipes.  You have two HANDLES to one pipe - one handle is used for writing 
data into the pipe, and the other handle is for reading data from the pipe. 
 You give one handle to the process/script, and you use the other handle 
in your code.

If you want to redirect hStdInput, you need to assign the pipe's READING 
handle, and then use its WRITING handle to send data to the process.

If you want to redirect hStdOutput or hStdError, you need to assign the pipe's 
WRITING handle, and then use its READING handle to receive data from the 
process.

If you want to redirect multiple items, you need multiple pipes, and thus 
multiple calls to CreatePipe().  So, if you were to redirect all three items, 
you would make 3 calls to CreatePipe(), resulting in 6 handles, 2 for each 
pipe.

--
Remy Lebeau (TeamB)

Vote for best answer.
Score: 0  # Vote:  0
Date Posted: 23-Sep-2014, at 4:29 PM EST
From: Remy Lebeau (TeamB)
 
Re: Running VBScripts through Winapi [Edit] [Edit] [Edit]  
News Group: embarcadero.public.delphi.nativeapi
I'm really not sure I'm approaching this correctly, but I've been taking my best guesses and I'm getting inconsistent results.

So I thought that if the script failed for any reason, syntax or otherwise, that I would get a return value of 0 from GetExitCodeProcess. This is not the case. If I have a programmer/syntax error in my VBScript, I have a return value of 1. If I have another type of error, e.g. an insert fails returning a database error (Oracle in this case), then I get a 0. So in my code example, the check versus zero is invalid. This seems confusing to me as I would expect a failure to return the same value regardless of
 what stopped the script from running correctly (unless I specifically return a value.) My script is not using On Error Resume Next, so any errors just stop the script from running.

Since it looks like the ReadPipe is the one I am using to pull out data, I tried assigning that to Start.hStdError instead of Start.hStdInput. I got the same results. I'm a little confused as to why ReadPipe is being assigned to Input and not Output. I am guessing because it is input from the process/scripts perspective, not the applications? I'm also confused as to how I am supposed to assign two pipes to three possible elements (input, output, error). --I tried assigning the ReadPipe object to both Inpu
t and Error and that just locked things up.-- Edit: Assigning ReadPipe to bothin input and error doesn't seem to have any detrimental or beneficial effect. I must have tried assigning it to output and error. Doh!

I have done next to none when it comes to WinApi programming. I really appreciate the prompt, informative responses so far and the patience.

Edited by: Rod Sherer on Sep 23, 2014 1:06 PM

Edited by: Rod Sherer on Sep 23, 2014 1:08 PM

Vote for best answer.
Score: 0  # Vote:  0
Date Posted: 23-Sep-2014, at 1:09 PM EST
From: Rod Sherer
 
Re: Running VBScripts through Winapi [Edit] [Edit]  
News Group: embarcadero.public.delphi.nativeapi
Rod wrote:

> So based on your comment, can I assume that Application.ProcessMessages
> has nothing to do with the reason the pipe was only returning the "header"
> message and not the actual error?

Yes.

> I really think I am close with that approach, I just can't seem to get
> the actual error message out.

My guess would be that CScript is using STDERR to output the error info. 
 The code you are using is only reading from STDOUT.

--
Remy Lebeau (TeamB)

Vote for best answer.
Score: 0  # Vote:  0
Date Posted: 23-Sep-2014, at 10:09 AM EST
From: Remy Lebeau (TeamB)