为什么此PhantomJS进程导致“目录<x>不存在”。错误?

时间:2019-04-02 14:28:13

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

这是我的第一篇文章,如果问题需要更改,我们深表歉意。我已经尽力解决了这个问题,但是这里有很多组件,因此这篇文章相当庞大...

我们的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方法(带有不同参数)的所有后续调用均可以正常工作。但是,副作用是一个完全损坏的站点:(

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

2 个答案:

答案 0 :(得分:0)

恐怕您的ASP.NET应用程序的功能无法在Azure WebApp中正常运行,例如派生一个进程来运行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或更高版本上(请注意,当前该网站不适用于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 +中启用这些框架。

  3.   

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

解决方案是将应用程序部署在Azure VM而非WebApp上。

答案 1 :(得分:0)

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

这不能解释发生了什么,但至少可以解决问题。