这是我的第一篇文章,如果问题需要更改,我们深表歉意。我已经尽力解决了这个问题,但是这里有很多组件,因此这篇文章相当庞大...
我们的ASP.NET MVC站点作为应用程序服务部署在Azure上。我正在使用API控制器方法来生成同一站点上存在的页面的PDF。为此,控制器创建一个PhantomJS进程,等待成功,然后返回其创建的文件的内容。一切正常,但是此后网站上的多个视图会产生如下错误:
“ /”应用程序中的服务器错误。
目录“ D:\ home \ site \ wwwroot \ Views \ Location”不存在。无法开始监视文件更改。
说明:在执行当前Web请求期间发生未处理的异常。请查看堆栈跟踪,以获取有关错误及其在代码中起源的更多信息。
异常详细信息: System.Web.HttpException:目录“ D:\ home \ site \ wwwroot \ Views \ Location”不存在。无法开始监视文件更改。
一段时间后,错误将更改:
“ /”应用程序中的服务器错误。
找不到视图“ LocationList”或其主视图,或者没有视图引擎支持搜索到的位置。搜索了以下位置:
说明:在执行当前Web请求期间发生未处理的异常。请查看堆栈跟踪,以获取有关错误及其在代码中起源的详细信息。
〜/ Views / Location / LocationList.aspx
〜/ Views / Location / LocationList.ascx
〜/ Views / Shared / LocationList.aspx
〜 /Views/Shared/LocationList.ascx
〜/ Views / Location / LocationList.cshtml
〜/ Views / Location / LocationList.vbhtml
〜/ Views / Shared / LocationList.cshtml
〜/ Views / Shared / LocationList.vbhtml
异常详细信息: System.InvalidOperationException:找不到视图“ LocationList”或其主视图,或者没有视图引擎支持搜索到的位置。搜索了以下位置:
〜/ Views / Location / LocationList.aspx
〜/ Views / Location / LocationList.ascx
〜/ Views / Shared / LocationList.aspx
〜 /Views/Shared/LocationList.ascx
〜/ Views / Location / LocationList.cshtml
〜/ Views / Location / LocationList.vbhtml
〜/ Views / Shared / LocationList.cshtml
〜/ Views / Shared / LocationList.vbhtml
这仅适用于尚未 尚未编译 的视图,或以前未访问过的任何其他文件。修复它的唯一方法是手动停止并启动Web应用程序。我可以确认并非所有进程都发生这种情况(运行“ echo.exe”而不是“ phantomjs.exe”不会导致行为中断)。
我仔细阅读了所有可以想到的日志,没有发现任何异常。我最好的猜测是某个过程被强制或意外终止,但是对于什么以及为什么,我一无所知。也许有一些我不知道的重要日志?
以下是相关的c#代码:
private static async Task<int> ExecuteSimpleAsync(string workingDir, double? timeout,
string command, params string[] parameters)
{
var paramStr = string.Join(" ", parameters.Select(x => x == null ? "" : $"\"{x}\"").ToList());
var processInfo = new ProcessStartInfo(command, paramStr) {
WorkingDirectory = workingDir,
UseShellExecute = false,
CreateNoWindow = true,
};
Process process = null;
int exitCode = -1;
using (process = new Process() { StartInfo = processInfo }) {
process.Start();
await process.WaitForExitAsync(timeout); // simple extension function to check for 'Process.HasExited' periodically
exitCode = process.ExitCode;
}
return exitCode;
}
private static async Task<byte[]> GetFileContents(string filePath) {
byte[] bytes = null;
using (FileStream file = new FileStream(filePath, FileMode.Open, FileAccess.Read)) {
bytes = new byte[file.Length];
await file.ReadAsync(bytes, 0, (int) file.Length);
}
return bytes;
}
public static async Task<byte[]> RenderPdfAsync(
string cookiesB64, string localUrl, string baseFilename, double? timeout = 60)
{
....
// filesPath: (directory for temporary output)
// timeout: 60.000 (60 seconds)
// PhantomJSExePath: (absolute path containing 'phantomjs.exe')
// scriptFile: "rasterize_simple.js"
// requestUrl: "TestReport/ForUserAndTestPdf/1002/10"
// outputFile: "phantomjs-output-<timestamp>.pdf"
// cookiesB64: (base64-encoded authentication cookies passed to request in PhantomJS)
var exitCode = await ExecuteSimpleAsync(filesPath, timeout, PhantomJSExePath + @"\phantomjs.exe",
scriptFile, requestUrl, outputFile, cookiesB64);
if (exitCode != 0)
return null;
return await GetFileContents(outputFile);
}
[Authorize]
[HttpGet]
[Route("TestReport/ForUserAndTestPdf/{userId}/{testId}")]
public async Task<HttpResponseMessage> ForUserAndTestPdfAsync(int userId, int testId) {
// produce a slightly-modified version of the current URL:
// /TestReport/ForUserAndTest/<userid>/<testid>
// => /TestReport/ForUserAndTestPdf/<userid>/<testid>?print=true
var url = Request.RequestUri.GetLocalPathWithParams("print=true").Replace("ForUserAndTest", "ForUserAndTestPdf");
// get the cookies used in the current request and convert to a base64-encoded JSON object
var cookiesB64 = Request.GetCookiesJsonB64();
var bytes = await PhantomJSHelpers.RenderPdfAsync(cookiesB64, url, "phantomjs-output", 60);
var message = new HttpResponseMessage(HttpStatusCode.OK);
message.Content = new StreamContent(new MemoryStream(bytes));
message.Content.Headers.ContentLength = bytes.Length;
message.Content.Headers.ContentType = new MediaTypeHeaderValue("application/pdf");
return message;
}
这是PhantomJS使用的“ rasterize_simple.js”脚本的相关部分,没有设置页面大小,Cookie等:
page.open(address, function(status) {
page.render(outputFilename);
phantom.exit(0);
});
所有这些的预期结果是生成的PDF文件,并且对该API方法(带有不同参数)的所有后续调用均可以正常工作。但是,副作用是一个完全损坏的站点:(
这里的任何帮助将不胜感激!
答案 0 :(得分:0)
恐怕您的ASP.NET应用程序的功能无法在Azure WebApp中正常运行,例如派生一个进程来运行PhantomJS并生成PDF文件,因为存在许多限制,不允许这样做,请参阅Kudu Wiki页面Azure Web App sandbox
了解更多信息。
我认为您有一些限制。
从HTML生成PDF 有多个库可用于将HTML转换为PDF。许多Windows / .NET特定版本使用IE API,因此广泛使用User32 / GDI32。这些API在沙盒中受到很大程度的阻止(无论计划如何),因此这些框架在沙盒中不起作用。
不受支持的框架 这是由于上述一个或多个限制而无法使用的框架和场景的列表。可以想象,随着沙箱的发展,将来会支持其中一些。
PDF生成器由于上述限制而失败:
融合 西伯力士 尖顶PDF 支持以下PDF生成器:
SQL报告框架:要求网站运行在Basic或更高版本上(请注意,当前该网站不适用于Consumptions模式下的Functions应用程序) EVOPDF:请参阅http://www.evopdf.com/azure-html-to-pdf-converter.aspx了解供应商解决方案 Telerik报告:要求站点在Basic或更高版本中运行。更多信息在这里 Rotativa / wkhtmltopdf:要求网站在Basic或更高版本中运行。 NReco PdfGenerator(wkhtmltopdf):需要基本或更高订阅计划 基于wkhtmltopdf或phantomjs的所有PDF生成器的已知问题:由于即使在基于VM的Azure Apps计划(基本或更高版本)中也存在沙箱GDI API限制,因此不呈现自定义字体(改用系统安装的字体)。
。不支持的其他方案:
PhantomJS / Selenium:尝试连接到本地地址,并且还使用GDI +。
有些框架没有充分利用User32 / GDI32(例如,wkhtmltopdf),我们正在以与启用SQL报表相同的方式在Basic +中启用这些框架。
本地地址请求 尝试连接到本地地址(例如localhost,127.0.0.1)和计算机自己的IP都将失败,除非同一沙箱中的另一个进程在目标端口上创建了侦听套接字。
解决方案是将应用程序部署在Azure VM而非WebApp上。
答案 1 :(得分:0)
发布自己的答案是因为Peter Pan's answer为我指明了正确的方向,但我找到了不同的解决方案。看来此问题是由于写入沙箱中的受保护区域(D:\ home中的任何内容)引起的。从Path.GetTempPath()
运行PhantomJS并在那里写文件似乎可以完全解决问题。
这不能解释发生了什么,但至少可以解决问题。