mirror of
https://github.com/gotenberg/gotenberg.git
synced 2026-08-08 00:22:14 +01:00
rolling back from previous implementation, now using master version of unoconv
This commit is contained in:
@@ -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
|
||||
@@ -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.
|
||||
|
||||
@@ -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())
|
||||
|
||||
2
go.mod
2
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
|
||||
|
||||
4
go.sum
4
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=
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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)
|
||||
|
||||
143
internal/pkg/chrome/chrome.go
Normal file
143
internal/pkg/chrome/chrome.go
Normal file
@@ -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)
|
||||
}
|
||||
1
internal/pkg/chrome/doc.go
Normal file
1
internal/pkg/chrome/doc.go
Normal file
@@ -0,0 +1 @@
|
||||
package chrome
|
||||
@@ -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
|
||||
|
||||
@@ -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))
|
||||
)
|
||||
@@ -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
|
||||
@@ -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))
|
||||
)
|
||||
@@ -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
|
||||
}
|
||||
@@ -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))
|
||||
)
|
||||
@@ -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))
|
||||
)
|
||||
@@ -1 +0,0 @@
|
||||
package prinery
|
||||
@@ -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
|
||||
}
|
||||
@@ -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))
|
||||
)
|
||||
@@ -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))
|
||||
)
|
||||
@@ -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 = "<html><head></head><body></body></html>"
|
||||
return ChromePrintOptions{
|
||||
WaitDelay: 0.0,
|
||||
HeaderHTML: defaultHeaderFooterHTML,
|
||||
FooterHTML: defaultHeaderFooterHTML,
|
||||
PaperWidth: 8.27,
|
||||
PaperHeight: 11.7,
|
||||
MarginTop: 1.0,
|
||||
MarginBottom: 1.0,
|
||||
MarginLeft: 1.0,
|
||||
MarginRight: 1.0,
|
||||
Landscape: false,
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
@@ -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))
|
||||
)
|
||||
@@ -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
|
||||
}
|
||||
@@ -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 = "<html><head></head><body></body></html>"
|
||||
return ChromePrintOptions{
|
||||
WaitDelay: 0.0,
|
||||
HeaderHTML: defaultHeaderFooterHTML,
|
||||
FooterHTML: defaultHeaderFooterHTML,
|
||||
PaperWidth: 8.27,
|
||||
PaperHeight: 11.7,
|
||||
MarginTop: 1.0,
|
||||
MarginBottom: 1.0,
|
||||
MarginLeft: 1.0,
|
||||
MarginRight: 1.0,
|
||||
Landscape: false,
|
||||
}
|
||||
}
|
||||
|
||||
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))
|
||||
)
|
||||
@@ -1 +0,0 @@
|
||||
package print
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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))
|
||||
)
|
||||
@@ -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
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
@@ -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))
|
||||
)
|
||||
@@ -1 +0,0 @@
|
||||
package process
|
||||
@@ -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))
|
||||
)
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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))
|
||||
)
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
/*
|
||||
Package xerrgroup helps running
|
||||
many functions simultaneously and wait until
|
||||
execution has completed or an error is encountered.
|
||||
*/
|
||||
package xerrgroup
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user