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
219 type RunOptions struct {
220 DriverDirectory string
221 SkipInstallBrowsers bool
222 Browsers []string
223 Verbose bool
224 }
225
226
227
228
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
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