unit Setup.ScriptRunner; { Inno Setup Copyright (C) 1997-2024 Jordan Russell Portions by Martijn Laan For conditions of distribution and use, see LICENSE.TXT. Script runner } interface uses uPSRuntime, uPSDebugger, uPSUtils; type TScriptRunnerOnLog = procedure(const S: String); TScriptRunnerOnLogFmt = procedure(const S: String; const Args: array of const); TScriptRunnerOnDllImport = procedure(var DllName: String; var ForceDelayLoad: Boolean); TScriptRunnerOnDebug = function(const Position: LongInt; var ContinueStepOver: Boolean): Boolean; TScriptRunnerOnDebugIntermediate = function(const Position: LongInt; var ContinueStepOver: Boolean): Boolean; TScriptRunnerOnException = procedure(const Exception: AnsiString; const Position: LongInt); TBreakCondition = (bcNone, bcTrue, bcFalse, bcNonZero, bcNonEmpty); TScriptRunner = class private FNamingAttribute: String; FPSExec: TPSDebugExec; FClassImporter: TPSRuntimeClassImporter; FOnLog: TScriptRunnerOnLog; FOnLogFmt: TScriptRunnerOnLogFmt; FOnDllImport: TScriptRunnerOnDllImport; FOnDebug: TScriptRunnerOnDebug; FOnDebugIntermediate: TScriptRunnerOnDebugIntermediate; FOnException: TScriptRunnerOnException; function GetProcNos(const Name: AnsiString; const CheckNamingAttribute: Boolean; const ProcNos: TPSList): Integer; procedure InternalRunProcedure(const Name: AnsiString; const Parameters: array of Const; const CheckNamingAttribute, MustExist: Boolean); function InternalRunBooleanFunction(const Name: AnsiString; const Parameters: array of Const; const CheckNamingAttribute: Boolean; const BreakCondition: TBreakCondition; const MustExist, Default: Boolean): Boolean; function InternalRunIntegerFunction(const Name: AnsiString; const Parameters: array of Const; const CheckNamingAttribute: Boolean; const BreakCondition: TBreakCondition; const MustExist: Boolean; const Default: Integer): Integer; function InternalRunStringFunction(const Name: AnsiString; const Parameters: array of Const; const CheckNamingAttribute: Boolean; const BreakCondition: TBreakCondition; const MustExist: Boolean; const Default: String): String; procedure Log(const S: String); procedure LogFmt(const S: String; const Args: array of const); procedure RaisePSExecException; procedure SetPSExecParameters(const Parameters: array of Const; Params: TPSList); procedure SetPSExecReturnValue(Params: TPSList; BaseType: TPSBaseType; var Res: PPSVariant); public constructor Create; destructor Destroy; override; procedure LoadScript(const CompiledScriptText, CompiledScriptDebugInfo: AnsiString); function FunctionExists(const Name: AnsiString; const CheckNamingAttribute: Boolean): Boolean; procedure RunProcedure(const Name: AnsiString; const Parameters: array of Const; const MustExist: Boolean); procedure RunProcedures(const Name: AnsiString; const Parameters: array of Const; const MustExist: Boolean); function RunBooleanFunction(const Name: AnsiString; const Parameters: array of Const; const MustExist, Default: Boolean): Boolean; function RunBooleanFunctions(const Name: AnsiString; const Parameters: array of Const; const BreakCondition: TBreakCondition; const MustExist, Default: Boolean): Boolean; function RunIntegerFunction(const Name: AnsiString; const Parameters: array of Const; const MustExist: Boolean; const Default: Integer): Integer; function RunIntegerFunctions(const Name: AnsiString; const Parameters: array of Const; const BreakCondition: TBreakCondition; const MustExist: Boolean; Default: Integer): Integer; function RunStringFunction(const Name: AnsiString; const Parameters: array of Const; const MustExist: Boolean; const Default: String): String; function RunStringFunctions(const Name: AnsiString; const Parameters: array of Const; const BreakCondition: TBreakCondition; const MustExist: Boolean; Default: String): String; function EvaluateUsedVariable(const Param1, Param2, Param3: LongInt; const Param4: AnsiString): String; function GetCallStack(var CallStackCount: Cardinal): String; property NamingAttribute: String write FNamingAttribute; property OnLog: TScriptRunnerOnLog read FOnLog write FOnLog; property OnLogFmt: TScriptRunnerOnLogFmt read FOnLogFmt write FOnLogFmt; property OnDllImport: TScriptRunnerOnDllImport read FOnDllImport write FOnDllImport; property OnDebug: TScriptRunnerOnDebug read FOnDebug write FOnDebug; property OnDebugIntermediate: TScriptRunnerOnDebugIntermediate read FOnDebugIntermediate write FOnDebugIntermediate; property OnException: TScriptRunnerOnException read FOnException write FOnException; end; implementation uses Windows, Forms, SysUtils, uPSR_dll, Setup.ScriptClasses, Setup.ScriptFunc; {---} { Note: Originally this unit used String() casts to avoid "Implicit string cast" warnings on Delphi 2009, but the casts were found to cause non-Unicode Setup to crash during tooltip variable evaluation due to some kind of code generation bug in Delphi 2. Removed all casts, and added the following to simply disable the warning. } {$IFDEF UNICODE} {$WARN IMPLICIT_STRING_CAST OFF} {$ENDIF} procedure TScriptRunner.Log(const S: String); begin if Assigned(FOnLog) then FOnLog(S); end; procedure TScriptRunner.LogFmt(const S: String; const Args: array of const); begin if Assigned(FOnLogFmt) then FOnLogFmt(S, Args); end; procedure ShowError(const Error: String); begin raise Exception.Create(Error); end; procedure ShowPSExecError(const Error: TPSError); begin ShowError('Script error: ' + PSErrorToString(Error, '')); end; procedure TScriptRunner.RaisePSExecException; var E: TObject; begin try FPSExec.RaiseCurrentException; except { Note: Don't use 'on E: Exception do' since that will also match 'Exception' objects raised from other modules (which we mustn't modify) } E := ExceptObject; if E is Exception then begin Exception(E).Message := Format('Runtime error (at %d:%d):'#13#10#13#10, [FPSExec.ExceptionProcNo, FPSExec.ExceptionPos]) + Exception(E).Message; raise; end else begin { If we don't see it as an Exception, it was likely raised by another module } raise Exception.CreateFmt('Runtime error (at %d:%d):'#13#10#13#10 + 'Exception "%s" at address %p', [FPSExec.ExceptionProcNo, FPSExec.ExceptionPos, E.ClassName, ExceptAddr]); end; end; end; procedure TScriptRunner.SetPSExecParameters(const Parameters: array of Const; Params: TPSList); var Param: PPSVariant; I: Integer; begin for I := High(Parameters) downto Low(Parameters) do begin case Parameters[I].vType of vtAnsiString: begin Param := CreateHeapVariant(FPSExec.FindType2(btString)); PPSVariantAString(Param).Data := AnsiString(Parameters[I].vAnsiString); end; vtWideString: begin Param := CreateHeapVariant(FPSExec.FindType2(btWideString)); PPSVariantWString(Param).Data := WideString(Parameters[I].VWideString); end; vtUnicodeString: begin Param := CreateHeapVariant(FPSExec.FindType2(btUnicodeString)); PPSVariantUString(Param).Data := UnicodeString(Parameters[I].VUnicodeString); end; vtInteger: begin Param := CreateHeapVariant(FPSExec.FindType2(btS32)); PPSVariantS32(Param).Data := Parameters[I].vInteger; end; vtBoolean: begin Param := CreateHeapVariant(FPSExec.FindType2(btU8)); PPSVariantU8(Param).Data := Byte(Parameters[I].vBoolean); end; vtPointer: begin { Pointers are assumed to be pointers to Booleans } Param := CreateHeapVariant(FPSExec.FindType2(btU8)); PPSVariantU8(Param).Data := Byte(Boolean(Parameters[I].VPointer^)); end; else raise Exception.Create('TScriptRunner.SetPSExecParameters: Invalid type'); end; Params.Add(Param); end; end; procedure TScriptRunner.SetPSExecReturnValue(Params: TPSList; BaseType: TPSBaseType; var Res: PPSVariant); begin Res := CreateHeapVariant(FPSExec.FindType2(BaseType)); Params.Add(Res); end; {---} function EncodeDLLFilenameForROPS(const Filename: String): AnsiString; begin Result := ''; if Filename <> '' then Result := AnsiString('') + UTF8Encode(Filename); end; function NewUnloadDLLProc(Caller: TPSExec; Proc: TPSExternalProcRec; Global, Stack: TPSStack): Boolean; begin UnloadDLL(Caller, EncodeDLLFilenameForROPS(Stack.GetString(-1))); Result := True; end; function PSExecOnSpecialProcImport(Sender: TPSExec; p: TPSExternalProcRec; Tag: Pointer): Boolean; const SYesNo: array[Boolean] of String = ('No', 'Yes'); var ScriptRunner: TScriptRunner; S, DllName, FunctionName: AnsiString; UnicodeDllName: String; I: Integer; ForceDelayLoad: Boolean; ErrorCode: LongInt; begin ScriptRunner := Sender.ID; ForceDelayLoad := False; ScriptRunner.Log('-- DLL function import --'); S := p.Decl; I := Pos(AnsiString('dll:'), S); if I <> 1 then begin Result := False; Exit; end; Delete(S, 1, Length('dll:')); I := Pos(AnsiString(#0), S); if I = 0 then begin Result := False; Exit; end; DllName := Copy(S, 1, I-1); Delete(S, 1, I); I := Pos(AnsiString(#0), S); if I = 0 then begin Result := False; Exit; end; FunctionName := Copy(S, 1, I-1); UnicodeDllName := UTF8ToString(DllName); ScriptRunner.LogFmt('Function and DLL name: %s@%s', [FunctionName, UnicodeDllName]); if Assigned(ScriptRunner.FOnDllImport) then begin ScriptRunner.FOnDllImport(UnicodeDllName, ForceDelayLoad); DllName := EncodeDLLFilenameForROPS(UnicodeDllName); p.Decl := AnsiString('dll:') + DllName + Copy(p.Decl, Pos(AnsiString(#0), p.Decl), MaxInt); end; if DllName <> '' then ScriptRunner.LogFmt('Importing the DLL function. Dest DLL name: %s', [UnicodeDllName]) else ScriptRunner.Log('Skipping.'); { We're actually still going to call ProcessDllImport but this doesn't matter to the user. } var DelayLoaded: Boolean; Result := ProcessDllImportEx2(Sender, p, ForceDelayLoad, DelayLoaded, ErrorCode); if DllName <> '' then begin if Result then ScriptRunner.LogFmt('Successfully imported the DLL function. Delay loaded? %s', [SYesNo[DelayLoaded]]) else ScriptRunner.LogFmt('Failed to import the DLL function (%d).', [ErrorCode]); end; end; procedure PSExecOnSourceLine(Sender: TPSDebugExec; const Name: AnsiString; Position, Row, Col: Cardinal); var ScriptRunner: TScriptRunner; ContinueStepOver, NeedToResume: Boolean; begin ScriptRunner := Sender.ID; ContinueStepOver := False; if Sender.DebugMode = dmPaused then begin if Assigned(ScriptRunner.FOnDebug) then ScriptRunner.FOnDebug(Position, ContinueStepOver); NeedToResume := True; end else begin { Normally the debugger does not pause when it receives an 'intermediate' notification. However, it can happen if the user clicks Step Over and then Pause before the function call being stepped over has returned. } NeedToResume := False; if Assigned(ScriptRunner.FOnDebugIntermediate) then NeedToResume := ScriptRunner.FOnDebugIntermediate(Position, ContinueStepOver); end; if NeedToResume then begin if ContinueStepOver then Sender.StepOver() else Sender.StepInto(); end; end; procedure PSExecOnException(Sender: TPSExec; ExError: TPSError; const ExParam: AnsiString; ExObject: TObject; ProcNo, Position: Cardinal); var ScriptRunner: TScriptRunner; begin ScriptRunner := Sender.ID; if Assigned(ScriptRunner.FOnException) then ScriptRunner.FOnException(PSErrorToString(ExError, ExParam), ScriptRunner.FPSExec.TranslatePosition(ProcNo, Position)); { Clear any previous 'step over' state after an exception. Like Delphi, when F8 is pressed after an exception it should go to the first line of the nearest 'except' handler, not to the next line of some higher-level function that the user was stepping over prior to the exception. } ScriptRunner.FPSExec.StepInto(); end; {---} constructor TScriptRunner.Create(); begin FPSExec := TPSDebugExec.Create(); FPSExec.ID := Self; FPSExec.AddSpecialProcImport('dll', @PSExecOnSpecialProcImport, nil); FPSExec.OnSourceLine := PSExecOnSourceLine; FPSExec.OnException := PSExecOnException; RegisterDLLRuntimeEx(FPSExec, False, False); FPSExec.RegisterFunctionName('UNLOADDLL', NewUnloadDLLProc, nil, nil); FClassImporter := ScriptClassesLibraryRegister_R(FPSExec); ScriptFuncLibraryRegister_R(FPSExec); end; destructor TScriptRunner.Destroy; begin FPSExec.Free(); FClassImporter.Free(); end; procedure TScriptRunner.LoadScript(const CompiledScriptText, CompiledScriptDebugInfo: AnsiString); begin if FPSExec.LoadData(CompiledScriptText) then begin FPSExec.DebugEnabled := CompiledScriptDebugInfo <> ''; if FPSExec.DebugEnabled then FPSExec.LoadDebugData(CompiledScriptDebugInfo); FPSExec.StepInto(); end else begin RaisePSExecException; { In the case the above for some reason doesn't raise an exception, raise our own: } raise Exception.Create('TScriptRunner.LoadScript failed'); end; end; function TScriptRunner.GetProcNos(const Name: AnsiString; const CheckNamingAttribute: Boolean; const ProcNos: TPSList): Integer; var MainProcNo, ProcNo: Cardinal; Proc: PIFProcRec; Attr: TPSRuntimeAttribute; begin Result := 0; { Locate main implementation. Will add later. } MainProcNo := FPSExec.GetProc(Name); { Locate other implementations using attributes. } if CheckNamingAttribute and (FNamingAttribute <> '') then begin for ProcNo := 0 to FPSExec.GetProcCount-1 do begin if ProcNo <> MainProcNo then begin Proc := FPSExec.GetProcNo(ProcNo); if Proc.Attributes.Count > 0 then begin Attr := Proc.Attributes.FindAttribute(AnsiString(FNamingAttribute)); if (Attr <> nil) and (Attr.ValueCount = 1) and (((Attr.Value[0].FType.BaseType = btUnicodeString) and (CompareText(PPSVariantUString(Attr.Value[0]).Data, Name) = 0)) or ((Attr.Value[0].FType.BaseType = btString) and (CompareText(PPSVariantAString(Attr.Value[0]).Data, Name) = 0))) then begin if ProcNos <> nil then ProcNos.Add(Pointer(ProcNo)); Inc(Result); end; end; end; end; end; { Add main implementation. Doing this last so it will be called last always. } if MainProcNo <> Cardinal(-1) then begin if ProcNos <> nil then ProcNos.Add(Pointer(MainProcNo)); Inc(Result); end; end; function TScriptRunner.FunctionExists(const Name: AnsiString; const CheckNamingAttribute: Boolean): Boolean; begin Result := GetProcNos(Name, CheckNamingAttribute, nil) <> 0; end; procedure WriteBackParameters(const Parameters: array of Const; const Params: TPSList); var I: Integer; begin { Write back new Boolean values to vtPointer-type parameters } for I := 0 to High(Parameters) do if Parameters[I].vType = vtPointer then Boolean(Parameters[I].VPointer^) := (PPSVariantU8(Params[High(Parameters)-I]).Data = 1); end; procedure TScriptRunner.InternalRunProcedure(const Name: AnsiString; const Parameters: array of Const; const CheckNamingAttribute, MustExist: Boolean); var ProcNos, Params: TPSList; I: Integer; begin ProcNos := TPSList.Create; try if GetProcNos(Name, CheckNamingAttribute, ProcNos) <> 0 then begin ScriptClassesLibraryUpdateVars(FPSExec); for I := 0 to ProcNos.Count-1 do begin Params := TPSList.Create(); try SetPSExecParameters(Parameters, Params); FPSExec.RunProc(Params, Cardinal(ProcNos[I])); WriteBackParameters(Parameters, Params); RaisePSExecException; finally FreePSVariantList(Params); end; end; end else begin if MustExist then ShowPSExecError(erCouldNotCallProc); end; finally ProcNos.Free; end; end; procedure TScriptRunner.RunProcedure(const Name: AnsiString; const Parameters: array of Const; const MustExist: Boolean); begin InternalRunProcedure(Name, Parameters, False, MustExist); end; procedure TScriptRunner.RunProcedures(const Name: AnsiString; const Parameters: array of Const; const MustExist: Boolean); begin InternalRunProcedure(Name, Parameters, True, MustExist); end; function TScriptRunner.InternalRunBooleanFunction(const Name: AnsiString; const Parameters: array of Const; const CheckNamingAttribute: Boolean; const BreakCondition: TBreakCondition; const MustExist, Default: Boolean): Boolean; var ProcNos, Params: TPSList; Res: PPSVariant; I: Integer; begin ProcNos := TPSList.Create; try if GetProcNos(Name, CheckNamingAttribute, ProcNos) <> 0 then begin if not (BreakCondition in [bcNone, bcTrue, bcFalse]) or ((BreakCondition = bcNone) and (ProcNos.Count > 1)) then ShowError('Internal error: InternalRunBooleanFunction: invalid BreakCondition'); Result := True; { Silence compiler } ScriptClassesLibraryUpdateVars(FPSExec); for I := 0 to ProcNos.Count-1 do begin Params := TPSList.Create(); try SetPSExecParameters(Parameters, Params); SetPSExecReturnValue(Params, btU8, Res); FPSExec.RunProc(Params, Cardinal(ProcNos[I])); WriteBackParameters(Parameters, Params); RaisePSExecException; Result := PPSVariantU8(Res).Data = 1; if (Result and (BreakCondition = bcTrue)) or (not Result and (BreakCondition = bcFalse)) then Exit; finally FreePSVariantList(Params); end; end; end else begin if MustExist then ShowPSExecError(erCouldNotCallProc); Result := Default; end; finally ProcNos.Free; end; end; function TScriptRunner.RunBooleanFunction(const Name: AnsiString; const Parameters: array of Const; const MustExist, Default: Boolean): Boolean; begin Result := InternalRunBooleanFunction(Name, Parameters, False, bcNone, MustExist, Default); end; function TScriptRunner.RunBooleanFunctions(const Name: AnsiString; const Parameters: array of Const; const BreakCondition: TBreakCondition; const MustExist, Default: Boolean): Boolean; begin Result := InternalRunBooleanFunction(Name, Parameters, True, BreakCondition, MustExist, Default); end; function TScriptRunner.InternalRunIntegerFunction(const Name: AnsiString; const Parameters: array of Const; const CheckNamingAttribute: Boolean; const BreakCondition: TBreakCondition; const MustExist: Boolean; const Default: Integer): Integer; var ProcNos, Params: TPSList; Res: PPSVariant; I: Integer; begin ProcNos := TPSList.Create; try if GetProcNos(Name, CheckNamingAttribute, ProcNos) <> 0 then begin if not (BreakCondition in [bcNone, bcNonZero]) or ((BreakCondition = bcNone) and (ProcNos.Count > 1)) then ShowError('Internal error: InternalRunIntegerFunction: invalid BreakCondition'); Result := 0; { Silence compiler } ScriptClassesLibraryUpdateVars(FPSExec); for I := 0 to ProcNos.Count-1 do begin Params := TPSList.Create(); try SetPSExecParameters(Parameters, Params); SetPSExecReturnValue(Params, btS32, Res); FPSExec.RunProc(Params, Cardinal(ProcNos[I])); WriteBackParameters(Parameters, Params); RaisePSExecException; Result := PPSVariantS32(Res).Data; if (Result <> 0) and (BreakCondition = bcNonZero) then Exit; finally FreePSVariantList(Params); end; end; end else begin if MustExist then ShowPSExecError(erCouldNotCallProc); Result := Default; end; finally ProcNos.Free; end; end; function TScriptRunner.RunIntegerFunction(const Name: AnsiString; const Parameters: array of Const; const MustExist: Boolean; const Default: Integer): Integer; begin Result := InternalRunIntegerFunction(Name, Parameters, False, bcNone, MustExist, Default); end; function TScriptRunner.RunIntegerFunctions(const Name: AnsiString; const Parameters: array of Const; const BreakCondition: TBreakCondition; const MustExist: Boolean; Default: Integer): Integer; begin Result := InternalRunIntegerFunction(Name, Parameters, True, BreakCondition, MustExist, Default); end; function TScriptRunner.InternalRunStringFunction(const Name: AnsiString; const Parameters: array of Const; const CheckNamingAttribute: Boolean; const BreakCondition: TBreakCondition; const MustExist: Boolean; const Default: String): String; var ProcNos, Params: TPSList; Res: PPSVariant; I: Integer; begin ProcNos := TPSList.Create; try if GetProcNos(Name, CheckNamingAttribute, ProcNos) <> 0 then begin if not (BreakCondition in [bcNone, bcNonEmpty]) or ((BreakCondition = bcNone) and (ProcNos.Count > 1)) then ShowError('Internal error: InternalRunStringFunction: invalid BreakCondition'); Result := ''; { Silence compiler } ScriptClassesLibraryUpdateVars(FPSExec); for I := 0 to ProcNos.Count-1 do begin Params := TPSList.Create(); try SetPSExecParameters(Parameters, Params); SetPSExecReturnValue(Params, btUnicodeString, Res); FPSExec.RunProc(Params, Cardinal(ProcNos[I])); WriteBackParameters(Parameters, Params); RaisePSExecException; Result := PPSVariantUString(Res).Data; if (Result <> '') and (BreakCondition = bcNonEmpty) then Exit; finally FreePSVariantList(Params); end; end; end else begin if MustExist then ShowPSExecError(erCouldNotCallProc); Result := Default; end; finally ProcNos.Free; end; end; function TScriptRunner.RunStringFunction(const Name: AnsiString; const Parameters: array of Const; const MustExist: Boolean; const Default: String): String; begin Result := InternalRunStringFunction(Name, Parameters, False, bcNone, MustExist, Default); end; function TScriptRunner.RunStringFunctions(const Name: AnsiString; const Parameters: array of Const; const BreakCondition: TBreakCondition; const MustExist: Boolean; Default: String): String; begin Result := InternalRunStringFunction(Name, Parameters, True, BreakCondition, MustExist, Default); end; function TScriptRunner.EvaluateUsedVariable(const Param1, Param2, Param3: LongInt; const Param4: AnsiString): String; function VariantToString(const p: TPSVariantIFC; const ClassProperties: AnsiString): String; begin //PSVariantToString isn't Unicode enabled, handle strings ourselves //doesn't handle more complex types as records, arrays and objects if p.Dta <> nil then begin case p.aType.BaseType of btWideChar: Result := '''' + tbtWideChar(p.Dta^) + ''''; btWideString: Result := '''' + tbtWideString(p.Dta^) + ''''; btUnicodeString: Result := '''' + tbtUnicodeString(p.Dta^) + ''''; else Result := PSVariantToString(p, ClassProperties); end; end else Result := PSVariantToString(p, ClassProperties); end; begin case TPSVariableType(Param1) of ivtGlobal: begin Result := FPSExec.GlobalVarNames[Param3]; if Param4 <> '' then Result := Result + '.' + Param4; Result := Result + ' = ' + VariantToString(NewTPSVariantIFC(FPSExec.GetGlobalVar(Param3), False), Param4); end; ivtParam: begin if Param2 = LongInt(FPSExec.GetCurrentProcNo) then begin Result := FPSExec.CurrentProcParams[Param3]; if Param4 <> '' then Result := Result + '.' + Param4; Result := Result + ' = ' + VariantToString(NewTPSVariantIFC(FPSExec.GetProcParam(Param3), False), Param4); end else Result := ''; end; ivtVariable: begin if Param2 = LongInt(FPSExec.GetCurrentProcNo) then begin Result := FPSExec.CurrentProcVars[Param3]; if Param4 <> '' then Result := Result + '.' + Param4; Result := Result + ' = ' + VariantToString(NewTPSVariantIFC(FPSExec.GetProcVar(Param3), False), Param4); end else Result := ''; end; end; end; function TScriptRunner.GetCallStack(var CallStackCount: Cardinal): String; begin Result := FPSExec.GetCallStack(CallStackCount); end; end.