diff options
Diffstat (limited to 'vendor/github.com/playwright-community/playwright-go/run.go')
| -rw-r--r-- | vendor/github.com/playwright-community/playwright-go/run.go | 409 |
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 +} |
