Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Changelog

All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [1.8.0] - 2022-06-03

### Added

- Add timeout support https://github.com/webgio/Rotativa/issues/203

### Fixed

- fix when WkhtmlDriver exits with error code https://github.com/webgio/Rotativa/issues/189
- fix using HttpContext.Current instead passed ControllerContext https://github.com/webgio/Rotativa/pull/178

100 changes: 79 additions & 21 deletions Rotativa.UnitTests/BinaryTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,44 +23,102 @@ namespace Rotativa.UnitTests
[TestFixture]
public class BinaryTests
{
private const string TestUrl = "https://github.com/webgio/Rotativa";

[Test]
public void Can_build_the_pdf_binary()
{
var localPath = Directory.GetParent(AppDomain.CurrentDomain.BaseDirectory);
var solutionDir = localPath.Parent.Parent.Parent.FullName;
var wkhtmltopdfPath = Path.Combine(solutionDir, "Rotativa", "Rotativa");
var actionResult = new UrlAsPdf("https://github.com/webgio/Rotativa")
{
WkhtmltopdfPath = wkhtmltopdfPath
};
var builder = new TestControllerBuilder();
var controller = new HomeController();
builder.InitializeController(controller);
var pdfBinary = actionResult.BuildPdf(controller.ControllerContext);
//Arrange
var actionResult = CreatePdfActionResult();
var controller = CreateTestController();

//Act
var pdfBinary = actionResult.BuildFile(controller.ControllerContext);

//Assert
var pdfTester = new PdfTester();
pdfTester.LoadPdf(pdfBinary);
pdfTester.PdfIsValid.Should().Be.True();
pdfTester.PdfContains("Rotativa").Should().Be.True();
}

[Test]
public void Can_build_the_image_binary()
public void Failed_to_build_the_pdf_binary_when_timeout_exceed()

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

some code refactor & create tests for checking timeout's

{
var localPath = Directory.GetParent(AppDomain.CurrentDomain.BaseDirectory);
var solutionDir = localPath.Parent.Parent.Parent.FullName;
var wkhtmltoimagePath = Path.Combine(solutionDir, "Rotativa", "Rotativa");
var actionResult = new UrlAsImage("https://github.com/webgio/Rotativa")
//Arrange
var actionResult = CreatePdfActionResult(convertTimeout: 1);
var controller = CreateTestController();
void TestMethod()
{
WkhtmlPath = wkhtmltoimagePath
};
var builder = new TestControllerBuilder();
var controller = new HomeController();
builder.InitializeController(controller);
actionResult.BuildFile(controller.ControllerContext);
}
//Act

//Assert
Assert.Throws<TimeoutException>(TestMethod);

}

[Test]
public void Can_build_the_image_binary()
{
//Arrange
var actionResult = CreateImageActionResult();
var controller = CreateTestController();

//Act
var imageBinary = actionResult.BuildFile(controller.ControllerContext);

//Assert
var image = Image.FromStream(new MemoryStream(imageBinary));
image.Should().Not.Be.Null();
image.RawFormat.Should().Be.EqualTo(ImageFormat.Jpeg);
}

[Test]
public void Failed_to_build_the_image_binary_when_timeout_exceed()
{
//Arrange
var actionResult = CreateImageActionResult(convertTimeout: 1);
var controller = CreateTestController();
void TestMethod()
{
actionResult.BuildFile(controller.ControllerContext);
}
//Act

//Assert
Assert.Throws<TimeoutException>(TestMethod);
}

private static AsImageResultBase CreateImageActionResult(string url = TestUrl, int? convertTimeout = null)
=> new UrlAsImage(url)
{
WkhtmlPath = GetWkhtmlPath(),
ConvertTimeout = convertTimeout,
};

private static AsPdfResultBase CreatePdfActionResult(string url = TestUrl, int? convertTimeout = null)
=> new UrlAsPdf(url)
{
WkhtmlPath = GetWkhtmlPath(),
ConvertTimeout = convertTimeout,
};

private static string GetWkhtmlPath()
{
var localPath = Directory.GetParent(AppDomain.CurrentDomain.BaseDirectory);
var solutionDir = localPath.Parent.Parent.Parent.FullName;
var wkhtmlPath = Path.Combine(solutionDir, "Rotativa", "Rotativa");
return wkhtmlPath;
}

