From 24e6a86bf57a6e3efb5b5d3a6a2c162ed4be15a3 Mon Sep 17 00:00:00 2001 From: Julien Neuhart Date: Thu, 26 Sep 2019 18:07:43 +0200 Subject: [PATCH] rolling back from previous implementation, now using master version of unoconv --- build/base/Dockerfile | 40 +- build/tests/docker-entrypoint.sh | 1 + cmd/gotenberg/main.go | 15 +- go.mod | 2 + go.sum | 4 + internal/app/xhttp/handler.go | 139 ++----- internal/app/xhttp/handler_test.go | 28 +- internal/app/xhttp/middleware.go | 5 +- internal/app/xhttp/option.go | 53 ++- internal/app/xhttp/pkg/context/context.go | 33 +- .../app/xhttp/pkg/context/context_test.go | 22 -- internal/app/xhttp/pkg/resource/arg.go | 28 -- internal/app/xhttp/xhttp.go | 5 +- internal/app/xhttp/xhttp_test.go | 9 +- internal/pkg/chrome/chrome.go | 143 +++++++ internal/pkg/chrome/doc.go | 1 + internal/pkg/conf/conf.go | 10 - internal/pkg/pm2/chrome.go | 114 ------ internal/pkg/pm2/doc.go | 12 - internal/pkg/pm2/dummy.go | 46 --- internal/pkg/pm2/pm2.go | 165 -------- internal/pkg/pm2/unoconv.go | 103 ----- internal/pkg/prinery/chrome.go | 327 ---------------- internal/pkg/prinery/doc.go | 1 - internal/pkg/prinery/markdown.go | 68 ---- internal/pkg/prinery/merge.go | 39 -- internal/pkg/prinery/pm2.go | 365 ------------------ internal/pkg/prinery/prinery.go | 90 ----- internal/pkg/prinery/soffice.go | 158 -------- internal/pkg/prinery/unit.go | 125 ------ internal/pkg/print/chrome.go | 265 ------------- internal/pkg/print/doc.go | 1 - internal/pkg/print/markdown.go | 70 ---- internal/pkg/print/merge.go | 44 --- internal/pkg/print/office.go | 126 ------ internal/pkg/print/print.go | 13 - internal/pkg/print/url.go | 15 - internal/pkg/printer/office.go | 57 +-- internal/pkg/process/chrome.go | 106 ----- internal/pkg/process/doc.go | 1 - internal/pkg/process/pm2.go | 324 ---------------- internal/pkg/process/process.go | 40 -- internal/pkg/process/soffice.go | 74 ---- internal/pkg/xassert/xassert.go | 49 +++ internal/pkg/xerrgroup/doc.go | 6 - internal/pkg/xerrgroup/xerrgroup.go | 20 - test/cmd/pm2/pm2.go | 18 - 47 files changed, 346 insertions(+), 3034 deletions(-) create mode 100644 internal/pkg/chrome/chrome.go create mode 100644 internal/pkg/chrome/doc.go delete mode 100644 internal/pkg/pm2/chrome.go delete mode 100644 internal/pkg/pm2/doc.go delete mode 100644 internal/pkg/pm2/dummy.go delete mode 100644 internal/pkg/pm2/pm2.go delete mode 100644 internal/pkg/pm2/unoconv.go delete mode 100644 internal/pkg/prinery/chrome.go delete mode 100644 internal/pkg/prinery/doc.go delete mode 100644 internal/pkg/prinery/markdown.go delete mode 100644 internal/pkg/prinery/merge.go delete mode 100644 internal/pkg/prinery/pm2.go delete mode 100644 internal/pkg/prinery/prinery.go delete mode 100644 internal/pkg/prinery/soffice.go delete mode 100644 internal/pkg/prinery/unit.go delete mode 100644 internal/pkg/print/chrome.go delete mode 100644 internal/pkg/print/doc.go delete mode 100644 internal/pkg/print/markdown.go delete mode 100644 internal/pkg/print/merge.go delete mode 100644 internal/pkg/print/office.go delete mode 100644 internal/pkg/print/print.go delete mode 100644 internal/pkg/print/url.go delete mode 100644 internal/pkg/process/chrome.go delete mode 100644 internal/pkg/process/doc.go delete mode 100644 internal/pkg/process/pm2.go delete mode 100644 internal/pkg/process/process.go delete mode 100644 internal/pkg/process/soffice.go delete mode 100644 internal/pkg/xerrgroup/doc.go delete mode 100644 internal/pkg/xerrgroup/xerrgroup.go delete mode 100644 test/cmd/pm2/pm2.go diff --git a/build/base/Dockerfile b/build/base/Dockerfile index d074eb89..fe323674 100644 --- a/build/base/Dockerfile +++ b/build/base/Dockerfile @@ -9,19 +9,7 @@ FROM debian:buster-slim RUN echo "deb http://httpredir.debian.org/debian/ buster main contrib non-free" > /etc/apt/sources.list &&\ apt-get update &&\ - apt-get install -y curl wget python3-pip ttf-mscorefonts-installer - -# |-------------------------------------------------------------------------- -# | PM2 -# |-------------------------------------------------------------------------- -# | -# | Installs PM2 for launching programs in background and with failure -# | recovering. In our case: Google Chrome (headless) and unoconv. -# | - -RUN curl -sL https://deb.nodesource.com/setup_12.x | bash - &&\ - apt-get install -y nodejs &&\ - npm install -g pm2 + apt-get install -y curl wget gnupg ttf-mscorefonts-installer # |-------------------------------------------------------------------------- # | Chrome @@ -36,19 +24,33 @@ RUN wget -q -O - https://dl.google.com/linux/linux_signing_key.pub | apt-key add apt-get -y --allow-unauthenticated install google-chrome-stable # |-------------------------------------------------------------------------- -# | Unoconv +# | LibreOffice # |-------------------------------------------------------------------------- # | -# | Installs unoconv and LibreOffice. +# | Installs LibreOffice. # | -RUN pip3 install unoconv &&\ - # https://github.com/nextcloud/docker/issues/380 - mkdir -p /usr/share/man/man1mkdir -p /usr/share/man/man1 &&\ +# https://github.com/nextcloud/docker/issues/380 +RUN mkdir -p /usr/share/man/man1mkdir -p /usr/share/man/man1 &&\ echo "deb http://httpredir.debian.org/debian/ buster-backports main contrib non-free" >> /etc/apt/sources.list &&\ apt-get update &&\ apt-get -t buster-backports -y install libreoffice +# |-------------------------------------------------------------------------- +# | Unoconv +# |-------------------------------------------------------------------------- +# | +# | Installs unoconv. +# | + +ENV UNO_URL=https://raw.githubusercontent.com/dagwieers/unoconv/master/unoconv + +RUN apt-get install -y python3 &&\ + curl -Ls $UNO_URL -o /usr/bin/unoconv &&\ + chmod +x /usr/bin/unoconv &&\ + ln -s /usr/bin/python3 /usr/bin/python &&\ + unoconv --version + # |-------------------------------------------------------------------------- # | PDFtk # |-------------------------------------------------------------------------- @@ -108,4 +110,4 @@ RUN groupadd --gid 1001 gotenberg \ && mkdir /gotenberg \ && chown gotenberg: /gotenberg -ENV PM2_HOME=/gotenberg/.pm2 +RUN apt-get install -y procps \ No newline at end of file diff --git a/build/tests/docker-entrypoint.sh b/build/tests/docker-entrypoint.sh index ad81d21f..32294499 100755 --- a/build/tests/docker-entrypoint.sh +++ b/build/tests/docker-entrypoint.sh @@ -11,6 +11,7 @@ fi # Start the PM2 processes # (Google Chrome headless & unoconv listener). +# TODO just start chrome go run github.com/thecodingmachine/gotenberg/test/cmd/pm2 # Run our tests. diff --git a/cmd/gotenberg/main.go b/cmd/gotenberg/main.go index d32b3440..8d27da5a 100644 --- a/cmd/gotenberg/main.go +++ b/cmd/gotenberg/main.go @@ -1,14 +1,15 @@ package main import ( + "context" "fmt" "net/http" "os" "os/signal" "github.com/thecodingmachine/gotenberg/internal/app/xhttp" + "github.com/thecodingmachine/gotenberg/internal/pkg/chrome" "github.com/thecodingmachine/gotenberg/internal/pkg/conf" - "github.com/thecodingmachine/gotenberg/internal/pkg/prinery" "github.com/thecodingmachine/gotenberg/internal/pkg/xcontext" "github.com/thecodingmachine/gotenberg/internal/pkg/xlog" ) @@ -26,18 +27,14 @@ func main() { } systemLogger.InfofOp(op, "Gotenberg %s", version) systemLogger.DebugfOp(op, "configuration: %+v", config) - // create our prinery. - prinry, err := prinery.NewPM2Prinery(systemLogger, config) + // start Google Chrome. + // TODO kill proc. + _, err = chrome.Start(context.Background(), systemLogger) if err != nil { systemLogger.FatalOp(op, err) } - // TODO use chan - // TODO stop prinery - if err = prinry.Start(make(chan error, 1)); err != nil { - systemLogger.FatalOp(op, err) - } // create our API. - srv := xhttp.New(config, prinry) + srv := xhttp.New(config) // run our API in a goroutine so that it doesn't block. go func() { systemLogger.InfofOp(op, "http server started on port '%d'", config.DefaultListenPort()) diff --git a/go.mod b/go.mod index b46352c2..575bad5a 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/thecodingmachine/gotenberg go 1.12 require ( + github.com/dustin/go-humanize v1.0.0 github.com/google/go-cmp v0.3.1 // indirect github.com/gorilla/websocket v1.4.0 // indirect github.com/konsorten/go-windows-terminal-sequences v1.0.2 // indirect @@ -12,6 +13,7 @@ require ( github.com/mafredri/cdp v0.24.2 github.com/mattn/go-isatty v0.0.9 github.com/microcosm-cc/bluemonday v1.0.2 + github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2 github.com/russross/blackfriday/v2 v2.0.1 github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect github.com/sirupsen/logrus v1.4.2 diff --git a/go.sum b/go.sum index 3db58652..933fa201 100644 --- a/go.sum +++ b/go.sum @@ -3,6 +3,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= +github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/google/go-cmp v0.3.1 h1:Xye71clBPdm5HgqGwUkwhbynsUJZhDbS20FvLhQ2izg= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/gorilla/websocket v1.4.0 h1:WDFjx/TMzVgy9VdMMQi2K2Emtwi2QcUQsztZ/zLaH/Q= @@ -30,6 +32,8 @@ github.com/mattn/go-isatty v0.0.9 h1:d5US/mDsogSGW37IV293h//ZFaeajb69h+EHFsv2xGg github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= github.com/microcosm-cc/bluemonday v1.0.2 h1:5lPfLTTAvAbtS0VqT+94yOtFnGfUWYyx0+iToC3Os3s= github.com/microcosm-cc/bluemonday v1.0.2/go.mod h1:iVP4YcDBq+n/5fb23BhYFvIMq/leAFZyRl6bYmGDlGc= +github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2 h1:JhzVVoYvbOACxoUmOs6V/G4D5nPVUW73rKvXxP4XUJc= +github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2/go.mod h1:iIss55rKnNBTvrwdmkUpLnDpZoAHvWaiq5+iMmen4AE= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q= diff --git a/internal/app/xhttp/handler.go b/internal/app/xhttp/handler.go index 7c43d537..dda5d68e 100644 --- a/internal/app/xhttp/handler.go +++ b/internal/app/xhttp/handler.go @@ -1,7 +1,6 @@ package xhttp import ( - timeoutContext "context" "fmt" "net/http" "os" @@ -9,10 +8,8 @@ import ( "github.com/labstack/echo/v4" "github.com/thecodingmachine/gotenberg/internal/app/xhttp/pkg/context" "github.com/thecodingmachine/gotenberg/internal/app/xhttp/pkg/resource" - "github.com/thecodingmachine/gotenberg/internal/pkg/pm2" - "github.com/thecodingmachine/gotenberg/internal/pkg/xcontext" + "github.com/thecodingmachine/gotenberg/internal/pkg/printer" "github.com/thecodingmachine/gotenberg/internal/pkg/xerror" - "github.com/thecodingmachine/gotenberg/internal/pkg/xlog" "github.com/thecodingmachine/gotenberg/internal/pkg/xrand" "github.com/thecodingmachine/gotenberg/internal/pkg/xtime" ) @@ -33,23 +30,6 @@ func pingHandler(c echo.Context) error { ctx := context.MustCastFromEchoContext(c) logger := ctx.XLogger() logger.DebugOp(op, "handling ping request...") - resolver := func() error { - if err := ctx.ProcessesHealthcheck(); err != nil { - return err - } - if logger.Level() != xlog.DebugLevel { - return nil - } - // TODO - list, err := pm2.List() - if err != nil { - return err - } - return ctx.JSON(http.StatusOK, list) - } - if err := resolver(); err != nil { - return xerror.New(op, err) - } return nil } @@ -61,21 +41,17 @@ func mergeHandler(c echo.Context) error { ctx := context.MustCastFromEchoContext(c) logger := ctx.XLogger() logger.DebugOp(op, "handling merge request...") - config := ctx.Config() r := ctx.MustResource() - timeout, err := resource.WaitTimeoutAndWaitDelayArg(r, config) + opts, err := mergePrinterOptions(r, ctx.Config()) if err != nil { - return err + return xerror.New(op, err) } fpaths, err := r.Fpaths(".pdf") if err != nil { return err } - prinry := ctx.Prinery() - printFunc := func(ctx timeoutContext.Context, logger xlog.Logger, dest string) error { - return prinry.Merge(ctx, logger, dest, fpaths) - } - return convert(ctx, timeout, printFunc) + p := printer.NewMergePrinter(logger, fpaths, opts) + return convert(ctx, p) } if err := resolver(); err != nil { return xerror.New(op, err) @@ -91,13 +67,8 @@ func htmlHandler(c echo.Context) error { ctx := context.MustCastFromEchoContext(c) logger := ctx.XLogger() logger.DebugOp(op, "handling HTML request...") - config := ctx.Config() r := ctx.MustResource() - timeout, err := resource.WaitTimeoutAndWaitDelayArg(r, config) - if err != nil { - return err - } - opts, err := chromePrintOptions(r, config) + opts, err := chromePrinterOptions(r, ctx.Config()) if err != nil { return err } @@ -105,11 +76,8 @@ func htmlHandler(c echo.Context) error { if err != nil { return err } - prinry := ctx.Prinery() - printFunc := func(ctx timeoutContext.Context, logger xlog.Logger, dest string) error { - return prinry.HTML(ctx, logger, dest, fpath, opts) - } - return convert(ctx, timeout, printFunc) + p := printer.NewHTMLPrinter(logger, fpath, opts) + return convert(ctx, p) } if err := resolver(); err != nil { return xerror.New(op, err) @@ -125,13 +93,8 @@ func urlHandler(c echo.Context) error { ctx := context.MustCastFromEchoContext(c) logger := ctx.XLogger() logger.DebugOp(op, "handling URL request...") - config := ctx.Config() r := ctx.MustResource() - timeout, err := resource.WaitTimeoutAndWaitDelayArg(r, config) - if err != nil { - return err - } - opts, err := chromePrintOptions(r, config) + opts, err := chromePrinterOptions(r, ctx.Config()) if err != nil { return err } @@ -146,11 +109,8 @@ func urlHandler(c echo.Context) error { if err != nil { return err } - prinry := ctx.Prinery() - printFunc := func(ctx timeoutContext.Context, logger xlog.Logger, dest string) error { - return prinry.URL(ctx, logger, dest, remoteURL, opts) - } - return convert(ctx, timeout, printFunc) + p := printer.NewURLPrinter(logger, remoteURL, opts) + return convert(ctx, p) } if err := resolver(); err != nil { return xerror.New(op, err) @@ -166,13 +126,8 @@ func markdownHandler(c echo.Context) error { ctx := context.MustCastFromEchoContext(c) logger := ctx.XLogger() logger.DebugOp(op, "handling Markdown request...") - config := ctx.Config() r := ctx.MustResource() - timeout, err := resource.WaitTimeoutAndWaitDelayArg(r, config) - if err != nil { - return err - } - opts, err := chromePrintOptions(r, config) + opts, err := chromePrinterOptions(r, ctx.Config()) if err != nil { return err } @@ -180,11 +135,11 @@ func markdownHandler(c echo.Context) error { if err != nil { return err } - prinry := ctx.Prinery() - printFunc := func(ctx timeoutContext.Context, logger xlog.Logger, dest string) error { - return prinry.Markdown(ctx, logger, dest, fpath, opts) + p, err := printer.NewMarkdownPrinter(logger, fpath, opts) + if err != nil { + return err } - return convert(ctx, timeout, printFunc) + return convert(ctx, p) } if err := resolver(); err != nil { return xerror.New(op, err) @@ -200,13 +155,8 @@ func officeHandler(c echo.Context) error { ctx := context.MustCastFromEchoContext(c) logger := ctx.XLogger() logger.DebugOp(op, "handling Office request...") - config := ctx.Config() r := ctx.MustResource() - timeout, err := resource.WaitTimeoutAndWaitDelayArg(r, config) - if err != nil { - return err - } - opts, err := unoconvPrintOptions(r, config) + opts, err := officePrinterOptions(r, ctx.Config()) if err != nil { return err } @@ -227,11 +177,8 @@ func officeHandler(c echo.Context) error { if err != nil { return err } - prinry := ctx.Prinery() - printFunc := func(ctx timeoutContext.Context, logger xlog.Logger, dest string) error { - return prinry.Office(ctx, logger, dest, fpaths, opts) - } - return convert(ctx, timeout, printFunc) + p := printer.NewOfficePrinter(logger, fpaths, opts) + return convert(ctx, p) } if err := resolver(); err != nil { return xerror.New(op, err) @@ -239,11 +186,7 @@ func officeHandler(c echo.Context) error { return nil } -func convert( - ctx context.Context, - timeout float64, - printFunc func(ctx timeoutContext.Context, logger xlog.Logger, dest string) error, -) error { +func convert(ctx context.Context, p printer.Printer) error { const op string = "xhttp.convert" resolver := func() error { logger := ctx.XLogger() @@ -256,13 +199,13 @@ func convert( // or an error. if !r.HasArg(resource.WebhookURLArgKey) { logger.DebugfOp(op, "no '%s' found, converting synchronously", resource.WebhookURLArgKey) - return convertSync(ctx, timeout, filename, fpath, printFunc) + return convertSync(ctx, p, filename, fpath) } // as a webhook URL has been given, we // run the following lines in a goroutine so that // it doesn't block. logger.DebugfOp(op, "'%s' found, converting asynchronously", resource.WebhookURLArgKey) - return convertAsync(ctx, timeout, filename, fpath, printFunc) + return convertAsync(ctx, p, filename, fpath) } if err := resolver(); err != nil { return xerror.New(op, err) @@ -270,19 +213,13 @@ func convert( return nil } -func convertSync( - ctx context.Context, - timeout float64, - filename, dest string, - printFunc func(ctx timeoutContext.Context, logger xlog.Logger, dest string) error, -) error { +func convertSync(ctx context.Context, p printer.Printer, filename, fpath string) error { const op = "xhttp.convertSync" - logger := ctx.XLogger() - r := ctx.MustResource() - timeoutCtx, cancel := xcontext.WithTimeout(logger, timeout) - defer cancel() resolver := func() error { - if err := printFunc(timeoutCtx, logger, dest); err != nil { + logger := ctx.XLogger() + r := ctx.MustResource() + + if err := p.Print(fpath); err != nil { return err } if !r.HasArg(resource.ResultFilenameArgKey) { @@ -292,7 +229,7 @@ func convertSync( resource.RemoteURLArgKey, filename, ) - if err := ctx.Attachment(dest, filename); err != nil { + if err := ctx.Attachment(fpath, filename); err != nil { return err } return nil @@ -306,26 +243,18 @@ func convertSync( if err != nil { return err } - if err := ctx.Attachment(dest, filename); err != nil { + if err := ctx.Attachment(fpath, filename); err != nil { return err } return nil } if err := resolver(); err != nil { - return xcontext.MustHandleError( - timeoutCtx, - xerror.New(op, err), - ) + return xerror.New(op, err) } return nil } -func convertAsync( - ctx context.Context, - timeout float64, - filename, dest string, - printFunc func(ctx timeoutContext.Context, logger xlog.Logger, dest string) error, -) error { +func convertAsync(ctx context.Context, p printer.Printer, filename, fpath string) error { const op = "xhttp.convertAsync" logger := ctx.XLogger() r := ctx.MustResource() @@ -339,14 +268,12 @@ func convertAsync( } go func() { defer r.Close() // nolint: errcheck - timeoutCtx, cancel := xcontext.WithTimeout(logger, timeout) - defer cancel() - if err := printFunc(timeoutCtx, logger, dest); err != nil { + if err := p.Print(fpath); err != nil { xerr := xerror.New(op, err) logger.ErrorOp(xerror.Op(xerr), xerr) return } - f, err := os.Open(dest) + f, err := os.Open(fpath) if err != nil { xerr := xerror.New(op, err) logger.ErrorOp(xerror.Op(xerr), xerr) diff --git a/internal/app/xhttp/handler_test.go b/internal/app/xhttp/handler_test.go index c1a5355b..c4c78add 100644 --- a/internal/app/xhttp/handler_test.go +++ b/internal/app/xhttp/handler_test.go @@ -19,8 +19,8 @@ import ( func TestPingHandler(t *testing.T) { // should return 200. config := conf.DefaultConfig() - // TODO - srv := New(config, nil, nil) + srv := New(config) + srv = New(config) req := httptest.NewRequest(http.MethodGet, pingEndpoint, nil) test.AssertStatusCode(t, http.StatusOK, srv, req) // should returns a JSON as @@ -28,8 +28,7 @@ func TestPingHandler(t *testing.T) { os.Setenv(conf.LogLevelEnvVar, "DEBUG") config, err := conf.FromEnv() assert.Nil(t, err) - // TODO - srv = New(config, nil, nil) + srv = New(config) req = httptest.NewRequest(http.MethodGet, pingEndpoint, nil) rec := httptest.NewRecorder() srv.ServeHTTP(rec, req) @@ -40,8 +39,7 @@ func TestPingHandler(t *testing.T) { func TestMergeHandler(t *testing.T) { config := conf.DefaultConfig() - // TODO - srv := New(config, nil, nil) + srv := New(config) // should return 200. body, contentType := test.MergeMultipartForm(t, nil) req := httptest.NewRequest(http.MethodPost, mergeEndpoint, body) @@ -74,8 +72,7 @@ func TestMergeHandler(t *testing.T) { func TestHTMLHandler(t *testing.T) { config := conf.DefaultConfig() - // TODO - srv := New(config, nil, nil) + srv := New(config) endpoint := fmt.Sprintf("%s%s", convertGroupEndpoint, htmlEndpoint) // should return 200. body, contentType := test.HTMLMultipartForm(t, nil) @@ -205,8 +202,7 @@ func TestHTMLHandler(t *testing.T) { func TestURLHandler(t *testing.T) { config := conf.DefaultConfig() - // TODO - srv := New(config, nil, nil) + srv := New(config) endpoint := fmt.Sprintf("%s%s", convertGroupEndpoint, urlEndpoint) // should return 200. body, contentType := test.URLMultipartForm(t, nil) @@ -336,8 +332,7 @@ func TestURLHandler(t *testing.T) { func TestMarkdownHandler(t *testing.T) { config := conf.DefaultConfig() - // TODO - srv := New(config, nil, nil) + srv := New(config) endpoint := fmt.Sprintf("%s%s", convertGroupEndpoint, markdownEndpoint) // should return 200. body, contentType := test.MarkdownMultipartForm(t, nil) @@ -467,8 +462,7 @@ func TestMarkdownHandler(t *testing.T) { func TestOfficeHandler(t *testing.T) { config := conf.DefaultConfig() - // TODO - srv := New(config, nil, nil) + srv := New(config) endpoint := fmt.Sprintf("%s%s", convertGroupEndpoint, officeEndpoint) // should return 200. body, contentType := test.OfficeMultipartForm(t, nil) @@ -530,8 +524,7 @@ func TestWebhook(t *testing.T) { rcv.Start(":3001") }() config := conf.DefaultConfig() - // TODO - srv := New(config, nil, nil) + srv := New(config) // our custom server should receive the PDF. body, contentType := test.MergeMultipartForm(t, map[string]string{string(resource.WebhookURLArgKey): "http://localhost:3001/foo"}) req := httptest.NewRequest(http.MethodPost, mergeEndpoint, body) @@ -543,8 +536,7 @@ func TestWebhook(t *testing.T) { func TestResultFilename(t *testing.T) { config := conf.DefaultConfig() - // TODO - srv := New(config, nil, nil) + srv := New(config) body, contentType := test.MergeMultipartForm(t, map[string]string{string(resource.ResultFilenameArgKey): "foo.pdf"}) req := httptest.NewRequest(http.MethodPost, mergeEndpoint, body) req.Header.Set(echo.HeaderContentType, contentType) diff --git a/internal/app/xhttp/middleware.go b/internal/app/xhttp/middleware.go index 8119f963..12aada23 100644 --- a/internal/app/xhttp/middleware.go +++ b/internal/app/xhttp/middleware.go @@ -7,7 +7,6 @@ import ( "github.com/thecodingmachine/gotenberg/internal/app/xhttp/pkg/context" "github.com/thecodingmachine/gotenberg/internal/app/xhttp/pkg/resource" "github.com/thecodingmachine/gotenberg/internal/pkg/conf" - "github.com/thecodingmachine/gotenberg/internal/pkg/prinery" "github.com/thecodingmachine/gotenberg/internal/pkg/xerror" "github.com/thecodingmachine/gotenberg/internal/pkg/xlog" "github.com/thecodingmachine/gotenberg/internal/pkg/xrand" @@ -15,7 +14,7 @@ import ( // contextMiddleware extends the default echo.Context with // our custom context.Context. -func contextMiddleware(config conf.Config, prinry prinery.Prinery) echo.MiddlewareFunc { +func contextMiddleware(config conf.Config) echo.MiddlewareFunc { return func(next echo.HandlerFunc) echo.HandlerFunc { return func(c echo.Context) error { // generate a unique identifier for the request. @@ -25,7 +24,7 @@ func contextMiddleware(config conf.Config, prinry prinery.Prinery) echo.Middlewa logger := xlog.New(config.LogLevel(), trace) // extend the current echo context with our custom // context. - ctx := context.New(c, logger, config, prinry) + ctx := context.New(c, logger, config) // if its an healthcheck request, there // is no need to create a Resource. if ctx.Path() == pingEndpoint { diff --git a/internal/app/xhttp/option.go b/internal/app/xhttp/option.go index 1c4f8ca6..d79841e8 100644 --- a/internal/app/xhttp/option.go +++ b/internal/app/xhttp/option.go @@ -3,37 +3,53 @@ package xhttp import ( "github.com/thecodingmachine/gotenberg/internal/app/xhttp/pkg/resource" "github.com/thecodingmachine/gotenberg/internal/pkg/conf" - "github.com/thecodingmachine/gotenberg/internal/pkg/prinery" + "github.com/thecodingmachine/gotenberg/internal/pkg/printer" "github.com/thecodingmachine/gotenberg/internal/pkg/xerror" ) -func chromePrintOptions(r resource.Resource, config conf.Config) (prinery.ChromePrintOptions, error) { - const op string = "xhttp.chromePrintOptions" - resolver := func() (prinery.ChromePrintOptions, error) { +func mergePrinterOptions(r resource.Resource, config conf.Config) (printer.MergePrinterOptions, error) { + const op string = "xhttp.mergePrinterOptions" + waitTimeout, err := resource.WaitTimeoutArg(r, config) + if err != nil { + return printer.MergePrinterOptions{}, xerror.New(op, err) + } + return printer.MergePrinterOptions{ + WaitTimeout: waitTimeout, + }, nil +} + +func chromePrinterOptions(r resource.Resource, config conf.Config) (printer.ChromePrinterOptions, error) { + const op string = "xhttp.chromePrinterOptions" + resolver := func() (printer.ChromePrinterOptions, error) { + waitTimeout, err := resource.WaitTimeoutArg(r, config) + if err != nil { + return printer.ChromePrinterOptions{}, err + } waitDelay, err := resource.WaitDelayArg(r, config) if err != nil { - return prinery.ChromePrintOptions{}, err + return printer.ChromePrinterOptions{}, err } headerHTML, footerHTML, err := resource.HeaderFooterContents(r, config) if err != nil { - return prinery.ChromePrintOptions{}, err + return printer.ChromePrinterOptions{}, err } paperWidth, paperHeight, err := resource.PaperSizeArgs(r, config) if err != nil { - return prinery.ChromePrintOptions{}, err + return printer.ChromePrinterOptions{}, err } marginTop, marginBottom, marginLeft, marginRight, err := resource.MarginArgs(r, config) if err != nil { - return prinery.ChromePrintOptions{}, err + return printer.ChromePrinterOptions{}, err } landscape, err := r.BoolArg(resource.LandscapeArgKey, false) if err != nil { - return prinery.ChromePrintOptions{}, err + return printer.ChromePrinterOptions{}, err } - return prinery.ChromePrintOptions{ + return printer.ChromePrinterOptions{ + WaitTimeout: waitTimeout, WaitDelay: waitDelay, HeaderHTML: headerHTML, FooterHTML: footerHTML, @@ -53,15 +69,20 @@ func chromePrintOptions(r resource.Resource, config conf.Config) (prinery.Chrome return opts, nil } -func unoconvPrintOptions(r resource.Resource, config conf.Config) (prinery.UnoconvPrintOptions, error) { - const op string = "xhttp.unoconvPrintOptions" - resolver := func() (prinery.UnoconvPrintOptions, error) { +func officePrinterOptions(r resource.Resource, config conf.Config) (printer.OfficePrinterOptions, error) { + const op string = "xhttp.officePrinterOptions" + resolver := func() (printer.OfficePrinterOptions, error) { + waitTimeout, err := resource.WaitTimeoutArg(r, config) + if err != nil { + return printer.OfficePrinterOptions{}, err + } landscape, err := r.BoolArg(resource.LandscapeArgKey, false) if err != nil { - return prinery.UnoconvPrintOptions{}, err + return printer.OfficePrinterOptions{}, err } - return prinery.UnoconvPrintOptions{ - Landscape: landscape, + return printer.OfficePrinterOptions{ + WaitTimeout: waitTimeout, + Landscape: landscape, }, nil } opts, err := resolver() diff --git a/internal/app/xhttp/pkg/context/context.go b/internal/app/xhttp/pkg/context/context.go index 75cc9add..d1065b58 100644 --- a/internal/app/xhttp/pkg/context/context.go +++ b/internal/app/xhttp/pkg/context/context.go @@ -13,7 +13,6 @@ import ( "github.com/thecodingmachine/gotenberg/internal/app/xhttp/pkg/resource" "github.com/thecodingmachine/gotenberg/internal/pkg/conf" "github.com/thecodingmachine/gotenberg/internal/pkg/normalize" - "github.com/thecodingmachine/gotenberg/internal/pkg/prinery" "github.com/thecodingmachine/gotenberg/internal/pkg/xerror" "github.com/thecodingmachine/gotenberg/internal/pkg/xlog" ) @@ -23,23 +22,16 @@ type Context struct { echo.Context logger xlog.Logger config conf.Config - prinry prinery.Prinery resource resource.Resource startTime time.Time } // New creates a new Context. -func New( - c echo.Context, - logger xlog.Logger, - config conf.Config, - prinry prinery.Prinery, -) Context { +func New(c echo.Context, logger xlog.Logger, config conf.Config) Context { return Context{ c, logger, config, - prinry, resource.Resource{}, time.Now(), } @@ -78,29 +70,6 @@ func (ctx Context) Config() conf.Config { return ctx.config } -// ProcessesHealthcheck returns an error if -// one of the processes is not viable. -func (ctx Context) ProcessesHealthcheck() error { - const op string = "context.Context.ProcessesHealthcheck" - // TODO - /*processes := ctx.manager.All() - for _, p := range processes { - if !ctx.manager.IsViable(p) { - return xerror.New( - op, - fmt.Errorf("'%s' is not viable", p.ID()), - ) - } - }*/ - return nil -} - -// Prinery returns the instance of prinery.Prinery -// associated with the Context. -func (ctx Context) Prinery() prinery.Prinery { - return ctx.prinry -} - // WithResource creates a resource.Resource and // adds it to the Context. func (ctx *Context) WithResource(directoryName string) error { diff --git a/internal/app/xhttp/pkg/context/context_test.go b/internal/app/xhttp/pkg/context/context_test.go index f6a1ac3e..9b803460 100644 --- a/internal/app/xhttp/pkg/context/context_test.go +++ b/internal/app/xhttp/pkg/context/context_test.go @@ -6,7 +6,6 @@ import ( "github.com/stretchr/testify/assert" "github.com/thecodingmachine/gotenberg/internal/pkg/conf" - "github.com/thecodingmachine/gotenberg/internal/pkg/pm2" "github.com/thecodingmachine/gotenberg/test" ) @@ -27,27 +26,6 @@ func TestMustCastFromEchoContext(t *testing.T) { }) } -func TestProcessesHealthcheck(t *testing.T) { - // process is viable. - ctx := New( - test.DummyEchoContext(), - test.DebugLogger(), - conf.DefaultConfig(), - pm2.NewDummyProcess(true), - ) - err := ctx.ProcessesHealthcheck() - assert.Nil(t, err) - // process is not viable. - ctx = New( - test.DummyEchoContext(), - test.DebugLogger(), - conf.DefaultConfig(), - pm2.NewDummyProcess(false), - ) - err = ctx.ProcessesHealthcheck() - test.AssertError(t, err) -} - func TestLogRequestResult(t *testing.T) { ctx := New( test.DummyEchoContext(), diff --git a/internal/app/xhttp/pkg/resource/arg.go b/internal/app/xhttp/pkg/resource/arg.go index b196281d..35c7800b 100644 --- a/internal/app/xhttp/pkg/resource/arg.go +++ b/internal/app/xhttp/pkg/resource/arg.go @@ -76,34 +76,6 @@ func ArgKeys() []ArgKey { } } -/* -WaitTimeoutAndWaitDelayArg is a helper for retrieving -the sum of "waitTimeout" and "waitDelay" arguments -as float64. - -It also validates them against the application -configuration. -*/ -func WaitTimeoutAndWaitDelayArg(r Resource, config conf.Config) (float64, error) { - const op string = "resource.WaitTimeoutAndWaitDelayArg" - resolver := func() (float64, error) { - waitTimeout, err := WaitTimeoutArg(r, config) - if err != nil { - return waitTimeout, err - } - waitDelay, err := WaitDelayArg(r, config) - if err != nil { - return waitDelay, err - } - return waitTimeout + waitDelay, nil - } - combined, err := resolver() - if err != nil { - return combined, xerror.New(op, err) - } - return combined, nil -} - /* WaitTimeoutArg is a helper for retrieving the "waitTimeout" argument as float64. diff --git a/internal/app/xhttp/xhttp.go b/internal/app/xhttp/xhttp.go index 6b0da703..7324997e 100644 --- a/internal/app/xhttp/xhttp.go +++ b/internal/app/xhttp/xhttp.go @@ -3,15 +3,14 @@ package xhttp import ( "github.com/labstack/echo/v4" "github.com/thecodingmachine/gotenberg/internal/pkg/conf" - "github.com/thecodingmachine/gotenberg/internal/pkg/prinery" ) // New returns a custom echo.Echo. -func New(config conf.Config, prinry prinery.Prinery) *echo.Echo { +func New(config conf.Config) *echo.Echo { srv := echo.New() srv.HideBanner = true srv.HidePort = true - srv.Use(contextMiddleware(config, prinry)) + srv.Use(contextMiddleware(config)) srv.Use(loggerMiddleware()) srv.Use(cleanupMiddleware()) srv.Use(errorMiddleware()) diff --git a/internal/app/xhttp/xhttp_test.go b/internal/app/xhttp/xhttp_test.go index 0d317e3d..685fb0a1 100644 --- a/internal/app/xhttp/xhttp_test.go +++ b/internal/app/xhttp/xhttp_test.go @@ -17,8 +17,7 @@ func TestDisableChromeEndpoints(t *testing.T) { os.Setenv(conf.DisableGoogleChromeEnvVar, "1") config, err := conf.FromEnv() assert.Nil(t, err) - // TODO - srv := New(config, nil, nil) + srv := New(config) // Ping endpoint should return 200. req := httptest.NewRequest(http.MethodGet, pingEndpoint, nil) test.AssertStatusCode(t, http.StatusOK, srv, req) @@ -55,8 +54,7 @@ func TestDisableUnoconvEndpoints(t *testing.T) { os.Setenv(conf.DisableUnoconvEnvVar, "1") config, err := conf.FromEnv() assert.Nil(t, err) - // TODO - srv := New(config, nil, nil) + srv := New(config) // Ping endpoint should return 200. req := httptest.NewRequest(http.MethodGet, pingEndpoint, nil) test.AssertStatusCode(t, http.StatusOK, srv, req) @@ -93,8 +91,7 @@ func TestDisableChromeAndUnoconvEndpoints(t *testing.T) { os.Setenv(conf.DisableUnoconvEnvVar, "1") config, err := conf.FromEnv() assert.Nil(t, err) - // TODO - srv := New(config, nil, nil) + srv := New(config) // Ping endpoint should return 200. req := httptest.NewRequest(http.MethodGet, pingEndpoint, nil) test.AssertStatusCode(t, http.StatusOK, srv, req) diff --git a/internal/pkg/chrome/chrome.go b/internal/pkg/chrome/chrome.go new file mode 100644 index 00000000..1be662ac --- /dev/null +++ b/internal/pkg/chrome/chrome.go @@ -0,0 +1,143 @@ +package chrome + +import ( + "context" + "os" + "os/exec" + "time" + + "github.com/mafredri/cdp/devtool" + "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/xtime" +) + +func Start(ctx context.Context, logger xlog.Logger) (*os.Process, error) { + const op string = "chrome.Start" + logger.DebugOp(op, "starting new Google Chrome process on port 9222...") + resolver := func() (*os.Process, error) { + binary := "google-chrome-stable" + args := []string{ + "--no-sandbox", + "--headless", + // see https://github.com/GoogleChrome/puppeteer/issues/2410. + "--font-render-hinting=medium", + "--remote-debugging-port=9222", + "--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", + } + cmd, err := xexec.CommandContext(ctx, logger, binary, args...) + if err != nil { + return nil, err + } + // we try to start the process. + xexec.LogBeforeExecute(logger, cmd) + if err := cmd.Start(); err != nil { + return cmd.Process, err + } + // we wait the process to be ready. + warmup(logger) + // if the process failed to start correctly, + // we have to restart it. + if !isViable(logger) { + return restart(ctx, logger, cmd) + } + return cmd.Process, nil + } + proc, err := resolver() + if err != nil { + if errKill := Kill(logger, proc); errKill != nil { + logger.ErrorOp(op, errKill) + } + return nil, xerror.New(op, err) + } + return proc, nil +} + +func Kill(logger xlog.Logger, proc *os.Process) error { + const op string = "chrome.Kill" + resolver := func() error { + logger.DebugOp(op, "removing Google Chrome process using port 9222...") + if proc == nil { + logger.DebugOp(op, "no Google Chrome process using port 9222 found, skipping") + return nil + } + return proc.Kill() + } + if err := resolver(); err != nil { + return xerror.New(op, err) + } + return nil +} + +func restart(ctx context.Context, logger xlog.Logger, cmd *exec.Cmd) (*os.Process, error) { + const op string = "chrome.restart" + resolver := func() (*os.Process, error) { + // we try to restart the process. + xexec.LogBeforeExecute(logger, cmd) + if err := cmd.Start(); err != nil { + return cmd.Process, err + } + // we wait the process to be ready. + warmup(logger) + // if the process failed to restart correctly, + // we have to restart it again. + if !isViable(logger) { + return restart(ctx, logger, cmd) + } + return cmd.Process, nil + } + proc, err := resolver() + if err != nil { + return proc, xerror.New(op, err) + } + return proc, err +} + +func isViable(logger xlog.Logger) bool { + const op string = "chrome.isViable" + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + endpoint := "http://localhost:9222" + logger.DebugfOp( + op, + "checking Google Chrome process viability via endpoint '%s/json/version'", + endpoint, + ) + v, err := devtool.New(endpoint).Version(ctx) + if err != nil { + logger.ErrorfOp( + op, + "Google Chrome is not viable as endpoint returned '%v'", + err, + ) + return false + } + logger.DebugfOp( + op, + "Google Chrome is viable as endpoint returned '%v'", + v, + ) + return true +} + +func warmup(logger xlog.Logger) { + const op string = "chrome.warmup" + warmupTime := xtime.Duration(10) + logger.DebugfOp( + op, + "waiting '%v' for allowing Google Chrome to warmup", + warmupTime, + ) + time.Sleep(warmupTime) +} diff --git a/internal/pkg/chrome/doc.go b/internal/pkg/chrome/doc.go new file mode 100644 index 00000000..7b442802 --- /dev/null +++ b/internal/pkg/chrome/doc.go @@ -0,0 +1 @@ +package chrome diff --git a/internal/pkg/conf/conf.go b/internal/pkg/conf/conf.go index b3f76cbd..d0aca24a 100644 --- a/internal/pkg/conf/conf.go +++ b/internal/pkg/conf/conf.go @@ -201,16 +201,6 @@ func (c Config) DefaultListenPort() int64 { 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 Google Chrome is disabled in the diff --git a/internal/pkg/pm2/chrome.go b/internal/pkg/pm2/chrome.go deleted file mode 100644 index 0a599dd9..00000000 --- a/internal/pkg/pm2/chrome.go +++ /dev/null @@ -1,114 +0,0 @@ -package pm2 - -import ( - "context" - "time" - - "github.com/mafredri/cdp/devtool" - "github.com/thecodingmachine/gotenberg/internal/pkg/xerror" - "github.com/thecodingmachine/gotenberg/internal/pkg/xlog" -) - -type chromeProcess struct { - logger xlog.Logger -} - -// NewChromeProcess returns a Google Chrome -// headless process. -func NewChromeProcess(logger xlog.Logger) Process { - return chromeProcess{ - logger: logger, - } -} - -func (p chromeProcess) Fullname() string { - return "Google Chrome headless" -} - -func (p chromeProcess) Start() error { - const op string = "pm2.chromeProcess.Start" - if err := start(p.logger, p); err != nil { - return xerror.New(op, err) - } - return nil -} - -func (p chromeProcess) IsViable() bool { - const op string = "pm2.chromeProcess.IsViable" - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - p.logger.DebugfOp( - op, - "checking '%s' viability via endpoint '%s'", - p.Fullname(), - "http://localhost:9222/json/version", - ) - v, err := devtool.New("http://localhost:9222").Version(ctx) - if err != nil { - p.logger.ErrorfOp( - op, - "'%s' is not viable as endpoint returned '%v'", - p.Fullname(), - err, - ) - return false - } - p.logger.DebugfOp( - op, - "'%s' is viable as endpoint returned '%v'", - p.Fullname(), - v, - ) - return true -} - -func (p chromeProcess) Stop() error { - const op string = "pm2.chromeProcess.Stop" - if err := stop(p.logger, p); err != nil { - return xerror.New(op, err) - } - return nil -} - -func (p chromeProcess) args() []string { - return []string{ - "--no-sandbox", - "--headless", - "--font-render-hinting=medium", - "--remote-debugging-port=9222", - "--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) binary() string { - return "google-chrome-stable" -} - -func (p chromeProcess) warmup() { - const ( - op string = "pm2.chromeProcess.warmup" - warmupTime time.Duration = 10 * time.Second - ) - p.logger.DebugfOp( - op, - "waiting '%v' for allowing '%s' to warmup", - warmupTime, - p.Fullname(), - ) - time.Sleep(warmupTime) -} - -// Compile-time checks to ensure type implements desired interfaces. -var ( - _ = Process(new(chromeProcess)) -) diff --git a/internal/pkg/pm2/doc.go b/internal/pkg/pm2/doc.go deleted file mode 100644 index d64ac2a0..00000000 --- a/internal/pkg/pm2/doc.go +++ /dev/null @@ -1,12 +0,0 @@ -/* -Package pm2 facilitates starting external -processes on which our application depends. - -For instance, it may start Google Chrome headless and -unoconv listener with PM2. - -The PM2 process manager launch those processes and keep -them running in the background. If for some reason they -crash, it will also restart them. -*/ -package pm2 diff --git a/internal/pkg/pm2/dummy.go b/internal/pkg/pm2/dummy.go deleted file mode 100644 index a65376e4..00000000 --- a/internal/pkg/pm2/dummy.go +++ /dev/null @@ -1,46 +0,0 @@ -package pm2 - -type dummyProcess struct { - isViable bool -} - -// NewDummyProcess returns a dummy -// process. -func NewDummyProcess(isViable bool) Process { - return dummyProcess{ - isViable: isViable, - } -} - -func (p dummyProcess) Fullname() string { - return "dummy process" -} - -func (p dummyProcess) Start() error { - return nil -} - -func (p dummyProcess) IsViable() bool { - return p.isViable -} - -func (p dummyProcess) Stop() error { - return nil -} - -func (p dummyProcess) args() []string { - return []string{} -} - -func (p dummyProcess) binary() string { - return "dummy" -} - -func (p dummyProcess) warmup() { - // let's do nothing. -} - -// Compile-time checks to ensure type implements desired interfaces. -var ( - _ = Process(new(dummyProcess)) -) diff --git a/internal/pkg/pm2/pm2.go b/internal/pkg/pm2/pm2.go deleted file mode 100644 index 533dc0a9..00000000 --- a/internal/pkg/pm2/pm2.go +++ /dev/null @@ -1,165 +0,0 @@ -package pm2 - -import ( - "encoding/json" - "fmt" - "os/exec" - - "github.com/thecodingmachine/gotenberg/internal/pkg/xerror" - "github.com/thecodingmachine/gotenberg/internal/pkg/xexec" - "github.com/thecodingmachine/gotenberg/internal/pkg/xlog" -) - -// Process is a type that can start or -// stop a process with PM2. -type Process interface { - Fullname() string - Start() error - IsViable() bool - Stop() error - args() []string - binary() string - warmup() -} - -type pm2Command string - -const ( - startCommand pm2Command = "start" - restartCommand pm2Command = "restart" - stopCommand pm2Command = "stop" - logsCommand pm2Command = "logs" - jlistCommand pm2Command = "jlist" -) - -/* -JListItem is a struct -used for unmarshaling -ONE item of the result -of the command "pm2 jlist". -*/ -type JListItem struct { - Name string `json:"name"` - PM2Env struct { - Status string `json:"status"` - RestartTime int64 `json:"restart_time"` - } `json:"pm2_env"` - Monit struct { - Memory int64 `json:"memory"` - CPU float64 `json:"cpu"` - } `json:"monit"` -} - -/* -JList is a struct -used for unmarshaling -the result of the command -"pm2 jlist". -*/ -type JList []JListItem - -func (list JList) isOnline(p Process) bool { - const onlineStatus string = "online" - for _, item := range list { - if item.Name == p.binary() { - return item.PM2Env.Status == onlineStatus - } - } - return false -} - -// List returns the -// processes details. -func List() (*JList, error) { - const op = "pm2.List" - resolver := func() (*JList, error) { - out, err := exec. - Command("pm2", string(jlistCommand)). - Output() - if err != nil { - return nil, err - } - data := &JList{} - if err := json.Unmarshal(out, data); err != nil { - return nil, err - } - return data, nil - } - list, err := resolver() - if err != nil { - return nil, xerror.New(op, err) - } - return list, nil -} - -func start(logger xlog.Logger, process Process) error { - const ( - op string = "pm2.start" - maximumAttempts int = 3 - ) - resolver := func() error { - // first, we try to start the process. - if err := run(logger, startCommand, process); err != nil { - return err - } - // we wait the process to be ready. - process.warmup() - // if the process failed to start correctly, - // we have to restart it. - if !process.IsViable() { - attempts := 0 - for attempts < maximumAttempts && !process.IsViable() { - if err := run(logger, restartCommand, process); err != nil { - return err - } - process.warmup() - attempts++ - } - if !process.IsViable() { - return fmt.Errorf("failed to start '%s'", process.Fullname()) - } - } - // the process is viable, let's log its - // output. - if err := run(logger, logsCommand, process); err != nil { - return err - } - return nil - } - if err := resolver(); err != nil { - return xerror.New(op, err) - } - return nil -} - -func stop(logger xlog.Logger, process Process) error { - const op string = "pm2.stop" - if err := run(logger, stopCommand, process); err != nil { - return xerror.New(op, err) - } - return nil -} - -func run(logger xlog.Logger, pm2Cmd pm2Command, process Process) error { - const op string = "pm2.run" - resolver := func() error { - args := []string{ - string(pm2Cmd), - process.binary(), - } - if pm2Cmd == startCommand { - args = append(args, "--interpreter=none", "--") - args = append(args, process.args()...) - } - cmd, err := xexec.Command(logger, "pm2", args...) - if err != nil { - return err - } - xexec.LogBeforeExecute(logger, cmd) - return cmd.Start() - } - if err := resolver(); err != nil { - return xerror.New(op, err) - } - return nil -} diff --git a/internal/pkg/pm2/unoconv.go b/internal/pkg/pm2/unoconv.go deleted file mode 100644 index 219dc554..00000000 --- a/internal/pkg/pm2/unoconv.go +++ /dev/null @@ -1,103 +0,0 @@ -package pm2 - -import ( - "time" - - "github.com/thecodingmachine/gotenberg/internal/pkg/xerror" - "github.com/thecodingmachine/gotenberg/internal/pkg/xlog" -) - -type unoconvProcess struct { - logger xlog.Logger -} - -// NewUnoconvProcess returns a unoconv listener -// process. -func NewUnoconvProcess(logger xlog.Logger) Process { - return unoconvProcess{ - logger: logger, - } -} - -func (p unoconvProcess) Fullname() string { - return "unoconv listener" -} - -func (p unoconvProcess) Start() error { - const op string = "pm2.unoconvProcess.Start" - if err := start(p.logger, p); err != nil { - return xerror.New(op, err) - } - return nil -} - -func (p unoconvProcess) IsViable() bool { - const op string = "pm2.unoconvProcess.IsViable" - p.logger.DebugfOp( - op, - "checking '%s' viability via PM2", - p.Fullname(), - ) - list, err := List() - if err != nil { - p.logger.ErrorfOp( - op, - "'%s' seems not viable as retrieving the list of processes via 'pm2 jlist' returned '%v'", - p.Fullname(), - err, - ) - return false - } - if list.isOnline(p) { - p.logger.DebugfOp( - op, - "'%s' is viable as its status is 'online'", - p.Fullname(), - ) - return true - } - p.logger.DebugfOp( - op, - "'%s' is not viable as its status is not 'online'", - p.Fullname(), - ) - return false -} - -func (p unoconvProcess) Stop() error { - const op string = "pm2.unoconvProcess.Stop" - if err := stop(p.logger, p); err != nil { - return xerror.New(op, err) - } - return nil -} - -func (p unoconvProcess) args() []string { - return []string{ - "--listener", - "--verbose", - } -} - -func (p unoconvProcess) binary() string { - return "unoconv" -} - -func (p unoconvProcess) warmup() { - const ( - op string = "pm2.unoconvProcess.warmup" - warmupTime time.Duration = 3 * time.Second - ) - p.logger.DebugfOp( - op, - "waiting '%v' for allowing '%s' to warmup", - warmupTime, - p.Fullname(), - ) - time.Sleep(warmupTime) -} - -// Compile-time checks to ensure type implements desired interfaces. -var ( - _ = Process(new(unoconvProcess)) -) diff --git a/internal/pkg/prinery/chrome.go b/internal/pkg/prinery/chrome.go deleted file mode 100644 index 8731054e..00000000 --- a/internal/pkg/prinery/chrome.go +++ /dev/null @@ -1,327 +0,0 @@ -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)) -) diff --git a/internal/pkg/prinery/doc.go b/internal/pkg/prinery/doc.go deleted file mode 100644 index d16971e5..00000000 --- a/internal/pkg/prinery/doc.go +++ /dev/null @@ -1 +0,0 @@ -package prinery diff --git a/internal/pkg/prinery/markdown.go b/internal/pkg/prinery/markdown.go deleted file mode 100644 index 0c3cdfcb..00000000 --- a/internal/pkg/prinery/markdown.go +++ /dev/null @@ -1,68 +0,0 @@ -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 -} diff --git a/internal/pkg/prinery/merge.go b/internal/pkg/prinery/merge.go deleted file mode 100644 index 377c6fc0..00000000 --- a/internal/pkg/prinery/merge.go +++ /dev/null @@ -1,39 +0,0 @@ -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)) -) diff --git a/internal/pkg/prinery/pm2.go b/internal/pkg/prinery/pm2.go deleted file mode 100644 index 8e4b954c..00000000 --- a/internal/pkg/prinery/pm2.go +++ /dev/null @@ -1,365 +0,0 @@ -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)) -) diff --git a/internal/pkg/prinery/prinery.go b/internal/pkg/prinery/prinery.go deleted file mode 100644 index e21bc03b..00000000 --- a/internal/pkg/prinery/prinery.go +++ /dev/null @@ -1,90 +0,0 @@ -package prinery - -import ( - "context" - "time" - - "github.com/thecodingmachine/gotenberg/internal/pkg/xlog" -) - -type processSpec interface { - id() string - host() string - port() uint -} - -type process interface { - spec() processSpec - binary() string - args() []string - warmupTime() time.Duration - viabilityFunc() func(logger xlog.Logger) bool -} - -func processesToSpecs(processes []process) []processSpec { - specs := make([]processSpec, len(processes)) - for i, p := range processes { - specs[i] = p.spec() - } - 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 = "" - 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, - } -} - -// UnoconvPrintOptions helps customizing the -// LibreOffice print result. -type UnoconvPrintOptions struct { - Landscape bool -} - -// DefaultUnoconvPrinterOptions returns the default -// LibreOffice print options. -func DefaultUnoconvPrinterOptions() UnoconvPrintOptions { - return UnoconvPrintOptions{ - Landscape: false, - } -} - -type Prinery interface { - Start(emergency chan error) error - HTML(ctx context.Context, logger xlog.Logger, dest, fpath string, opts ChromePrintOptions) error - URL(ctx context.Context, logger xlog.Logger, dest, URL string, opts ChromePrintOptions) error - Markdown(ctx context.Context, logger xlog.Logger, dest, fpath string, opts ChromePrintOptions) error - Office(ctx context.Context, logger xlog.Logger, dest string, fpaths []string, opts UnoconvPrintOptions) error - Merge(ctx context.Context, logger xlog.Logger, dest string, fpaths []string) error -} diff --git a/internal/pkg/prinery/soffice.go b/internal/pkg/prinery/soffice.go deleted file mode 100644 index 8a4db4ab..00000000 --- a/internal/pkg/prinery/soffice.go +++ /dev/null @@ -1,158 +0,0 @@ -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)) -) diff --git a/internal/pkg/prinery/unit.go b/internal/pkg/prinery/unit.go deleted file mode 100644 index 58d49414..00000000 --- a/internal/pkg/prinery/unit.go +++ /dev/null @@ -1,125 +0,0 @@ -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 -} diff --git a/internal/pkg/print/chrome.go b/internal/pkg/print/chrome.go deleted file mode 100644 index b1abdf8a..00000000 --- a/internal/pkg/print/chrome.go +++ /dev/null @@ -1,265 +0,0 @@ -package print - -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/process" - "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 chromePrint struct { - logger xlog.Logger - url string - opts ChromePrintOptions -} - -// 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 = "" - 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, - } -} - -func (p chromePrint) Print(ctx context.Context, dest string, proc process.Process) error { - const op string = "print.chromePrint.Print" - resolver := func() error { - devtEndpoint := fmt.Sprintf("http://%s:%d", proc.Host(), proc.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", proc.Host(), proc.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 chromePrint) enableEvents(ctx context.Context, client *cdp.Client) error { - const op string = "print.chromePrint.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 chromePrint) listenEvents(ctx context.Context, client *cdp.Client) error { - const op string = "print.chromePrint.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 ( - _ = Print(new(chromePrint)) -) diff --git a/internal/pkg/print/doc.go b/internal/pkg/print/doc.go deleted file mode 100644 index 91d3830c..00000000 --- a/internal/pkg/print/doc.go +++ /dev/null @@ -1 +0,0 @@ -package print diff --git a/internal/pkg/print/markdown.go b/internal/pkg/print/markdown.go deleted file mode 100644 index 0be02349..00000000 --- a/internal/pkg/print/markdown.go +++ /dev/null @@ -1,70 +0,0 @@ -package print - -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" -) - -// NewMarkdownPrint returns a Print for -// converting a Markdown file to PDF. -func NewMarkdownPrint(logger xlog.Logger, fpath string, opts ChromePrintOptions) (Print, error) { - const op string = "print.NewMarkdownPrint" - 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 chromePrint{}, xerror.New(op, err) - } - return chromePrint{ - logger: logger, - url: URL, - opts: opts, - }, nil -} - -type templateData struct { - DirPath string -} - -func markdownToHTML(dirPath, filename string) (template.HTML, error) { - const op string = "print.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 -} diff --git a/internal/pkg/print/merge.go b/internal/pkg/print/merge.go deleted file mode 100644 index a961c4fd..00000000 --- a/internal/pkg/print/merge.go +++ /dev/null @@ -1,44 +0,0 @@ -package print - -import ( - "context" - - "github.com/thecodingmachine/gotenberg/internal/pkg/process" - "github.com/thecodingmachine/gotenberg/internal/pkg/xerror" - "github.com/thecodingmachine/gotenberg/internal/pkg/xexec" - "github.com/thecodingmachine/gotenberg/internal/pkg/xlog" -) - -type mergePrint struct { - logger xlog.Logger - fpaths []string -} - -// NewMergePrint returns a Print for -// merging PDF files. -func NewMergePrint(logger xlog.Logger, fpaths []string) Print { - return mergePrint{ - logger: logger, - fpaths: fpaths, - } -} - -func (p mergePrint) Print(ctx context.Context, dest string, proc process.Process) error { - const op string = "print.mergePrint.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 -} diff --git a/internal/pkg/print/office.go b/internal/pkg/print/office.go deleted file mode 100644 index 1ec02792..00000000 --- a/internal/pkg/print/office.go +++ /dev/null @@ -1,126 +0,0 @@ -package print - -import ( - "context" - "fmt" - "os" - "path/filepath" - - "github.com/thecodingmachine/gotenberg/internal/pkg/process" - "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 officePrint struct { - logger xlog.Logger - fpaths []string - opts OfficePrintOptions -} - -// OfficePrintOptions helps customizing the -// LibreOffice Print result. -type OfficePrintOptions struct { - Landscape bool -} - -// DefaultOfficePrinterOptions returns the default -// LibreOffice Print options. -func DefaultOfficePrinterOptions() OfficePrintOptions { - return OfficePrintOptions{ - Landscape: false, - } -} - -// NewOfficePrint returns a Print for -// converting Office documents to PDF. -func NewOfficePrint(logger xlog.Logger, fpaths []string, opts OfficePrintOptions) Print { - return officePrint{ - logger: logger, - fpaths: fpaths, - opts: opts, - } -} - -func (p officePrint) Print(ctx context.Context, dest string, proc process.Process) error { - const op string = "print.officePrint.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 := unoconv( - ctx, - p.logger, - proc, - p.opts, - 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 := NewMergePrint(p.logger, fpaths) - return merger.Print(ctx, dest, nil) - } - if err := resolver(); err != nil { - return xerror.New(op, err) - } - return nil -} - -func unoconv( - ctx context.Context, - logger xlog.Logger, - proc process.Process, - opts OfficePrintOptions, - fpath, dest string, -) error { - const op string = "print.unoconv" - resolver := func() error { - args := []string{ - "--server", - proc.Host(), - "--port", - fmt.Sprintf("%d", proc.Port()), - "--format", - "pdf", - } - if opts.Landscape { - args = append(args, "--printer", "PaperOrientation=landscape") - } - args = append(args, "--output", dest, fpath) - cmd, err := xexec.CommandContext( - ctx, - logger, - "unoconv", - args..., - ) - if err != nil { - return err - } - xexec.LogBeforeExecute(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 ( - _ = Print(new(officePrint)) -) diff --git a/internal/pkg/print/print.go b/internal/pkg/print/print.go deleted file mode 100644 index 4b641c8e..00000000 --- a/internal/pkg/print/print.go +++ /dev/null @@ -1,13 +0,0 @@ -package print - -import ( - "context" - - "github.com/thecodingmachine/gotenberg/internal/pkg/process" -) - -// Print is a type that can create a PDF file from a source. -// The source is defined in the underlying implementation. -type Print interface { - Print(ctx context.Context, dest string, proc process.Process) error -} diff --git a/internal/pkg/print/url.go b/internal/pkg/print/url.go deleted file mode 100644 index 9031e21d..00000000 --- a/internal/pkg/print/url.go +++ /dev/null @@ -1,15 +0,0 @@ -package print - -import ( - "github.com/thecodingmachine/gotenberg/internal/pkg/xlog" -) - -// NewURLPrint returns a Print for -// converting a URL to PDF. -func NewURLPrint(logger xlog.Logger, url string, opts ChromePrintOptions) Print { - return chromePrint{ - logger: logger, - url: url, - opts: opts, - } -} diff --git a/internal/pkg/printer/office.go b/internal/pkg/printer/office.go index 34ebdcb4..eb7e111a 100644 --- a/internal/pkg/printer/office.go +++ b/internal/pkg/printer/office.go @@ -5,7 +5,9 @@ import ( "fmt" "os" "path/filepath" + "syscall" + "github.com/phayes/freeport" "github.com/thecodingmachine/gotenberg/internal/pkg/conf" "github.com/thecodingmachine/gotenberg/internal/pkg/xcontext" "github.com/thecodingmachine/gotenberg/internal/pkg/xerror" @@ -66,10 +68,7 @@ func (p officePrinter) Print(destination string) error { } if len(fpaths) == 1 { p.logger.DebugOp(op, "only one PDF created, nothing to merge") - if err := os.Rename(fpaths[0], destination); err != nil { - return err - } - return nil + return os.Rename(fpaths[0], destination) } m := mergePrinter{ logger: p.logger, @@ -87,13 +86,18 @@ func (p officePrinter) Print(destination string) error { return nil } -// nolint: gochecknoglobals -var lockUnoconv = make(chan struct{}, 1) - func unoconv(ctx context.Context, logger xlog.Logger, fpath, destination string, opts OfficePrinterOptions) error { const op string = "printer.unoconv" resolver := func() error { + port, err := freeport.GetFreePort() + if err != nil { + return err + } args := []string{ + "--user-profile", + fmt.Sprintf("///tmp/%d", port), + "--port", + fmt.Sprintf("%d", port), "--format", "pdf", } @@ -101,35 +105,36 @@ func unoconv(ctx context.Context, logger xlog.Logger, fpath, destination string, args = append(args, "--printer", "PaperOrientation=landscape") } args = append(args, "--output", destination, fpath) - cmd, err := xexec.CommandContext( - ctx, + cmd, err := xexec.Command( logger, "unoconv", args..., ) + cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true} if err != nil { return err } xexec.LogBeforeExecute(logger, cmd) - return cmd.Run() - } - logger.DebugOp(op, "waiting lock to be acquired...") - select { - case lockUnoconv <- struct{}{}: - // lock acquired. - logger.DebugOp(op, "lock acquired") - if err := resolver(); err != nil { - <-lockUnoconv // we release the lock. - return xerror.New(op, err) + if err := cmd.Start(); err != nil { + return err + } + result := make(chan error, 1) + go func() { + result <- cmd.Wait() + }() + select { + case err := <-result: + syscall.Kill(-cmd.Process.Pid, syscall.SIGKILL) + return err + case <-ctx.Done(): + syscall.Kill(-cmd.Process.Pid, syscall.SIGKILL) + return ctx.Err() } - <-lockUnoconv // we release the lock. - return nil - case <-ctx.Done(): - // failed to acquire lock before - // deadline. - logger.DebugOp(op, "failed to acquire lock before context.Context deadline") - return xerror.New(op, ctx.Err()) } + if err := resolver(); err != nil { + return xerror.New(op, err) + } + return nil } // Compile-time checks to ensure type implements desired interfaces. diff --git a/internal/pkg/process/chrome.go b/internal/pkg/process/chrome.go deleted file mode 100644 index c93dbe51..00000000 --- a/internal/pkg/process/chrome.go +++ /dev/null @@ -1,106 +0,0 @@ -package process - -import ( - "context" - "fmt" - "time" - - "github.com/mafredri/cdp/devtool" - "github.com/thecodingmachine/gotenberg/internal/pkg/xlog" -) - -const ChromeKey Key = "chrome" - -type chromeProcess struct { - id string - host string - port int -} - -// NewChromeProcess returns a Google Chrome -// headless process. -func NewChromeProcess(id, host string, port int) Process { - return chromeProcess{ - id: id, - host: host, - port: port, - } -} - -func (p chromeProcess) ID() string { - return p.id -} - -func (p chromeProcess) Host() string { - return p.host -} - -func (p chromeProcess) Port() int { - return p.port -} - -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 = "process.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 - } -} - -// Compile-time checks to ensure type implements desired interfaces. -var ( - _ = Process(new(chromeProcess)) -) diff --git a/internal/pkg/process/doc.go b/internal/pkg/process/doc.go deleted file mode 100644 index f2ef9d28..00000000 --- a/internal/pkg/process/doc.go +++ /dev/null @@ -1 +0,0 @@ -package process diff --git a/internal/pkg/process/pm2.go b/internal/pkg/process/pm2.go deleted file mode 100644 index 8be78c26..00000000 --- a/internal/pkg/process/pm2.go +++ /dev/null @@ -1,324 +0,0 @@ -package process - -import ( - "encoding/json" - "fmt" - "os/exec" - "sync" - "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" - "github.com/thecodingmachine/gotenberg/internal/pkg/xtime" -) - -type jlistItem struct { - Name string `json:"name"` - PM2Env struct { - Status string `json:"status"` - RestartTime int64 `json:"restart_time"` - } `json:"pm2_env"` - Monit struct { - Memory int64 `json:"memory"` - CPU float64 `json:"cpu"` - } `json:"monit"` -} - -type jlist []jlistItem - -func (list jlist) toList() List { - var result List - for _, current := range list { - item := ListItem{ - Name: current.Name, - Status: current.PM2Env.Status, - Restart: current.PM2Env.RestartTime, - Memory: current.Monit.Memory, // TODO humanize? - CPU: current.Monit.CPU, - } - result = append(result, item) - } - return result -} - -func (list jlist) isOnline(p Process) bool { - const onlineStatus string = "online" - for _, item := range list { - if item.Name == p.ID() { - return item.PM2Env.Status == onlineStatus - } - } - return false -} - -func (list jlist) memory(p Process) (int64, error) { - const op string = "process.jlist.memory" - for _, item := range list { - if item.Name == p.ID() { - return item.Monit.Memory, nil - } - } - return 0, xerror.New( - op, - fmt.Errorf("'%s' does not exist in the list of PM2 processes", p.ID()), - ) -} - -type command string - -const ( - startCommand command = "start" - restartCommand command = "restart" - stopCommand command = "stop" - logsCommand command = "logs" - jlistCommand command = "jlist" -) - -const maximumRestartAttempts uint = 3 - -type pm2Manager struct { - logger xlog.Logger - config conf.Config - pool map[Key][]Process - list *jlist - listLock *sync.Mutex -} - -// NewPM2Manager returns a PM2 manager. -func NewPM2Manager(logger xlog.Logger, config conf.Config) Manager { - const op string = "process.NewPM2Manager" - m := &pm2Manager{ - logger: logger, - config: config, - pool: make(map[Key][]Process), - listLock: &sync.Mutex{}, - } - if !config.DisableGoogleChrome() { - processes := make([]Process, 2) - availablePort := 9222 - // TODO from config - for i := 0; i < 2; i++ { - proc := chromeProcess{ - host: "127.0.0.1", - port: availablePort, - } - proc.id = fmt.Sprintf("%s-%d", proc.binary(), proc.port) - processes[i] = proc - logger.DebugfOp(op, "added new process %v", proc) - availablePort++ - } - m.pool[ChromeKey] = processes - } - if !config.DisableUnoconv() { - processes := make([]Process, 2) - availablePort := 2002 - // TODO from config - for i := 0; i < 2; i++ { - proc := sofficeProcess{ - host: "127.0.0.1", - port: availablePort, - } - proc.id = fmt.Sprintf("%s-%d", proc.binary(), proc.port) - processes[i] = proc - logger.DebugfOp(op, "added new process %v", proc) - availablePort++ - } - m.pool[SofficeKey] = processes - } - // update the manager processes list - // only if there are processes. - if !config.DisableGoogleChrome() || !config.DisableUnoconv() { - go m.jlistTimer() - } - return m -} - -func (m *pm2Manager) jlistTimer() { - const op string = "process.pm2Manager.jlistTimer" - duration := xtime.Duration(10) - resolver := func() error { - m.listLock.Lock() - defer m.listLock.Unlock() - out, err := exec. - Command("pm2", string(jlistCommand)). - Output() - if err != nil { - return err - } - data := &jlist{} - if err := json.Unmarshal(out, data); err != nil { - return err - } - m.list = data - return nil - } - // update every x seconds the - // list from the manager. - for range time.Tick(duration) { - if err := resolver(); err != nil { - m.logger.ErrorOp(op, xerror.New(op, err)) - } - } -} - -func (m *pm2Manager) Start() error { - const op string = "process.pm2Manager.Start" - for _, processes := range m.pool { - for _, proc := range processes { - if err := m.start(proc); err != nil { - return xerror.New(op, err) - } - } - } - return nil -} - -func (m *pm2Manager) start(p Process) error { - const op string = "process.pm2Manager.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 *pm2Manager) List() List { - m.listLock.Lock() - defer m.listLock.Unlock() - return m.list.toList() -} - -func (m *pm2Manager) All() []Process { - var result []Process - for _, processes := range m.pool { - result = append(result, processes...) - } - return result -} - -func (m *pm2Manager) Processes(key Key) []Process { - return m.pool[key] -} - -func (m *pm2Manager) Restart(p Process) error { - const op string = "process.pm2Manager.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.ID()) - } - if err := resolver(); err != nil { - return xerror.New(op, err) - } - return nil -} - -func (m *pm2Manager) Stop(p Process) error { - const op string = "process.pm2Manager.Stop" - if err := m.run(stopCommand, p); err != nil { - return xerror.New(op, err) - } - return nil -} - -func (m *pm2Manager) IsViable(p Process) bool { - if !p.viabilityFunc()(m.logger) { - return false - } - m.listLock.Lock() - defer m.listLock.Unlock() - return m.list.isOnline(p) -} - -func (m *pm2Manager) Memory(p Process) (int64, error) { - const op string = "process.pm2Manager.Memory" - m.listLock.Lock() - defer m.listLock.Unlock() - memory, err := m.list.memory(p) - if err != nil { - return 0, xerror.New(op, err) - } - return memory, nil -} - -func (m *pm2Manager) warmup(p Process) { - const op string = "process.pm2Manager.warmup" - warmupTime := p.warmupTime() - m.logger.DebugfOp( - op, - "waiting '%v' for allowing '%s' to warmup", - warmupTime, - p.ID(), - ) - time.Sleep(warmupTime) -} - -func (m *pm2Manager) logs(p Process) error { - const op string = "process.pm2Manager.logs" - if m.config.LogLevel() == xlog.DebugLevel { - if err := m.run(logsCommand, p); err != nil { - return xerror.New(op, err) - } - } - return nil -} - -func (m *pm2Manager) run(pm2Cmd command, p Process) error { - const op string = "process.pm2Manager.run" - resolver := func() error { - args := []string{ - string(pm2Cmd), - p.binary(), - } - if pm2Cmd == startCommand { - args = append(args, fmt.Sprintf("--name=%s", p.ID())) - args = append(args, "--interpreter=none", "--") - args = append(args, p.args()...) - } - 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 ( - _ = Manager(new(pm2Manager)) -) diff --git a/internal/pkg/process/process.go b/internal/pkg/process/process.go deleted file mode 100644 index 898d53bc..00000000 --- a/internal/pkg/process/process.go +++ /dev/null @@ -1,40 +0,0 @@ -package process - -import ( - "time" - - "github.com/thecodingmachine/gotenberg/internal/pkg/xlog" -) - -type Process interface { - ID() string - Host() string - Port() int - binary() string - args() []string - warmupTime() time.Duration - viabilityFunc() func(logger xlog.Logger) bool -} - -type ListItem struct { - Name string `json:"name"` - Status string `json:"status"` - Restart int64 `json:"restart"` - Memory int64 `json:"memory"` - CPU float64 `json:"cpu"` -} - -type List []ListItem - -type Key string - -type Manager interface { - Start() error - List() List - All() []Process - Processes(key Key) []Process - Restart(proc Process) error - Stop(proc Process) error - IsViable(proc Process) bool - Memory(proc Process) (int64, error) -} diff --git a/internal/pkg/process/soffice.go b/internal/pkg/process/soffice.go deleted file mode 100644 index 4f7a3be9..00000000 --- a/internal/pkg/process/soffice.go +++ /dev/null @@ -1,74 +0,0 @@ -package process - -import ( - "fmt" - "time" - - "github.com/thecodingmachine/gotenberg/internal/pkg/xlog" -) - -const SofficeKey Key = "soffice" - -type sofficeProcess struct { - id string - host string - port int -} - -// NewSofficeProcess returns a LibreOffice -// headless process. -func NewSofficeProcess(id, host string, port int) Process { - return sofficeProcess{ - id: id, - host: host, - port: port, - } -} - -func (p sofficeProcess) ID() string { - return p.id -} - -func (p sofficeProcess) Host() string { - return p.host -} - -func (p sofficeProcess) Port() int { - return p.port -} - -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 = "process.sofficeProcess.viabilityFunc" - return func(logger xlog.Logger) bool { - // TODO find a way to check. - return true - } -} - -// Compile-time checks to ensure type implements desired interfaces. -var ( - _ = Process(new(sofficeProcess)) -) diff --git a/internal/pkg/xassert/xassert.go b/internal/pkg/xassert/xassert.go index 574008ff..8ad5a55a 100644 --- a/internal/pkg/xassert/xassert.go +++ b/internal/pkg/xassert/xassert.go @@ -5,6 +5,7 @@ import ( "os" "strconv" + "github.com/dustin/go-humanize" "github.com/thecodingmachine/gotenberg/internal/pkg/xerror" ) @@ -183,3 +184,51 @@ func BoolFromEnv(envVar string, defaultValue bool) (bool, error) { } return result, nil } + +/* +Bytes tries to convert a string to a int64. + +If string is empty or conversion fails, returns the +default value. + +The key is used to identify the value. +*/ +func Bytes(key, value string, defaultValue int64, rules ...RuleInt64) (int64, error) { + const op string = "xassert.Bytes" + result := defaultValue + if value != "" { + parsedValue, err := humanize.ParseBigBytes(value) + if err != nil { + return defaultValue, xerror.Invalid( + op, + fmt.Sprintf("'%s' is not a correct bytes representation, got '%s'", key, value), + err, + ) + } + result = parsedValue.Int64() + } + for _, rule := range rules { + rule.with(key, result) + if err := rule.validate(); err != nil { + return defaultValue, xerror.New(op, err) + } + } + return result, nil +} + +/* +BytesFromEnv returns the int64 representation of the +value of given environment variable. + +If not found, empty or conversion fails, returns the +default value. +*/ +func BytesFromEnv(envVar string, defaultValue int64, rules ...RuleInt64) (int64, error) { + const op string = "xassert.BytesFromEnv" + value := os.Getenv(envVar) + result, err := Bytes(envVar, value, defaultValue) + if err != nil { + return result, xerror.New(op, err) + } + return result, nil +} diff --git a/internal/pkg/xerrgroup/doc.go b/internal/pkg/xerrgroup/doc.go deleted file mode 100644 index 2134c8cb..00000000 --- a/internal/pkg/xerrgroup/doc.go +++ /dev/null @@ -1,6 +0,0 @@ -/* -Package xerrgroup helps running -many functions simultaneously and wait until -execution has completed or an error is encountered. -*/ -package xerrgroup diff --git a/internal/pkg/xerrgroup/xerrgroup.go b/internal/pkg/xerrgroup/xerrgroup.go deleted file mode 100644 index fb4d0e24..00000000 --- a/internal/pkg/xerrgroup/xerrgroup.go +++ /dev/null @@ -1,20 +0,0 @@ -package xerrgroup - -import ( - "github.com/thecodingmachine/gotenberg/internal/pkg/xerror" - "golang.org/x/sync/errgroup" -) - -// Run runs all functions simultaneously and wait until -// execution has completed or an error is encountered. -func Run(fn ...func() error) error { - const op string = "xerrgroup.Run" - eg := errgroup.Group{} - for _, f := range fn { - eg.Go(f) - } - if err := eg.Wait(); err != nil { - return xerror.New(op, err) - } - return nil -} diff --git a/test/cmd/pm2/pm2.go b/test/cmd/pm2/pm2.go deleted file mode 100644 index b2454d17..00000000 --- a/test/cmd/pm2/pm2.go +++ /dev/null @@ -1,18 +0,0 @@ -package main - -import ( - "github.com/thecodingmachine/gotenberg/internal/pkg/pm2" - "github.com/thecodingmachine/gotenberg/test" -) - -func main() { - logger := test.DebugLogger() - process := pm2.NewChromeProcess(logger) - if err := process.Start(); err != nil { - panic(err) - } - process = pm2.NewUnoconvProcess(logger) - if err := process.Start(); err != nil { - panic(err) - } -}