【问题标题】:Why does this PhantomJS process cause "Directory <x> does not exist." errors?为什么这个 PhantomJS 进程会导致“目录 <x> 不存在”。错误?
【发布时间】:2019-08-23 22:12:02
【问题描述】:

这是我的第一篇文章,如果问题需要一些更改,我们深表歉意。我已经尽可能地简化了这个问题,但是这里有很多组件,所以这篇文章非常庞大......

我们的 ASP.NET MVC 站点部署为 Azure 上的应用服务。我正在使用 API 控制器方法来生成存在于同一站点上的页面的 PDF。为此,控制器创建一个 PhantomJS 进程,等待成功,然后返回它创建的文件的内容。这一切都很好,但之后网站上的几个视图会产生如下错误:

“/”应用程序中的服务器错误。

目录 'D:\home\site\wwwroot\Views\Location' 不存在。无法开始监控文件更改。

说明:在执行当前 Web 请求期间发生了未处理的异常。请查看堆栈跟踪以获取有关错误及其源自代码的位置的更多信息。

异常详细信息:System.Web.HttpException:目录“D:\home\site\wwwroot\Views\Location”不存在。无法开始监控文件更改。

一段时间后,错误发生变化:

“/”应用程序中的服务器错误。

未找到视图“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 请求期间发生了未处理的异常。请查看堆栈跟踪以获取有关错误及其源自代码的位置的更多信息。

异常详细信息: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 方法的所有后续调用(使用不同的参数)都可以正常工作。然而,副作用是一个完全损坏的网站:(

这里的任何帮助将不胜感激!

【问题讨论】:

    标签: azure asp.net-mvc-4 asp.net-web-api phantomjs azure-web-app-service


    【解决方案1】:

    发布我自己的答案是因为Peter Pan's answer 为我指明了正确的方向,但我找到了不同的解决方案。该问题似乎是由写入沙盒中的受保护区域(D:\home 中的任何内容)引起的。从Path.GetTempPath() 运行 PhantomJS 并在那里写入文件似乎可以完全解决问题。

    这并不能准确解释发生了什么,但至少问题得到了解决。

    【讨论】:

    • 我遇到了同样的问题,但即使从临时路径 (D:\local\temp) 运行 PhantomJS 并将其配置为在那里写入临时文件,一段时间后我仍然面临同样的问题。我正在使用 NReco 的 PhantomJS 包装器,所以想知道它的工作设置可能与您的设置不同。
    • 好的,我发现是其他原因造成的。我仍在主进程中写入,而不是在 PhantomJS 中写入 d:\home 目录,但奇怪的是我已经这样做了一段时间没有问题,所以想知道是什么原因导致将单个文件写入那里导致之后要打破的沙盒。
    • 好的,当我要求它保存屏幕截图并将完整路径传递给主文件夹中的文件夹时,我发现一个代码路径仍在写入 d:\home 目录。在纠正最后一次出现后,问题就消失了!
    • 在查看 documentation 关于沙盒限制时,没有任何类似的弹出窗口,因此针对该 Github 存储库打开 an issue,看看他们是否可以提供任何见解。
    【解决方案2】:

    恐怕您的 ASP.NET 应用程序的功能在 Azure WebApp 中无法正常工作,例如 fork 一个进程以运行 PhantomJS 并生成 PDF 文件,因为有很多限制不允许这样做,请参考 Kudu wiki 页面Azure Web App sandbox 了解更多信息。

    我认为你有一些限制。

    1. 从 HTML 生成 PDF 有多个库用于将 HTML 转换为 PDF。许多 Windows/.NET 特定版本利用 IE API,因此广泛利用 User32/GDI32。这些 API 在沙箱中大部分被阻止(无论计划如何),因此这些框架在沙箱中不起作用。

    2. 不支持的框架 以下是由于上述一个或多个限制而被发现不可用的框架和场景的列表。可以想象,随着沙盒的发展,将来会支持一些。

      PDF 生成器因上述限制而失败:

      同步融合 西伯利亚 尖顶.PDF 支持以下 PDF 生成器:

      SQL 报告框架:要求网站在 Basic 或更高版本中运行(请注意,目前这在消耗模式下的 Functions 应用中不起作用) EVOPDF:有关供应商解决方案,请参阅http://www.evopdf.com/azure-html-to-pdf-converter.aspx Telerik 报告:要求站点以基本或更高版本运行。更多信息在这里 Rotativa / wkhtmltopdf:要求站点以基本或更高版本运行。 NReco PdfGenerator (wkhtmltopdf):需要订阅计划 Basic 或更高版本 所有基于 wkhtmltopdf 或 phantomjs 的 PDF 生成器的已知问题:由于沙盒 GDI API 限制,即使在基于 VM 的 Azure 应用计划(基本或更高版本)中也存在沙盒 GDI API 限制,因此不会呈现自定义字体(而是使用系统安装的字体) .

      其他不支持的场景:

      PhantomJS/Selenium:尝试连接本地地址,也使用 GDI+。

      有些框架没有广泛利用 User32/GDI32(例如 wkhtmltopdf),我们正在努力在 Basic+ 中启用这些框架,就像启用 SQL 报告一样。

    3. 本地地址请求 与本地地址(例如 localhost、127.0.0.1)和机器自身 IP 的连接尝试将失败,除非同一沙箱中的另一个进程在目标端口上创建了侦听套接字。

    解决方案是将应用部署在 Azure VM 上,而不是 WebApp。

    【讨论】:

    • 谢谢 - 我不能赞成这个答案,因为我的声誉太低 :( 但我将其标记为正确,至少现在是因为沙盒文档可能已经引导我找到解决方案。 .. 上面的一些问题适用(例如,自定义字体渲染不起作用),但是 PDF 的 正在生成 正确,该站点之后就中断了。一时兴起,我让 PhantomJS 从临时文件夹(通过 Path.GetTempPath() 和被记录在某处?
    • @Synival 除了 Azure WebApp 上的D:\home,在其他目录写入数据会导致Permission deniedAccess denied 错误。例如,通过touch 命令创建文件时会出现Permission denied 错误。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2023-04-10
    • 1970-01-01
    • 2012-05-04
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多