将 RAD 服务器后端部署到最终生产环境(Internet 信息服务器)时,我们遇到问题。如果我们通过同一 \Inetpub\RADServer\EMSServer 文件夹中的 EMSDevServer.exe 执行包,则一切运行正常,但通过 IIS 运行该包时,一些操作无法正确执行。具体来说,Fast-Reports 始终返回 0 大小的空白 PDF,并且对 WinCrypt 的调用返回 AV 错误。
考虑到在 EMSDevServer.exe 下一切运行正常,与 IIS 位于同一文件夹和服务器上,那么问题一定出在其权限和设置上。前者在管理员帐户下在前台运行,而后者在本地系统帐户下在后台运行。
我们按照 Embarcadero 的博客 https://blogs.embarcadero.com/creating-pdf-reports-in-rad-server/ 编写了快速报告,它在 EMSDevServer 下工作得很好,它只是在 IIS 下返回空文件。我们已经按照另一个博客https://blogs.embarcadero.com/how-to-deploy-your-rad-server-project-on-windows-with-iis/配置了 IIS,并编译了我们的 RAD 服务器包在 32 位 Delphi 12.1 上。
我们可以使用 ProcessExplorer 获取我们的 EMSDevServer 运行使用的所有 DLL 依赖项的列表,但是我们如何知道该列表中哪些是有问题的 DLL?以及 IIS 运行无权访问的 DLL?例如,即使将 ADVAPI32.dll 复制到 Inetpub\RADServer\EMSServer 文件夹中,也无法解决调用其 CryptDecryt 函数时的 AV 问题,因此该调用肯定还有其他被阻止的子依赖项,但我们如何识别它们呢?.
或者,我们还更改了 IIS 中 RADServer 应用程序池的身份设置中的帐户,以使我们的包在管理员帐户下运行。我们有问题的函数(Fast-Reports 和 CryptDecrypt)仍然无法工作,但奇怪的是行为发生了轻微的变化。 Fast-Reports 仍然返回 0 大小的 PDF,但对 CryptDecrypt 的启动调用现在可以在包初始化时正确运行(它配置用于创建池连接定义的密码),但所有后续 CryptDecrypt 调用的范围端点调用现在返回“文件未找到”错误而不是 AV。它在启动/加载线程上的行为与在后请求线程上的行为不同。
总而言之,我们需要了解在 IIS 下识别损坏的依赖关系的推荐步骤以及如何解决它们(只需将这些 DLL 复制到 Inetpub\RADServer\EMSServer 文件夹?)。
一些论坛建议启用 TRESTRequest.SynchronizedEvents,但这在我们的案例中似乎没有多大作用。你会推荐它吗?它应该解决什么样的问题?.
您知道使用 Apache for Windows 而不是 IIS 是否有助于解决这些问题吗?
最后,我们试图完全盲目地解决这些问题,因为日志记录不起作用,所以如果 Fast-Reports 引发任何异常,我们不会看到它。在 EMSServer.ini 的 [Server.Logging] 条目上设置文件名不会激活该日志记录,这可能是什么错误?使用 Delphi 11.3 编译和部署的先前测试在激活该日志记录时没有任何问题,但我们当前的版本(在 12.1 下编译和部署)似乎无法激活该 IIS 日志记录。
PS:我已经向 Embarcadero 提交了支持请求,如果他们帮助我们找到解决方案,我将在这里分享解决方案。
事实证明,我从动态创建的 FastReports 生成 PDF 的例程没有任何问题。问题在于将数据返回到 EndpointResponse(但是盲目地、没有日志,花了我很多时间才意识到这一点)。
如果有人想从服务器端完全动态地生成报告,这是我的例行公事。为了方便起见,我使用 SmartPointers,因此组件可以自行释放。
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.
我的问题是 BuildReport 返回了一个 TSmartPointer,它在 EMSDevServer 上工作正常,但在 IIS 下看起来它在 IIS 读取其值之前已被释放。
更改该函数以返回 TStream 而不是 TSmartPointer 解决了问题。
终点原来是
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;
要从 Stream 而不是 TSmartPointer 返回数据,只需更改为:
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;
使用的 SmartPointer 是 Marco Cantu 推荐的。