diff --git a/ISSUE_TEMPLATE.md b/ISSUE_TEMPLATE.md index b2f7b259f..88d747007 100644 --- a/ISSUE_TEMPLATE.md +++ b/ISSUE_TEMPLATE.md @@ -19,7 +19,7 @@ E.g. ```csharp // Arrange var options = new LaunchOptions { /* */ }; -var chromiumRevision = Downloader.DefaultRevision; +var chromiumRevision = BrowserFetcher.DefaultRevision; var browser = await Puppeteer.LaunchAsync(options, chromiumRevision); var page = browser.NewPageAsync(); diff --git a/README.md b/README.md index 83edf1961..3ef959227 100644 --- a/README.md +++ b/README.md @@ -25,11 +25,11 @@ Puppeteer Sharp is a .NET port of the official [Node.JS Puppeteer API](https://g ## Take screenshots ```cs -await Downloader.CreateDefault().DownloadRevisionAsync(chromiumRevision); +await Downloader.CreateDefault().DownloadAsync(chromiumRevision); var browser = await Puppeteer.LaunchAsync(new LaunchOptions { Headless = true -}, chromiumRevision); +}); var page = await browser.NewPageAsync(); await page.GoToAsync("http://www.google.com"); await page.ScreenshotAsync(outputFile); @@ -50,11 +50,11 @@ await page.SetViewport(new ViewPortOptions ## Generate PDF files ```cs -await Downloader.CreateDefault().DownloadRevisionAsync(chromiumRevision); +await new BrowserFetcher().DownloadAsync(BrowserFetcher.DefaultRevision); var browser = await Puppeteer.LaunchAsync(new LaunchOptions { Headless = true -}, chromiumRevision); +}); var page = await browser.NewPageAsync(); await page.GoToAsync("http://www.google.com"); await page.PdfAsync(outputFile); diff --git a/demos/PuppeteerSharpPdfDemo/Program.cs b/demos/PuppeteerSharpPdfDemo/Program.cs index df14829bb..3791d42b8 100644 --- a/demos/PuppeteerSharpPdfDemo/Program.cs +++ b/demos/PuppeteerSharpPdfDemo/Program.cs @@ -16,10 +16,10 @@ public static async Task Main(string[] args) }; Console.WriteLine("Downloading chromium"); - await Downloader.CreateDefault().DownloadRevisionAsync(Downloader.DefaultRevision); + await new BrowserFetcher().DownloadAsync(BrowserFetcher.DefaultRevision); Console.WriteLine("Navigating google"); - using (var browser = await Puppeteer.LaunchAsync(options, Downloader.DefaultRevision)) + using (var browser = await Puppeteer.LaunchAsync(options)) using (var page = await browser.NewPageAsync()) { await page.GoToAsync("http://www.google.com"); diff --git a/docfx_project/api/index.md b/docfx_project/api/index.md index 79ac4f006..033a8b78d 100644 --- a/docfx_project/api/index.md +++ b/docfx_project/api/index.md @@ -7,11 +7,11 @@ Puppeteer Sharp is a .NET port of the official [Node.JS Puppeteer API](https://g ## Take screenshots ```cs -await Downloader.CreateDefault().DownloadRevisionAsync(chromiumRevision); +await Downloader.CreateDefault().DownloadAsync(chromiumRevision); var browser = await Puppeteer.LaunchAsync(new LaunchOptions { Headless = true -}, chromiumRevision); +}); var page = await browser.NewPageAsync(); await page.GoToAsync("http://www.google.com"); await page.ScreenshotAsync(outputFile); @@ -32,11 +32,11 @@ await page.SetViewport(new ViewPortOptions ## Generate PDF files ```cs -await Downloader.CreateDefault().DownloadRevisionAsync(chromiumRevision); +await new BrowserFetcher().DownloadAsync(BrowserFetcher.DefaultRevision); var browser = await Puppeteer.LaunchAsync(new LaunchOptions { Headless = true -}, chromiumRevision); +}); var page = await browser.NewPageAsync(); await page.GoToAsync("http://www.google.com"); await page.PdfAsync(outputFile); diff --git a/docfx_project/index.md b/docfx_project/index.md index fb5ab3983..921b333d0 100644 --- a/docfx_project/index.md +++ b/docfx_project/index.md @@ -7,11 +7,11 @@ Puppeteer Sharp is a .NET port of the official [Node.JS Puppeteer API](https://g ## Take screenshots ```cs -await Downloader.CreateDefault().DownloadRevisionAsync(chromiumRevision); +await Downloader.CreateDefault().DownloadAsync(chromiumRevision); var browser = await Puppeteer.LaunchAsync(new LaunchOptions { Headless = true -}, chromiumRevision); +}); var page = await browser.NewPageAsync(); await page.GoToAsync("http://www.google.com"); await page.ScreenshotAsync(outputFile); @@ -32,11 +32,11 @@ await page.SetViewport(new ViewPortOptions ## Generate PDF files ```cs -await Downloader.CreateDefault().DownloadRevisionAsync(chromiumRevision); +await new BrowserFetcher().DownloadAsync(BrowserFetcher.DefaultRevision); var browser = await Puppeteer.LaunchAsync(new LaunchOptions { Headless = true -}, chromiumRevision); +}); var page = await browser.NewPageAsync(); await page.GoToAsync("http://www.google.com"); await page.PdfAsync(outputFile); diff --git a/lib/PuppeteerSharp.TestServer/wwwroot/chromium-linux.zip b/lib/PuppeteerSharp.TestServer/wwwroot/chromium-linux.zip new file mode 100644 index 000000000..9c00ec080 Binary files /dev/null and b/lib/PuppeteerSharp.TestServer/wwwroot/chromium-linux.zip differ diff --git a/lib/PuppeteerSharp.Tests/BrowserTests/Events/DisconnectedTests.cs b/lib/PuppeteerSharp.Tests/BrowserTests/Events/DisconnectedTests.cs index a1a268522..823accc8f 100644 --- a/lib/PuppeteerSharp.Tests/BrowserTests/Events/DisconnectedTests.cs +++ b/lib/PuppeteerSharp.Tests/BrowserTests/Events/DisconnectedTests.cs @@ -14,7 +14,7 @@ public DisconnectedTests(ITestOutputHelper output) : base(output) [Fact] public async Task ShouldEmittedWhenBrowserGetsClosedDisconnectedOrUnderlyingWebsocketGetsClosed() { - var originalBrowser = await Puppeteer.LaunchAsync(TestConstants.DefaultBrowserOptions(), TestConstants.ChromiumRevision, TestConstants.LoggerFactory); + var originalBrowser = await Puppeteer.LaunchAsync(TestConstants.DefaultBrowserOptions(), TestConstants.LoggerFactory); var connectOptions = new ConnectOptions { BrowserWSEndpoint = originalBrowser.WebSocketEndpoint }; var remoteBrowser1 = await Puppeteer.ConnectAsync(connectOptions, TestConstants.LoggerFactory); var remoteBrowser2 = await Puppeteer.ConnectAsync(connectOptions, TestConstants.LoggerFactory); @@ -37,4 +37,4 @@ public async Task ShouldEmittedWhenBrowserGetsClosedDisconnectedOrUnderlyingWebs Assert.Equal(1, disconnectedRemote2); } } -} +} \ No newline at end of file diff --git a/lib/PuppeteerSharp.Tests/Issues/Issue0128.cs b/lib/PuppeteerSharp.Tests/Issues/Issue0128.cs index 514a28703..8c693e280 100644 --- a/lib/PuppeteerSharp.Tests/Issues/Issue0128.cs +++ b/lib/PuppeteerSharp.Tests/Issues/Issue0128.cs @@ -15,8 +15,8 @@ await Assert.ThrowsAsync(async () => { var options = TestConstants.DefaultBrowserOptions(); options.Args = new[] { "-remote-debugging-port=-2" }; - await Puppeteer.LaunchAsync(options, TestConstants.ChromiumRevision, TestConstants.LoggerFactory); + await Puppeteer.LaunchAsync(options, TestConstants.LoggerFactory); }); } } -} +} \ No newline at end of file diff --git a/lib/PuppeteerSharp.Tests/PuppeteerBrowserBaseTest.cs b/lib/PuppeteerSharp.Tests/PuppeteerBrowserBaseTest.cs index b6c576abd..cbe91b223 100644 --- a/lib/PuppeteerSharp.Tests/PuppeteerBrowserBaseTest.cs +++ b/lib/PuppeteerSharp.Tests/PuppeteerBrowserBaseTest.cs @@ -24,7 +24,7 @@ public PuppeteerBrowserBaseTest(ITestOutputHelper output) : base(output) protected virtual async Task InitializeAsync() { - Browser = await Puppeteer.LaunchAsync(TestConstants.DefaultBrowserOptions(), TestConstants.ChromiumRevision, TestConstants.LoggerFactory); + Browser = await Puppeteer.LaunchAsync(TestConstants.DefaultBrowserOptions(), TestConstants.LoggerFactory); } protected virtual async Task DisposeAsync() => await Browser.CloseAsync(); diff --git a/lib/PuppeteerSharp.Tests/PuppeteerLoaderFixture.cs b/lib/PuppeteerSharp.Tests/PuppeteerLoaderFixture.cs index 06a74796a..c3cbbdf4a 100644 --- a/lib/PuppeteerSharp.Tests/PuppeteerLoaderFixture.cs +++ b/lib/PuppeteerSharp.Tests/PuppeteerLoaderFixture.cs @@ -21,7 +21,7 @@ public void Dispose() private async Task SetupAsync() { - var downloaderTask = Downloader.CreateDefault().DownloadRevisionAsync(TestConstants.ChromiumRevision); + var downloaderTask = new BrowserFetcher().DownloadAsync(BrowserFetcher.DefaultRevision); Server = SimpleServer.Create(TestConstants.Port, TestUtils.FindParentDirectory("PuppeteerSharp.TestServer")); HttpsServer = SimpleServer.CreateHttps(TestConstants.HttpsPort, TestUtils.FindParentDirectory("PuppeteerSharp.TestServer")); @@ -32,4 +32,4 @@ private async Task SetupAsync() await Task.WhenAll(downloaderTask, serverStart, httpsServerStart); } } -} +} \ No newline at end of file diff --git a/lib/PuppeteerSharp.Tests/PuppeteerTests/AppModeTests.cs b/lib/PuppeteerSharp.Tests/PuppeteerTests/AppModeTests.cs index cbbd7c64f..1e55c284c 100644 --- a/lib/PuppeteerSharp.Tests/PuppeteerTests/AppModeTests.cs +++ b/lib/PuppeteerSharp.Tests/PuppeteerTests/AppModeTests.cs @@ -20,7 +20,7 @@ public async Task ShouldWork() var options = TestConstants.DefaultBrowserOptions(); options.AppMode = true; - using (var browser = await Puppeteer.LaunchAsync(options, TestConstants.ChromiumRevision, TestConstants.LoggerFactory)) + using (var browser = await Puppeteer.LaunchAsync(options, TestConstants.LoggerFactory)) using (var page = await browser.NewPageAsync()) { Assert.Equal(121, await page.EvaluateExpressionAsync("11 * 11")); diff --git a/lib/PuppeteerSharp.Tests/PuppeteerTests/BrowserFetcherTests.cs b/lib/PuppeteerSharp.Tests/PuppeteerTests/BrowserFetcherTests.cs new file mode 100644 index 000000000..08458ede5 --- /dev/null +++ b/lib/PuppeteerSharp.Tests/PuppeteerTests/BrowserFetcherTests.cs @@ -0,0 +1,42 @@ +using System; +using System.IO; +using System.Threading.Tasks; +using Xunit; +using Xunit.Abstractions; + +namespace PuppeteerSharp.Tests.PuppeteerTests +{ + [Collection("PuppeteerLoaderFixture collection")] + public class BrowserFetcherTests : PuppeteerBaseTest + { + public BrowserFetcherTests(ITestOutputHelper output) : base(output) { } + + [Fact] + public async Task ShouldDownloadAndExtractLinuxBinary() + { + var downloadsFolder = Path.Combine(Directory.GetCurrentDirectory(), ".test-chromium"); + var browserFetcher = Puppeteer.CreateBrowserFetcher(new BrowserFetcherOptions + { + Platform = Platform.Linux, + Path = downloadsFolder, + Host = TestConstants.ServerUrl + }); + var revisionInfo = browserFetcher.RevisionInfo(123456); + + Server.SetRedirect(revisionInfo.Url.Substring(TestConstants.ServerUrl.Length), "/chromium-linux.zip"); + Assert.False(revisionInfo.Local); + Assert.Equal(Platform.Linux, revisionInfo.Platform); + Assert.False(await browserFetcher.CanDownloadAsync(100000)); + Assert.True(await browserFetcher.CanDownloadAsync(123456)); + + revisionInfo = await browserFetcher.DownloadAsync(123456); + Assert.True(revisionInfo.Local); + Assert.Equal("LINUX BINARY\n", File.ReadAllText(revisionInfo.ExecutablePath)); + Assert.Equal(new[] { 123456 }, browserFetcher.LocalRevisions()); + browserFetcher.Remove(123456); + Assert.Empty(browserFetcher.LocalRevisions()); + + new DirectoryInfo(downloadsFolder).Delete(true); + } + } +} \ No newline at end of file diff --git a/lib/PuppeteerSharp.Tests/PuppeteerTests/DownloaderTests.cs b/lib/PuppeteerSharp.Tests/PuppeteerTests/DownloaderTests.cs deleted file mode 100644 index fd11bbebe..000000000 --- a/lib/PuppeteerSharp.Tests/PuppeteerTests/DownloaderTests.cs +++ /dev/null @@ -1,26 +0,0 @@ -using System; -using System.IO; -using System.Threading.Tasks; -using Xunit; - -namespace PuppeteerSharp.Tests.PuppeteerTests -{ - public class DownloaderTests - { - [Fact(Skip = "Long run")] - public async Task ShouldDownloadChromium() - { - var downloadsFolder = Path.Combine(Directory.GetCurrentDirectory(), ".test-chromium"); - var dirInfo = new DirectoryInfo(downloadsFolder); - var downloader = new Downloader(downloadsFolder); - - if (dirInfo.Exists) - { - dirInfo.Delete(true); - } - - await downloader.DownloadRevisionAsync(TestConstants.ChromiumRevision); - Assert.True(new FileInfo(downloader.GetExecutablePath(TestConstants.ChromiumRevision)).Exists); - } - } -} diff --git a/lib/PuppeteerSharp.Tests/PuppeteerTests/PuppeteerLaunchTests.cs b/lib/PuppeteerSharp.Tests/PuppeteerTests/PuppeteerLaunchTests.cs index ce519d625..1b2f8d407 100644 --- a/lib/PuppeteerSharp.Tests/PuppeteerTests/PuppeteerLaunchTests.cs +++ b/lib/PuppeteerSharp.Tests/PuppeteerTests/PuppeteerLaunchTests.cs @@ -20,7 +20,7 @@ public async Task ShouldSupportIgnoreHTTPSErrorsOption() var options = TestConstants.DefaultBrowserOptions(); options.IgnoreHTTPSErrors = true; - using (var browser = await Puppeteer.LaunchAsync(options, TestConstants.ChromiumRevision, TestConstants.LoggerFactory)) + using (var browser = await Puppeteer.LaunchAsync(options, TestConstants.LoggerFactory)) using (var page = await browser.NewPageAsync()) { var response = await page.GoToAsync(TestConstants.HttpsPrefix + "/empty.html"); @@ -39,7 +39,7 @@ public async Task NetworkRedirectsShouldReportSecurityDetails() HttpsServer.SetRedirect("/plzredirect", "/empty.html"); - using (var browser = await Puppeteer.LaunchAsync(options, TestConstants.ChromiumRevision, TestConstants.LoggerFactory)) + using (var browser = await Puppeteer.LaunchAsync(options, TestConstants.LoggerFactory)) using (var page = await browser.NewPageAsync()) { page.Response += (sender, e) => responses.Add(e.Response); @@ -57,7 +57,7 @@ public async Task ShouldWorkInRealLife() { var options = TestConstants.DefaultBrowserOptions(); - using (var browser = await Puppeteer.LaunchAsync(options, TestConstants.ChromiumRevision, TestConstants.LoggerFactory)) + using (var browser = await Puppeteer.LaunchAsync(options, TestConstants.LoggerFactory)) using (var page = await browser.NewPageAsync()) { var response = await page.GoToAsync("https://www.google.com"); @@ -70,7 +70,7 @@ public async Task ShouldWorkInRealLifeWithOptions() { var options = TestConstants.DefaultBrowserOptions(); - using (var browser = await Puppeteer.LaunchAsync(options, TestConstants.ChromiumRevision, TestConstants.LoggerFactory)) + using (var browser = await Puppeteer.LaunchAsync(options, TestConstants.LoggerFactory)) using (var page = await browser.NewPageAsync()) { var response = await page.GoToAsync( @@ -89,7 +89,7 @@ public async Task ShouldRejectAllPromisesWhenBrowserIsClosed() { using (var browser = await Puppeteer.LaunchAsync( TestConstants.DefaultBrowserOptions(), - TestConstants.ChromiumRevision, TestConstants.LoggerFactory)) + TestConstants.LoggerFactory)) using (var page = await browser.NewPageAsync()) { var neverResolves = page.EvaluateFunctionHandleAsync("() => new Promise(r => {})"); @@ -107,7 +107,7 @@ public async Task ShouldRejectIfExecutablePathIsInvalid() var exception = await Assert.ThrowsAsync(() => { - return Puppeteer.LaunchAsync(options, TestConstants.ChromiumRevision, TestConstants.LoggerFactory); + return Puppeteer.LaunchAsync(options, TestConstants.LoggerFactory); }); Assert.Equal("Failed to launch chrome! path to executable does not exist", exception.Message); @@ -122,7 +122,7 @@ public async Task UserDataDirOption() var options = TestConstants.DefaultBrowserOptions(); options.UserDataDir = userDataDir; - using (var browser = await launcher.LaunchAsync(options, TestConstants.ChromiumRevision)) + using (var browser = await launcher.LaunchAsync(options)) { Assert.True(Directory.GetFiles(userDataDir).Length > 0); await browser.CloseAsync(); @@ -139,7 +139,7 @@ public async Task UserDataDirArgument() var options = TestConstants.DefaultBrowserOptions(); options.Args = options.Args.Concat(new[] { $"--user-data-dir=\"{userDataDir}\"" }).ToArray(); - using (var browser = await launcher.LaunchAsync(options, TestConstants.ChromiumRevision)) + using (var browser = await launcher.LaunchAsync(options)) { Assert.True(Directory.GetFiles(userDataDir).Length > 0); await browser.CloseAsync(); @@ -156,14 +156,14 @@ public async Task UserDataDirOptionShouldRestoreState() var options = TestConstants.DefaultBrowserOptions(); options.Args = options.Args.Concat(new[] { $"--user-data-dir=\"{userDataDir}\"" }).ToArray(); - using (var browser = await launcher.LaunchAsync(options, TestConstants.ChromiumRevision)) + using (var browser = await launcher.LaunchAsync(options)) { var page = await browser.NewPageAsync(); await page.GoToAsync(TestConstants.EmptyPage); await page.EvaluateExpressionAsync("localStorage.hey = 'hello'"); } - using (var browser2 = await Puppeteer.LaunchAsync(options, TestConstants.ChromiumRevision, TestConstants.LoggerFactory)) + using (var browser2 = await Puppeteer.LaunchAsync(options, TestConstants.LoggerFactory)) { var page2 = await browser2.NewPageAsync(); await page2.GoToAsync(TestConstants.EmptyPage); @@ -181,7 +181,7 @@ public async Task UserDataDirOptionShouldRestoreCookies() var options = TestConstants.DefaultBrowserOptions(); options.Args = options.Args.Concat(new[] { $"--user-data-dir=\"{userDataDir}\"" }).ToArray(); - using (var browser = await launcher.LaunchAsync(options, TestConstants.ChromiumRevision)) + using (var browser = await launcher.LaunchAsync(options)) { var page = await browser.NewPageAsync(); await page.GoToAsync(TestConstants.EmptyPage); @@ -189,7 +189,7 @@ await page.EvaluateExpressionAsync( "document.cookie = 'doSomethingOnlyOnce=true; expires=Fri, 31 Dec 9999 23:59:59 GMT'"); } - using (var browser2 = await Puppeteer.LaunchAsync(options, TestConstants.ChromiumRevision, TestConstants.LoggerFactory)) + using (var browser2 = await Puppeteer.LaunchAsync(options, TestConstants.LoggerFactory)) { var page2 = await browser2.NewPageAsync(); await page2.GoToAsync(TestConstants.EmptyPage); @@ -208,7 +208,7 @@ public async Task HeadlessShouldBeAbleToReadCookiesWrittenByHeadful() options.Args = options.Args.Concat(new[] { $"--user-data-dir=\"{userDataDir}\"" }).ToArray(); options.Headless = false; - using (var browser = await launcher.LaunchAsync(options, TestConstants.ChromiumRevision)) + using (var browser = await launcher.LaunchAsync(options)) { var page = await browser.NewPageAsync(); await page.GoToAsync(TestConstants.EmptyPage); @@ -217,7 +217,7 @@ await page.EvaluateExpressionAsync( } options.Headless = true; - using (var browser2 = await Puppeteer.LaunchAsync(options, TestConstants.ChromiumRevision, TestConstants.LoggerFactory)) + using (var browser2 = await Puppeteer.LaunchAsync(options, TestConstants.LoggerFactory)) { var page2 = await browser2.NewPageAsync(); await page2.GoToAsync(TestConstants.EmptyPage); @@ -240,7 +240,7 @@ public async Task ChromeShouldBeClosed() var options = TestConstants.DefaultBrowserOptions(); var launcher = new Launcher(TestConstants.LoggerFactory); - using (var browser = await launcher.LaunchAsync(options, TestConstants.ChromiumRevision)) + using (var browser = await launcher.LaunchAsync(options)) using (var page = await browser.NewPageAsync()) { var response = await page.GoToAsync(TestConstants.EmptyPage); @@ -258,7 +258,7 @@ public async Task ChromeShouldBeClosedOnDispose() var options = TestConstants.DefaultBrowserOptions(); var launcher = new Launcher(TestConstants.LoggerFactory); - using (var browser = await launcher.LaunchAsync(options, TestConstants.ChromiumRevision)) + using (var browser = await launcher.LaunchAsync(options)) using (var page = await browser.NewPageAsync()) { var response = await page.GoToAsync(TestConstants.EmptyPage); @@ -272,18 +272,14 @@ public async Task ChromeShouldBeClosedOnDispose() public async Task ShouldNotOpenTwoChromesUsingTheSameLauncher() { var launcher = new Launcher(TestConstants.LoggerFactory); - using (var browser = await launcher.LaunchAsync( - TestConstants.DefaultBrowserOptions(), - TestConstants.ChromiumRevision)) + using (var browser = await launcher.LaunchAsync(TestConstants.DefaultBrowserOptions())) { var exception = await Assert.ThrowsAsync(() => { - return launcher.LaunchAsync( - TestConstants.DefaultBrowserOptions(), - TestConstants.ChromiumRevision); + return launcher.LaunchAsync(TestConstants.DefaultBrowserOptions()); }); Assert.Equal("Unable to create or connect to another chromium process", exception.Message); } } } -} +} \ No newline at end of file diff --git a/lib/PuppeteerSharp.Tests/TestConstants.cs b/lib/PuppeteerSharp.Tests/TestConstants.cs index 1042fbe33..13b2d35bf 100644 --- a/lib/PuppeteerSharp.Tests/TestConstants.cs +++ b/lib/PuppeteerSharp.Tests/TestConstants.cs @@ -14,7 +14,6 @@ public static class TestConstants public const string ServerUrl = "http://localhost:8907"; public const string ServerIpUrl = "http://127.0.0.1:8907"; public const string HttpsPrefix = "https://localhost:8908"; - public const int ChromiumRevision = Downloader.DefaultRevision; public const string AboutBlank = "about:blank"; public static readonly string CrossProcessHttpPrefix = "http://127.0.0.1:8907"; public static readonly string EmptyPage = $"{ServerUrl}/empty.html"; @@ -49,4 +48,4 @@ public static void SetupLogging(ITestOutputHelper output) } } } -} +} \ No newline at end of file diff --git a/lib/PuppeteerSharp/Browser.cs b/lib/PuppeteerSharp/Browser.cs index 4aa2d47f5..3fb5c3eff 100644 --- a/lib/PuppeteerSharp/Browser.cs +++ b/lib/PuppeteerSharp/Browser.cs @@ -16,7 +16,7 @@ namespace PuppeteerSharp /// An example of using a to create a : /// /// : /// /// + /// BrowserFetcher can download and manage different versions of Chromium. + /// BrowserFetcher operates on revision strings that specify a precise version of Chromium, e.g. 533271. Revision strings can be obtained from omahaproxy.appspot.com. + /// + /// + /// Example on how to use BrowserFetcher to download a specific version of Chromium and run Puppeteer against it: + /// + /// var browserFetcher = Puppeteer.CreateBrowserFetcher(); + /// var revisionInfo = await browserFetcher.DownloadAsync(533271); + /// var browser = await await Puppeteer.LaunchAsync(new LaunchOptions { ExecutablePath = revisionInfo.ExecutablePath}); + /// + /// + public class BrowserFetcher + { + private const string DefaultDownloadHost = "https://storage.googleapis.com"; + private static readonly Dictionary _downloadUrls = new Dictionary { + {Platform.Linux, "{0}/chromium-browser-snapshots/Linux_x64/{1}/chrome-linux.zip"}, + {Platform.MacOS, "{0}/chromium-browser-snapshots/Mac/{1}/chrome-mac.zip"}, + {Platform.Win32, "{0}/chromium-browser-snapshots/Win/{1}/chrome-win32.zip"}, + {Platform.Win64, "{0}/chromium-browser-snapshots/Win_x64/{1}/chrome-win32.zip"} + }; + + /// + /// Default chromiumg revision. + /// + public const int DefaultRevision = 536395; + + /// + /// Gets the downloads folder. + /// + /// The downloads folder. + public string DownloadsFolder { get; } + /// + /// A download host to be used. Defaults to https://storage.googleapis.com. + /// + /// The download host. + public string DownloadHost { get; } + /// + /// Gets the platform. + /// + /// The platform. + public Platform Platform { get; } + /// + /// Occurs when download progress in changes. + /// + public event DownloadProgressChangedEventHandler DownloadProgressChanged; + + /// + /// Initializes a new instance of the class. + /// + public BrowserFetcher() + { + DownloadsFolder = Path.Combine(Directory.GetCurrentDirectory(), ".local-chromium"); + DownloadHost = DefaultDownloadHost; + Platform = GetCurrentPlatform(); + } + + /// + /// Initializes a new instance of the class. + /// + /// Fetch options. + public BrowserFetcher(BrowserFetcherOptions options) + { + DownloadsFolder = string.IsNullOrEmpty(options.Path) ? + Path.Combine(Directory.GetCurrentDirectory(), ".local-chromium") : + options.Path; + DownloadHost = string.IsNullOrEmpty(options.Host) ? DefaultDownloadHost : options.Host; + Platform = options.Platform ?? GetCurrentPlatform(); + } + + #region Public Methods + + /// + /// The method initiates a HEAD request to check if the revision is available. + /// + /// Whether the version is available or not. + /// A revision to check availability. + public async Task CanDownloadAsync(int revision) + { + var url = string.Format(_downloadUrls[Platform], DownloadHost, revision); + + var client = new HttpClient(); + var response = await client.SendAsync(new HttpRequestMessage + { + RequestUri = new Uri(url), + Method = HttpMethod.Head + }); + + return response.IsSuccessStatusCode; + } + + /// + /// A list of all revisions available locally on disk. + /// + /// The available revisions. + public IEnumerable LocalRevisions() + { + var directoryInfo = new DirectoryInfo(DownloadsFolder); + + if (directoryInfo.Exists) + { + return directoryInfo.GetDirectories().Select(d => GetRevisionFromPath(d.Name)).Where(v => v > 0); + } + + return new int[] { }; + } + + /// + /// Removes a downloaded revision. + /// + /// Revision to remove. + public void Remove(int revision) + { + var directory = new DirectoryInfo(GetFolderPath(revision)); + if (directory.Exists) + { + directory.Delete(true); + } + } + + /// + /// Gets the revision info. + /// + /// Revision info. + /// A revision to get info for. + public RevisionInfo RevisionInfo(int revision) + { + var result = new RevisionInfo + { + FolderPath = GetFolderPath(revision), + Url = string.Format(_downloadUrls[Platform], DownloadHost, revision), + Revision = revision, + Platform = Platform + }; + result.ExecutablePath = GetExecutablePath(Platform, result.FolderPath); + result.Local = new DirectoryInfo(result.FolderPath).Exists; + + return result; + } + + /// + /// Downloads the revision. + /// + /// Task which resolves to the completed download. + /// Revision. + public async Task DownloadAsync(int revision) + { + var url = string.Format(_downloadUrls[Platform], DownloadHost, revision); + var zipPath = Path.Combine(DownloadsFolder, $"download-{Platform.ToString()}-{revision}.zip"); + var folderPath = GetFolderPath(revision); + + if (new DirectoryInfo(folderPath).Exists) + { + return null; + } + + var downloadFolder = new DirectoryInfo(DownloadsFolder); + if (!downloadFolder.Exists) + { + downloadFolder.Create(); + } + + var webClient = new WebClient(); + + if (DownloadProgressChanged != null) + { + webClient.DownloadProgressChanged += DownloadProgressChanged; + } + await webClient.DownloadFileTaskAsync(new Uri(url), zipPath); + + if (Platform == Platform.MacOS) + { + //ZipFile and many others unzip libraries have issues extracting .app files + //Until we have a clear solution we'll call the native unzip tool + //https://github.com/dotnet/corefx/issues/15516 + NativeExtractToDirectory(zipPath, folderPath); + } + else + { + ZipFile.ExtractToDirectory(zipPath, folderPath); + } + + new FileInfo(zipPath).Delete(); + + return RevisionInfo(revision); + } + + /// + /// Gets the executable path for a revision. + /// + /// The executable path. + /// Revision. + public string GetExecutablePath(int revision) + { + return GetExecutablePath(Platform, GetFolderPath(revision)); + } + + /// + /// Gets the executable path. + /// + /// The executable path. + /// Platform. + /// Folder path. + public static string GetExecutablePath(Platform platform, string folderPath) + { + switch (platform) + { + case Platform.MacOS: + return Path.Combine(folderPath, "chrome-mac", "Chromium.app", "Contents", + "MacOS", "Chromium"); + case Platform.Linux: + return Path.Combine(folderPath, "chrome-linux", "chrome"); + case Platform.Win32: + case Platform.Win64: + return Path.Combine(folderPath, "chrome-win32", "chrome.exe"); + default: + throw new ArgumentException("Invalid platform", nameof(platform)); + } + } + + #endregion + + #region Private Methods + + private static Platform GetCurrentPlatform() + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + return Platform.MacOS; + } + if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + return Platform.Linux; + } + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + return RuntimeInformation.OSArchitecture == Architecture.X64 ? Platform.Win64 : Platform.Win32; + } + + return Platform.Unknown; + } + private string GetFolderPath(int revision) + { + return Path.Combine(DownloadsFolder, $"{Platform.ToString()}-{revision}"); + } + + private void NativeExtractToDirectory(string zipPath, string folderPath) + { + var process = new Process(); + process.StartInfo.FileName = "unzip"; + process.StartInfo.Arguments = $"{zipPath} -d {folderPath}"; + process.Start(); + process.WaitForExit(); + } + + private int GetRevisionFromPath(string folderName) + { + var splits = folderName.Split('-'); + if (splits.Length != 2) + { + return 0; + } + Platform platform; + if (!Enum.TryParse(splits[0], out platform)) + { + platform = Platform.Unknown; + } + if (!_downloadUrls.Keys.Contains(platform)) + { + return 0; + } + int.TryParse(splits[1], out var revision); + return revision; + } + + #endregion + } +} \ No newline at end of file diff --git a/lib/PuppeteerSharp/BrowserFetcherOptions.cs b/lib/PuppeteerSharp/BrowserFetcherOptions.cs new file mode 100644 index 000000000..9df5a9a23 --- /dev/null +++ b/lib/PuppeteerSharp/BrowserFetcherOptions.cs @@ -0,0 +1,21 @@ +namespace PuppeteerSharp +{ + /// + /// Browser fetcher options used to construct a + /// + public class BrowserFetcherOptions + { + /// + /// Platform, defaults to currenct platform. + /// + public Platform? Platform { get; set; } + /// + /// A path for the downloads folder. Defaults to [root]/.local-chromium, where [root] is where the project binaries are located. + /// + public string Path { get; set; } + /// + /// A download host to be used. Defaults to https://storage.googleapis.com. + /// + public string Host { get; set; } + } +} \ No newline at end of file diff --git a/lib/PuppeteerSharp/Downloader.cs b/lib/PuppeteerSharp/Downloader.cs deleted file mode 100644 index 4603cd3a8..000000000 --- a/lib/PuppeteerSharp/Downloader.cs +++ /dev/null @@ -1,178 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.IO; -using System.IO.Compression; -using System.Net; -using System.Runtime.InteropServices; -using System.Threading.Tasks; - -namespace PuppeteerSharp -{ - /// - /// Downloader class used to download a chromium version from Google. - /// - public class Downloader - { - private readonly string _downloadsFolder; - private const string DefaultDownloadHost = "https://storage.googleapis.com"; - private static readonly Dictionary _downloadUrls = new Dictionary { - {Platform.Linux, "{0}/chromium-browser-snapshots/Linux_x64/{1}/chrome-linux.zip"}, - {Platform.MacOS, "{0}/chromium-browser-snapshots/Mac/{1}/chrome-mac.zip"}, - {Platform.Win32, "{0}/chromium-browser-snapshots/Win/{1}/chrome-win32.zip"}, - {Platform.Win64, "{0}/chromium-browser-snapshots/Win_x64/{1}/chrome-win32.zip"} - }; - private string _downloadHost; - - /// - /// Default chromiumg revision. - /// - public const int DefaultRevision = 536395; - - /// - /// Initializes a new instance of the class. - /// - /// Downloads folder. - public Downloader(string downloadsFolder) - { - _downloadsFolder = downloadsFolder; - _downloadHost = DefaultDownloadHost; - } - - #region Public Methods - - /// - /// Creates a class specifing a default download folder. - /// - /// A new downloader. - public static Downloader CreateDefault() - { - var downloadsFolder = Path.Combine(Directory.GetCurrentDirectory(), ".local-chromium"); - return new Downloader(downloadsFolder); - } - - internal static Platform CurrentPlatform - { - get - { - if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) - { - return Platform.MacOS; - } - if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) - { - return Platform.Linux; - } - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - { - return RuntimeInformation.OSArchitecture == Architecture.X64 ? Platform.Win64 : Platform.Win32; - } - - return Platform.Unknown; - } - } - - internal RevisionInfo RevisionInfo(Platform platform, int revision) - { - var result = new RevisionInfo - { - FolderPath = GetFolderPath(platform, revision), - Revision = revision - }; - result.ExecutablePath = GetExecutablePath(platform, result.FolderPath); - return result; - } - - /// - /// Downloads the revision. - /// - /// Task which resolves to the completed download. - /// Revision. - public async Task DownloadRevisionAsync(int revision) - { - var url = string.Format(_downloadUrls[CurrentPlatform], _downloadHost, revision); - var zipPath = Path.Combine(_downloadsFolder, $"download-{CurrentPlatform.ToString()}-{revision}.zip"); - var folderPath = GetFolderPath(CurrentPlatform, revision); - - if (new DirectoryInfo(folderPath).Exists) - { - return; - } - - var downloadFolder = new DirectoryInfo(_downloadsFolder); - if (!downloadFolder.Exists) - { - downloadFolder.Create(); - } - - await new WebClient().DownloadFileTaskAsync(new Uri(url), zipPath); - - if (CurrentPlatform == Platform.MacOS) - { - //ZipFile and many others unzip libraries have issues extracting .app files - //Until we have a clear solution we'll call the native unzip tool - //https://github.com/dotnet/corefx/issues/15516 - NativeExtractToDirectory(zipPath, folderPath); - } - else - { - ZipFile.ExtractToDirectory(zipPath, folderPath); - } - - new FileInfo(zipPath).Delete(); - } - - /// - /// Gets the executable path for a revision. - /// - /// The executable path. - /// Revision. - public string GetExecutablePath(int revision) - { - return GetExecutablePath(CurrentPlatform, GetFolderPath(CurrentPlatform, revision)); - } - - /// - /// Gets the executable path. - /// - /// The executable path. - /// Platform. - /// Folder path. - public static string GetExecutablePath(Platform platform, string folderPath) - { - switch (platform) - { - case Platform.MacOS: - return Path.Combine(folderPath, "chrome-mac", "Chromium.app", "Contents", - "MacOS", "Chromium"); - case Platform.Linux: - return Path.Combine(folderPath, "chrome-linux", "chrome"); - case Platform.Win32: - case Platform.Win64: - return Path.Combine(folderPath, "chrome-win32", "chrome.exe"); - default: - throw new ArgumentException("Invalid platform", nameof(platform)); - } - } - - #endregion - - #region Private Methods - - private string GetFolderPath(Platform platform, int revision) - { - return Path.Combine(_downloadsFolder, $"{platform.ToString()}-{revision}"); - } - - private void NativeExtractToDirectory(string zipPath, string folderPath) - { - var process = new Process(); - process.StartInfo.FileName = "unzip"; - process.StartInfo.Arguments = $"{zipPath} -d {folderPath}"; - process.Start(); - process.WaitForExit(); - } - - #endregion - } -} diff --git a/lib/PuppeteerSharp/Frame.cs b/lib/PuppeteerSharp/Frame.cs index e8a8bf300..f5337c178 100644 --- a/lib/PuppeteerSharp/Frame.cs +++ b/lib/PuppeteerSharp/Frame.cs @@ -19,7 +19,7 @@ namespace PuppeteerSharp /// An example of dumping frame tree /// /// WaitForSelectorAsync(string selector, WaitForSelector /// /// /// /// Options for launching Chrome - /// The revision of Chrome to launch. /// A connected browser. /// /// See this article /// for a description of the differences between Chromium and Chrome. /// This article describes some differences for Linux users. /// - public async Task LaunchAsync(LaunchOptions options, int chromiumRevision) + public async Task LaunchAsync(LaunchOptions options) { if (_chromiumLaunched) { @@ -101,9 +100,8 @@ public async Task LaunchAsync(LaunchOptions options, int chromiumRevisi if (string.IsNullOrEmpty(chromeExecutable)) { - var downloader = Downloader.CreateDefault(); - var revisionInfo = downloader.RevisionInfo(Downloader.CurrentPlatform, chromiumRevision); - chromeExecutable = revisionInfo.ExecutablePath; + var browserFetcher = new BrowserFetcher(); + chromeExecutable = browserFetcher.RevisionInfo(BrowserFetcher.DefaultRevision).ExecutablePath; } if (!File.Exists(chromeExecutable)) { @@ -190,8 +188,8 @@ public async Task TryDeleteUserDataDir(int times = 10, TimeSpan? delay = null) delay = new TimeSpan(0, 0, 0, 0, 100); } - string folder = string.IsNullOrEmpty(_temporaryUserDataDir) ? _options.UserDataDir : _temporaryUserDataDir; - int attempts = 0; + var folder = string.IsNullOrEmpty(_temporaryUserDataDir) ? _options.UserDataDir : _temporaryUserDataDir; + var attempts = 0; while (true) { try @@ -217,11 +215,7 @@ public async Task TryDeleteUserDataDir(int times = 10, TimeSpan? delay = null) /// /// The executable path. public static string GetExecutablePath() - { - var downloader = Downloader.CreateDefault(); - var revisionInfo = downloader.RevisionInfo(Downloader.CurrentPlatform, Downloader.DefaultRevision); - return revisionInfo.ExecutablePath; - } + => new BrowserFetcher().RevisionInfo(BrowserFetcher.DefaultRevision).ExecutablePath; /// /// Gets a temporary directory using and . @@ -464,4 +458,4 @@ private static void SetEnvVariables(IDictionary environment, IDi #endregion } -} +} \ No newline at end of file diff --git a/lib/PuppeteerSharp/Page.cs b/lib/PuppeteerSharp/Page.cs index 5675f2681..634304065 100644 --- a/lib/PuppeteerSharp/Page.cs +++ b/lib/PuppeteerSharp/Page.cs @@ -24,7 +24,7 @@ namespace PuppeteerSharp /// /// This example creates a page, navigates it to a URL, and then saves a screenshot: /// - /// var browser = await Puppeteer.LaunchAsync(new LaunchOptions(), Downloader.DefaultRevision); + /// var browser = await Puppeteer.LaunchAsync(new LaunchOptions()); /// var page = await browser.NewPageAsync(); /// await page.GoToAsync("https://example.com"); /// await page.ScreenshotAsync("screenshot.png"); @@ -1251,7 +1251,7 @@ public Task WaitForSelectorAsync(string selector, WaitForSelector /// /// /// - /// Platform used by a . + /// Platform used by a . /// public enum Platform { diff --git a/lib/PuppeteerSharp/Puppeteer.cs b/lib/PuppeteerSharp/Puppeteer.cs index 0f3b9ec16..a2af2ec32 100644 --- a/lib/PuppeteerSharp/Puppeteer.cs +++ b/lib/PuppeteerSharp/Puppeteer.cs @@ -9,13 +9,13 @@ namespace PuppeteerSharp /// /// The following is a typical example of using a Puppeteer to drive automation: /// - /// var browser = await Puppeteer.LaunchAsync(new LaunchOptions(), Downloader.DefaultRevision); + /// var browser = await Puppeteer.LaunchAsync(new LaunchOptions()); /// var page = await browser.NewPageAsync(); /// await page.GoToAsync("https://www.google.com"); /// await Browser.CloseAsync(); /// /// - public class Puppeteer + public static class Puppeteer { /// /// The default flags that Chromium will be launched with. @@ -32,7 +32,6 @@ public class Puppeteer /// The method launches a browser instance with given arguments. The browser will be closed when the Browser is disposed. /// /// Options for launching Chrome - /// The revision of Chrome to launch. /// The logger factory /// A connected browser. /// @@ -40,8 +39,8 @@ public class Puppeteer /// for a description of the differences between Chromium and Chrome. /// This article describes some differences for Linux users. /// - public static Task LaunchAsync(LaunchOptions options, int chromiumRevision, ILoggerFactory loggerFactory = null) - => new Launcher(loggerFactory).LaunchAsync(options, chromiumRevision); + public static Task LaunchAsync(LaunchOptions options, ILoggerFactory loggerFactory = null) + => new Launcher(loggerFactory).LaunchAsync(options); /// /// Attaches Puppeteer to an existing Chromium instance. The browser will be closed when the Browser is disposed. @@ -51,5 +50,13 @@ public static Task LaunchAsync(LaunchOptions options, int chromiumRevis /// A connected browser. public static Task ConnectAsync(ConnectOptions options, ILoggerFactory loggerFactory = null) => new Launcher(loggerFactory).ConnectAsync(options); + + /// + /// Creates the browser fetcher. + /// + /// The browser fetcher. + /// Options. + public static BrowserFetcher CreateBrowserFetcher(BrowserFetcherOptions options) + => new BrowserFetcher(options); } -} +} \ No newline at end of file diff --git a/lib/PuppeteerSharp/RevisionInfo.cs b/lib/PuppeteerSharp/RevisionInfo.cs index 5160a0af7..849f37ef4 100644 --- a/lib/PuppeteerSharp/RevisionInfo.cs +++ b/lib/PuppeteerSharp/RevisionInfo.cs @@ -6,7 +6,7 @@ namespace PuppeteerSharp /// /// Revision info. /// - public struct RevisionInfo + public class RevisionInfo { /// /// Gets or sets the revision. @@ -28,5 +28,17 @@ public struct RevisionInfo /// /// true if exists; otherwise, false. public bool Downloaded => Directory.Exists(FolderPath); + /// + /// URL this revision can be downloaded from. + /// + public string Url { get; set; } + /// + /// Whether the revision is locally available on disk. + /// + public bool Local { get; set; } + /// + /// Revision platform. + /// + public Platform Platform { get; set; } } -} +} \ No newline at end of file