process balancing: better implementation (wip)

This commit is contained in:
Julien Neuhart
2019-09-23 18:29:24 +02:00
parent 171f93662f
commit 88f74d5e9c
16 changed files with 1270 additions and 262 deletions

View File

@@ -9,7 +9,6 @@ import (
"github.com/thecodingmachine/gotenberg/internal/app/xhttp" "github.com/thecodingmachine/gotenberg/internal/app/xhttp"
"github.com/thecodingmachine/gotenberg/internal/pkg/conf" "github.com/thecodingmachine/gotenberg/internal/pkg/conf"
"github.com/thecodingmachine/gotenberg/internal/pkg/prinery" "github.com/thecodingmachine/gotenberg/internal/pkg/prinery"
"github.com/thecodingmachine/gotenberg/internal/pkg/process"
"github.com/thecodingmachine/gotenberg/internal/pkg/xcontext" "github.com/thecodingmachine/gotenberg/internal/pkg/xcontext"
"github.com/thecodingmachine/gotenberg/internal/pkg/xlog" "github.com/thecodingmachine/gotenberg/internal/pkg/xlog"
) )
@@ -27,30 +26,18 @@ func main() {
} }
systemLogger.InfofOp(op, "Gotenberg %s", version) systemLogger.InfofOp(op, "Gotenberg %s", version)
systemLogger.DebugfOp(op, "configuration: %+v", config) systemLogger.DebugfOp(op, "configuration: %+v", config)
// create PM2 manager and start processes. // create our prinery.
manager := process.NewPM2Manager(systemLogger, config) prinry, err := prinery.NewPM2Prinery(systemLogger, config)
if err := manager.Start(); err != nil { if err != nil {
systemLogger.FatalOp(op, err) systemLogger.FatalOp(op, err)
} }
// create prineries. // TODO use chan
var chromePrinery *prinery.Prinery // TODO stop prinery
if !config.DisableGoogleChrome() { if err = prinry.Start(make(chan error, 1)); err != nil {
chromePrinery, err = prinery.New(systemLogger, manager, process.ChromeKey) systemLogger.FatalOp(op, err)
if err != nil {
systemLogger.FatalOp(op, err)
}
go chromePrinery.Start()
}
var sofficePrinery *prinery.Prinery
if !config.DisableUnoconv() {
sofficePrinery, err = prinery.New(systemLogger, manager, process.SofficeKey)
if err != nil {
systemLogger.FatalOp(op, err)
}
go sofficePrinery.Start()
} }
// create our API. // create our API.
srv := xhttp.New(config, manager, chromePrinery, sofficePrinery) srv := xhttp.New(config, prinry)
// run our API in a goroutine so that it doesn't block. // run our API in a goroutine so that it doesn't block.
go func() { go func() {
systemLogger.InfofOp(op, "http server started on port '%d'", config.DefaultListenPort()) systemLogger.InfofOp(op, "http server started on port '%d'", config.DefaultListenPort())

View File

@@ -1,6 +1,7 @@
package xhttp package xhttp
import ( import (
timeoutContext "context"
"fmt" "fmt"
"net/http" "net/http"
"os" "os"
@@ -9,8 +10,6 @@ import (
"github.com/thecodingmachine/gotenberg/internal/app/xhttp/pkg/context" "github.com/thecodingmachine/gotenberg/internal/app/xhttp/pkg/context"
"github.com/thecodingmachine/gotenberg/internal/app/xhttp/pkg/resource" "github.com/thecodingmachine/gotenberg/internal/app/xhttp/pkg/resource"
"github.com/thecodingmachine/gotenberg/internal/pkg/pm2" "github.com/thecodingmachine/gotenberg/internal/pkg/pm2"
"github.com/thecodingmachine/gotenberg/internal/pkg/prinery"
"github.com/thecodingmachine/gotenberg/internal/pkg/print"
"github.com/thecodingmachine/gotenberg/internal/pkg/xcontext" "github.com/thecodingmachine/gotenberg/internal/pkg/xcontext"
"github.com/thecodingmachine/gotenberg/internal/pkg/xerror" "github.com/thecodingmachine/gotenberg/internal/pkg/xerror"
"github.com/thecodingmachine/gotenberg/internal/pkg/xlog" "github.com/thecodingmachine/gotenberg/internal/pkg/xlog"
@@ -72,8 +71,11 @@ func mergeHandler(c echo.Context) error {
if err != nil { if err != nil {
return err return err
} }
p := print.NewMergePrint(logger, fpaths) prinry := ctx.Prinery()
return convert(ctx, nil, p, timeout) printFunc := func(ctx timeoutContext.Context, logger xlog.Logger, dest string) error {
return prinry.Merge(ctx, logger, dest, fpaths)
}
return convert(ctx, timeout, printFunc)
} }
if err := resolver(); err != nil { if err := resolver(); err != nil {
return xerror.New(op, err) return xerror.New(op, err)
@@ -87,7 +89,6 @@ func htmlHandler(c echo.Context) error {
const op string = "xhttp.htmlHandler" const op string = "xhttp.htmlHandler"
resolver := func() error { resolver := func() error {
ctx := context.MustCastFromEchoContext(c) ctx := context.MustCastFromEchoContext(c)
prinry := ctx.MustChromePrinery()
logger := ctx.XLogger() logger := ctx.XLogger()
logger.DebugOp(op, "handling HTML request...") logger.DebugOp(op, "handling HTML request...")
config := ctx.Config() config := ctx.Config()
@@ -104,8 +105,11 @@ func htmlHandler(c echo.Context) error {
if err != nil { if err != nil {
return err return err
} }
p := print.NewHTMLPrint(logger, fpath, opts) prinry := ctx.Prinery()
return convert(ctx, prinry, p, timeout) printFunc := func(ctx timeoutContext.Context, logger xlog.Logger, dest string) error {
return prinry.HTML(ctx, logger, dest, fpath, opts)
}
return convert(ctx, timeout, printFunc)
} }
if err := resolver(); err != nil { if err := resolver(); err != nil {
return xerror.New(op, err) return xerror.New(op, err)
@@ -119,7 +123,6 @@ func urlHandler(c echo.Context) error {
const op string = "xhttp.urlHandler" const op string = "xhttp.urlHandler"
resolver := func() error { resolver := func() error {
ctx := context.MustCastFromEchoContext(c) ctx := context.MustCastFromEchoContext(c)
prinry := ctx.MustChromePrinery()
logger := ctx.XLogger() logger := ctx.XLogger()
logger.DebugOp(op, "handling URL request...") logger.DebugOp(op, "handling URL request...")
config := ctx.Config() config := ctx.Config()
@@ -143,8 +146,11 @@ func urlHandler(c echo.Context) error {
if err != nil { if err != nil {
return err return err
} }
p := print.NewURLPrint(logger, remoteURL, opts) prinry := ctx.Prinery()
return convert(ctx, prinry, p, timeout) printFunc := func(ctx timeoutContext.Context, logger xlog.Logger, dest string) error {
return prinry.URL(ctx, logger, dest, remoteURL, opts)
}
return convert(ctx, timeout, printFunc)
} }
if err := resolver(); err != nil { if err := resolver(); err != nil {
return xerror.New(op, err) return xerror.New(op, err)
@@ -158,7 +164,6 @@ func markdownHandler(c echo.Context) error {
const op string = "xhttp.markdownHandler" const op string = "xhttp.markdownHandler"
resolver := func() error { resolver := func() error {
ctx := context.MustCastFromEchoContext(c) ctx := context.MustCastFromEchoContext(c)
prinry := ctx.MustChromePrinery()
logger := ctx.XLogger() logger := ctx.XLogger()
logger.DebugOp(op, "handling Markdown request...") logger.DebugOp(op, "handling Markdown request...")
config := ctx.Config() config := ctx.Config()
@@ -175,11 +180,11 @@ func markdownHandler(c echo.Context) error {
if err != nil { if err != nil {
return err return err
} }
p, err := print.NewMarkdownPrint(logger, fpath, opts) prinry := ctx.Prinery()
if err != nil { printFunc := func(ctx timeoutContext.Context, logger xlog.Logger, dest string) error {
return err return prinry.Markdown(ctx, logger, dest, fpath, opts)
} }
return convert(ctx, prinry, p, timeout) return convert(ctx, timeout, printFunc)
} }
if err := resolver(); err != nil { if err := resolver(); err != nil {
return xerror.New(op, err) return xerror.New(op, err)
@@ -193,7 +198,6 @@ func officeHandler(c echo.Context) error {
const op string = "xhttp.officeHandler" const op string = "xhttp.officeHandler"
resolver := func() error { resolver := func() error {
ctx := context.MustCastFromEchoContext(c) ctx := context.MustCastFromEchoContext(c)
prinry := ctx.MustSofficePrinery()
logger := ctx.XLogger() logger := ctx.XLogger()
logger.DebugOp(op, "handling Office request...") logger.DebugOp(op, "handling Office request...")
config := ctx.Config() config := ctx.Config()
@@ -202,7 +206,7 @@ func officeHandler(c echo.Context) error {
if err != nil { if err != nil {
return err return err
} }
opts, err := officePrintOptions(r, config) opts, err := unoconvPrintOptions(r, config)
if err != nil { if err != nil {
return err return err
} }
@@ -223,8 +227,11 @@ func officeHandler(c echo.Context) error {
if err != nil { if err != nil {
return err return err
} }
p := print.NewOfficePrint(logger, fpaths, opts) prinry := ctx.Prinery()
return convert(ctx, prinry, p, timeout) printFunc := func(ctx timeoutContext.Context, logger xlog.Logger, dest string) error {
return prinry.Office(ctx, logger, dest, fpaths, opts)
}
return convert(ctx, timeout, printFunc)
} }
if err := resolver(); err != nil { if err := resolver(); err != nil {
return xerror.New(op, err) return xerror.New(op, err)
@@ -232,7 +239,11 @@ func officeHandler(c echo.Context) error {
return nil return nil
} }
func convert(ctx context.Context, prinry *prinery.Prinery, prnt print.Print, timeout float64) error { func convert(
ctx context.Context,
timeout float64,
printFunc func(ctx timeoutContext.Context, logger xlog.Logger, dest string) error,
) error {
const op string = "xhttp.convert" const op string = "xhttp.convert"
resolver := func() error { resolver := func() error {
logger := ctx.XLogger() logger := ctx.XLogger()
@@ -245,13 +256,13 @@ func convert(ctx context.Context, prinry *prinery.Prinery, prnt print.Print, tim
// or an error. // or an error.
if !r.HasArg(resource.WebhookURLArgKey) { if !r.HasArg(resource.WebhookURLArgKey) {
logger.DebugfOp(op, "no '%s' found, converting synchronously", resource.WebhookURLArgKey) logger.DebugfOp(op, "no '%s' found, converting synchronously", resource.WebhookURLArgKey)
return convertSync(ctx, prinry, prnt, timeout, filename, fpath) return convertSync(ctx, timeout, filename, fpath, printFunc)
} }
// as a webhook URL has been given, we // as a webhook URL has been given, we
// run the following lines in a goroutine so that // run the following lines in a goroutine so that
// it doesn't block. // it doesn't block.
logger.DebugfOp(op, "'%s' found, converting asynchronously", resource.WebhookURLArgKey) logger.DebugfOp(op, "'%s' found, converting asynchronously", resource.WebhookURLArgKey)
return convertAsync(ctx, prinry, prnt, timeout, filename, fpath) return convertAsync(ctx, timeout, filename, fpath, printFunc)
} }
if err := resolver(); err != nil { if err := resolver(); err != nil {
return xerror.New(op, err) return xerror.New(op, err)
@@ -259,19 +270,19 @@ func convert(ctx context.Context, prinry *prinery.Prinery, prnt print.Print, tim
return nil return nil
} }
func convertSync(ctx context.Context, prinry *prinery.Prinery, prnt print.Print, timeout float64, filename, fpath string) error { func convertSync(
ctx context.Context,
timeout float64,
filename, dest string,
printFunc func(ctx timeoutContext.Context, logger xlog.Logger, dest string) error,
) error {
const op = "xhttp.convertSync" const op = "xhttp.convertSync"
logger := ctx.XLogger() logger := ctx.XLogger()
r := ctx.MustResource() r := ctx.MustResource()
timeoutCtx, cancel := xcontext.WithTimeout(logger, timeout) timeoutCtx, cancel := xcontext.WithTimeout(logger, timeout)
defer cancel() defer cancel()
resolver := func() error { resolver := func() error {
if prinry == nil { if err := printFunc(timeoutCtx, logger, dest); err != nil {
// case: merge.
if err := prnt.Print(timeoutCtx, fpath, nil); err != nil {
return err
}
} else if err := prinry.PrintRequest(timeoutCtx, logger, prnt, fpath); err != nil {
return err return err
} }
if !r.HasArg(resource.ResultFilenameArgKey) { if !r.HasArg(resource.ResultFilenameArgKey) {
@@ -281,7 +292,7 @@ func convertSync(ctx context.Context, prinry *prinery.Prinery, prnt print.Print,
resource.RemoteURLArgKey, resource.RemoteURLArgKey,
filename, filename,
) )
if err := ctx.Attachment(fpath, filename); err != nil { if err := ctx.Attachment(dest, filename); err != nil {
return err return err
} }
return nil return nil
@@ -295,7 +306,7 @@ func convertSync(ctx context.Context, prinry *prinery.Prinery, prnt print.Print,
if err != nil { if err != nil {
return err return err
} }
if err := ctx.Attachment(fpath, filename); err != nil { if err := ctx.Attachment(dest, filename); err != nil {
return err return err
} }
return nil return nil
@@ -309,7 +320,12 @@ func convertSync(ctx context.Context, prinry *prinery.Prinery, prnt print.Print,
return nil return nil
} }
func convertAsync(ctx context.Context, prinry *prinery.Prinery, prnt print.Print, timeout float64, filename, fpath string) error { func convertAsync(
ctx context.Context,
timeout float64,
filename, dest string,
printFunc func(ctx timeoutContext.Context, logger xlog.Logger, dest string) error,
) error {
const op = "xhttp.convertAsync" const op = "xhttp.convertAsync"
logger := ctx.XLogger() logger := ctx.XLogger()
r := ctx.MustResource() r := ctx.MustResource()
@@ -325,19 +341,12 @@ func convertAsync(ctx context.Context, prinry *prinery.Prinery, prnt print.Print
defer r.Close() // nolint: errcheck defer r.Close() // nolint: errcheck
timeoutCtx, cancel := xcontext.WithTimeout(logger, timeout) timeoutCtx, cancel := xcontext.WithTimeout(logger, timeout)
defer cancel() defer cancel()
if prinry == nil { if err := printFunc(timeoutCtx, logger, dest); err != nil {
// case: merge.
if err := prnt.Print(timeoutCtx, fpath, nil); err != nil {
xerr := xerror.New(op, err)
logger.ErrorOp(xerror.Op(xerr), xerr)
return
}
} else if err := prinry.PrintRequest(timeoutCtx, logger, prnt, fpath); err != nil {
xerr := xerror.New(op, err) xerr := xerror.New(op, err)
logger.ErrorOp(xerror.Op(xerr), xerr) logger.ErrorOp(xerror.Op(xerr), xerr)
return return
} }
f, err := os.Open(fpath) f, err := os.Open(dest)
if err != nil { if err != nil {
xerr := xerror.New(op, err) xerr := xerror.New(op, err)
logger.ErrorOp(xerror.Op(xerr), xerr) logger.ErrorOp(xerror.Op(xerr), xerr)

View File

@@ -8,7 +8,6 @@ import (
"github.com/thecodingmachine/gotenberg/internal/app/xhttp/pkg/resource" "github.com/thecodingmachine/gotenberg/internal/app/xhttp/pkg/resource"
"github.com/thecodingmachine/gotenberg/internal/pkg/conf" "github.com/thecodingmachine/gotenberg/internal/pkg/conf"
"github.com/thecodingmachine/gotenberg/internal/pkg/prinery" "github.com/thecodingmachine/gotenberg/internal/pkg/prinery"
"github.com/thecodingmachine/gotenberg/internal/pkg/process"
"github.com/thecodingmachine/gotenberg/internal/pkg/xerror" "github.com/thecodingmachine/gotenberg/internal/pkg/xerror"
"github.com/thecodingmachine/gotenberg/internal/pkg/xlog" "github.com/thecodingmachine/gotenberg/internal/pkg/xlog"
"github.com/thecodingmachine/gotenberg/internal/pkg/xrand" "github.com/thecodingmachine/gotenberg/internal/pkg/xrand"
@@ -16,12 +15,7 @@ import (
// contextMiddleware extends the default echo.Context with // contextMiddleware extends the default echo.Context with
// our custom context.Context. // our custom context.Context.
func contextMiddleware( func contextMiddleware(config conf.Config, prinry prinery.Prinery) echo.MiddlewareFunc {
config conf.Config,
manager process.Manager,
chromePrinery *prinery.Prinery,
sofficePrinery *prinery.Prinery,
) echo.MiddlewareFunc {
return func(next echo.HandlerFunc) echo.HandlerFunc { return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error { return func(c echo.Context) error {
// generate a unique identifier for the request. // generate a unique identifier for the request.
@@ -31,7 +25,7 @@ func contextMiddleware(
logger := xlog.New(config.LogLevel(), trace) logger := xlog.New(config.LogLevel(), trace)
// extend the current echo context with our custom // extend the current echo context with our custom
// context. // context.
ctx := context.New(c, logger, config, manager, chromePrinery, sofficePrinery) ctx := context.New(c, logger, config, prinry)
// if its an healthcheck request, there // if its an healthcheck request, there
// is no need to create a Resource. // is no need to create a Resource.
if ctx.Path() == pingEndpoint { if ctx.Path() == pingEndpoint {

View File

@@ -3,37 +3,37 @@ package xhttp
import ( import (
"github.com/thecodingmachine/gotenberg/internal/app/xhttp/pkg/resource" "github.com/thecodingmachine/gotenberg/internal/app/xhttp/pkg/resource"
"github.com/thecodingmachine/gotenberg/internal/pkg/conf" "github.com/thecodingmachine/gotenberg/internal/pkg/conf"
"github.com/thecodingmachine/gotenberg/internal/pkg/print" "github.com/thecodingmachine/gotenberg/internal/pkg/prinery"
"github.com/thecodingmachine/gotenberg/internal/pkg/xerror" "github.com/thecodingmachine/gotenberg/internal/pkg/xerror"
) )
func chromePrintOptions(r resource.Resource, config conf.Config) (print.ChromePrintOptions, error) { func chromePrintOptions(r resource.Resource, config conf.Config) (prinery.ChromePrintOptions, error) {
const op string = "xhttp.chromePrintOptions" const op string = "xhttp.chromePrintOptions"
resolver := func() (print.ChromePrintOptions, error) { resolver := func() (prinery.ChromePrintOptions, error) {
waitDelay, err := resource.WaitDelayArg(r, config) waitDelay, err := resource.WaitDelayArg(r, config)
if err != nil { if err != nil {
return print.ChromePrintOptions{}, err return prinery.ChromePrintOptions{}, err
} }
headerHTML, footerHTML, headerHTML, footerHTML,
err := resource.HeaderFooterContents(r, config) err := resource.HeaderFooterContents(r, config)
if err != nil { if err != nil {
return print.ChromePrintOptions{}, err return prinery.ChromePrintOptions{}, err
} }
paperWidth, paperHeight, paperWidth, paperHeight,
err := resource.PaperSizeArgs(r, config) err := resource.PaperSizeArgs(r, config)
if err != nil { if err != nil {
return print.ChromePrintOptions{}, err return prinery.ChromePrintOptions{}, err
} }
marginTop, marginBottom, marginLeft, marginRight, marginTop, marginBottom, marginLeft, marginRight,
err := resource.MarginArgs(r, config) err := resource.MarginArgs(r, config)
if err != nil { if err != nil {
return print.ChromePrintOptions{}, err return prinery.ChromePrintOptions{}, err
} }
landscape, err := r.BoolArg(resource.LandscapeArgKey, false) landscape, err := r.BoolArg(resource.LandscapeArgKey, false)
if err != nil { if err != nil {
return print.ChromePrintOptions{}, err return prinery.ChromePrintOptions{}, err
} }
return print.ChromePrintOptions{ return prinery.ChromePrintOptions{
WaitDelay: waitDelay, WaitDelay: waitDelay,
HeaderHTML: headerHTML, HeaderHTML: headerHTML,
FooterHTML: footerHTML, FooterHTML: footerHTML,
@@ -53,14 +53,14 @@ func chromePrintOptions(r resource.Resource, config conf.Config) (print.ChromePr
return opts, nil return opts, nil
} }
func officePrintOptions(r resource.Resource, config conf.Config) (print.OfficePrintOptions, error) { func unoconvPrintOptions(r resource.Resource, config conf.Config) (prinery.UnoconvPrintOptions, error) {
const op string = "xhttp.officePrintOptions" const op string = "xhttp.unoconvPrintOptions"
resolver := func() (print.OfficePrintOptions, error) { resolver := func() (prinery.UnoconvPrintOptions, error) {
landscape, err := r.BoolArg(resource.LandscapeArgKey, false) landscape, err := r.BoolArg(resource.LandscapeArgKey, false)
if err != nil { if err != nil {
return print.OfficePrintOptions{}, err return prinery.UnoconvPrintOptions{}, err
} }
return print.OfficePrintOptions{ return prinery.UnoconvPrintOptions{
Landscape: landscape, Landscape: landscape,
}, nil }, nil
} }

View File

@@ -14,7 +14,6 @@ import (
"github.com/thecodingmachine/gotenberg/internal/pkg/conf" "github.com/thecodingmachine/gotenberg/internal/pkg/conf"
"github.com/thecodingmachine/gotenberg/internal/pkg/normalize" "github.com/thecodingmachine/gotenberg/internal/pkg/normalize"
"github.com/thecodingmachine/gotenberg/internal/pkg/prinery" "github.com/thecodingmachine/gotenberg/internal/pkg/prinery"
"github.com/thecodingmachine/gotenberg/internal/pkg/process"
"github.com/thecodingmachine/gotenberg/internal/pkg/xerror" "github.com/thecodingmachine/gotenberg/internal/pkg/xerror"
"github.com/thecodingmachine/gotenberg/internal/pkg/xlog" "github.com/thecodingmachine/gotenberg/internal/pkg/xlog"
) )
@@ -22,13 +21,11 @@ import (
// Context extends the default echo.Context. // Context extends the default echo.Context.
type Context struct { type Context struct {
echo.Context echo.Context
logger xlog.Logger logger xlog.Logger
config conf.Config config conf.Config
manager process.Manager prinry prinery.Prinery
chromePrinery *prinery.Prinery resource resource.Resource
sofficePrinery *prinery.Prinery startTime time.Time
resource resource.Resource
startTime time.Time
} }
// New creates a new Context. // New creates a new Context.
@@ -36,17 +33,13 @@ func New(
c echo.Context, c echo.Context,
logger xlog.Logger, logger xlog.Logger,
config conf.Config, config conf.Config,
manager process.Manager, prinry prinery.Prinery,
chromePrinery *prinery.Prinery,
sofficePrinery *prinery.Prinery,
) Context { ) Context {
return Context{ return Context{
c, c,
logger, logger,
config, config,
manager, prinry,
chromePrinery,
sofficePrinery,
resource.Resource{}, resource.Resource{},
time.Now(), time.Now(),
} }
@@ -89,7 +82,8 @@ func (ctx Context) Config() conf.Config {
// one of the processes is not viable. // one of the processes is not viable.
func (ctx Context) ProcessesHealthcheck() error { func (ctx Context) ProcessesHealthcheck() error {
const op string = "context.Context.ProcessesHealthcheck" const op string = "context.Context.ProcessesHealthcheck"
processes := ctx.manager.All() // TODO
/*processes := ctx.manager.All()
for _, p := range processes { for _, p := range processes {
if !ctx.manager.IsViable(p) { if !ctx.manager.IsViable(p) {
return xerror.New( return xerror.New(
@@ -97,42 +91,14 @@ func (ctx Context) ProcessesHealthcheck() error {
fmt.Errorf("'%s' is not viable", p.ID()), fmt.Errorf("'%s' is not viable", p.ID()),
) )
} }
} }*/
return nil return nil
} }
/* // Prinery returns the instance of prinery.Prinery
MustChromePrinery returns the instance of // associated with the Context.
prinery.Prinery associated with the Context. func (ctx Context) Prinery() prinery.Prinery {
return ctx.prinry
This prinery.Prinery handles Google Chrome
headless.
It panics if no instance of prinery.Prinery.
*/
func (ctx Context) MustChromePrinery() *prinery.Prinery {
const op string = "context.Context.MustChromePrinery"
if ctx.chromePrinery == nil {
panic(fmt.Sprintf("%s: unable to retrieve the instance of Google Chrome Headless prinery.Prinery from our custom context.Context", op))
}
return ctx.chromePrinery
}
/*
MustSofficePrinery returns the instance of
prinery.Prinery associated with the Context.
This prinery.Prinery handles LibreOffice
headless.
It panics if no instance of prinery.Prinery.
*/
func (ctx Context) MustSofficePrinery() *prinery.Prinery {
const op string = "context.Context.MustSofficePrinery"
if ctx.sofficePrinery == nil {
panic(fmt.Sprintf("%s: unable to retrieve the instance of LibreOffice Headless prinery.Prinery from our custom context.Context", op))
}
return ctx.sofficePrinery
} }
// WithResource creates a resource.Resource and // WithResource creates a resource.Resource and

View File

@@ -4,20 +4,14 @@ import (
"github.com/labstack/echo/v4" "github.com/labstack/echo/v4"
"github.com/thecodingmachine/gotenberg/internal/pkg/conf" "github.com/thecodingmachine/gotenberg/internal/pkg/conf"
"github.com/thecodingmachine/gotenberg/internal/pkg/prinery" "github.com/thecodingmachine/gotenberg/internal/pkg/prinery"
"github.com/thecodingmachine/gotenberg/internal/pkg/process"
) )
// New returns a custom echo.Echo. // New returns a custom echo.Echo.
func New( func New(config conf.Config, prinry prinery.Prinery) *echo.Echo {
config conf.Config,
manager process.Manager,
chromePrinery *prinery.Prinery,
sofficePrinery *prinery.Prinery,
) *echo.Echo {
srv := echo.New() srv := echo.New()
srv.HideBanner = true srv.HideBanner = true
srv.HidePort = true srv.HidePort = true
srv.Use(contextMiddleware(config, manager, chromePrinery, sofficePrinery)) srv.Use(contextMiddleware(config, prinry))
srv.Use(loggerMiddleware()) srv.Use(loggerMiddleware())
srv.Use(cleanupMiddleware()) srv.Use(cleanupMiddleware())
srv.Use(errorMiddleware()) srv.Use(errorMiddleware())

View File

@@ -201,6 +201,16 @@ func (c Config) DefaultListenPort() int64 {
return c.defaultListenPort return c.defaultListenPort
} }
func (c Config) GoogleChromeInstances() int64 {
// TODO: replace DisableGoogleChrome
return 2
}
func (c Config) SofficeInstances() int64 {
// TODO: replace DisableUnoconv
return 2
}
/* /*
DisableGoogleChrome returns true if DisableGoogleChrome returns true if
Google Chrome is disabled in the Google Chrome is disabled in the

View File

@@ -0,0 +1,327 @@
package prinery
import (
"context"
"fmt"
"io/ioutil"
"time"
"github.com/mafredri/cdp"
"github.com/mafredri/cdp/devtool"
"github.com/mafredri/cdp/protocol/network"
"github.com/mafredri/cdp/protocol/page"
"github.com/mafredri/cdp/protocol/target"
"github.com/mafredri/cdp/rpcc"
"github.com/thecodingmachine/gotenberg/internal/pkg/xerrgroup"
"github.com/thecodingmachine/gotenberg/internal/pkg/xerror"
"github.com/thecodingmachine/gotenberg/internal/pkg/xlog"
"github.com/thecodingmachine/gotenberg/internal/pkg/xtime"
)
type chromeProcess struct {
devtPort uint
}
func newChromeProcesses(nInstances int64) []process {
processes := make([]process, nInstances)
var i int64
var currentPort uint = 9222
for i = 0; i < nInstances; i++ {
processes[i] = chromeProcess{
devtPort: currentPort,
}
currentPort++
}
return processes
}
func (p chromeProcess) id() string {
return fmt.Sprintf("%s-%d", p.binary(), p.port())
}
func (p chromeProcess) host() string {
return "127.0.0.1"
}
func (p chromeProcess) port() uint {
return p.devtPort
}
func (p chromeProcess) spec() processSpec {
return p
}
func (p chromeProcess) binary() string {
return "google-chrome-stable"
}
func (p chromeProcess) args() []string {
return []string{
"--no-sandbox",
"--headless",
// see https://github.com/GoogleChrome/puppeteer/issues/2410.
"--font-render-hinting=medium",
fmt.Sprintf("--remote-debugging-port=%d", p.port()),
"--disable-gpu",
"--disable-translate",
"--disable-extensions",
"--disable-background-networking",
"--safebrowsing-disable-auto-update",
"--disable-sync",
"--disable-default-apps",
"--hide-scrollbars",
"--metrics-recording-only",
"--mute-audio",
"--no-first-run",
}
}
func (p chromeProcess) warmupTime() time.Duration {
return 10 * time.Second
}
func (p chromeProcess) viabilityFunc() func(logger xlog.Logger) bool {
const op string = "prinery.chromeProcess.viabilityFunc"
return func(logger xlog.Logger) bool {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
endpoint := fmt.Sprintf("http://%s:%d" /*p.host()*/, "localhost", p.port())
logger.DebugfOp(
op,
"checking '%s' viability via endpoint '%s/json/version'",
p.id(),
endpoint,
)
v, err := devtool.New(endpoint).Version(ctx)
if err != nil {
logger.ErrorfOp(
op,
"'%s' is not viable as endpoint returned '%v'",
p.id(),
err,
)
return false
}
logger.DebugfOp(
op,
"'%s' is viable as endpoint returned '%v'",
p.id(),
v,
)
return true
}
}
type chromePrinter struct {
logger xlog.Logger
url string
opts ChromePrintOptions
}
func (p chromePrinter) print(ctx context.Context, spec processSpec, dest string) error {
const op string = "prinery.chromePrinter.print"
resolver := func() error {
devtEndpoint := fmt.Sprintf("http://%s:%d", spec.host(), spec.port())
devt, err := devtool.New(devtEndpoint).Version(ctx)
if err != nil {
return err
}
// connect to WebSocket URL (page) that speaks the Chrome DevTools Protocol.
devtConn, err := rpcc.DialContext(ctx, devt.WebSocketDebuggerURL)
if err != nil {
return err
}
defer devtConn.Close() // nolint: errcheck
// create a new CDP Client that uses conn.
devtClient := cdp.NewClient(devtConn)
newContextTarget, err := devtClient.Target.CreateBrowserContext(ctx)
if err != nil {
return err
}
/*
close the browser context when done.
we're not using the "default" context
as it may timeout before actually closing
the browser context.
see: https://github.com/mafredri/cdp/issues/101#issuecomment-524533670
*/
disposeBrowserContextArgs := target.NewDisposeBrowserContextArgs(newContextTarget.BrowserContextID)
defer devtClient.Target.DisposeBrowserContext(context.Background(), disposeBrowserContextArgs) // nolint: errcheck
// create a new blank target with the new browser context.
createTargetArgs := target.
NewCreateTargetArgs("about:blank").
SetBrowserContextID(newContextTarget.BrowserContextID)
newTarget, err := devtClient.Target.CreateTarget(ctx, createTargetArgs)
if err != nil {
return err
}
// connect the client to the new target.
newTargetWsURL := fmt.Sprintf("ws://%s:%d/devtools/page/%s", spec.host(), spec.port(), newTarget.TargetID)
newContextConn, err := rpcc.DialContext(ctx, newTargetWsURL)
if err != nil {
return err
}
defer newContextConn.Close() // nolint: errcheck
// create a new CDP Client that uses newContextConn.
targetClient := cdp.NewClient(newContextConn)
/*
close the target when done.
we're not using the "default" context
as it may timeout before actually closing
the target.
see: https://github.com/mafredri/cdp/issues/101#issuecomment-524533670
*/
closeTargetArgs := target.NewCloseTargetArgs(newTarget.TargetID)
defer targetClient.Target.CloseTarget(context.Background(), closeTargetArgs) // nolint: errcheck
// enable all events.
if err := p.enableEvents(ctx, targetClient); err != nil {
return err
}
// listen for all events.
if err := p.listenEvents(ctx, targetClient); err != nil {
return err
}
// apply a wait delay (if any).
if p.opts.WaitDelay > 0.0 {
// wait for a given amount of time (useful for javascript delay).
p.logger.DebugfOp(op, "applying a wait delay of '%.2fs'...", p.opts.WaitDelay)
time.Sleep(xtime.Duration(p.opts.WaitDelay))
} else {
p.logger.DebugOp(op, "no wait delay to apply, moving on...")
}
// print the page to PDF.
print, err := targetClient.Page.PrintToPDF(
ctx,
page.NewPrintToPDFArgs().
SetPaperWidth(p.opts.PaperWidth).
SetPaperHeight(p.opts.PaperHeight).
SetMarginTop(p.opts.MarginTop).
SetMarginBottom(p.opts.MarginBottom).
SetMarginLeft(p.opts.MarginLeft).
SetMarginRight(p.opts.MarginRight).
SetLandscape(p.opts.Landscape).
SetDisplayHeaderFooter(true).
SetHeaderTemplate(p.opts.HeaderHTML).
SetFooterTemplate(p.opts.FooterHTML).
SetPrintBackground(true),
)
if err != nil {
return err
}
if err := ioutil.WriteFile(dest, print.Data, 0644); err != nil {
return err
}
return nil
}
if err := resolver(); err != nil {
return xerror.New(op, err)
}
return nil
}
func (p chromePrinter) enableEvents(ctx context.Context, client *cdp.Client) error {
const op string = "prinery.chromePrinter.enableEvents"
// enable all the domain events that we're interested in.
if err := xerrgroup.Run(
func() error { return client.DOM.Enable(ctx) },
func() error { return client.Network.Enable(ctx, network.NewEnableArgs()) },
func() error { return client.Page.Enable(ctx) },
func() error {
return client.Page.SetLifecycleEventsEnabled(ctx, page.NewSetLifecycleEventsEnabledArgs(true))
},
func() error { return client.Runtime.Enable(ctx) },
); err != nil {
return xerror.New(op, err)
}
return nil
}
func (p chromePrinter) listenEvents(ctx context.Context, client *cdp.Client) error {
const op string = "prinery.chromePrinter.listenEvents"
resolver := func() error {
// make sure Page events are enabled.
if err := client.Page.Enable(ctx); err != nil {
return err
}
// make sure Network events are enabled.
if err := client.Network.Enable(ctx, nil); err != nil {
return err
}
// create all clients for events.
domContentEventFired, err := client.Page.DOMContentEventFired(ctx)
if err != nil {
return err
}
defer domContentEventFired.Close() // nolint: errcheck
loadEventFired, err := client.Page.LoadEventFired(ctx)
if err != nil {
return err
}
defer loadEventFired.Close() // nolint: errcheck
lifecycleEvent, err := client.Page.LifecycleEvent(ctx)
if err != nil {
return err
}
defer lifecycleEvent.Close() // nolint: errcheck
loadingFinished, err := client.Network.LoadingFinished(ctx)
if err != nil {
return err
}
defer loadingFinished.Close() // nolint: errcheck
if _, err := client.Page.Navigate(ctx, page.NewNavigateArgs(p.url)); err != nil {
return err
}
// wait for all events.
return xerrgroup.Run(
func() error {
_, err := domContentEventFired.Recv()
if err != nil {
return err
}
p.logger.DebugOp(op, "event 'domContentEventFired' received")
return nil
},
func() error {
_, err := loadEventFired.Recv()
if err != nil {
return err
}
p.logger.DebugOp(op, "event 'loadEventFired' received")
return nil
},
func() error {
const networkIdleEventName string = "networkIdle"
for {
ev, err := lifecycleEvent.Recv()
if err != nil {
return err
}
p.logger.DebugfOp(op, "event '%s' received", ev.Name)
if ev.Name == networkIdleEventName {
break
}
}
return nil
},
func() error {
_, err := loadingFinished.Recv()
if err != nil {
return err
}
p.logger.DebugOp(op, "event 'loadingFinished' received")
return nil
},
)
}
if err := resolver(); err != nil {
return xerror.New(op, err)
}
return nil
}
// Compile-time checks to ensure type implements desired interfaces.
var (
_ = processSpec(new(chromeProcess))
_ = process(new(chromeProcess))
_ = printer(new(chromePrinter))
)

View File

@@ -0,0 +1,68 @@
package prinery
import (
"bytes"
"fmt"
"html/template"
"io/ioutil"
"path/filepath"
"github.com/microcosm-cc/bluemonday"
"github.com/russross/blackfriday/v2"
"github.com/thecodingmachine/gotenberg/internal/pkg/xerror"
"github.com/thecodingmachine/gotenberg/internal/pkg/xlog"
"github.com/thecodingmachine/gotenberg/internal/pkg/xrand"
)
func newMarkdownPrinter(logger xlog.Logger, fpath string, opts ChromePrintOptions) (printer, error) {
const op string = "prinery.newMarkdownPrinter"
resolver := func() (string, error) {
tmpl, err := template.
New(filepath.Base(fpath)).
Funcs(template.FuncMap{"toHTML": markdownToHTML}).
ParseFiles(fpath)
if err != nil {
return "", err
}
dirPath := filepath.Dir(fpath)
data := &templateData{DirPath: dirPath}
logger.DebugOp(op, "converting Markdown files to HTML...")
var buffer bytes.Buffer
if err := tmpl.Execute(&buffer, data); err != nil {
return "", err
}
baseFilename := xrand.Get()
dst := fmt.Sprintf("%s/%s.html", dirPath, baseFilename)
logger.DebugOp(op, "writing the HTML from previous conversion(s) into new file...")
if err := ioutil.WriteFile(dst, buffer.Bytes(), 0644); err != nil {
return "", err
}
return fmt.Sprintf("file://%s", dst), nil
}
URL, err := resolver()
if err != nil {
return nil, xerror.New(op, err)
}
return chromePrinter{
logger: logger,
url: URL,
opts: opts,
}, nil
}
type templateData struct {
DirPath string
}
func markdownToHTML(dirPath, filename string) (template.HTML, error) {
const op string = "prinery.markdownToHTML"
fpath := fmt.Sprintf("%s/%s", dirPath, filename)
b, err := ioutil.ReadFile(fpath)
if err != nil {
return "", xerror.New(op, err)
}
unsafe := blackfriday.Run(b)
content := bluemonday.UGCPolicy().SanitizeBytes(unsafe)
/* #nosec */
return template.HTML(content), nil
}

View File

@@ -0,0 +1,39 @@
package prinery
import (
"context"
"github.com/thecodingmachine/gotenberg/internal/pkg/xerror"
"github.com/thecodingmachine/gotenberg/internal/pkg/xexec"
"github.com/thecodingmachine/gotenberg/internal/pkg/xlog"
)
type mergePrinter struct {
logger xlog.Logger
fpaths []string
}
func (p mergePrinter) print(ctx context.Context, spec processSpec, dest string) error {
const op string = "prinery.mergePrinter.print"
p.logger.DebugfOp(op, "merging '%v'...", p.fpaths)
resolver := func() error {
var args []string
args = append(args, p.fpaths...)
args = append(args, "cat", "output", dest)
cmd, err := xexec.CommandContext(ctx, p.logger, "pdftk", args...)
if err != nil {
return err
}
xexec.LogBeforeExecute(p.logger, cmd)
return cmd.Run()
}
if err := resolver(); err != nil {
return xerror.New(op, err)
}
return nil
}
// Compile-time checks to ensure type implements desired interfaces.
var (
_ = printer(new(mergePrinter))
)

365
internal/pkg/prinery/pm2.go Normal file
View File

@@ -0,0 +1,365 @@
package prinery
import (
"context"
"errors"
"fmt"
"time"
"github.com/thecodingmachine/gotenberg/internal/pkg/conf"
"github.com/thecodingmachine/gotenberg/internal/pkg/xerror"
"github.com/thecodingmachine/gotenberg/internal/pkg/xexec"
"github.com/thecodingmachine/gotenberg/internal/pkg/xlog"
)
type command string
const (
startCommand command = "start"
restartCommand command = "restart"
stopCommand command = "stop"
logsCommand command = "logs"
jlistCommand command = "jlist"
)
const maximumRestartAttempts uint = 3
type pm2 struct {
logger xlog.Logger
config conf.Config
chromeUnit *unit
sofficeUnit *unit
processes []process
}
func NewPM2Prinery(logger xlog.Logger, config conf.Config) (Prinery, error) {
const op string = "prinery.NewPM2Prinery"
resolver := func() (*pm2, error) {
m := &pm2{
logger: logger,
config: config,
}
chromeProcesses := newChromeProcesses(config.GoogleChromeInstances())
if len(chromeProcesses) > 0 {
unit, err := newUnit(logger, processesToSpecs(chromeProcesses))
if err != nil {
return nil, err
}
m.chromeUnit = unit
}
sofficeProcesses := newSofficesProcesses(config.SofficeInstances())
if len(sofficeProcesses) > 0 {
unit, err := newUnit(logger, processesToSpecs(sofficeProcesses))
if err != nil {
return nil, err
}
m.sofficeUnit = unit
}
m.processes = append(chromeProcesses, sofficeProcesses...)
return m, nil
}
p, err := resolver()
if err != nil {
return nil, xerror.New(op, err)
}
return p, nil
}
func (m *pm2) Start(emergency chan error) error {
const op string = "prinery.pm2.Start"
// start processes.
// those lines "works" but processes
// fail to start...
/*var wg sync.WaitGroup
result := make(chan error, len(m.processes))
for _, proc := range m.processes {
wg.Add(1)
go func(proc process, result chan error) {
result <- m.start(proc)
wg.Done()
}(proc, result)
}
wg.Wait()
close(result)
for err := range result {
if err != nil {
return xerror.New(op, err)
}
}*/
for _, proc := range m.processes {
if err := m.start(proc); err != nil {
return xerror.New(op, err)
}
}
// start units.
if m.chromeUnit != nil {
go m.chromeUnit.start(emergency)
}
if m.sofficeUnit != nil {
go m.sofficeUnit.start(emergency)
}
return nil
}
func (m *pm2) HTML(
ctx context.Context,
logger xlog.Logger,
dest, fpath string,
opts ChromePrintOptions,
) error {
const op string = "prinery.pm2.HTML"
resolver := func() error {
if m.chromeUnit == nil {
return errors.New("cannot handle HTML print request as there is no chromeUnit")
}
URL := fmt.Sprintf("file://%s", fpath)
p := chromePrinter{
logger: logger,
url: URL,
opts: opts,
}
req := request{
ctx: ctx,
logger: logger,
printer: p,
dest: dest,
result: make(chan error),
}
m.chromeUnit.dispatch(req)
err := <-req.result
return err
}
if err := resolver(); err != nil {
return xerror.New(op, err)
}
return nil
}
func (m *pm2) URL(
ctx context.Context,
logger xlog.Logger, dest, URL string,
opts ChromePrintOptions,
) error {
const op string = "prinery.pm2.URL"
resolver := func() error {
if m.chromeUnit == nil {
return errors.New("cannot handle URL print request as there is no chromeUnit")
}
p := chromePrinter{
logger: logger,
url: URL,
opts: opts,
}
req := request{
ctx: ctx,
logger: logger,
printer: p,
dest: dest,
result: make(chan error),
}
m.chromeUnit.dispatch(req)
err := <-req.result
return err
}
if err := resolver(); err != nil {
return xerror.New(op, err)
}
return nil
}
func (m *pm2) Markdown(
ctx context.Context,
logger xlog.Logger,
dest, fpath string,
opts ChromePrintOptions,
) error {
const op string = "prinery.pm2.Markdown"
resolver := func() error {
if m.chromeUnit == nil {
return errors.New("cannot handle Markdown print request as there is no chromeUnit")
}
p, err := newMarkdownPrinter(logger, fpath, opts)
if err != nil {
return err
}
req := request{
ctx: ctx,
logger: logger,
printer: p,
dest: dest,
result: make(chan error),
}
m.chromeUnit.dispatch(req)
err = <-req.result
return err
}
if err := resolver(); err != nil {
return xerror.New(op, err)
}
return nil
}
func (m *pm2) Office(
ctx context.Context,
logger xlog.Logger,
dest string,
fpaths []string,
opts UnoconvPrintOptions,
) error {
const op string = "prinery.pm2.Office"
resolver := func() error {
if m.sofficeUnit == nil {
return errors.New("cannot handle Office print request as there is no sofficeUnit")
}
p := unoconvPrinter{
logger: logger,
fpaths: fpaths,
opts: opts,
}
req := request{
ctx: ctx,
logger: logger,
printer: p,
dest: dest,
result: make(chan error),
}
m.sofficeUnit.dispatch(req)
err := <-req.result
return err
}
if err := resolver(); err != nil {
return xerror.New(op, err)
}
return nil
}
func (m *pm2) Merge(
ctx context.Context,
logger xlog.Logger,
dest string,
fpaths []string,
) error {
const op string = "prinery.pm2.Merge"
p := mergePrinter{
logger: logger,
fpaths: fpaths,
}
if err := p.print(ctx, nil, dest); err != nil {
return xerror.New(op, err)
}
return nil
}
func (m *pm2) start(p process) error {
const op string = "prinery.pm2.start"
resolver := func() error {
// first, we try to start the process.
if err := m.run(startCommand, p); err != nil {
return err
}
// we wait the process to be ready.
m.warmup(p)
// if the process failed to start correctly,
// we have to restart it.
if !m.isViable(p) && maximumRestartAttempts > 0 {
return m.restart(p)
}
// the process is viable, let's log its
// output.
return m.logs(p)
}
if err := resolver(); err != nil {
return xerror.New(op, err)
}
return nil
}
func (m *pm2) restart(p process) error {
const op string = "prinery.pm2.restart"
resolver := func() error {
var attempts uint
for attempts < maximumRestartAttempts {
// we restart the process.
if err := m.run(restartCommand, p); err != nil {
return err
}
// we wait the process to be ready.
m.warmup(p)
attempts++
// if the process is viable, we
// leave.
if m.isViable(p) {
return m.logs(p)
}
}
return fmt.Errorf("failed to start '%s'", p.spec().id())
}
if err := resolver(); err != nil {
return xerror.New(op, err)
}
return nil
}
func (m *pm2) isViable(p process) bool {
if !p.viabilityFunc()(m.logger) {
return false
}
/*m.listLock.Lock()
defer m.listLock.Unlock()
return m.list.isOnline(p)*/
return true
}
func (m *pm2) warmup(p process) {
const op string = "prinery.pm2.warmup"
warmupTime := p.warmupTime()
m.logger.DebugfOp(
op,
"waiting '%v' for allowing '%s' to warmup",
warmupTime,
p.spec().id(),
)
time.Sleep(warmupTime)
}
func (m *pm2) logs(p process) error {
const op string = "prinery.pm2.logs"
if m.config.LogLevel() == xlog.DebugLevel {
if err := m.run(logsCommand, p); err != nil {
return xerror.New(op, err)
}
}
return nil
}
func (m *pm2) run(pm2Cmd command, p process) error {
const op string = "prinery.pm2.run"
resolver := func() error {
args := []string{
string(pm2Cmd),
}
if pm2Cmd == startCommand {
args = append(args, p.binary())
args = append(args, fmt.Sprintf("--name=%s", p.spec().id()))
args = append(args, "--interpreter=none", "--")
args = append(args, p.args()...)
} else {
args = append(args, p.spec().id())
}
cmd, err := xexec.Command(m.logger, "pm2", args...)
if err != nil {
return err
}
xexec.LogBeforeExecute(m.logger, cmd)
return cmd.Start()
}
if err := resolver(); err != nil {
return xerror.New(op, err)
}
return nil
}
// Compile-time checks to ensure type implements desired interfaces.
var (
_ = Prinery(new(pm2))
)

View File

@@ -2,122 +2,89 @@ package prinery
import ( import (
"context" "context"
"fmt" "time"
"github.com/thecodingmachine/gotenberg/internal/pkg/print"
"github.com/thecodingmachine/gotenberg/internal/pkg/process"
"github.com/thecodingmachine/gotenberg/internal/pkg/xerror"
"github.com/thecodingmachine/gotenberg/internal/pkg/xlog" "github.com/thecodingmachine/gotenberg/internal/pkg/xlog"
) )
type request struct { type processSpec interface {
ctx context.Context id() string
logger xlog.Logger host() string
print print.Print port() uint
dest string
result chan error
} }
type worker struct { type process interface {
work chan request spec() processSpec
proc process.Process binary() string
args() []string
warmupTime() time.Duration
viabilityFunc() func(logger xlog.Logger) bool
} }
func (w *worker) do(done chan *worker) { func processesToSpecs(processes []process) []processSpec {
for { specs := make([]processSpec, len(processes))
req := <-w.work for i, p := range processes {
req.result <- req.print.Print(req.ctx, req.dest, w.proc) specs[i] = p.spec()
done <- w }
return specs
}
type printer interface {
print(ctx context.Context, spec processSpec, dest string) error
}
// ChromePrintOptions helps customizing the
// Google Chrome print result.
type ChromePrintOptions struct {
WaitDelay float64
HeaderHTML string
FooterHTML string
PaperWidth float64
PaperHeight float64
MarginTop float64
MarginBottom float64
MarginLeft float64
MarginRight float64
Landscape bool
}
// DefaultChromePrintOptions returns the default
// Google Chrome print options.
func DefaultChromePrintOptions() ChromePrintOptions {
const defaultHeaderFooterHTML string = "<html><head></head><body></body></html>"
return ChromePrintOptions{
WaitDelay: 0.0,
HeaderHTML: defaultHeaderFooterHTML,
FooterHTML: defaultHeaderFooterHTML,
PaperWidth: 8.27,
PaperHeight: 11.7,
MarginTop: 1.0,
MarginBottom: 1.0,
MarginLeft: 1.0,
MarginRight: 1.0,
Landscape: false,
} }
} }
type Prinery struct { // UnoconvPrintOptions helps customizing the
logger xlog.Logger // LibreOffice print result.
manager process.Manager type UnoconvPrintOptions struct {
work chan request Landscape bool
pool chan *worker
done chan *worker
} }
func New(logger xlog.Logger, manager process.Manager, key process.Key) (*Prinery, error) { // DefaultUnoconvPrinterOptions returns the default
const op string = "prinery.New" // LibreOffice print options.
processes := manager.Processes(key) func DefaultUnoconvPrinterOptions() UnoconvPrintOptions {
nWorkers := len(processes) return UnoconvPrintOptions{
if nWorkers == 0 { Landscape: false,
err := fmt.Errorf("no processes found for key '%s'", string(key))
return nil, xerror.New(op, err)
}
logger.DebugfOp(op, "found '%d' processes for key '%s'", nWorkers, string(key))
work := make(chan request, 1)
pool := make(chan *worker, nWorkers)
done := make(chan *worker, nWorkers)
for _, p := range processes {
w := &worker{
work: work,
proc: p,
}
pool <- w
go w.do(done)
}
return &Prinery{
logger: logger,
manager: manager,
work: work,
pool: pool,
done: done,
}, nil
}
func (p *Prinery) PrintRequest(ctx context.Context, logger xlog.Logger, prnt print.Print, dest string) error {
const op string = "prinery.Prinery.PrintRequest"
req := request{
ctx: ctx,
logger: logger,
print: prnt,
dest: dest,
result: make(chan error),
}
p.dispatch(req)
err := <-req.result
if err != nil {
return xerror.New(op, err)
}
return nil
}
func (p *Prinery) Start() {
for {
select {
case req := <-p.work:
p.dispatch(req)
case w := <-p.done:
p.completed(w)
}
} }
} }
func (p *Prinery) dispatch(req request) { type Prinery interface {
const op string = "prinery.Prinery.dispatch" Start(emergency chan error) error
select { HTML(ctx context.Context, logger xlog.Logger, dest, fpath string, opts ChromePrintOptions) error
case w := <-p.pool: URL(ctx context.Context, logger xlog.Logger, dest, URL string, opts ChromePrintOptions) error
w.work <- req Markdown(ctx context.Context, logger xlog.Logger, dest, fpath string, opts ChromePrintOptions) error
case <-req.ctx.Done(): Office(ctx context.Context, logger xlog.Logger, dest string, fpaths []string, opts UnoconvPrintOptions) error
req.result <- xerror.New(op, req.ctx.Err()) Merge(ctx context.Context, logger xlog.Logger, dest string, fpaths []string) error
}
}
func (p *Prinery) completed(w *worker) {
const op string = "prinery.Prinerty.completed"
go func() {
// check process viability.
isViable := p.manager.IsViable(w.proc)
// check process memory usage.
// TODO handle error.
memory, _ := p.manager.Memory(w.proc)
p.logger.DebugfOp(op, "%s: isViable = %t, memory = %d", w.proc.ID(), isViable, memory)
// TODO manage viability and memory usage.
// pushing back the worker.
p.pool <- w
}()
} }

View File

@@ -0,0 +1,158 @@
package prinery
import (
"context"
"fmt"
"os"
"path/filepath"
"time"
"github.com/thecodingmachine/gotenberg/internal/pkg/xerror"
"github.com/thecodingmachine/gotenberg/internal/pkg/xexec"
"github.com/thecodingmachine/gotenberg/internal/pkg/xlog"
"github.com/thecodingmachine/gotenberg/internal/pkg/xrand"
)
type sofficeProcess struct {
unoPort uint
}
func newSofficesProcesses(nInstances int64) []process {
processes := make([]process, nInstances)
var i int64
var currentPort uint = 2002
for i = 0; i < nInstances; i++ {
processes[i] = sofficeProcess{
unoPort: currentPort,
}
currentPort++
}
return processes
}
func (p sofficeProcess) id() string {
return fmt.Sprintf("%s-%d", p.binary(), p.port())
}
func (p sofficeProcess) host() string {
return "127.0.0.1"
}
func (p sofficeProcess) port() uint {
return p.unoPort
}
func (p sofficeProcess) spec() processSpec {
return p
}
func (p sofficeProcess) binary() string {
return "soffice"
}
func (p sofficeProcess) args() []string {
return []string{
// see https://ask.libreoffice.org/en/question/42975/how-can-i-run-multiple-instances-of-sofficebin-at-a-time/.
fmt.Sprintf("-env:UserInstallation=file:///tmp/%d", p.port()),
"--headless",
"--invisible",
"--nocrashreport",
"--nodefault",
"--nofirststartwizard",
"--nologo",
"--norestore",
fmt.Sprintf("--accept=socket,host=%s,port=%d,tcpNoDelay=1;urp;StarOffice.ComponentContext", p.host(), p.port()),
}
}
func (p sofficeProcess) warmupTime() time.Duration {
return 3 * time.Second
}
func (p sofficeProcess) viabilityFunc() func(logger xlog.Logger) bool {
const op string = "prinery.sofficeProcess.viabilityFunc"
return func(logger xlog.Logger) bool {
// TODO find a way to check.
return true
}
}
type unoconvPrinter struct {
logger xlog.Logger
fpaths []string
opts UnoconvPrintOptions
}
func (p unoconvPrinter) print(ctx context.Context, spec processSpec, dest string) error {
const op string = "prinery.unoconvPrinter.print"
resolver := func() error {
fpaths := make([]string, len(p.fpaths))
dirPath := filepath.Dir(dest)
for i, fpath := range p.fpaths {
baseFilename := xrand.Get()
tmpDest := fmt.Sprintf("%s/%d%s.pdf", dirPath, i, baseFilename)
p.logger.DebugfOp(op, "converting '%s' to PDF...", fpath)
if err := p.unoconv(ctx, spec, fpath, tmpDest); err != nil {
return err
}
p.logger.DebugfOp(op, "'%s.pdf' created", baseFilename)
fpaths[i] = tmpDest
}
if len(fpaths) == 1 {
p.logger.DebugOp(op, "only one PDF created, nothing to merge")
if err := os.Rename(fpaths[0], dest); err != nil {
return err
}
return nil
}
merger := mergePrinter{
logger: p.logger,
fpaths: fpaths,
}
return merger.print(ctx, nil, dest)
}
if err := resolver(); err != nil {
return xerror.New(op, err)
}
return nil
}
func (p unoconvPrinter) unoconv(ctx context.Context, spec processSpec, fpath, dest string) error {
const op string = "prinery.unoconvPrinter.unoconv"
resolver := func() error {
args := []string{
"--server",
spec.host(),
"--port",
fmt.Sprintf("%d", spec.port()),
"--format",
"pdf",
}
if p.opts.Landscape {
args = append(args, "--printer", "PaperOrientation=landscape")
}
args = append(args, "--output", dest, fpath)
cmd, err := xexec.CommandContext(
ctx,
p.logger,
"unoconv",
args...,
)
if err != nil {
return err
}
xexec.LogBeforeExecute(p.logger, cmd)
return cmd.Run()
}
if err := resolver(); err != nil {
return xerror.New(op, err)
}
return nil
}
// Compile-time checks to ensure type implements desired interfaces.
var (
_ = processSpec(new(sofficeProcess))
_ = process(new(sofficeProcess))
_ = printer(new(unoconvPrinter))
)

View File

@@ -0,0 +1,125 @@
package prinery
import (
"context"
"errors"
"github.com/thecodingmachine/gotenberg/internal/pkg/xerror"
"github.com/thecodingmachine/gotenberg/internal/pkg/xlog"
)
type request struct {
ctx context.Context
logger xlog.Logger
printer printer
dest string
result chan error
}
type worker struct {
work chan request
spec processSpec
}
func (w *worker) do(done chan *worker) {
for {
req := <-w.work
req.result <- req.printer.print(req.ctx, w.spec, req.dest)
done <- w
}
}
type unit struct {
logger xlog.Logger
workers []*worker
work chan request
pool chan *worker
done chan *worker
}
func newUnit(logger xlog.Logger, specs []processSpec) (*unit, error) {
const op string = "prinery.newUnit"
resolver := func() (*unit, error) {
nWorkers := len(specs)
if nWorkers == 0 {
return nil, errors.New("no workers to create")
}
logger.DebugfOp(op, "creating '%d' workers...", nWorkers)
workers := make([]*worker, nWorkers)
work := make(chan request, 1)
pool := make(chan *worker, nWorkers)
done := make(chan *worker, nWorkers)
for i, spec := range specs {
w := &worker{
work: work,
spec: spec,
}
logger.DebugfOp(op, "worker '%s' created", w.spec.id())
workers[i] = w
}
return &unit{
logger: logger,
workers: workers,
work: work,
pool: pool,
done: done,
}, nil
}
u, err := resolver()
if err != nil {
return nil, xerror.New(op, err)
}
return u, nil
}
func (u *unit) start(emergency chan error) {
const op string = "prinery.unit.start"
for _, w := range u.workers {
u.logger.DebugfOp(op, "starting worker '%s'...", w.spec.id())
go w.do(u.done)
u.pool <- w
}
for {
select {
case req := <-u.work:
u.dispatch(req)
case w := <-u.done:
u.completed(w, emergency)
}
}
}
func (u *unit) request(ctx context.Context, logger xlog.Logger, printer printer, dest string) error {
const op string = "prinery.unit.request"
req := request{
ctx: ctx,
logger: logger,
printer: printer,
dest: dest,
result: make(chan error),
}
u.dispatch(req)
err := <-req.result
if err != nil {
return xerror.New(op, err)
}
return nil
}
func (u *unit) dispatch(req request) {
const op string = "prinery.unit.dispatch"
select {
case w := <-u.pool:
u.logger.DebugfOp(op, "worker '%s' is now in use", w.spec.id())
w.work <- req
case <-req.ctx.Done():
req.result <- xerror.New(op, req.ctx.Err())
}
}
func (u *unit) completed(w *worker, emergency chan error) {
const op string = "prinery.unit.completed"
// TODO check viability, handles errors via emergency chan.
u.logger.DebugfOp(op, "worker '%s' is ready for a new request", w.spec.id())
u.pool <- w
}

View File

@@ -1,18 +0,0 @@
package print
import (
"fmt"
"github.com/thecodingmachine/gotenberg/internal/pkg/xlog"
)
// NewHTMLPrint returns a Print for
// converting an HTML file to PDF.
func NewHTMLPrint(logger xlog.Logger, fpath string, opts ChromePrintOptions) Print {
URL := fmt.Sprintf("file://%s", fpath)
return chromePrint{
logger: logger,
url: URL,
opts: opts,
}
}

View File

@@ -52,7 +52,14 @@ func (p officePrint) Print(ctx context.Context, dest string, proc process.Proces
baseFilename := xrand.Get() baseFilename := xrand.Get()
tmpDest := fmt.Sprintf("%s/%d%s.pdf", dirPath, i, baseFilename) tmpDest := fmt.Sprintf("%s/%d%s.pdf", dirPath, i, baseFilename)
p.logger.DebugfOp(op, "converting '%s' to PDF...", fpath) p.logger.DebugfOp(op, "converting '%s' to PDF...", fpath)
if err := unoconv(ctx, p.logger, fpath, tmpDest, p.opts); err != nil { if err := unoconv(
ctx,
p.logger,
proc,
p.opts,
fpath,
tmpDest,
); err != nil {
return err return err
} }
p.logger.DebugfOp(op, "'%s.pdf' created", baseFilename) p.logger.DebugfOp(op, "'%s.pdf' created", baseFilename)
@@ -74,10 +81,20 @@ func (p officePrint) Print(ctx context.Context, dest string, proc process.Proces
return nil return nil
} }
func unoconv(ctx context.Context, logger xlog.Logger, fpath, dest string, opts OfficePrintOptions) error { func unoconv(
ctx context.Context,
logger xlog.Logger,
proc process.Process,
opts OfficePrintOptions,
fpath, dest string,
) error {
const op string = "print.unoconv" const op string = "print.unoconv"
resolver := func() error { resolver := func() error {
args := []string{ args := []string{
"--server",
proc.Host(),
"--port",
fmt.Sprintf("%d", proc.Port()),
"--format", "--format",
"pdf", "pdf",
} }