diff --git a/action.go b/action.go
index 533e4ca..e2c4db5 100644
--- a/action.go
+++ b/action.go
@@ -110,7 +110,7 @@ func FileOpen(filePath string) string {
// FileClose .
func FileClose() string {
- return "file-close:"
+ return "file-close"
}
// SelectAll .
@@ -144,15 +144,15 @@ func SelectClear() string {
}
// InvertOption define option when invert selection
-type InvertOption string
+type InvertOption string
// invert selection option
const (
- InvertOptionAll InvertOption = "all"
- InvertOptionLayers = "layers"
- InvertOptionNoLayers = "no-layers"
- InvertOptionGroup = "group"
- InvertOptionNoGroup = "no-group"
+ InvertOptionAll InvertOption = "all"
+ InvertOptionLayers = "layers"
+ InvertOptionNoLayers = "no-layers"
+ InvertOptionGroup = "group"
+ InvertOptionNoGroup = "no-group"
)
// SelectInvert .
@@ -164,3 +164,8 @@ func SelectInvert(option InvertOption) string {
func SelectList() string {
return "select-list"
}
+
+// Version print inksscape version and return
+func Version() string {
+ return "inkscape-version"
+}
diff --git a/circle.svg b/circle.svg
new file mode 100644
index 0000000..2ae5da0
--- /dev/null
+++ b/circle.svg
@@ -0,0 +1,3 @@
+
\ No newline at end of file
diff --git a/cmd/svg2pdf/main.go b/cmd/svg2pdf/main.go
index 395a4c5..9210db0 100644
--- a/cmd/svg2pdf/main.go
+++ b/cmd/svg2pdf/main.go
@@ -11,6 +11,7 @@ import (
var (
svgInput string
pdfOutput string
+ verbose bool
)
func handleErr(err error) {
@@ -23,6 +24,7 @@ func handleErr(err error) {
func main() {
flag.StringVar(&svgInput, "input", "", "svg input")
flag.StringVar(&pdfOutput, "output", "result.pdf", "pdf output")
+ flag.BoolVar(&verbose, "verbose", false, "verbose output")
flag.Parse()
if svgInput == "" {
@@ -30,7 +32,8 @@ func main() {
os.Exit(1)
}
- proxy := inkscape.NewProxy(inkscape.Verbose(true))
+ proxy := inkscape.NewProxy(inkscape.Verbose(verbose))
+
err := proxy.Run()
handleErr(err)
defer proxy.Close()
diff --git a/cmd/svg2pdf/maincon.svg b/cmd/svg2pdf/maincon.svg
new file mode 100644
index 0000000..609057a
--- /dev/null
+++ b/cmd/svg2pdf/maincon.svg
@@ -0,0 +1,790 @@
+
+
diff --git a/cmd/svg2pdf/xxx.svg b/cmd/svg2pdf/xxx.svg
new file mode 100644
index 0000000..0a35231
--- /dev/null
+++ b/cmd/svg2pdf/xxx.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/option.go b/option.go
index adfaf1d..ebdd108 100644
--- a/option.go
+++ b/option.go
@@ -13,6 +13,9 @@ type Options struct {
// maximum retry attempt
maxRetry int
+ // maximum command queue size
+ commandQueueLength int
+
// set verbosity
verbose bool
}
@@ -33,6 +36,13 @@ func MaxRetry(retry int) Option {
}
}
+// CommandQueueLength override maximum command queue size
+func CommandQueueLength(length int) Option {
+ return func(o *Options) {
+ o.commandQueueLength = length
+ }
+}
+
// Verbose override log verbosity
// useful for debugging
func Verbose(verbose bool) Option {
diff --git a/proxy.go b/proxy.go
index 79e7013..5c9a598 100644
--- a/proxy.go
+++ b/proxy.go
@@ -5,12 +5,9 @@ import (
"context"
"errors"
"fmt"
- "io"
- "io/ioutil"
"log"
"os/exec"
"strings"
- "sync"
"time"
"github.com/galihrivanto/runner"
@@ -19,6 +16,7 @@ import (
const (
defaultCmdName = "inkscape"
shellModeBanner = "Inkscape interactive shell mode"
+ quitCommand = "quit"
)
// defines common errors in library
@@ -37,8 +35,7 @@ func debug(v ...interface{}) {
return
}
- log.Print("proxy:")
- log.Println(v...)
+ log.Print(append([]interface{}{"proxy:"}, v...)...)
}
type chanWriter struct {
@@ -68,11 +65,11 @@ type Proxy struct {
ctx context.Context
cancel context.CancelFunc
- cmd *exec.Cmd
+ // limiter to allow one command processed at time
+ requestLimiter chan struct{}
- // input
- lock sync.RWMutex
- stdin io.WriteCloser
+ // queue of request
+ requestQueue chan []byte
// output
stdout chan []byte
@@ -90,31 +87,83 @@ func (p *Proxy) runBackground(ctx context.Context, commandPath string, vars ...s
}
cmd := exec.CommandContext(ctx, commandPath, args...)
- cmd.Stdout = &chanWriter{p.stdout}
- cmd.Stderr = &chanWriter{p.stderr}
+ // pipe stderr
+ stderrC := make(chan []byte)
+ defer close(stderrC)
+
+ cmd.Stderr = &chanWriter{out: stderrC}
+
+ // pipe stdout
+ stdoutC := make(chan []byte)
+ defer close(stdoutC)
+
+ cmd.Stdout = &chanWriter{out: stdoutC}
+
+ // pipe stdin
stdin, err := cmd.StdinPipe()
if err != nil {
return err
}
+ defer stdin.Close()
- p.lock.Lock()
- p.stdin = stdin
- p.lock.Unlock()
-
- defer func() {
- // only close channel when command closes
- close(p.stdout)
- close(p.stderr)
- }()
-
+ // start command and wait it close
+ debug("run in background")
if err := cmd.Start(); err != nil {
return err
}
- debug("run in background")
+ // make first command available
+ // after received prompt
+wait:
+ for {
+ bytesOut := <-stdoutC
+ bytesOut = bytes.TrimSpace(bytesOut)
+ parts := bytes.Split(bytesOut, []byte("\n"))
+ for _, part := range parts {
+ if isPrompt(part) {
+ break wait
+ }
+ }
+ }
- return cmd.Wait()
+ select {
+ case p.requestLimiter <- struct{}{}:
+ default:
+ // discard
+ }
+
+ // handle command and output
+ for {
+ select {
+ case <-ctx.Done():
+ return cmd.Wait()
+
+ case command := <-p.requestQueue:
+ debug("write command ", string(command))
+ if _, err := stdin.Write(command); err != nil {
+ p.stderr <- []byte(err.Error())
+ }
+
+ case bytesErr := <-stderrC:
+ if len(bytesErr) == 0 {
+ break
+ }
+
+ if bytes.Contains(bytesErr, []byte("WARNING")) {
+ break
+ }
+
+ p.stderr <- bytes.TrimSpace(bytesErr)
+
+ case bytesOut := <-stdoutC:
+ if len(bytesOut) == 0 {
+ break
+ }
+
+ p.stdout <- bytes.TrimSpace(bytesOut)
+ }
+ }
}
// Run start inkscape proxy
@@ -138,95 +187,86 @@ func (p *Proxy) Run(args ...string) error {
)
}()
+ // print inkscape version
+ res, _ := p.RawCommands(Version())
+ fmt.Println(string(res))
+
return nil
}
// Close satisfy io.Closer interface
func (p *Proxy) Close() error {
+ // send quit command
+ _, err := p.sendCommand([]byte(quitCommand), false)
+
p.cancel()
- p.stdin.Close()
+ close(p.requestLimiter)
+ close(p.requestQueue)
+ close(p.stderr)
+ close(p.stdout)
- return nil
+ return err
}
-// waitReady wait until background process
-// ready accepting command
-func (p *Proxy) waitReady(timeout time.Duration) error {
- ready := make(chan struct{})
- go func() {
- for {
- // query stdin availability every second
- p.lock.RLock()
- if p.stdin != nil {
- p.lock.RUnlock()
- close(ready)
- return
- }
- p.lock.RUnlock()
+func (p *Proxy) sendCommand(b []byte, waitPrompt ...bool) ([]byte, error) {
+ wait := true
+ if len(waitPrompt) > 0 {
+ wait = waitPrompt[0]
+ }
- <-time.After(time.Second)
- }
+ // wait available
+ debug("wait prompt available")
+ <-p.requestLimiter
+ defer func() {
+ // make it available again
+ p.requestLimiter <- struct{}{}
}()
- select {
- case <-time.After(timeout):
- return ErrCommandNotReady
- case <-ready:
- return nil
- }
-}
-
-func (p *Proxy) sendCommand(b []byte) ([]byte, error) {
- debug("wait ready")
- err := p.waitReady(30 * time.Second)
- if err != nil {
- return nil, err
- }
+ debug("send command to stdin ", string(b))
- debug("send command to stdin", string(b))
+ // drain old err and out
+ drain(p.stderr)
+ drain(p.stdout)
// append new line
if !bytes.HasSuffix(b, []byte{'\n'}) {
b = append(b, '\n')
}
- _, err = p.stdin.Write(b)
- if err != nil {
- return nil, err
- }
+ p.requestQueue <- b
- // wait output
- var output []byte
+ var (
+ output []byte
+ err error
+ )
+
+ // immediate return
+ if !wait {
+ <-time.After(time.Second)
+ return []byte{}, nil
+ }
waitLoop:
for {
select {
case bytesErr := <-p.stderr:
- // for now, we can only check error message pattern
- // ignore WARNING
- if bytes.Contains(output, []byte("WARNING")) {
- debug(string(bytesErr))
- break
- }
-
+ debug(string(bytesErr))
err = fmt.Errorf("%s", string(bytesErr))
break waitLoop
- case output = <-p.stdout:
- if len(output) == 0 {
- break
- }
-
- // check if shell mode banner
- if bytes.Contains(output, []byte(shellModeBanner)) {
- debug(string(output))
- break
+ case bytesOut := <-p.stdout:
+ debug(string(bytesOut))
+ parts := bytes.Split(bytesOut, []byte("\n"))
+ for _, part := range parts {
+ if isPrompt(part) {
+ break waitLoop
+ }
}
- break waitLoop
+ output = append(output, bytesOut...)
}
}
- return output, nil
+ return output, err
}
// RawCommands send inkscape shell commands
@@ -237,7 +277,9 @@ func (p *Proxy) RawCommands(args ...string) ([]byte, error) {
// construct command buffer
buffer.WriteString(strings.Join(args, ";"))
- return p.sendCommand(buffer.Bytes())
+ res, err := p.sendCommand(buffer.Bytes())
+
+ return res, err
}
// Svg2Pdf convert svg input file to output pdf file
@@ -260,26 +302,39 @@ func (p *Proxy) Svg2Pdf(svgIn, pdfOut string) error {
// NewProxy create new inkscape proxy instance
func NewProxy(opts ...Option) *Proxy {
// default value
- init := Options{
+ options := Options{
commandName: defaultCmdName,
maxRetry: 5,
verbose: false,
}
// merge options
- options := mergeOptions(init, opts...)
+ options = mergeOptions(options, opts...)
// check verbosity
- if !options.verbose {
- log.SetOutput(ioutil.Discard)
- }
-
- stdout := make(chan []byte)
- stderr := make(chan []byte)
+ verbose = options.verbose
return &Proxy{
options: options,
- stdout: stdout,
- stderr: stderr,
+ stdout: make(chan []byte, 100),
+ stderr: make(chan []byte, 100),
+
+ // limit request to one request at time
+ requestLimiter: make(chan struct{}, 1),
+ requestQueue: make(chan []byte, 100),
+ }
+}
+
+func isPrompt(data []byte) bool {
+ return bytes.Equal(data, []byte(">"))
+}
+
+func drain(c chan []byte) {
+ for {
+ select {
+ case <-c:
+ default:
+ return
+ }
}
}
diff --git a/proxy_test.go b/proxy_test.go
new file mode 100644
index 0000000..325d879
--- /dev/null
+++ b/proxy_test.go
@@ -0,0 +1,29 @@
+package inkscape
+
+import (
+ "fmt"
+ "testing"
+)
+
+func TestConcurrent(t *testing.T) {
+ tempFiles := make([]string, 0)
+ // defer func() {
+ // for _, t := range tempFiles {
+ // os.Remove(t)
+ // }
+ // }()
+
+ proxy := NewProxy(Verbose(true))
+ proxy.Run()
+
+ for i := 0; i < 10; i++ {
+ temp := fmt.Sprintf("%d.pdf", i)
+ tempFiles = append(tempFiles, temp)
+
+ go func() {
+ if err := proxy.Svg2Pdf("circle.svg", temp); err != nil {
+ t.Error(err)
+ }
+ }()
+ }
+}