delphiiisdelphi-12-athensrad-server

RAD Server deployment issues to IIS


We face issues when deploying our RAD Server backend to the final production environment (Internet Information Server). Everything runs fine if we execute our package through EMSDevServer.exe within the same \Inetpub\RADServer\EMSServer folder, but when running that package through IIS a few actions are not executed correctly. Concretely, Fast-Reports always returns 0-size blank PDFs, and the calls to WinCrypt return an AV error.

Considering that everything runs fine under EMSDevServer.exe, on the same folder and server than IIS, then the problems must be on its permissions and settings. The former runs on foreground under an Administrator account while the latter runs on background under a LocalSystem account.

We have coded Fast-Reports following Embarcadero's blog https://blogs.embarcadero.com/creating-pdf-reports-in-rad-server/, and it works perfectly fine under EMSDevServer, it's just under IIS that it returns empty files. We have configured IIS following this other blog https://blogs.embarcadero.com/how-to-deploy-your-rad-server-project-on-windows-with-iis/, and we compile our RAD Server package on 32bits Delphi 12.1.

We can use ProcessExplorer to get a list of all the DLL dependencies that our EMSDevServer run uses, but how can we know which are the problematic DLLs from that list ?, the ones that an IIS run doesn't have access to ?. For example, even copying ADVAPI32.dll to the Inetpub\RADServer\EMSServer folder doesn't solve the AV when calling its CryptDecryt function, so that call must have other blocked subdependencies but how can we identify them ?.

Alternately we have also changed the account on the Identity Settings for the RADServer Application Pool in IIS, to make our package run under an Administrator account. Our problematic functions (Fast-Reports & CryptDecrypt) still doesn't work but bizarrely the behavior has changed slightly. Fast-Reports still returns 0-sized PDFs, but an starting call to CryptDecrypt now runs correctly at the initialization of the package (it configures the password for the creation of the Pooled Connection definition), but all the posterior CryptDecrypt calls on the scope of and endpoint call now return a "file not found" error instead of an AV. It behaves differently on the starting/loading thread than on the posterior request threads.

Summarizing, we need to know the recommended steps to identify broken dependencies under IIS and how to solve them (simply copying those DLLs to the Inetpub\RADServer\EMSServer folder ?).

Some forums recommend to enable TRESTRequest.SynchronizedEvents but that doesn't seem to do much in our case. Would you recommend it ?, what kind of problems should it solve ?.

Do you know if using Apache for Windows instead of IIS would help on those problems ?

Finally, we are trying to solve those problems completely blind, because the logging is not working, so if Fast-Reports is raising any exception we are not seeing it. What can possibly be wrong that setting a Filename on the [Server.Logging] entry of EMSServer.ini doesn't activate that logging ?. Prior tests compiled & deployed using Delphi 11.3 didn't have any problem activating that Logging, but our current version, compiled & deployed under 12.1 seems incapable to activate that IIS logging.

PS: I have submitted a support request with Embarcadero, I will share here the solution if they help us to find one.


