summaryrefslogtreecommitdiff
path: root/vendor/github.com/playwright-community/playwright-go/run.go
diff options
context:
space:
mode:
Diffstat (limited to 'vendor/github.com/playwright-community/playwright-go/run.go')
-rw-r--r--vendor/github.com/playwright-community/playwright-go/run.go409
1 files changed, 409 insertions, 0 deletions
diff --git a/vendor/github.com/playwright-community/playwright-go/run.go b/vendor/github.com/playwright-community/playwright-go/run.go
new file mode 100644
index 0000000..79ad11e
--- /dev/null
+++ b/vendor/github.com/playwright-community/playwright-go/run.go
@@ -0,0 +1,409 @@
+package playwright
+
+import (
+ "archive/zip"
+ "bytes"
+ "errors"
+ "fmt"
+ "io"
+ "log"
+ "log/slog"
+ "net/http"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "runtime"
+ "strings"
+)
+
+const playwrightCliVersion = "1.52.0"
+
+var (
+ logger = slog.Default()
+ playwrightCDNMirrors = []string{
+ "https://playwright.azureedge.net",
+ "https://playwright-akamai.azureedge.net",
+ "https://playwright-verizon.azureedge.net",
+ }
+)
+
+// PlaywrightDriver wraps the Playwright CLI of upstream Playwright.
+//
+// It's required for playwright-go to work.
+type PlaywrightDriver struct {
+ Version string
+ options *RunOptions
+}
+
+func NewDriver(options ...*RunOptions) (*PlaywrightDriver, error) {
+ transformed, err := transformRunOptions(options...) // get default values
+ if err != nil {
+ return nil, err
+ }
+ return &PlaywrightDriver{
+ options: transformed,
+ Version: playwrightCliVersion,
+ }, nil
+}
+
+func getDefaultCacheDirectory() (string, error) {
+ userHomeDir, err := os.UserHomeDir()
+ if err != nil {
+ return "", fmt.Errorf("could not get user home directory: %w", err)
+ }
+ switch runtime.GOOS {
+ case "windows":
+ return filepath.Join(userHomeDir, "AppData", "Local"), nil
+ case "darwin":
+ return filepath.Join(userHomeDir, "Library", "Caches"), nil
+ case "linux":
+ return filepath.Join(userHomeDir, ".cache"), nil
+ }
+ return "", errors.New("could not determine cache directory")
+}
+
+func (d *PlaywrightDriver) isUpToDateDriver() (bool, error) {
+ if _, err := os.Stat(d.options.DriverDirectory); os.IsNotExist(err) {
+ if err := os.MkdirAll(d.options.DriverDirectory, 0o777); err != nil {
+ return false, fmt.Errorf("could not create driver directory: %w", err)
+ }
+ }
+ if _, err := os.Stat(getDriverCliJs(d.options.DriverDirectory)); os.IsNotExist(err) {
+ return false, nil
+ } else if err != nil {
+ return false, fmt.Errorf("could not check if driver is up2date: %w", err)
+ }
+ cmd := d.Command("--version")
+ output, err := cmd.Output()
+ if err != nil {
+ return false, fmt.Errorf("could not run driver: %w", err)
+ }
+ if bytes.Contains(output, []byte(d.Version)) {
+ return true, nil
+ }
+ // avoid triggering downloads and accidentally overwriting files
+ return false, fmt.Errorf("driver exists but version not %s in : %s", d.Version, d.options.DriverDirectory)
+}
+
+// Command returns an exec.Cmd for the driver.
+func (d *PlaywrightDriver) Command(arg ...string) *exec.Cmd {
+ cmd := exec.Command(getNodeExecutable(d.options.DriverDirectory), append([]string{getDriverCliJs(d.options.DriverDirectory)}, arg...)...)
+ cmd.SysProcAttr = defaultSysProcAttr
+ return cmd
+}
+
+// Install downloads the driver and the browsers depending on [RunOptions].
+func (d *PlaywrightDriver) Install() error {
+ if err := d.DownloadDriver(); err != nil {
+ return fmt.Errorf("could not install driver: %w", err)
+ }
+ if d.options.SkipInstallBrowsers {
+ return nil
+ }
+
+ d.log("Downloading browsers...")
+ if err := d.installBrowsers(); err != nil {
+ return fmt.Errorf("could not install browsers: %w", err)
+ }
+ d.log("Downloaded browsers successfully")
+
+ return nil
+}
+
+// Uninstall removes the driver and the browsers.
+func (d *PlaywrightDriver) Uninstall() error {
+ d.log("Removing browsers...")
+ if err := d.uninstallBrowsers(); err != nil {
+ return fmt.Errorf("could not uninstall browsers: %w", err)
+ }
+
+ d.log("Removing driver...")
+ if err := os.RemoveAll(d.options.DriverDirectory); err != nil {
+ return fmt.Errorf("could not remove driver directory: %w", err)
+ }
+
+ d.log("Uninstall driver successfully")
+ return nil
+}
+
+// DownloadDriver downloads the driver only
+func (d *PlaywrightDriver) DownloadDriver() error {
+ up2Date, err := d.isUpToDateDriver()
+ if err != nil {
+ return err
+ }
+ if up2Date {
+ return nil
+ }
+
+ d.log("Downloading driver", "path", d.options.DriverDirectory)
+
+ body, err := downloadDriver(d.getDriverURLs())
+ if err != nil {
+ return err
+ }
+ zipReader, err := zip.NewReader(bytes.NewReader(body), int64(len(body)))
+ if err != nil {
+ return fmt.Errorf("could not read zip content: %w", err)
+ }
+
+ for _, zipFile := range zipReader.File {
+ zipFileDiskPath := filepath.Join(d.options.DriverDirectory, zipFile.Name)
+ if zipFile.FileInfo().IsDir() {
+ if err := os.MkdirAll(zipFileDiskPath, os.ModePerm); err != nil {
+ return fmt.Errorf("could not create directory: %w", err)
+ }
+ continue
+ }
+
+ outFile, err := os.Create(zipFileDiskPath)
+ if err != nil {
+ return fmt.Errorf("could not create driver: %w", err)
+ }
+ file, err := zipFile.Open()
+ if err != nil {
+ return fmt.Errorf("could not open zip file: %w", err)
+ }
+ if _, err = io.Copy(outFile, file); err != nil {
+ return fmt.Errorf("could not copy response body to file: %w", err)
+ }
+ if err := outFile.Close(); err != nil {
+ return fmt.Errorf("could not close file (driver): %w", err)
+ }
+ if err := file.Close(); err != nil {
+ return fmt.Errorf("could not close file (zip file): %w", err)
+ }
+ if zipFile.Mode().Perm()&0o100 != 0 && runtime.GOOS != "windows" {
+ if err := makeFileExecutable(zipFileDiskPath); err != nil {
+ return fmt.Errorf("could not make executable: %w", err)
+ }
+ }
+ }
+
+ d.log("Downloaded driver successfully")
+
+ return nil
+}
+
+func (d *PlaywrightDriver) log(msg string, args ...any) {
+ if d.options.Verbose {
+ logger.Info(msg, args...)
+ }
+}
+
+func (d *PlaywrightDriver) run() (*connection, error) {
+ transport, err := newPipeTransport(d, d.options.Stderr)
+ if err != nil {
+ return nil, err
+ }
+ connection := newConnection(transport)
+ return connection, nil
+}
+
+func (d *PlaywrightDriver) installBrowsers() error {
+ additionalArgs := []string{"install"}
+ if d.options.Browsers != nil {
+ additionalArgs = append(additionalArgs, d.options.Browsers...)
+ }
+
+ if d.options.OnlyInstallShell {
+ additionalArgs = append(additionalArgs, "--only-shell")
+ }
+
+ if d.options.DryRun {
+ additionalArgs = append(additionalArgs, "--dry-run")
+ }
+
+ cmd := d.Command(additionalArgs...)
+ cmd.Stdout = d.options.Stdout
+ cmd.Stderr = d.options.Stderr
+ return cmd.Run()
+}
+
+func (d *PlaywrightDriver) uninstallBrowsers() error {
+ cmd := d.Command("uninstall")
+ cmd.Stdout = d.options.Stdout
+ cmd.Stderr = d.options.Stderr
+ return cmd.Run()
+}
+
+// RunOptions are custom options to run the driver
+type RunOptions struct {
+ // DriverDirectory points to the playwright driver directory.
+ // It should have two subdirectories: node and package.
+ // You can also specify it using the environment variable PLAYWRIGHT_DRIVER_PATH.
+ //
+ // Default is user cache directory + "/ms-playwright-go/x.xx.xx":
+ // - Windows: %USERPROFILE%\AppData\Local
+ // - macOS: ~/Library/Caches
+ // - Linux: ~/.cache
+ DriverDirectory string
+ // OnlyInstallShell only downloads the headless shell. (For chromium browsers only)
+ OnlyInstallShell bool
+ SkipInstallBrowsers bool
+ // if not set and SkipInstallBrowsers is false, will download all browsers (chromium, firefox, webkit)
+ Browsers []string
+ Verbose bool // default true
+ Stdout io.Writer
+ Stderr io.Writer
+ Logger *slog.Logger
+ // DryRun does not install browser/dependencies. It will only print information.
+ DryRun bool
+}
+
+// Install does download the driver and the browsers.
+//
+// Use this before playwright.Run() or use playwright cli to install the driver and browsers
+func Install(options ...*RunOptions) error {
+ driver, err := NewDriver(options...)
+ if err != nil {
+ return fmt.Errorf("could not get driver instance: %w", err)
+ }
+ if err := driver.Install(); err != nil {
+ return fmt.Errorf("could not install driver: %w", err)
+ }
+ return nil
+}
+
+// Run starts a Playwright instance.
+//
+// Requires the driver and the browsers to be installed before.
+// Either use Install() or use playwright cli.
+func Run(options ...*RunOptions) (*Playwright, error) {
+ driver, err := NewDriver(options...)
+ if err != nil {
+ return nil, fmt.Errorf("could not get driver instance: %w", err)
+ }
+ up2date, err := driver.isUpToDateDriver()
+ if err != nil || !up2date {
+ return nil, fmt.Errorf("please install the driver (v%s) first: %w", playwrightCliVersion, err)
+ }
+ connection, err := driver.run()
+ if err != nil {
+ return nil, err
+ }
+ playwright, err := connection.Start()
+ return playwright, err
+}
+
+func transformRunOptions(options ...*RunOptions) (*RunOptions, error) {
+ option := &RunOptions{
+ Verbose: true,
+ }
+ if len(options) == 1 {
+ option = options[0]
+ }
+ if option.DriverDirectory == "" { // if user did not set it, try to get it from env
+ option.DriverDirectory = os.Getenv("PLAYWRIGHT_DRIVER_PATH")
+ }
+ if option.DriverDirectory == "" {
+ cacheDirectory, err := getDefaultCacheDirectory()
+ if err != nil {
+ return nil, fmt.Errorf("could not get default cache directory: %w", err)
+ }
+ option.DriverDirectory = filepath.Join(cacheDirectory, "ms-playwright-go", playwrightCliVersion)
+ }
+ if option.Stdout == nil {
+ option.Stdout = os.Stdout
+ }
+ if option.Stderr == nil {
+ option.Stderr = os.Stderr
+ } else if option.Logger == nil {
+ log.SetOutput(option.Stderr)
+ }
+ if option.Logger != nil {
+ logger = option.Logger
+ }
+ return option, nil
+}
+
+func getNodeExecutable(driverDirectory string) string {
+ envPath := os.Getenv("PLAYWRIGHT_NODEJS_PATH")
+ if envPath != "" {
+ return envPath
+ }
+
+ node := "node"
+ if runtime.GOOS == "windows" {
+ node = "node.exe"
+ }
+ return filepath.Join(driverDirectory, node)
+}
+
+func getDriverCliJs(driverDirectory string) string {
+ return filepath.Join(driverDirectory, "package", "cli.js")
+}
+
+func (d *PlaywrightDriver) getDriverURLs() []string {
+ platform := ""
+ switch runtime.GOOS {
+ case "windows":
+ platform = "win32_x64"
+ case "darwin":
+ if runtime.GOARCH == "arm64" {
+ platform = "mac-arm64"
+ } else {
+ platform = "mac"
+ }
+ case "linux":
+ if runtime.GOARCH == "arm64" {
+ platform = "linux-arm64"
+ } else {
+ platform = "linux"
+ }
+ }
+
+ baseURLs := []string{}
+ pattern := "%s/builds/driver/playwright-%s-%s.zip"
+ if !d.isReleaseVersion() {
+ pattern = "%s/builds/driver/next/playwright-%s-%s.zip"
+ }
+
+ if hostEnv := os.Getenv("PLAYWRIGHT_DOWNLOAD_HOST"); hostEnv != "" {
+ baseURLs = append(baseURLs, fmt.Sprintf(pattern, hostEnv, d.Version, platform))
+ } else {
+ for _, mirror := range playwrightCDNMirrors {
+ baseURLs = append(baseURLs, fmt.Sprintf(pattern, mirror, d.Version, platform))
+ }
+ }
+ return baseURLs
+}
+
+// isReleaseVersion checks if the version is not a beta or alpha release
+// this helps to determine the url from where to download the driver
+func (d *PlaywrightDriver) isReleaseVersion() bool {
+ return !strings.Contains(d.Version, "beta") && !strings.Contains(d.Version, "alpha") && !strings.Contains(d.Version, "next")
+}
+
+func makeFileExecutable(path string) error {
+ stats, err := os.Stat(path)
+ if err != nil {
+ return fmt.Errorf("could not stat driver: %w", err)
+ }
+ if err := os.Chmod(path, stats.Mode()|0x40); err != nil {
+ return fmt.Errorf("could not set permissions: %w", err)
+ }
+ return nil
+}
+
+func downloadDriver(driverURLs []string) (body []byte, e error) {
+ for _, driverURL := range driverURLs {
+ resp, err := http.Get(driverURL)
+ if err != nil {
+ e = errors.Join(e, fmt.Errorf("could not download driver from %s: %w", driverURL, err))
+ continue
+ }
+ defer resp.Body.Close()
+ if resp.StatusCode != http.StatusOK {
+ e = errors.Join(e, fmt.Errorf("error: got non 200 status code: %d (%s) from %s", resp.StatusCode, resp.Status, driverURL))
+ continue
+ }
+ body, err = io.ReadAll(resp.Body)
+ if err != nil {
+ e = errors.Join(e, fmt.Errorf("could not read response body: %w", err))
+ continue
+ }
+ return body, nil
+ }
+ return nil, e
+}