rolling back from previous implementation, now using master version of unoconv

This commit is contained in:
Julien Neuhart
2019-09-26 18:07:43 +02:00
parent 88f74d5e9c
commit 24e6a86bf5
47 changed files with 346 additions and 3034 deletions

View File

@@ -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

View File

@@ -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.

View File

@@ -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
View File

@@ -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
View File

@@ -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=

View File

@@ -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)

View File

@@ -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)

View File

@@ -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 {

View File

@@ -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()

View File

@@ -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 {

View File

@@ -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(),

View File

@@ -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.

View File

@@ -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())

View File

@@ -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)

View 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)
}

View File

@@ -0,0 +1 @@
package chrome

View File

@@ -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

View File

@@ -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))
)

View File

@@ -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

View File

@@ -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))
)

View File

@@ -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
}

View File

@@ -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))
)

View File

@@ -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))
)

View File

@@ -1 +0,0 @@
package prinery

View File

@@ -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
}

View File

@@ -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))
)

View File

@@ -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))
)

View File

@@ -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
}

View File

@@ -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))
)

View File

@@ -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
}

View File

@@ -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))
)

View File

@@ -1 +0,0 @@
package print

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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))
)

View File

@@ -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
}

View File

@@ -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,
}
}

View File

@@ -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.

View File

@@ -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))
)

View File

@@ -1 +0,0 @@
package process

View File

@@ -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))
)

View File

@@ -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)
}

View File

@@ -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))
)

View File

@@ -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
}

View File

@@ -1,6 +0,0 @@
/*
Package xerrgroup helps running
many functions simultaneously and wait until
execution has completed or an error is encountered.
*/
package xerrgroup

View File

@@ -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
}

View File

@@ -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)
}
}