Solution

  • Turns out there was nothing wrong on my routine to generate PDF from dynamically created FastReports, and there were no rare broken dependencies. Fastreports worked just fine, the problem was returning that data to the EndpointResponse (but going blind, without Logs, took me a lot of time to realize it).

    If anyone wants to generate reports from a server side, completely dynamically, this is my routine to do so. For convenience I use SmartPointers, so the components free themselves (but you can easily remove them).

    unit MyReportUtilsUnit;
    
    interface
    
    uses
      System.Classes,
      System.SysUtils,
      System.Types,
      frxClass,
      frxExportPDF,
      Data.DB,
      FireDAC.Comp.DataSet,
      SmartPointerClass;
    
    type
      TKeyValue = record
        Key: string;
        Value: variant;
      end;
      TKeyValueArray = array of TKeyValue;
    
      TDatasetArray = array of TFDDataset;
    
      TMyReportUtils = record
        class function BuildReport(ReportOwner: TComponent; Template: TBytes; DataQuerys: TDatasetArray; Variables: TKeyValueArray): TfrxReport; static;
        class function ExportToPDF(ReportOwner: TComponent; Template: TBytes; DataQuerys: TDatasetArray; Variables: TKeyValueArray): TStream; static;
      end;
    
    var
      MyReportUtils: TMyReportUtils;
    
    implementation
    
    uses
      System.Variants,
      System.Diagnostics,
      System.Math,
      System.IOUtils,
      frxDBSet,
      frxBarcode,
      frxTableObject;
    
    class function TMyReportUtils.BuildReport(ReportOwner: TComponent; Template: TBytes; DataQuerys: TDatasetArray; Variables: TKeyValueArray): TfrxReport;
    var
      Report: TfrxReport;
      StreamTemplate: TSmartPointer<TBytesStream>;
      fdbDataset: TfrxDBDataset;
    begin
      Report := TfrxReport.Create(ReportOwner);
      Report.Clear;
    
      Report.EngineOptions.UseFileCache := False;
      Report.ShowProgress := False;
      Report.EngineOptions.SilentMode := True;
    
      StreamTemplate := TBytesStream.Create(Template);
      StreamTemplate.Value.Position := 0;
      if StreamTemplate.Value.Size > 0 then
        Report.LoadFromStream(StreamTemplate.Value);
    
      for var InputItem in Variables do
        Report.Variables.AddVariable('Variables', InputItem.Key, VarToStr(InputItem.Value));
    
      Report.DataSets.Clear;
      for var DataQuery in DataQuerys do
      begin
        fdbDataset := TfrxDBDataset.Create(ReportOwner);
        fdbDataset.UserName := DataQuery.Name;
        fdbDataset.Name := 'fdb' + DataQuery.Name;
        fdbDataset.CloseDatasource := False;
        fdbDataset.DataSet := DataQuery;
        Report.DataSets.Add(fdbDataset);
      end;
    
      Report.PrepareReport(True);  // ToDo: Check why it needs to be prepared twice in order to load its datasets
      Report.PrepareReport(True);
    
      Result := Report;
    end;
    
    class function TMyReportUtils.ExportToPDF(ReportOwner: TComponent; Template: TBytes; DataQuerys: TDatasetArray; Variables: TKeyValueArray): TStream;
    var
      frReport: TfrxReport;
      frPDF: TSmartPointer<TfrxPDFExport>;
    begin
      Result := TBytesStream.Create;
    
      frPDF := TfrxPDFExport.Create(ReportOwner);
      frPDF.Value.Stream := Result;
      frPDF.Value.Compressed := True;
      frPDF.Value.EmbeddedFonts := False;
      frPDF.Value.Outline := False;
      frPDF.Value.OpenAfterExport := False;
      frPDF.Value.ShowProgress := False;
      frPDF.Value.ShowDialog := False;
      frPDF.Value.PrintOptimized := False;
      frPDF.Value.PictureDPI :=  600;
      frPDF.Value.Quality := 95;
      frPDF.Value.UseFileCache := False;
      frPDF.Value.Background := False;
      frPDF.Value.CenterWindow := False;
      frPDF.Value.DataOnly := False;
      frPDF.value.EmbedFontsIfProtected := False;
      frPDF.Value.FitWindow := False;
      frPDF.value.HideMenubar := False;
      frPDF.Value.HideToolbar := False;
      frPDF.Value.HideWindowUI := False;
      frPDF.Value.OverwritePrompt := False;
      frPDF.Value.PdfA := False;
      frPDF.Value.PrintScaling := False;
      frPDF.Value.HTMLTags := False;
      frPDF.Value.Transparency := False;
    
      frReport := BuildReport(ReportOwner, Template, DataQuerys, Params);
      frReport.Export(frPDF);
    
      Result.Position := 0;
    end;
    
    end.
    

    My problem was that BuildReport returned a TSmartPointer, which worked fine on EMSDevServer, but under IIS looks like it was freed before IIS could read its value.

    Changing that function to return a TStream instead of a TSmartPointer solved the issue.

    The endpoint originally was

    procedure TdmTestResource.GetReport(const AContext: TEndpointContext; const ARequest: TEndpointRequest; const AResponse: TEndpointResponse);
    begin
      var Template: TBytes;
      var ReportDatasets: TDatasetArray;
      var ReportVariables: TKeyValueArray;
    
      GetData(Self, Template, ReportDatasets, ReportVariables);
    
      var StreamSmartPtr := MyReportUtils.ExportToPDF(Self, Template, ReportDatasets, ReportVariables);
      AResponse.Body.SetStream(StreamSmartPtr.Value, 'application/pdf', False);
    end;
    

    And to return the data from an Stream instead of an TSmartPointer it only needed to be changed to:

    procedure TdmTestResource.GetReport(const AContext: TEndpointContext; const ARequest: TEndpointRequest; const AResponse: TEndpointResponse);
    begin
      var Template: TBytes;
      var ReportDatasets: TDatasetArray;
      var ReportVariables: TKeyValueArray;
    
      GetData(Self, Template, ReportDatasets, ReportVariables);
    
      var Stream := MyReportUtils.ExportToPDF(Self, Template, ReportDatasets, ReportVariables);
      AResponse.Body.SetStream(Stream, 'application/pdf', True);
    end;
    

    The SmartPointers used are the ones recommended by Marco Cantu.

    https://github.com/marcocantu/DelphiSessions/blob/master/DelphiLanguageCodeRage2018/02_SmartPointers/SmartPointerClass.pas