func (c *Gcd) probeDebugPort(endpoint string) {
	ticker := time.NewTicker(time.Millisecond * 100)
	timeoutTicker := time.NewTicker(c.timeout)
	defer func() {
		ticker.Stop()
		timeoutTicker.Stop()
	}()
	for {
		select {
		case <-ticker.C:
			resp, err := http.Get(endpoint)
			if err != nil {
				continue
			}
			defer resp.Body.Close()
			c.readyChErr <- nil
			return
		case <-timeoutTicker.C:
			c.readyChErr <- fmt.Errorf("Unable to contact debugger at %s after %v, gave up", c.apiEndpoint, c.timeout)
			return
		}
	}
}
probes the debugger report and signals when it's available.
package gcd
import (
	"context"
	"errors"
	"fmt"
	"io"
	"io/ioutil"
	"net/http"
	"os"
	"os/exec"
	"sync"
	"time"
	"github.com/wirepair/gcd/v2/observer"
	jsoniter "github.com/json-iterator/go"
	"github.com/wirepair/gcd/v2/gcdapi"
)
var json = jsoniter.ConfigCompatibleWithStandardLibrary
var GCDVERSION = "v2.2.3"
var (
	ErrNoTabAvailable = errors.New("no available tab found")
)
type GcdBodyReadErr struct {
	Message string
}
func (g *GcdBodyReadErr) Error() string {
	return "error reading response body: " + g.Message
}
type GcdDecodingErr struct {
	Message string
}
func (g *GcdDecodingErr) Error() string {
	return "error decoding inspectable page: " + g.Message
}
type TerminatedHandler func(reason string)
type OnChromeExitHandler func(profileDir string, err error)
type Gcd struct {
	processLock *sync.RWMutex  
	timeout             time.Duration  
	chromeProcess       *os.Process
	chromeCmd           *exec.Cmd
	chromeCmdOutput     io.Writer
	terminatedHandler   TerminatedHandler
	onChromeExitHandler OnChromeExitHandler
	port                string
	host                string
	addr                string
	profileDir          string
	deleteProfile       bool
	readyChErr          chan error
	apiEndpoint         string
	flags               []string
	env                 []string
	chomeApiVersion     string
	eventQueueSize      int
	ctx                 context.Context
	logger              Log
	debugEvents         bool
	debug               bool
	messageObserver     observer.MessageObserver
}
func NewChromeDebugger(opts ...func(*Gcd)) *Gcd {
	c := &Gcd{processLock: &sync.RWMutex{}}
	c.timeout = time.Second * 15
	c.host = "localhost"
	c.readyChErr = make(chan error)
	c.terminatedHandler = nil
	c.onChromeExitHandler = nil
	c.flags = make([]string, 0)
	c.env = make([]string, 0)
	c.eventQueueSize = 256
	c.ctx = context.Background()
	c.logger = LogDiscarder{}
	c.messageObserver = observer.NewIgnoreMessagesObserver()
	for _, o := range opts {
		o(c)
	}
	return c
}
func WithEventQueueSize(queueSize int) func(*Gcd) {
	return func(g *Gcd) {
		g.eventQueueSize = queueSize
	}
}
func WithTerminationHandler(handler TerminatedHandler) func(*Gcd) {
	return func(g *Gcd) {
		g.terminatedHandler = handler
	}
}
func WithOnChromeExitHandler(handler OnChromeExitHandler) func(*Gcd) {
	return func(g *Gcd) {
		g.onChromeExitHandler = handler
	}
}
func WithDebugPortTimeout(timeout time.Duration) func(*Gcd) {
	return func(g *Gcd) {
		g.timeout = timeout
	}
}
func WithChromeCmdOutput(output io.Writer) func(*Gcd) {
	return func(g *Gcd) {
		g.chromeCmdOutput = output
	}
}
func WithFlags(flags []string) func(*Gcd) {
	return func(g *Gcd) {
		g.flags = append(g.flags, flags...)
	}
}
func WithEnvironmentVars(vars []string) func(*Gcd) {
	return func(g *Gcd) {
		g.env = append(g.env, vars...)
	}
}
func WithDeleteProfileOnExit() func(*Gcd) {
	return func(g *Gcd) {
		g.deleteProfile = true
	}
}
func WithLogger(l Log) func(*Gcd) {
	return func(g *Gcd) {
		g.logger = l
	}
}
func WithContext(ctx context.Context) func(*Gcd) {
	return func(g *Gcd) {
		g.ctx = ctx
	}
}
func WithEventDebugging() func(*Gcd) {
	return func(g *Gcd) {
		g.debugEvents = true
	}
}
func WithInternalDebugMessages() func(*Gcd) {
	return func(g *Gcd) {
		g.debug = true
	}
}
func WithMessageObserver(observer observer.MessageObserver) func(*Gcd) {
	return func(g *Gcd) {
		g.messageObserver = observer
	}
}
func (c *Gcd) Port() string {
	return c.port
}
func (c *Gcd) Host() string {
	return c.host
}
func (c *Gcd) StartProcess(exePath, userDir, port string) error {
	c.port = port
	c.addr = fmt.Sprintf("%s:%s", c.host, c.port)
	c.profileDir = userDir
	c.apiEndpoint = fmt.Sprintf("http://%s/json", c.addr)
	c.flags = append(c.flags, fmt.Sprintf("--user-data-dir=%s", c.profileDir))
	c.flags = append(c.flags, fmt.Sprintf("--remote-debugging-port=%s", port))
	c.flags = append(c.flags, "--no-first-run")
	c.flags = append(c.flags, "--no-default-browser-check")
	c.chromeCmd = exec.Command(exePath, c.flags...)
	if c.chromeCmdOutput != nil {
		c.chromeCmd.Stdout = c.chromeCmdOutput
		c.chromeCmd.Stderr = c.chromeCmdOutput
	}
	c.chromeCmd.Env = os.Environ()
	c.chromeCmd.Env = append(c.chromeCmd.Env, c.env...)
	return c.startProcess()
}
func (c *Gcd) StartProcessCustom(cmd *exec.Cmd, userDir, port string) error {
	c.port = port
	c.addr = fmt.Sprintf("%s:%s", c.host, c.port)
	c.profileDir = userDir
	c.apiEndpoint = fmt.Sprintf("http://%s/json", c.addr)
	c.chromeCmd = cmd
	return c.startProcess()
}
func (c *Gcd) startProcess() error {
	go func() {
		err := c.chromeCmd.Start()
		if err != nil {
			msg := fmt.Sprintf("failed to start chrome: %s", err)
			if c.terminatedHandler != nil {
				c.terminatedHandler(msg)
			} else {
				c.logger.Println(msg)
			}
		}
		c.processLock.Lock()
		c.chromeProcess = c.chromeCmd.Process
		c.processLock.Unlock()
		err = c.chromeCmd.Wait()
		if c.onChromeExitHandler != nil {
			c.onChromeExitHandler(c.profileDir, err)
		}
		c.removeProfileDir()
		closeMessage := "exited"
		if err != nil {
			closeMessage = err.Error()
		}
		if c.terminatedHandler != nil {
			c.terminatedHandler(closeMessage)
		}
	}()
	go func(endpoint string) {
		c.probeDebugPort(endpoint)
	}(c.apiEndpoint)
	err := <-c.readyChErr
	return err
}
func (c *Gcd) ExitProcess() error {
	c.processLock.Lock()
	err := c.chromeProcess.Kill()
	c.processLock.Unlock()
	return err
}
func (c *Gcd) PID() int {
	c.processLock.Lock()
	pid := c.chromeProcess.Pid
	c.processLock.Unlock()
	return pid
}
func (c *Gcd) removeProfileDir() {
	if c.deleteProfile {
		time.Sleep(time.Second * 1)
		if err := os.RemoveAll(c.profileDir); err != nil {
			c.logger.Println("error deleting profile directory", err)
		}
	}
}
func (c *Gcd) ConnectToInstance(host string, port string) error {
	c.host = host
	c.port = port
	c.addr = fmt.Sprintf("%s:%s", c.host, c.port)
	c.apiEndpoint = fmt.Sprintf("http://%s/json", c.addr)
	go func(endpoint string) {
		c.probeDebugPort(endpoint)
	}(c.apiEndpoint)
	err := <-c.readyChErr
	return err
}
func (c *Gcd) GetTargets() ([]*ChromeTarget, error) {
	empty := make(map[string]struct{}, 0)
	return c.GetNewTargets(empty)
}
func (c *Gcd) GetNewTargets(knownIds map[string]struct{}) ([]*ChromeTarget, error) {
	connectableTargets, err := c.getConnectableTargets()
	if err != nil {
		return nil, err
	}
	chromeTargets := make([]*ChromeTarget, 0)
	for _, connectableTarget := range connectableTargets {
		if _, ok := knownIds[connectableTarget.Id]; !ok {
			target, err := openChromeTarget(c, connectableTarget, c.messageObserver)
			if err != nil {
				return nil, err
			}
			chromeTargets = append(chromeTargets, target)
		}
	}
	return chromeTargets, nil
}
func (c *Gcd) getConnectableTargets() ([]*TargetInfo, error) {
	for i := 0; i < 4; i++ {
		resp, err := http.Get(c.apiEndpoint)
		if err != nil {
			return nil, err
		}
		defer resp.Body.Close()
		body, errRead := ioutil.ReadAll(resp.Body)
		if errRead != nil {
			return nil, &GcdBodyReadErr{Message: errRead.Error()}
		}
		targets := make([]*TargetInfo, 0)
		err = json.Unmarshal(body, &targets)
		if err != nil {
			return nil, &GcdDecodingErr{Message: err.Error()}
		}
		connectableTargets := make([]*TargetInfo, 0)
		for _, v := range targets {
			if v.WebSocketDebuggerUrl != "" {
				connectableTargets = append(connectableTargets, v)
			}
		}
		if len(connectableTargets) > 0 {
			return connectableTargets, nil
		}
		time.Sleep(time.Millisecond * 350)
	}
	return nil, ErrNoTabAvailable
}
func (c *Gcd) NewTab() (*ChromeTarget, error) {
	resp, err := http.Get(c.apiEndpoint + "/new")
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()
	body, errRead := ioutil.ReadAll(resp.Body)
	if errRead != nil {
		return nil, &GcdBodyReadErr{Message: errRead.Error()}
	}
	tabTarget := &TargetInfo{}
	err = json.Unmarshal(body, &tabTarget)
	if err != nil {
		return nil, &GcdDecodingErr{Message: err.Error()}
	}
	return openChromeTarget(c, tabTarget, c.messageObserver)
}
func (c *Gcd) GetFirstTab() (*ChromeTarget, error) {
	connectableTargets, err := c.getConnectableTargets()
	if err != nil {
		return nil, err
	}
	for _, tabTarget := range connectableTargets {
		if tabTarget.Type == "page" {
			return openChromeTarget(c, tabTarget, c.messageObserver)
		}
	}
	return nil, ErrNoTabAvailable
}
func (c *Gcd) GetRevision() string {
	return gcdapi.CHROME_VERSION
}
func (c *Gcd) CloseTab(target *ChromeTarget) error {
	resp, err := http.Get(fmt.Sprintf("%s/close/%s", c.apiEndpoint, target.Target.Id))
	if err != nil {
		return err
	}
	defer resp.Body.Close()
	_, errRead := ioutil.ReadAll(resp.Body)
	return errRead
}
func (c *Gcd) ActivateTab(target *ChromeTarget) error {
	resp, err := http.Get(fmt.Sprintf("%s/activate/%s", c.apiEndpoint, target.Target.Id))
	if err != nil {
		return err
	}
	defer resp.Body.Close()
	_, errRead := ioutil.ReadAll(resp.Body)
	return errRead
}