mirror of
https://github.com/gotenberg/gotenberg.git
synced 2026-08-15 03:42:15 +01:00
huge refactoring
This commit is contained in:
@@ -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))
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
Reference in New Issue
Block a user