...

Source file src/github.com/playwright-community/playwright-go/run.go

Documentation: github.com/playwright-community/playwright-go

     1  package playwright
     2  
     3  import (
     4  	"archive/zip"
     5  	"bytes"
     6  	"errors"
     7  	"fmt"
     8  	"io"
     9  	"io/ioutil"
    10  	"log"
    11  	"net/http"
    12  	"os"
    13  	"os/exec"
    14  	"path/filepath"
    15  	"runtime"
    16  )
    17  
    18  const playwrightCliVersion = "1.20.0-beta-1647057403000"
    19  
    20  type PlaywrightDriver struct {
    21  	DriverDirectory, DriverBinaryLocation, Version string
    22  	options                                        *RunOptions
    23  }
    24  
    25  func NewDriver(options *RunOptions) (*PlaywrightDriver, error) {
    26  	baseDriverDirectory := options.DriverDirectory
    27  	if baseDriverDirectory == "" {
    28  		var err error
    29  		baseDriverDirectory, err = getDefaultCacheDirectory()
    30  		if err != nil {
    31  			return nil, fmt.Errorf("could not get default cache directory: %v", err)
    32  		}
    33  	}
    34  	driverDirectory := filepath.Join(baseDriverDirectory, "ms-playwright-go", playwrightCliVersion)
    35  	driverBinaryLocation := filepath.Join(driverDirectory, getDriverName())
    36  	return &PlaywrightDriver{
    37  		options:              options,
    38  		DriverBinaryLocation: driverBinaryLocation,
    39  		DriverDirectory:      driverDirectory,
    40  		Version:              playwrightCliVersion,
    41  	}, nil
    42  }
    43  
    44  func getDefaultCacheDirectory() (string, error) {
    45  	userHomeDir, err := os.UserHomeDir()
    46  	if err != nil {
    47  		return "", fmt.Errorf("could not get user home directory: %v", err)
    48  	}
    49  	switch runtime.GOOS {
    50  	case "windows":
    51  		return filepath.Join(userHomeDir, "AppData", "Local"), nil
    52  	case "darwin":
    53  		return filepath.Join(userHomeDir, "Library", "Caches"), nil
    54  	case "linux":
    55  		return filepath.Join(userHomeDir, ".cache"), nil
    56  	}
    57  	return "", errors.New("could not determine cache directory")
    58  }
    59  
    60  func (d *PlaywrightDriver) isUpToDateDriver() (bool, error) {
    61  	if _, err := os.Stat(d.DriverDirectory); os.IsNotExist(err) {
    62  		if err := os.MkdirAll(d.DriverDirectory, 0777); err != nil {
    63  			return false, fmt.Errorf("could not create driver directory: %w", err)
    64  		}
    65  	}
    66  	if _, err := os.Stat(d.DriverBinaryLocation); os.IsNotExist(err) {
    67  		return false, nil
    68  	}
    69  	cmd := exec.Command(d.DriverBinaryLocation, "--version")
    70  	output, err := cmd.Output()
    71  	if err != nil {
    72  		return false, fmt.Errorf("could not run driver: %w", err)
    73  	}
    74  	if bytes.Contains(output, []byte(d.Version)) {
    75  		return true, nil
    76  	}
    77  	return false, nil
    78  }
    79  
    80  func (d *PlaywrightDriver) install() error {
    81  	if err := d.DownloadDriver(); err != nil {
    82  		return fmt.Errorf("could not install driver: %w", err)
    83  	}
    84  	if d.options.SkipInstallBrowsers {
    85  		return nil
    86  	}
    87  	if d.options.Verbose {
    88  		log.Println("Downloading browsers...")
    89  	}
    90  	if err := d.installBrowsers(d.DriverBinaryLocation); err != nil {
    91  		return fmt.Errorf("could not install browsers: %w", err)
    92  	}
    93  	if d.options.Verbose {
    94  		log.Println("Downloaded browsers successfully")
    95  	}
    96  	return nil
    97  }
    98  func (d *PlaywrightDriver) DownloadDriver() error {
    99  	up2Date, err := d.isUpToDateDriver()
   100  	if err != nil {
   101  		return fmt.Errorf("could not check if driver is up2date: %w", err)
   102  	}
   103  	if up2Date {
   104  		return nil
   105  	}
   106  
   107  	log.Printf("Downloading driver to %s", d.DriverDirectory)
   108  	driverURL := d.getDriverURL()
   109  	resp, err := http.Get(driverURL)
   110  	if err != nil {
   111  		return fmt.Errorf("could not download driver: %w", err)
   112  	}
   113  	if resp.StatusCode != http.StatusOK {
   114  		return fmt.Errorf("error: got non 200 status code: %d (%s)", resp.StatusCode, resp.Status)
   115  	}
   116  	defer resp.Body.Close()
   117  
   118  	body, err := ioutil.ReadAll(resp.Body)
   119  	if err != nil {
   120  		return fmt.Errorf("could not read response body: %w", err)
   121  	}
   122  	zipReader, err := zip.NewReader(bytes.NewReader(body), int64(len(body)))
   123  	if err != nil {
   124  		return fmt.Errorf("could not read zip content: %w", err)
   125  	}
   126  
   127  	for _, zipFile := range zipReader.File {
   128  		zipFileDiskPath := filepath.Join(d.DriverDirectory, zipFile.Name)
   129  		if zipFile.FileInfo().IsDir() {
   130  			if err := os.MkdirAll(zipFileDiskPath, os.ModePerm); err != nil {
   131  				return fmt.Errorf("could not create directory: %w", err)
   132  			}
   133  			continue
   134  		}
   135  
   136  		outFile, err := os.Create(zipFileDiskPath)
   137  		if err != nil {
   138  			return fmt.Errorf("could not create driver: %w", err)
   139  		}
   140  		file, err := zipFile.Open()
   141  		if err != nil {
   142  			return fmt.Errorf("could not open zip file: %w", err)
   143  		}
   144  		if _, err = io.Copy(outFile, file); err != nil {
   145  			return fmt.Errorf("could not copy response body to file: %w", err)
   146  		}
   147  		if err := outFile.Close(); err != nil {
   148  			return fmt.Errorf("could not close file (driver): %w", err)
   149  		}
   150  		if err := file.Close(); err != nil {
   151  			return fmt.Errorf("could not close file (zip file): %w", err)
   152  		}
   153  		if zipFile.Mode().Perm()&0100 != 0 && runtime.GOOS != "windows" {
   154  			if err := makeFileExecutable(zipFileDiskPath); err != nil {
   155  				return fmt.Errorf("could not make executable: %w", err)
   156  			}
   157  		}
   158  	}
   159  
   160  	log.Println("Downloaded driver successfully")
   161  	return nil
   162  }
   163  
   164  func (d *PlaywrightDriver) run() (*connection, error) {
   165  	cmd := exec.Command(d.DriverBinaryLocation, "run-driver")
   166  	cmd.Stderr = os.Stderr
   167  	stdin, err := cmd.StdinPipe()
   168  	if err != nil {
   169  		return nil, fmt.Errorf("could not get stdin pipe: %w", err)
   170  	}
   171  	stdout, err := cmd.StdoutPipe()
   172  	if err != nil {
   173  		return nil, fmt.Errorf("could not get stdout pipe: %w", err)
   174  	}
   175  	if err := cmd.Start(); err != nil {
   176  		return nil, fmt.Errorf("could not start driver: %w", err)
   177  	}
   178  	transport := newPipeTransport(stdin, stdout)
   179  	go func() {
   180  		if err := transport.Start(); err != nil {
   181  			log.Fatal(err)
   182  		}
   183  	}()
   184  	connection := newConnection(func() error {
   185  		if err := stdin.Close(); err != nil {
   186  			return fmt.Errorf("could not close stdin: %v", err)
   187  		}
   188  		if err := stdout.Close(); err != nil {
   189  			return fmt.Errorf("could not close stdout: %v", err)
   190  		}
   191  		if err := cmd.Process.Kill(); err != nil {
   192  			return fmt.Errorf("could not kill process: %v", err)
   193  		}
   194  		if _, err := cmd.Process.Wait(); err != nil {
   195  			return fmt.Errorf("could not wait for process: %v", err)
   196  		}
   197  		return nil
   198  	})
   199  	connection.onmessage = transport.Send
   200  	transport.onmessage = connection.Dispatch
   201  	return connection, nil
   202  }
   203  
   204  func (d *PlaywrightDriver) installBrowsers(driverPath string) error {
   205  	additionalArgs := []string{"install"}
   206  	if d.options.Browsers != nil {
   207  		additionalArgs = append(additionalArgs, d.options.Browsers...)
   208  	}
   209  	cmd := exec.Command(driverPath, additionalArgs...)
   210  	cmd.Stdout = os.Stdout
   211  	cmd.Stderr = os.Stderr
   212  	if err := cmd.Run(); err != nil {
   213  		return fmt.Errorf("could not install browsers: %w", err)
   214  	}
   215  	return nil
   216  }
   217  
   218  // RunOptions are custom options to run the driver
   219  type RunOptions struct {
   220  	DriverDirectory     string
   221  	SkipInstallBrowsers bool
   222  	Browsers            []string
   223  	Verbose             bool
   224  }
   225  
   226  // Install does download the driver and the browsers. If not called manually
   227  // before playwright.Run() it will get executed there and might take a few seconds
   228  // to download the Playwright suite.
   229  func Install(options ...*RunOptions) error {
   230  	driver, err := NewDriver(transformRunOptions(options))
   231  	if err != nil {
   232  		return fmt.Errorf("could not get driver instance: %w", err)
   233  	}
   234  	if err := driver.install(); err != nil {
   235  		return fmt.Errorf("could not install driver: %w", err)
   236  	}
   237  	return nil
   238  }
   239  
   240  // Run starts a Playwright instance
   241  func Run(options ...*RunOptions) (*Playwright, error) {
   242  	driver, err := NewDriver(transformRunOptions(options))
   243  	if err != nil {
   244  		return nil, fmt.Errorf("could not get driver instance: %w", err)
   245  	}
   246  	connection, err := driver.run()
   247  	if err != nil {
   248  		return nil, err
   249  	}
   250  	playwright := connection.Start()
   251  	return playwright, nil
   252  }
   253  
   254  func transformRunOptions(options []*RunOptions) *RunOptions {
   255  	if len(options) == 1 {
   256  		return options[0]
   257  	}
   258  	return &RunOptions{
   259  		Verbose: true,
   260  	}
   261  }
   262  
   263  func getDriverName() string {
   264  	switch runtime.GOOS {
   265  	case "windows":
   266  		return "playwright.cmd"
   267  	case "darwin":
   268  		fallthrough
   269  	case "linux":
   270  		return "playwright.sh"
   271  	}
   272  	panic("Not supported OS!")
   273  }
   274  
   275  func (d *PlaywrightDriver) getDriverURL() string {
   276  	platform := ""
   277  	switch runtime.GOOS {
   278  	case "windows":
   279  		platform = "win32_x64"
   280  	case "darwin":
   281  		if runtime.GOARCH == "arm64" {
   282  			platform = "mac-arm64"
   283  		} else {
   284  			platform = "mac"
   285  		}
   286  	case "linux":
   287  		if runtime.GOARCH == "arm64" {
   288  			platform = "linux-arm64"
   289  		} else {
   290  			platform = "linux"
   291  		}
   292  	}
   293  	return fmt.Sprintf("https://playwright.azureedge.net/builds/driver/next/playwright-%s-%s.zip", d.Version, platform)
   294  }
   295  
   296  func makeFileExecutable(path string) error {
   297  	stats, err := os.Stat(path)
   298  	if err != nil {
   299  		return fmt.Errorf("could not stat driver: %w", err)
   300  	}
   301  	if err := os.Chmod(path, stats.Mode()|0x40); err != nil {
   302  		return fmt.Errorf("could not set permissions: %w", err)
   303  	}
   304  	return nil
   305  }
   306  

View as plain text