private static Controller CreateTestController()
{
var builder = new TestControllerBuilder();
var controller = new HomeController();
builder.InitializeController(controller);
return controller;
}
}
}
2 changes: 1 addition & 1 deletion Rotativa/AsImageResultBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ public abstract class AsImageResultBase : AsResultBase

protected override byte[] WkhtmlConvert(string switches)
{
return WkhtmltoimageDriver.Convert(this.WkhtmlPath, switches);
return WkhtmltoimageDriver.Convert(this.WkhtmlPath, switches, timeout: ConvertTimeout);
}

protected override string GetContentType()
Expand Down
2 changes: 1 addition & 1 deletion Rotativa/AsPdfResultBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ protected AsPdfResultBase()

protected override byte[] WkhtmlConvert(string switches)
{
return WkhtmltopdfDriver.Convert(this.WkhtmlPath, switches);
return WkhtmltopdfDriver.Convert(this.WkhtmlPath, switches, timeout: ConvertTimeout);
}

protected override string GetContentType()
Expand Down
5 changes: 5 additions & 0 deletions Rotativa/AsResultBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,11 @@ public string CookieName

public ContentDisposition ContentDisposition { get; set; }

/// <summary>
/// Timeout for converting to PDF
/// </summary>
public int? ConvertTimeout { get; set; }

protected abstract string GetUrl(ControllerContext context);

/// <summary>
Expand Down
2 changes: 1 addition & 1 deletion Rotativa/Rotativa.nuspec
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<package >
<metadata>
<id>Rotativa</id>
<version>1.7.3</version>
<version>1.8.0</version>

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i've decided it is Minor, cuz I've added backwards compatible functionality to the public API (ConvertTimeout which is not required)
See https://semver.org/spec/v2.0.0.html#spec-item-7

<title>Rotativa</title>
<authors>Giorgio Bozio</authors>
<owners>Giorgio Bozio</owners>
Expand Down
2 changes: 1 addition & 1 deletion Rotativa/ViewAsImage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ protected override byte[] CallTheDriver(ControllerContext context)

ViewEngineResult viewResult = GetView(context, viewName, MasterName);
string html = context.GetHtmlFromView(viewResult, viewName, Model);
byte[] fileContent = WkhtmltoimageDriver.ConvertHtml(this.WkhtmlPath, this.GetConvertOptions(), html);
byte[] fileContent = WkhtmltoimageDriver.ConvertHtml(this.WkhtmlPath, this.GetConvertOptions(), html, timeout: ConvertTimeout);
return fileContent;
}
}
Expand Down
2 changes: 1 addition & 1 deletion Rotativa/ViewAsPdf.cs
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ protected override byte[] CallTheDriver(ControllerContext context)

