325 lines
7.4 KiB
Go
325 lines
7.4 KiB
Go
package fcw
|
|
|
|
/**
|
|
// MIT License
|
|
|
|
// Copyright (c) 2018 Serge Zaitsev
|
|
|
|
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
// of this software and associated documentation files (the "Software"), to deal
|
|
// in the Software without restriction, including without limitation the rights
|
|
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
// copies of the Software, and to permit persons to whom the Software is
|
|
// furnished to do so, subject to the following conditions:
|
|
|
|
// The above copyright notice and this permission notice shall be included in all
|
|
// copies or substantial portions of the Software.
|
|
|
|
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
// SOFTWARE.
|
|
**/
|
|
|
|
import (
|
|
"fmt"
|
|
"io/ioutil"
|
|
"log"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strings"
|
|
|
|
"sync"
|
|
)
|
|
|
|
type UI interface {
|
|
Done() <-chan struct{}
|
|
Close() error
|
|
}
|
|
|
|
// FirefoxExecutable returns a string which points to the preferred Firefox
|
|
// executable file.
|
|
var FirefoxExecutable = LocateFirefox
|
|
|
|
func PortablePath() string {
|
|
dir, err := filepath.Abs(filepath.Dir(os.Args[0]))
|
|
if err != nil {
|
|
log.Println("An error was encountered detecting the portable path", err)
|
|
}
|
|
listing, err := ioutil.ReadDir(dir)
|
|
for _, appdir := range listing {
|
|
if appdir.IsDir() {
|
|
for _, exe := range portableFiles() {
|
|
path := filepath.Join(dir, appdir.Name(), exe)
|
|
if _, err := os.Stat(path); os.IsNotExist(err) {
|
|
continue
|
|
}
|
|
log.Println(path)
|
|
return path
|
|
}
|
|
|
|
}
|
|
}
|
|
return "false"
|
|
}
|
|
|
|
func portableFiles() []string {
|
|
var paths []string
|
|
switch runtime.GOOS {
|
|
case "windows":
|
|
paths = []string{
|
|
"firefox.exe",
|
|
"icecat.exe",
|
|
"waterfox.exe",
|
|
}
|
|
default:
|
|
paths = []string{
|
|
"firefox-esr",
|
|
"firefox",
|
|
"waterfox",
|
|
"icecat",
|
|
"purebrowser",
|
|
}
|
|
}
|
|
return paths
|
|
}
|
|
|
|
// LocateFirefox returns a path to the Firefox binary, or an empty string if
|
|
// Firefox installation is not found.
|
|
func LocateFirefox() string {
|
|
|
|
// If env variable "LORCACHROME" specified and it exists
|
|
if path, ok := os.LookupEnv("FIREFOX_BIN"); ok {
|
|
if _, err := os.Stat(path); err == nil {
|
|
return path
|
|
}
|
|
}
|
|
|
|
portable := PortablePath()
|
|
if portable != "false" {
|
|
return portable
|
|
}
|
|
|
|
var paths []string
|
|
switch runtime.GOOS {
|
|
case "darwin":
|
|
paths = []string{
|
|
"/Applications/Moxilla Firefox.app/Contents/MacOS/Mozilla Firefox",
|
|
"/Applications/Firefox.app/Contents/MacOS/Mozilla Firefox",
|
|
"/usr/bin/firefox-esr",
|
|
"/usr/bin/firefox",
|
|
"/usr/bin/icecat",
|
|
}
|
|
case "windows":
|
|
paths = []string{
|
|
os.Getenv("LocalAppData") + "/Mozilla Firefox/firefox.exe",
|
|
os.Getenv("ProgramFiles") + "/Mozilla Firefox/firefox.exe",
|
|
os.Getenv("ProgramFiles(x86)") + "/Mozilla Firefox/firefox.exe",
|
|
os.Getenv("LocalAppData") + "/GNU Icecat/icecat.exe",
|
|
os.Getenv("ProgramFiles") + "/GNU Icecat/icecat.exe",
|
|
os.Getenv("ProgramFiles(x86)") + "/GNU Icecat/icecat.exe",
|
|
}
|
|
default:
|
|
paths = []string{
|
|
"/usr/bin/firefox-esr",
|
|
"/usr/bin/firefox",
|
|
"/usr/bin/waterfox",
|
|
"/usr/bin/icecat",
|
|
"/usr/bin/purebrowser",
|
|
}
|
|
}
|
|
|
|
for _, path := range paths {
|
|
if _, err := os.Stat(path); os.IsNotExist(err) {
|
|
continue
|
|
}
|
|
return path
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// PromptDownload asks user if he wants to download and install Firefox, and
|
|
// opens a download web page if the user agrees.
|
|
func PromptDownload() {
|
|
title := "Firefox not found"
|
|
text := "No Firefox installation was found. Would you like to download and install it now?"
|
|
|
|
// Ask user for confirmation
|
|
if !MessageBox(title, text) {
|
|
return
|
|
}
|
|
|
|
// Open download page
|
|
url := "https://www.mozilla.org/firefox/"
|
|
switch runtime.GOOS {
|
|
case "linux":
|
|
exec.Command("xdg-open", url).Run()
|
|
case "darwin":
|
|
exec.Command("open", url).Run()
|
|
case "windows":
|
|
r := strings.NewReplacer("&", "^&")
|
|
exec.Command("cmd", "/c", "start", r.Replace(url)).Run()
|
|
}
|
|
}
|
|
|
|
type firefox struct {
|
|
sync.Mutex
|
|
cmd *exec.Cmd
|
|
//ws *websocket.Conn
|
|
id int32
|
|
target string
|
|
session string
|
|
window int
|
|
//pending map[int]chan result
|
|
}
|
|
|
|
type ui struct {
|
|
firefox *firefox
|
|
done chan struct{}
|
|
tmpDir string
|
|
}
|
|
|
|
func (u *ui) Done() <-chan struct{} {
|
|
return u.done
|
|
}
|
|
|
|
func (u *ui) Close() error {
|
|
// ignore err, as the firefox process might be already dead, when user close the window.
|
|
u.firefox.kill()
|
|
<-u.done
|
|
if u.tmpDir != "" {
|
|
if err := os.RemoveAll(u.tmpDir); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
var firefoxArgs = []string{
|
|
"--no-remote",
|
|
"--new-instance",
|
|
}
|
|
|
|
func NewFirefox(url, dir string, width, height int, customArgs ...string) (UI, error) {
|
|
if url == "" {
|
|
url = "about:blank"
|
|
}
|
|
tmpDir := ""
|
|
if dir == "" {
|
|
name, err := ioutil.TempDir("", "ffox")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
dir, tmpDir = name, name
|
|
}
|
|
args := append(firefoxArgs, "--profile")
|
|
args = append(args, dir)
|
|
args = append(args, "--window-size")
|
|
args = append(args, fmt.Sprintf("%d,%d", width, height))
|
|
args = append(args, customArgs...)
|
|
args = append(args, url)
|
|
//args = append(args, "--remote-debugging-port=0")
|
|
log.Println(FirefoxExecutable(), args)
|
|
|
|
firefox, err := newFirefoxWithArgs(FirefoxExecutable(), args...)
|
|
done := make(chan struct{})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
go func() {
|
|
firefox.cmd.Wait()
|
|
close(done)
|
|
}()
|
|
return &ui{firefox: firefox, done: done, tmpDir: tmpDir}, nil
|
|
}
|
|
|
|
func (c *firefox) kill() error {
|
|
if state := c.cmd.ProcessState; state == nil || !state.Exited() {
|
|
return c.cmd.Process.Kill()
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func newFirefoxWithArgs(firefoxBinary string, args ...string) (*firefox, error) {
|
|
// The first two IDs are used internally during the initialization
|
|
if firefoxBinary == "" {
|
|
PromptDownload()
|
|
return nil, fmt.Errorf("Firefox not found.")
|
|
}
|
|
c := &firefox{
|
|
id: 2,
|
|
}
|
|
|
|
// Start firefox process
|
|
c.cmd = exec.Command(firefoxBinary, args...)
|
|
//pipe, err := c.cmd.StderrPipe()
|
|
//if err != nil {
|
|
//return nil, err
|
|
//}
|
|
if err := c.cmd.Start(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Wait for websocket address to be printed to stderr
|
|
/*re := regexp.MustCompile(`^DevTools listening on (ws://.*?)\r?\n$`)
|
|
m, err := readUntilMatch(pipe, re)
|
|
if err != nil {
|
|
c.kill()
|
|
return nil, err
|
|
}
|
|
wsURL := m[1]
|
|
|
|
// Open a websocket
|
|
c.ws, err = websocket.Dial(wsURL, "", "http://127.0.0.1")
|
|
if err != nil {
|
|
c.kill()
|
|
return nil, err
|
|
}
|
|
|
|
// Find target and initialize session
|
|
c.target, err = c.findTarget()
|
|
if err != nil {
|
|
c.kill()
|
|
return nil, err
|
|
}
|
|
|
|
c.session, err = c.startSession(c.target)
|
|
if err != nil {
|
|
c.kill()
|
|
return nil, err
|
|
}
|
|
go c.readLoop()
|
|
for method, args := range map[string]h{
|
|
"Page.enable": nil,
|
|
"Target.setAutoAttach": {"autoAttach": true, "waitForDebuggerOnStart": false},
|
|
"Network.enable": nil,
|
|
"Runtime.enable": nil,
|
|
"Security.enable": nil,
|
|
"Performance.enable": nil,
|
|
"Log.enable": nil,
|
|
} {
|
|
if _, err := c.send(method, args); err != nil {
|
|
c.kill()
|
|
c.cmd.Wait()
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
if !contains(args, "--headless") {
|
|
win, err := c.getWindowForTarget(c.target)
|
|
if err != nil {
|
|
c.kill()
|
|
return nil, err
|
|
}
|
|
c.window = win.WindowID
|
|
}*/
|
|
|
|
return c, nil
|
|
}
|