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

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