mirror of
https://github.com/gotenberg/gotenberg.git
synced 2026-08-17 04:32:15 +01:00
process balancing: better implementation (wip)
This commit is contained in:
@@ -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())
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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())
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
327
internal/pkg/prinery/chrome.go
Normal file
327
internal/pkg/prinery/chrome.go
Normal 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))
|
||||||
|
)
|
||||||
68
internal/pkg/prinery/markdown.go
Normal file
68
internal/pkg/prinery/markdown.go
Normal 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
|
||||||
|
}
|
||||||
39
internal/pkg/prinery/merge.go
Normal file
39
internal/pkg/prinery/merge.go
Normal 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
365
internal/pkg/prinery/pm2.go
Normal 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))
|
||||||
|
)
|
||||||
@@ -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
|
|
||||||
}()
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
158
internal/pkg/prinery/soffice.go
Normal file
158
internal/pkg/prinery/soffice.go
Normal 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))
|
||||||
|
)
|
||||||
125
internal/pkg/prinery/unit.go
Normal file
125
internal/pkg/prinery/unit.go
Normal 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
|
||||||
|
}
|
||||||
@@ -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,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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",
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user