huge refactoring

This commit is contained in:
Julien Neuhart
2019-07-23 13:57:18 +02:00
parent f6b357691c
commit 7bfbda4490
105 changed files with 4051 additions and 2667 deletions

View File

@@ -5,45 +5,72 @@ import (
"time"
"github.com/mafredri/cdp/devtool"
"github.com/thecodingmachine/gotenberg/internal/pkg/logger"
"github.com/thecodingmachine/gotenberg/internal/pkg/standarderror"
"github.com/thecodingmachine/gotenberg/internal/pkg/xerror"
"github.com/thecodingmachine/gotenberg/internal/pkg/xlog"
)
const chromeWarmupTime time.Duration = 10 * time.Second
type chrome struct {
manager *processManager
type chromeProcess struct {
logger xlog.Logger
}
// NewChrome returns a Google Chrome
// NewChromeProcess returns a Google Chrome
// headless process.
func NewChrome(logger *logger.Logger) Process {
return &chrome{
manager: &processManager{logger: logger},
func NewChromeProcess(logger xlog.Logger) Process {
return chromeProcess{
logger: logger,
}
}
func (p *chrome) Fullname() string {
func (p chromeProcess) Fullname() string {
return "Google Chrome headless"
}
func (p *chrome) Start() error {
const op string = "pm2.chrome.Start"
if err := p.manager.start(p); err != nil {
return &standarderror.Error{Op: op, Err: err}
func (p chromeProcess) Start() error {
const op string = "pm2.chromeProcess.Start"
if err := start(p.logger, p); err != nil {
return xerror.New(op, err)
}
return nil
}
func (p *chrome) Shutdown() error {
const op string = "pm2.chrome.Shutdown"
if err := p.manager.shutdown(p); err != nil {
return &standarderror.Error{Op: op, Err: err}
func (p chromeProcess) IsViable() bool {
const op string = "pm2.chromeProcess.IsViable"
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
p.logger.DebugfOp(
op,
"checking '%s' viability via endpoint '%s'",
p.Fullname(),
"http://localhost:9222/json/version",
)
v, err := devtool.New("http://localhost:9222").Version(ctx)
if err != nil {
p.logger.ErrorfOp(
op,
"'%s' is not viable as endpoint returned '%v'",
p.Fullname(),
err,
)
return false
}
p.logger.DebugfOp(
op,
"'%s' is viable as endpoint returned '%v'",
p.Fullname(),
v,
)
return true
}
func (p chromeProcess) Stop() error {
const op string = "pm2.chromeProcess.Stop"
if err := stop(p.logger, p); err != nil {
return xerror.New(op, err)
}
return nil
}
func (p *chrome) args() []string {
func (p chromeProcess) args() []string {
return []string{
"--no-sandbox",
"--headless",
@@ -62,47 +89,25 @@ func (p *chrome) args() []string {
}
}
func (p *chrome) name() string {
func (p chromeProcess) binary() string {
return "google-chrome-stable"
}
func (p *chrome) viable() bool {
const op string = "pm2.chrome.viable"
// check if Google Chrome is correctly running.
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
p.manager.logger.DebugfOp(
op,
"checking liveness via debug version endpoint http://localhost:9222/json/version",
func (p chromeProcess) warmup() {
const (
op string = "pm2.chromeProcess.warmup"
warmupTime time.Duration = 10 * time.Second
)
v, err := devtool.New("http://localhost:9222").Version(ctx)
if err != nil {
p.manager.logger.DebugfOp(
op,
"debug version endpoint returned error: %v",
err,
)
return false
}
p.manager.logger.DebugfOp(
p.logger.DebugfOp(
op,
"debug version endpoint returned version info: %+v",
*v,
"waiting '%v' for allowing '%s' to warmup",
warmupTime,
p.Fullname(),
)
return true
}
func (p *chrome) warmup() {
const op string = "pm2.chrome.warmup"
p.manager.logger.DebugfOp(
op,
"allowing %v to startup",
chromeWarmupTime,
)
time.Sleep(chromeWarmupTime)
time.Sleep(warmupTime)
}
// Compile-time checks to ensure type implements desired interfaces.
var (
_ = Process(new(chrome))
_ = Process(new(chromeProcess))
)

View File

@@ -1,20 +0,0 @@
package pm2
import (
"testing"
"github.com/stretchr/testify/require"
"github.com/thecodingmachine/gotenberg/test"
)
func TestChromeStart(t *testing.T) {
p := NewChrome(test.CreateTestLogger())
err := p.Start()
require.Nil(t, err)
}
func TestChromeShutdown(t *testing.T) {
p := NewChrome(test.CreateTestLogger())
err := p.Shutdown()
require.Nil(t, err)
}

View File

@@ -1,6 +1,6 @@
/*
Package pm2 facilitates starting external
processes on which our API depends.
processes on which our application depends.
For instance, it may start Google Chrome headless and
unoconv listener with PM2.

View File

@@ -1,124 +1,102 @@
package pm2
import (
"bufio"
"fmt"
"io"
"os/exec"
"strings"
"github.com/thecodingmachine/gotenberg/internal/pkg/logger"
"github.com/thecodingmachine/gotenberg/internal/pkg/standarderror"
)
const (
stoppedState int32 = iota
runningState
errorState
"github.com/thecodingmachine/gotenberg/internal/pkg/xerror"
"github.com/thecodingmachine/gotenberg/internal/pkg/xexec"
"github.com/thecodingmachine/gotenberg/internal/pkg/xlog"
)
// Process is a type that can start or
// shutdown a process with PM2.
// stop a process with PM2.
type Process interface {
Fullname() string
Start() error
Shutdown() error
IsViable() bool
Stop() error
args() []string
name() string
viable() bool
binary() string
warmup()
}
type processManager struct {
heuristicState int32
logger *logger.Logger
}
type pm2Command string
func (m *processManager) start(p Process) error {
const op string = "pm2.start"
if err := m.pm2(p, "start"); err != nil {
return &standarderror.Error{Op: op, Err: err}
}
p.warmup()
if !p.viable() {
attempts := 0
for attempts < 5 && !p.viable() {
if err := m.pm2(p, "restart"); err != nil {
m.heuristicState = errorState
return &standarderror.Error{Op: op, Err: err}
}
p.warmup()
attempts++
const (
startCommand pm2Command = "start"
restartCommand pm2Command = "restart"
stopCommand pm2Command = "stop"
logsCommand pm2Command = "logs"
)
func start(logger xlog.Logger, process Process) error {
const (
op string = "pm2.start"
maximumAttempts int = 3
)
resolver := func() error {
// first, we try to start the process.
if err := run(logger, startCommand, process); err != nil {
return err
}
if !p.viable() {
m.heuristicState = errorState
return &standarderror.Error{
Op: op,
Message: fmt.Sprintf("failed to launch %s", p.Fullname()),
// we wait the process to be ready.
process.warmup()
// if the process failed to start correctly,
// we have to restart it.
if !process.IsViable() {
attempts := 0
for attempts < maximumAttempts && !process.IsViable() {
if err := run(logger, restartCommand, process); err != nil {
return err
}
process.warmup()
attempts++
}
if !process.IsViable() {
return fmt.Errorf("failed to start '%s'", process.Fullname())
}
}
}
m.heuristicState = runningState
return nil
}
func (m *processManager) shutdown(p Process) error {
const op string = "pm2.shutdown"
if m.heuristicState != runningState {
// the process is viable, let's log its
// output.
if err := run(logger, logsCommand, process); err != nil {
return err
}
return nil
}
if err := m.pm2(p, "stop"); err != nil {
m.heuristicState = errorState
return &standarderror.Error{Op: op, Err: err}
if err := resolver(); err != nil {
return xerror.New(op, err)
}
m.heuristicState = stoppedState
return nil
}
func (m *processManager) pm2(p Process, cmdName string) error {
const op string = "pm2.pm2"
cmdArgs := []string{
cmdName,
p.name(),
}
if cmdName == "start" {
cmdArgs = append(cmdArgs, "--interpreter=none", "--")
cmdArgs = append(cmdArgs, p.args()...)
}
cmd := exec.Command(
"pm2",
cmdArgs...,
)
m.logger.DebugfOp(op, "executing command: %s", strings.Join(cmd.Args, " "))
processStdOut, err := cmd.StdoutPipe()
if err != nil {
return &standarderror.Error{Op: op, Err: err}
}
processStdErr, err := cmd.StderrPipe()
if err != nil {
return &standarderror.Error{Op: op, Err: err}
}
readFromPipe := func(outputType string, reader io.ReadCloser) {
readFromPipeOp := fmt.Sprintf("pm2.%s.%s", p.name(), outputType)
r := bufio.NewReader(reader)
defer reader.Close() // nolint: errcheck
for {
line, _, err := r.ReadLine()
if err != nil {
if err != io.EOF {
m.logger.ErrorOp(readFromPipeOp, err)
}
break
}
if len(line) != 0 {
m.logger.DebugfOp(readFromPipeOp, string(line))
}
}
}
go readFromPipe("stdout", processStdOut)
go readFromPipe("stderr", processStdErr)
if err := cmd.Start(); err != nil {
return &standarderror.Error{Op: op, Err: err}
func stop(logger xlog.Logger, process Process) error {
const op string = "pm2.stop"
if err := run(logger, stopCommand, process); err != nil {
return xerror.New(op, err)
}
return nil
}
func run(logger xlog.Logger, pm2Cmd pm2Command, process Process) error {
const op string = "pm2.run"
resolver := func() error {
args := []string{
string(pm2Cmd),
process.binary(),
}
if pm2Cmd == startCommand {
args = append(args, "--interpreter=none", "--")
args = append(args, process.args()...)
}
cmd, err := xexec.Command(logger, "pm2", args...)
if err != nil {
return err
}
xexec.LogBeforeExecute(logger, cmd)
return cmd.Start()
}
if err := resolver(); err != nil {
return xerror.New(op, err)
}
return nil
}

View File

@@ -3,73 +3,75 @@ package pm2
import (
"time"
"github.com/thecodingmachine/gotenberg/internal/pkg/logger"
"github.com/thecodingmachine/gotenberg/internal/pkg/standarderror"
"github.com/thecodingmachine/gotenberg/internal/pkg/xerror"
"github.com/thecodingmachine/gotenberg/internal/pkg/xlog"
)
const unoconvWarmupTime time.Duration = 5 * time.Second
type unoconv struct {
manager *processManager
type unoconvProcess struct {
logger xlog.Logger
}
// NewUnoconv returns a unoconv listener
// NewUnoconvProcess returns a unoconv listener
// process.
func NewUnoconv(logger *logger.Logger) Process {
return &unoconv{
manager: &processManager{logger: logger},
func NewUnoconvProcess(logger xlog.Logger) Process {
return unoconvProcess{
logger: logger,
}
}
func (p *unoconv) Fullname() string {
func (p unoconvProcess) Fullname() string {
return "unoconv listener"
}
func (p *unoconv) Start() error {
const op string = "pm2.unoconv.Start"
if err := p.manager.start(p); err != nil {
return &standarderror.Error{Op: op, Err: err}
func (p unoconvProcess) Start() error {
const op string = "pm2.unoconvProcess.Start"
if err := start(p.logger, p); err != nil {
return xerror.New(op, err)
}
return nil
}
func (p *unoconv) Shutdown() error {
const op string = "pm2.unoconv.Shutdown"
if err := p.manager.shutdown(p); err != nil {
return &standarderror.Error{Op: op, Err: err}
}
return nil
}
func (p *unoconv) args() []string {
return []string{
"--listener",
"--verbose",
}
}
func (p *unoconv) name() string {
return "unoconv"
}
func (p *unoconv) viable() bool {
func (p unoconvProcess) IsViable() bool {
// TODO find a way to check if
// the unoconv listener
// is correctly started?
return true
}
func (p *unoconv) warmup() {
const op string = "pm2.unoconv.warmup"
p.manager.logger.DebugfOp(
op,
"allowing %v to startup",
unoconvWarmupTime,
func (p unoconvProcess) Stop() error {
const op string = "pm2.unoconvProcess.Stop"
if err := stop(p.logger, p); err != nil {
return xerror.New(op, err)
}
return nil
}
func (p unoconvProcess) args() []string {
return []string{
"--listener",
"--verbose",
}
}
func (p unoconvProcess) binary() string {
return "unoconv"
}
func (p unoconvProcess) warmup() {
const (
op string = "pm2.unoconvProcess.warmup"
warmupTime time.Duration = 3 * time.Second
)
time.Sleep(unoconvWarmupTime)
p.logger.DebugfOp(
op,
"waiting '%v' for allowing '%s' to warmup",
warmupTime,
p.Fullname(),
)
time.Sleep(warmupTime)
}
// Compile-time checks to ensure type implements desired interfaces.
var (
_ = Process(new(unoconv))
_ = Process(new(unoconvProcess))
)

View File

@@ -1,20 +0,0 @@
package pm2
import (
"testing"
"github.com/stretchr/testify/require"
"github.com/thecodingmachine/gotenberg/test"
)
func TestUnoconvStart(t *testing.T) {
p := NewUnoconv(test.CreateTestLogger())
err := p.Start()
require.Nil(t, err)
}
func TestUnoconvShutdown(t *testing.T) {
p := NewUnoconv(test.CreateTestLogger())
err := p.Shutdown()
require.Nil(t, err)
}