Konfi-Castle-Kasino/vendor/github.com/asticode/go-astilectron/astilectron.go

390 lines
10 KiB
Go

package astilectron
import (
"flag"
"fmt"
"net"
"os"
"os/exec"
"os/signal"
"runtime"
"strings"
"syscall"
"time"
"github.com/asticode/go-astilog"
"github.com/asticode/go-astitools/context"
"github.com/asticode/go-astitools/exec"
"github.com/asticode/go-astitools/slice"
"github.com/pkg/errors"
)
// Versions
const (
VersionAstilectron = "0.6.0"
VersionElectron = "1.6.5"
)
// Misc vars
var (
astilectronDirectoryPath = flag.String("astilectron-directory-path", "", "the astilectron directory path")
validOSes = []string{"darwin", "linux", "windows"}
)
// App event names
const (
EventNameAppClose = "app.close"
EventNameAppCmdStop = "app.cmd.stop"
EventNameAppCrash = "app.crash"
EventNameAppErrorAccept = "app.error.accept"
EventNameAppEventReady = "app.event.ready"
EventNameAppNoAccept = "app.no.accept"
EventNameAppTooManyAccept = "app.too.many.accept"
)
// Astilectron represents an object capable of interacting with Astilectron
// TODO Fix race conditions
type Astilectron struct {
canceller *asticontext.Canceller
channelQuit chan bool
dispatcher *Dispatcher
displayPool *displayPool
identifier *identifier
listener net.Listener
options Options
paths *Paths
provisioner Provisioner
reader *reader
stderrWriter *astiexec.StdWriter
stdoutWriter *astiexec.StdWriter
writer *writer
}
// Options represents Astilectron options
type Options struct {
AppName string
AppIconDarwinPath string // Darwin systems requires a specific .icns file
AppIconDefaultPath string
BaseDirectoryPath string
}
// New creates a new Astilectron instance
func New(o Options) (a *Astilectron, err error) {
// Validate the OS
if err = validateOS(runtime.GOOS); err != nil {
err = errors.Wrap(err, "validating OS failed")
return
}
a = &Astilectron{
canceller: asticontext.NewCanceller(),
channelQuit: make(chan bool),
dispatcher: newDispatcher(),
displayPool: newDisplayPool(),
identifier: newIdentifier(),
options: o,
provisioner: DefaultProvisioner,
}
// Set paths
if a.paths, err = newPaths(runtime.GOOS, runtime.GOARCH, o); err != nil {
err = errors.Wrap(err, "creating new paths failed")
return
}
// Add default listeners
a.On(EventNameAppCmdStop, func(e Event) (deleteListener bool) {
a.Stop()
return
})
a.On(EventNameDisplayEventAdded, func(e Event) (deleteListener bool) {
a.displayPool.update(e.Displays)
return
})
a.On(EventNameDisplayEventMetricsChanged, func(e Event) (deleteListener bool) {
a.displayPool.update(e.Displays)
return
})
a.On(EventNameDisplayEventRemoved, func(e Event) (deleteListener bool) {
a.displayPool.update(e.Displays)
return
})
return
}
// validateOS validates the OS
func validateOS(os string) error {
if !astislice.InStringSlice(os, validOSes) {
return fmt.Errorf("OS %s is not supported", os)
}
return nil
}
// ValidOSes returns a slice containing the names of all currently supported operating systems
func ValidOSes() []string {
return append(make([]string, 0, len(validOSes)), validOSes...)
}
// SetProvisioner sets the provisioner
func (a *Astilectron) SetProvisioner(p Provisioner) *Astilectron {
a.provisioner = p
return a
}
// On implements the Listenable interface
func (a *Astilectron) On(eventName string, l Listener) {
a.dispatcher.addListener(mainTargetID, eventName, l)
}
// Start starts Astilectron
func (a *Astilectron) Start() (err error) {
// Log
astilog.Debug("Starting...")
// Start the dispatcher
go a.dispatcher.start()
// Provision
if err = a.provision(); err != nil {
return errors.Wrap(err, "provisioning failed")
}
// Unfortunately communicating with Electron through stdin/stdout doesn't work on Windows so all communications
// will be done through TCP
if err = a.listenTCP(); err != nil {
return errors.Wrap(err, "listening failed")
}
// Execute
if err = a.execute(); err != nil {
err = errors.Wrap(err, "executing failed")
return
}
return
}
// provision provisions Astilectron
func (a *Astilectron) provision() error {
astilog.Debug("Provisioning...")
var ctx, _ = a.canceller.NewContext()
return a.provisioner.Provision(ctx, *a.dispatcher, a.options.AppName, runtime.GOOS, *a.paths)
}
// listenTCP listens to the first TCP connection coming its way (this should be Astilectron)
func (a *Astilectron) listenTCP() (err error) {
// Log
astilog.Debug("Listening...")
// Listen
if a.listener, err = net.Listen("tcp", "127.0.0.1:"); err != nil {
return errors.Wrap(err, "tcp net.Listen failed")
}
// Check a connection has been accepted quickly enough
var chanAccepted = make(chan bool)
go a.watchNoAccept(30*time.Second, chanAccepted)
// Accept connections
go a.acceptTCP(chanAccepted)
return
}
// watchNoAccept checks whether a TCP connection is accepted quickly enough
func (a *Astilectron) watchNoAccept(timeout time.Duration, chanAccepted chan bool) {
var t = time.NewTimer(timeout)
defer t.Stop()
for {
select {
case <-chanAccepted:
return
case <-t.C:
astilog.Errorf("No TCP connection has been accepted in the past %s", timeout)
a.dispatcher.Dispatch(Event{Name: EventNameAppNoAccept, TargetID: mainTargetID})
a.dispatcher.Dispatch(Event{Name: EventNameAppCmdStop, TargetID: mainTargetID})
return
}
}
}
// watchAcceptTCP accepts TCP connections
func (a *Astilectron) acceptTCP(chanAccepted chan bool) {
for i := 0; i <= 1; i++ {
// Accept
var conn net.Conn
var err error
if conn, err = a.listener.Accept(); err != nil {
astilog.Errorf("%s while TCP accepting", err)
a.dispatcher.Dispatch(Event{Name: EventNameAppErrorAccept, TargetID: mainTargetID})
a.dispatcher.Dispatch(Event{Name: EventNameAppCmdStop, TargetID: mainTargetID})
return
}
// We only accept the first connection which should be Astilectron, close the next one and stop
// the app
if i > 0 {
astilog.Errorf("Too many TCP connections")
a.dispatcher.Dispatch(Event{Name: EventNameAppTooManyAccept, TargetID: mainTargetID})
a.dispatcher.Dispatch(Event{Name: EventNameAppCmdStop, TargetID: mainTargetID})
conn.Close()
return
}
// Let the timer know a connection has been accepted
chanAccepted <- true
// Create reader and writer
a.writer = newWriter(conn)
a.reader = newReader(a.dispatcher, conn)
go a.reader.read()
}
}
// execute executes Astilectron in Electron
func (a *Astilectron) execute() (err error) {
// Log
astilog.Debug("Executing...")
// Create command
var ctx, _ = a.canceller.NewContext()
var cmd = exec.CommandContext(ctx, a.paths.AppExecutable(), a.paths.AstilectronApplication(), a.listener.Addr().String())
a.stderrWriter = astiexec.NewStdWriter(func(i []byte) { astilog.Debugf("Stderr says: %s", i) })
a.stdoutWriter = astiexec.NewStdWriter(func(i []byte) { astilog.Debugf("Stdout says: %s", i) })
cmd.Stderr = a.stderrWriter
cmd.Stdout = a.stdoutWriter
// Execute command
if err = a.executeCmd(cmd); err != nil {
return errors.Wrap(err, "executing cmd failed")
}
return
}
// executeCmd executes the command
func (a *Astilectron) executeCmd(cmd *exec.Cmd) (err error) {
var e = synchronousFunc(a.canceller, a, func() {
// Start command
astilog.Debugf("Starting cmd %s", strings.Join(cmd.Args, " "))
if err = cmd.Start(); err != nil {
err = errors.Wrapf(err, "starting cmd %s failed", strings.Join(cmd.Args, " "))
return
}
// Watch command
go a.watchCmd(cmd)
}, EventNameAppEventReady)
// Update display pool
if e.Displays != nil {
a.displayPool.update(e.Displays)
}
return
}
// watchCmd watches the cmd execution
func (a *Astilectron) watchCmd(cmd *exec.Cmd) {
// Wait
cmd.Wait()
// Check the canceller to check whether it was a crash
if !a.canceller.Cancelled() {
astilog.Debug("App has crashed")
a.dispatcher.Dispatch(Event{Name: EventNameAppCrash, TargetID: mainTargetID})
} else {
astilog.Debug("App has closed")
a.dispatcher.Dispatch(Event{Name: EventNameAppClose, TargetID: mainTargetID})
}
a.dispatcher.Dispatch(Event{Name: EventNameAppCmdStop, TargetID: mainTargetID})
}
// Close closes Astilectron properly
func (a *Astilectron) Close() {
astilog.Debug("Closing...")
a.canceller.Cancel()
a.dispatcher.close()
if a.listener != nil {
a.listener.Close()
}
if a.reader != nil {
a.reader.close()
}
if a.stderrWriter != nil {
a.stderrWriter.Close()
}
if a.stdoutWriter != nil {
a.stdoutWriter.Close()
}
if a.writer != nil {
a.writer.close()
}
}
// HandleSignals handles signals
func (a *Astilectron) HandleSignals() {
ch := make(chan os.Signal, 1)
signal.Notify(ch, syscall.SIGABRT, syscall.SIGKILL, syscall.SIGINT, syscall.SIGQUIT, syscall.SIGTERM)
go func() {
for sig := range ch {
astilog.Debugf("Received signal %s", sig)
a.Stop()
}
}()
}
// Stop orders Astilectron to stop
func (a *Astilectron) Stop() {
astilog.Debug("Stopping...")
a.canceller.Cancel()
if a.channelQuit != nil {
close(a.channelQuit)
a.channelQuit = nil
}
}
// Wait is a blocking pattern
func (a *Astilectron) Wait() {
if a.channelQuit == nil {
return
}
<-a.channelQuit
}
// Displays returns the displays
func (a *Astilectron) Displays() []*Display {
return a.displayPool.all()
}
// PrimaryDisplay returns the primary display
func (a *Astilectron) PrimaryDisplay() *Display {
return a.displayPool.primary()
}
// NewMenu creates a new app menu
func (a *Astilectron) NewMenu(i []*MenuItemOptions) *Menu {
return newMenu(nil, a, i, a.canceller, a.dispatcher, a.identifier, a.writer)
}
// NewWindow creates a new window
func (a *Astilectron) NewWindow(url string, o *WindowOptions) (*Window, error) {
return newWindow(a.options, url, o, a.canceller, a.dispatcher, a.identifier, a.writer)
}
// NewWindowInDisplay creates a new window in a specific display
// This overrides the center attribute
func (a *Astilectron) NewWindowInDisplay(d *Display, url string, o *WindowOptions) (*Window, error) {
if o.X != nil {
*o.X += d.Bounds().X
} else {
o.X = PtrInt(d.Bounds().X)
}
if o.Y != nil {
*o.Y += d.Bounds().Y
} else {
o.Y = PtrInt(d.Bounds().Y)
}
return newWindow(a.options, url, o, a.canceller, a.dispatcher, a.identifier, a.writer)
}
// NewTray creates a new tray
func (a *Astilectron) NewTray(o *TrayOptions) *Tray {
return newTray(o, a.canceller, a.dispatcher, a.identifier, a.writer)
}