ViewEngineResult viewResult = GetView(context, viewName, MasterName);
string html = context.GetHtmlFromView(viewResult, viewName, Model);
byte[] fileContent = WkhtmltopdfDriver.ConvertHtml(this.WkhtmlPath, this.GetConvertOptions(), html);
byte[] fileContent = WkhtmltopdfDriver.ConvertHtml(this.WkhtmlPath, this.GetConvertOptions(), html, ConvertTimeout);
return fileContent;
}
}
Expand Down
36 changes: 35 additions & 1 deletion Rotativa/WkhtmlDriver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
using System.Diagnostics;
using System.IO;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace Rotativa
{
Expand All @@ -15,7 +17,33 @@ public abstract class WkhtmlDriver
/// <param name="html">String containing HTML code that should be converted to PDF.</param>
/// <param name="wkhtmlExe"></param>
/// <returns>PDF as byte array.</returns>
protected static byte[] Convert(string wkhtmlPath, string switches, string html, string wkhtmlExe)
protected static byte[] Convert(string wkhtmlPath, string switches, string html, string wkhtmlExe, int? timeout = null)
{
if (!timeout.HasValue)
{
return ConvertExecute(wkhtmlPath, ref switches, ref html, wkhtmlExe);
}
else
{
var cancellationTokenSource = new CancellationTokenSource();
var task = Task.Run(() => ConvertExecute(wkhtmlPath, ref switches, ref html, wkhtmlExe, cancellationTokenSource.Token));

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is not as I could imagine best, but it works.

I've tried to do it by calling
proc.WaitForExit(milliseconds: timeeout)
but this requires that output of process is asynchronous.

I've tried to do it also by implementing events:

proc.OutputDataReceived += (sender, outputLine) => ...
proc.ErrorDataReceived += (sender, errorLine) => ...

but I failed to convert outputLine.Data to bytes[]. Thats cuz it is String, not Stream (as in proc.StandardOutput.BaseStream).

I've tried provide some encoding to process output and convert it, but any way I tried to do it, the resulted PDF was broken.

Finally I gave up and it that way

task.ConfigureAwait(false);
if (!task.Wait(timeout.Value))
{
cancellationTokenSource.Cancel();
throw new TimeoutException($"Timeout in converting given URL or HTML string to PDF after {timeout.Value}");
}

if (task.IsFaulted)
{
throw task.Exception ?? new Exception("Failed in converting given URL or HTML string to PDF");
}

return task.Result;
}
}

private static byte[] ConvertExecute(string wkhtmlPath, ref string switches, ref string html, string wkhtmlExe, CancellationToken cancellationToken = default)
{
// switches:
// "-q" - silent output, only errors - no progress messages
Expand Down Expand Up @@ -44,6 +72,12 @@ protected static byte[] Convert(string wkhtmlPath, string switches, string html,
CreateNoWindow = true
}
};
cancellationToken.Register(() =>
{
if (!proc.HasExited)
proc.Kill();

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

need to kill manually process when timeouted

});

proc.Start();

// generate PDF from given HTML string, not from URL
Expand Down
8 changes: 4 additions & 4 deletions Rotativa/WkhtmltoimageDriver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@ public class WkhtmltoimageDriver : WkhtmlDriver
/// <param name="switches">Switches that will be passed to wkhtmltopdf binary.</param>
/// <param name="html">String containing HTML code that should be converted to PDF.</param>
/// <returns>PDF as byte array.</returns>
public static byte[] ConvertHtml(string wkhtmltopdfPath, string switches, string html)
public static byte[] ConvertHtml(string wkhtmltopdfPath, string switches, string html, int? timeout = null)
{
return Convert(wkhtmltopdfPath, switches, html, wkhtmlExe);
return Convert(wkhtmltopdfPath, switches, html, wkhtmlExe, timeout: timeout);
}

/// <summary>
Expand All @@ -22,9 +22,9 @@ public static byte[] ConvertHtml(string wkhtmltopdfPath, string switches, string
/// <param name="wkhtmltopdfPath">Path to wkthmltopdf.</param>
/// <param name="switches">Switches that will be passed to wkhtmltopdf binary.</param>
/// <returns>PDF as byte array.</returns>
public static byte[] Convert(string wkhtmltopdfPath, string switches)
public static byte[] Convert(string wkhtmltopdfPath, string switches, int? timeout = null)
{
return Convert(wkhtmltopdfPath, switches, null, wkhtmlExe);
return Convert(wkhtmltopdfPath, switches, null, wkhtmlExe, timeout: timeout);
}
}
}
8 changes: 4 additions & 4 deletions Rotativa/WkhtmltopdfDriver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@ public class WkhtmltopdfDriver : WkhtmlDriver
/// <param name="switches">Switches that will be passed to wkhtmltopdf binary.</param>
/// <param name="html">String containing HTML code that should be converted to PDF.</param>
/// <returns>PDF as byte array.</returns>
public static byte[] ConvertHtml(string wkhtmltopdfPath, string switches, string html)
public static byte[] ConvertHtml(string wkhtmltopdfPath, string switches, string html, int? timeout = null)
{
return Convert(wkhtmltopdfPath, switches, html, wkhtmlExe);
return Convert(wkhtmltopdfPath, switches, html, wkhtmlExe, timeout: timeout);
}

/// <summary>
Expand All @@ -22,9 +22,9 @@ public static byte[] ConvertHtml(string wkhtmltopdfPath, string switches, string
/// <param name="wkhtmltopdfPath">Path to wkthmltopdf.</param>
/// <param name="switches">Switches that will be passed to wkhtmltopdf binary.</param>
/// <returns>PDF as byte array.</returns>
public static byte[] Convert(string wkhtmltopdfPath, string switches)
public static byte[] Convert(string wkhtmltopdfPath, string switches, int? timeout = null)
{
return Convert(wkhtmltopdfPath, switches, null, wkhtmlExe);
return Convert(wkhtmltopdfPath, switches, null, wkhtmlExe, timeout: timeout);
}
}
}