From b53ac1ce45160c9a9c00f2863d6c8cdb83ea1ba8 Mon Sep 17 00:00:00 2001 From: ferhatb Date: Mon, 9 Mar 2020 17:03:17 -0700 Subject: [PATCH 1/2] Add support for firefox mac installer. Update web_ui pubspec for http.wq --- lib/web_ui/dev/common.dart | 27 ++++- lib/web_ui/dev/firefox.dart | 8 +- lib/web_ui/dev/firefox_installer.dart | 152 +++++++++++++++++++++++--- lib/web_ui/pubspec.yaml | 2 +- 4 files changed, 165 insertions(+), 24 deletions(-) diff --git a/lib/web_ui/dev/common.dart b/lib/web_ui/dev/common.dart index c45a343b3f989..9fa8d126c86ae 100644 --- a/lib/web_ui/dev/common.dart +++ b/lib/web_ui/dev/common.dart @@ -48,6 +48,7 @@ abstract class PlatformBinding { int getChromeBuild(YamlMap chromeLock); String getChromeDownloadUrl(String version); String getFirefoxDownloadUrl(String version); + String getFirefoxDownloadFilename(String version); String getChromeExecutablePath(io.Directory versionDir); String getFirefoxExecutablePath(io.Directory versionDir); String getFirefoxLatestVersionUrl(); @@ -75,7 +76,12 @@ class _WindowsBinding implements PlatformBinding { @override String getFirefoxDownloadUrl(String version) => - 'https://download-installer.cdn.mozilla.net/pub/firefox/releases/${version}/win64/en-US/firefox-${version}.exe'; + 'https://download-installer.cdn.mozilla.net/pub/firefox/releases/${version}/win64/en-US/' + '${getFirefoxDownloadFilename(version)}'; + + @override + String getFirefoxDownloadFilename(String version) => + 'firefox-${version}.exe'; @override String getFirefoxExecutablePath(io.Directory versionDir) => @@ -110,7 +116,12 @@ class _LinuxBinding implements PlatformBinding { @override String getFirefoxDownloadUrl(String version) => - 'https://download-installer.cdn.mozilla.net/pub/firefox/releases/${version}/linux-x86_64/en-US/firefox-${version}.tar.bz2'; + 'https://download-installer.cdn.mozilla.net/pub/firefox/releases/${version}/linux-x86_64/en-US/' + '${getFirefoxDownloadFilename(version)}'; + + @override + String getFirefoxDownloadFilename(String version) => + 'firefox-${version}.tar.bz2'; @override String getFirefoxExecutablePath(io.Directory versionDir) => @@ -150,12 +161,16 @@ class _MacBinding implements PlatformBinding { @override String getFirefoxDownloadUrl(String version) => - 'https://download-installer.cdn.mozilla.net/pub/firefox/releases/${version}/mac/en-US/firefox-${version}.dmg'; + 'https://download-installer.cdn.mozilla.net/pub/firefox/releases/${version}/mac/en-US/' + '${getFirefoxDownloadFilename(version)}'; @override - String getFirefoxExecutablePath(io.Directory versionDir) { - throw UnimplementedError(); - } + String getFirefoxDownloadFilename(String version) => + 'Firefox ${version}.dmg'; + + @override + String getFirefoxExecutablePath(io.Directory versionDir) => + path.join(versionDir.path, 'Firefox.app','Contents','MacOS', 'firefox'); @override String getFirefoxLatestVersionUrl() => diff --git a/lib/web_ui/dev/firefox.dart b/lib/web_ui/dev/firefox.dart index b1c89e3bfd75a..b9dc3a2a8826a 100644 --- a/lib/web_ui/dev/firefox.dart +++ b/lib/web_ui/dev/firefox.dart @@ -47,18 +47,20 @@ class Firefox extends Browser { // https://developer.mozilla.org/en-US/docs/Mozilla/Command_Line_Options#Browser // var dir = createTempDir(); + bool isMac = Platform.isMacOS; var args = [ url.toString(), '--headless', '-width $kMaxScreenshotWidth', '-height $kMaxScreenshotHeight', - '-new-window', - '-new-instance', + isMac ? '--new-window' : '-new-window', + isMac ? '--new-instance' : '-new-instance', '--start-debugger-server $kDevtoolsPort', ]; final Process process = - await Process.start(installation.executable, args); + await Process.start(installation.executable, args, + workingDirectory: dir); remoteDebuggerCompleter.complete( getRemoteDebuggerUrl(Uri.parse('http://localhost:$kDevtoolsPort'))); diff --git a/lib/web_ui/dev/firefox_installer.dart b/lib/web_ui/dev/firefox_installer.dart index b286db6b45cf3..5d4479a68f939 100644 --- a/lib/web_ui/dev/firefox_installer.dart +++ b/lib/web_ui/dev/firefox_installer.dart @@ -67,8 +67,9 @@ Future getOrInstallFirefox( }) async { // These tests are aimed to run only on the Linux containers in Cirrus. // Therefore Firefox installation is implemented only for Linux now. - if (!io.Platform.isLinux) { - throw UnimplementedError(); + if (!io.Platform.isLinux && !io.Platform.isMacOS) { + throw UnimplementedError('Firefox Installer is only supported on Linux ' + 'and Mac operating systems'); } infoLog ??= io.stdout; @@ -129,7 +130,9 @@ class FirefoxInstaller { } static Future latest() async { - final String latestVersion = await fetchLatestFirefoxVersion(); + final String latestVersion = io.Platform.isLinux + ? await fetchLatestFirefoxVersionLinux() + : await fetchLatestFirefoxVersionMacOS(); return FirefoxInstaller(version: latestVersion); } @@ -169,11 +172,15 @@ class FirefoxInstaller { /// Install the browser by downloading from the web. Future install() async { final io.File downloadedFile = await _download(); - await _uncompress(downloadedFile); + if (io.Platform.isLinux) { + await _uncompress(downloadedFile); + } else if (io.Platform.isMacOS) { + await _mountDmgAndCopy(downloadedFile); + } downloadedFile.deleteSync(); } - /// Downloads the browser version from web. + /// Downloads the browser version from web into a target file. /// See [version]. Future _download() async { if (versionDir.existsSync()) { @@ -188,16 +195,20 @@ class FirefoxInstaller { )); final io.File downloadedFile = - io.File(path.join(versionDir.path, 'firefox-${version}.tar.bz2')); - await download.stream.pipe(downloadedFile.openWrite()); + io.File(path.join(versionDir.path, PlatformBinding.instance.getFirefoxDownloadFilename(version))); + io.IOSink sink = downloadedFile.openWrite(); + await download.stream.pipe(sink); + await sink.flush(); + await sink.close(); return downloadedFile; } - /// Uncompress the downloaded browser files. + /// Uncompress the downloaded browser files for operating systems that + /// use a zip archive. /// See [version]. Future _uncompress(io.File downloadedFile) async { - final io.ProcessResult unzipResult = await io.Process.run('tar', [ + io.ProcessResult unzipResult = await io.Process.run('tar', [ '-x', '-f', downloadedFile.path, @@ -212,6 +223,77 @@ class FirefoxInstaller { } } + /// Mounts the dmg file using hdiutil, copies content of the volume to + /// target path and then unmounts dmg ready for deletion. + Future _mountDmgAndCopy(io.File dmgFile) async { + String volumeName = await _hdiUtilMount(dmgFile); + + final String sourcePath = '$volumeName/Firefox.app'; + final String targetPath = path.dirname(dmgFile.path); + try { + io.ProcessResult installResult = await io.Process.run('cp', [ + '-r', + sourcePath, + targetPath, + ]); + if (installResult.exitCode != 0) { + throw BrowserInstallerException( + 'Failed to copy Firefox disk image contents from ' + '$sourcePath to $targetPath.\n' + 'Exit code ${installResult.exitCode}.\n' + '${installResult.stderr}'); + } + } finally { + await _hdiUtilUnmount(volumeName); + } + } + + Future _hdiUtilMount(io.File dmgFile) async { + io.ProcessResult mountResult = await io.Process.run('hdiutil', [ + 'attach', + '-readonly', + '${dmgFile.path}', + ]); + if (mountResult.exitCode != 0) { + throw BrowserInstallerException( + 'Failed to mount Firefox disk image ${dmgFile.path}.\n' + 'Exit code ${mountResult.exitCode}.\n${mountResult.stderr}'); + } + + List processOutput = mountResult.stdout.split('\n'); + String volumePath = _volumeFromMountResult(processOutput); + if (volumePath == null) { + throw BrowserInstallerException( + 'Failed to parse mount dmg result ${processOutput.join('\n')}.\n' + 'Expected /Volumes/{volume name}'); + } + return volumePath; + } + + // Parses volume from mount result. + // Output is of form: {devicename} /Volumes/{name}. + String _volumeFromMountResult(List lines) { + for (String line in lines) { + int pos = line.indexOf('/Volumes'); + if (pos != -1) { + return line.substring(pos); + } + } + return null; + } + + Future _hdiUtilUnmount(String volumeName) async { + io.ProcessResult unmountResult = await io.Process.run('hdiutil', [ + 'unmount', + '$volumeName', + ]); + if (unmountResult.exitCode != 0) { + throw BrowserInstallerException( + 'Failed to unmount Firefox disk image ${volumeName}.\n' + 'Exit code ${unmountResult.exitCode}. ${unmountResult.stderr}'); + } + } + void close() { client.close(); } @@ -220,17 +302,22 @@ class FirefoxInstaller { Future _findSystemFirefoxExecutable() async { final io.ProcessResult which = await io.Process.run('which', ['firefox']); - - if (which.exitCode != 0) { + bool found = which.exitCode != 0; + const String fireFoxDefaultInstallPath = + '/Applications/Firefox.app/Contents/MacOS/firefox'; + if (!found) { + if (io.Platform.isMacOS && + io.File(fireFoxDefaultInstallPath).existsSync()) { + return Future.value(fireFoxDefaultInstallPath); + } throw BrowserInstallerException( 'Failed to locate system Firefox installation.'); } - return which.stdout; } -/// Fetches the latest available Chrome build version. -Future fetchLatestFirefoxVersion() async { +/// Fetches the latest available Firefox build version on Linux. +Future fetchLatestFirefoxVersionLinux() async { final RegExp forFirefoxVersion = RegExp("firefox-[0-9.]\+[0-9]"); final io.HttpClientRequest request = await io.HttpClient() .getUrl(Uri.parse(PlatformBinding.instance.getFirefoxLatestVersionUrl())); @@ -243,3 +330,40 @@ Future fetchLatestFirefoxVersion() async { return version.substring(version.lastIndexOf('-') + 1); } + +/// Fetches the latest available Firefox build version on Mac OS. +Future fetchLatestFirefoxVersionMacOS() async { + final RegExp forFirefoxVersion = RegExp("firefox\/releases\/[0-9.]\+[0-9]"); + final io.HttpClientRequest request = await io.HttpClient() + .getUrl(Uri.parse(PlatformBinding.instance.getFirefoxLatestVersionUrl())); + request.followRedirects = false; + // We will parse the HttpHeaders to find the redirect location. + final io.HttpClientResponse response = await request.close(); + + final String location = response.headers.value('location'); + final String version = forFirefoxVersion.stringMatch(location); + return version.substring(version.lastIndexOf('/') + 1); +} + +Future getInstaller({String requestedVersion = 'latest'}) async { + FirefoxInstaller installer; + try { + installer = requestedVersion == 'latest' + ? await FirefoxInstaller.latest() + : FirefoxInstaller(version: requestedVersion); + + if (installer.isInstalled) { + print('Installation was skipped because Firefox version ' + '${installer.version} is already installed.'); + } else { + print('Installing Firefox version: ${installer.version}'); + await installer.install(); + final BrowserInstallation installation = installer.getInstallation(); + print( + 'Installations complete. To launch it run ${installation.executable}'); + } + return installer.getInstallation(); + } finally { + installer?.close(); + } +} diff --git a/lib/web_ui/pubspec.yaml b/lib/web_ui/pubspec.yaml index 73ecfb5e625bf..12e7d8d8f7d7a 100644 --- a/lib/web_ui/pubspec.yaml +++ b/lib/web_ui/pubspec.yaml @@ -8,7 +8,7 @@ dependencies: meta: 1.1.7 dev_dependencies: - http: 0.12.0+2 + http: 0.12.0+4 image: 2.1.4 mockito: 4.1.1 path: 1.6.4 From 6420d7ecebb1ce820091cb9ac3cb62481a95bbf7 Mon Sep 17 00:00:00 2001 From: ferhatb Date: Mon, 9 Mar 2020 17:16:35 -0700 Subject: [PATCH 2/2] Addressed review comment 'final' --- lib/web_ui/dev/firefox_installer.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/web_ui/dev/firefox_installer.dart b/lib/web_ui/dev/firefox_installer.dart index 5d4479a68f939..d3043e9f68303 100644 --- a/lib/web_ui/dev/firefox_installer.dart +++ b/lib/web_ui/dev/firefox_installer.dart @@ -208,7 +208,7 @@ class FirefoxInstaller { /// use a zip archive. /// See [version]. Future _uncompress(io.File downloadedFile) async { - io.ProcessResult unzipResult = await io.Process.run('tar', [ + final io.ProcessResult unzipResult = await io.Process.run('tar', [ '-x', '-f', downloadedFile.path,