cache: lock the repo with a pid file; automatic cleaning

Michael Muré created

Change summary

cache/bug_cache.go  | 137 ++++++++++++++++++
cache/cache.go      | 342 +++++++++++++---------------------------------
cache/repo_cache.go | 144 +++++++++++++++++++
commands/root.go    |   9 +
commands/webui.go   |  58 +++++++
graphql/handler.go  |  20 ++
util/process.go     |  23 +++
7 files changed, 478 insertions(+), 255 deletions(-)

Detailed changes

cache/bug_cache.go 🔗

@@ -0,0 +1,137 @@
+package cache
+
+import (
+	"github.com/MichaelMure/git-bug/bug"
+	"github.com/MichaelMure/git-bug/bug/operations"
+	"github.com/MichaelMure/git-bug/repository"
+	"github.com/MichaelMure/git-bug/util"
+)
+
+type BugCacher interface {
+	Snapshot() *bug.Snapshot
+	ClearSnapshot()
+
+	// Mutations
+	AddComment(message string) error
+	AddCommentWithFiles(message string, files []util.Hash) error
+	ChangeLabels(added []string, removed []string) error
+	Open() error
+	Close() error
+	SetTitle(title string) error
+
+	Commit() error
+	CommitAsNeeded() error
+}
+
+type BugCache struct {
+	repo repository.Repo
+	bug  *bug.Bug
+	snap *bug.Snapshot
+}
+
+func NewBugCache(repo repository.Repo, b *bug.Bug) BugCacher {
+	return &BugCache{
+		repo: repo,
+		bug:  b,
+	}
+}
+
+func (c *BugCache) Snapshot() *bug.Snapshot {
+	if c.snap == nil {
+		snap := c.bug.Compile()
+		c.snap = &snap
+	}
+	return c.snap
+}
+
+func (c *BugCache) ClearSnapshot() {
+	c.snap = nil
+}
+
+func (c *BugCache) AddComment(message string) error {
+	return c.AddCommentWithFiles(message, nil)
+}
+
+func (c *BugCache) AddCommentWithFiles(message string, files []util.Hash) error {
+	author, err := bug.GetUser(c.repo)
+	if err != nil {
+		return err
+	}
+
+	operations.CommentWithFiles(c.bug, author, message, files)
+
+	// TODO: perf --> the snapshot could simply be updated with the new op
+	c.ClearSnapshot()
+
+	return nil
+}
+
+func (c *BugCache) ChangeLabels(added []string, removed []string) error {
+	author, err := bug.GetUser(c.repo)
+	if err != nil {
+		return err
+	}
+
+	err = operations.ChangeLabels(nil, c.bug, author, added, removed)
+	if err != nil {
+		return err
+	}
+
+	// TODO: perf --> the snapshot could simply be updated with the new op
+	c.ClearSnapshot()
+
+	return nil
+}
+
+func (c *BugCache) Open() error {
+	author, err := bug.GetUser(c.repo)
+	if err != nil {
+		return err
+	}
+
+	operations.Open(c.bug, author)
+
+	// TODO: perf --> the snapshot could simply be updated with the new op
+	c.ClearSnapshot()
+
+	return nil
+}
+
+func (c *BugCache) Close() error {
+	author, err := bug.GetUser(c.repo)
+	if err != nil {
+		return err
+	}
+
+	operations.Close(c.bug, author)
+
+	// TODO: perf --> the snapshot could simply be updated with the new op
+	c.ClearSnapshot()
+
+	return nil
+}
+
+func (c *BugCache) SetTitle(title string) error {
+	author, err := bug.GetUser(c.repo)
+	if err != nil {
+		return err
+	}
+
+	operations.SetTitle(c.bug, author, title)
+
+	// TODO: perf --> the snapshot could simply be updated with the new op
+	c.ClearSnapshot()
+
+	return nil
+}
+
+func (c *BugCache) Commit() error {
+	return c.bug.Commit(c.repo)
+}
+
+func (c *BugCache) CommitAsNeeded() error {
+	if c.bug.HasPendingOp() {
+		return c.bug.Commit(c.repo)
+	}
+	return nil
+}

cache/cache.go 🔗

@@ -3,56 +3,32 @@ package cache
 import (
 	"fmt"
 	"io"
-	"strings"
+	"io/ioutil"
+	"os"
+	"path"
+	"strconv"
 
-	"github.com/MichaelMure/git-bug/bug"
-	"github.com/MichaelMure/git-bug/bug/operations"
 	"github.com/MichaelMure/git-bug/repository"
 	"github.com/MichaelMure/git-bug/util"
 )
 
+const lockfile = "lock"
+
 type Cacher interface {
-	RegisterRepository(ref string, repo repository.Repo)
-	RegisterDefaultRepository(repo repository.Repo)
+	// RegisterRepository register a named repository. Use this for multi-repo setup
+	RegisterRepository(ref string, repo repository.Repo) error
+	// RegisterDefaultRepository register a unnamed repository. Use this for mono-repo setup
+	RegisterDefaultRepository(repo repository.Repo) error
 
+	// ResolveRepo retrieve a repository by name
 	ResolveRepo(ref string) (RepoCacher, error)
+	// DefaultRepo retrieve the default repository
 	DefaultRepo() (RepoCacher, error)
-}
-
-type RepoCacher interface {
-	Repository() repository.Repo
-	ResolveBug(id string) (BugCacher, error)
-	ResolveBugPrefix(prefix string) (BugCacher, error)
-	AllBugIds() ([]string, error)
-	ClearAllBugs()
-
-	// Mutations
-	NewBug(title string, message string) (BugCacher, error)
-	NewBugWithFiles(title string, message string, files []util.Hash) (BugCacher, error)
-	Fetch(remote string) (string, error)
-	MergeAll(remote string) <-chan bug.MergeResult
-	Pull(remote string, out io.Writer) error
-	Push(remote string) (string, error)
-}
-
-type BugCacher interface {
-	Snapshot() *bug.Snapshot
-	ClearSnapshot()
 
-	// Mutations
-	AddComment(message string) error
-	AddCommentWithFiles(message string, files []util.Hash) error
-	ChangeLabels(added []string, removed []string) error
-	Open() error
+	// Close will do anything that is needed to close the cache properly
 	Close() error
-	SetTitle(title string) error
-
-	Commit() error
-	CommitAsNeeded() error
 }
 
-// Cacher ------------------------
-
 type RootCache struct {
 	repos map[string]RepoCacher
 }
@@ -63,263 +39,139 @@ func NewCache() RootCache {
 	}
 }
 
-func (c *RootCache) RegisterRepository(ref string, repo repository.Repo) {
-	c.repos[ref] = NewRepoCache(repo)
-}
-
-func (c *RootCache) RegisterDefaultRepository(repo repository.Repo) {
-	c.repos[""] = NewRepoCache(repo)
-}
-
-func (c *RootCache) DefaultRepo() (RepoCacher, error) {
-	if len(c.repos) != 1 {
-		return nil, fmt.Errorf("repository is not unique")
-	}
-
-	for _, r := range c.repos {
-		return r, nil
-	}
-
-	panic("unreachable")
-}
-
-func (c *RootCache) ResolveRepo(ref string) (RepoCacher, error) {
-	r, ok := c.repos[ref]
-	if !ok {
-		return nil, fmt.Errorf("unknown repo")
-	}
-	return r, nil
-}
-
-// Repo ------------------------
-
-type RepoCache struct {
-	repo repository.Repo
-	bugs map[string]BugCacher
-}
-
-func NewRepoCache(r repository.Repo) RepoCacher {
-	return &RepoCache{
-		repo: r,
-		bugs: make(map[string]BugCacher),
-	}
-}
-
-func (c *RepoCache) Repository() repository.Repo {
-	return c.repo
-}
-
-func (c *RepoCache) ResolveBug(id string) (BugCacher, error) {
-	cached, ok := c.bugs[id]
-	if ok {
-		return cached, nil
-	}
-
-	b, err := bug.ReadLocalBug(c.repo, id)
+// RegisterRepository register a named repository. Use this for multi-repo setup
+func (c *RootCache) RegisterRepository(ref string, repo repository.Repo) error {
+	err := c.lockRepository(repo)
 	if err != nil {
-		return nil, err
+		return err
 	}
 
-	cached = NewBugCache(c.repo, b)
-	c.bugs[id] = cached
-
-	return cached, nil
+	c.repos[ref] = NewRepoCache(repo)
+	return nil
 }
 
-func (c *RepoCache) ResolveBugPrefix(prefix string) (BugCacher, error) {
-	// preallocate but empty
-	matching := make([]string, 0, 5)
-
-	for id := range c.bugs {
-		if strings.HasPrefix(id, prefix) {
-			matching = append(matching, id)
-		}
-	}
-
-	// TODO: should check matching bug in the repo as well
-
-	if len(matching) > 1 {
-		return nil, fmt.Errorf("Multiple matching bug found:\n%s", strings.Join(matching, "\n"))
-	}
-
-	if len(matching) == 1 {
-		b := c.bugs[matching[0]]
-		return b, nil
-	}
-
-	b, err := bug.FindLocalBug(c.repo, prefix)
-
+// RegisterDefaultRepository register a unnamed repository. Use this for mono-repo setup
+func (c *RootCache) RegisterDefaultRepository(repo repository.Repo) error {
+	err := c.lockRepository(repo)
 	if err != nil {
-		return nil, err
+		return err
 	}
 
-	cached := NewBugCache(c.repo, b)
-	c.bugs[b.Id()] = cached
-
-	return cached, nil
-}
-
-func (c *RepoCache) AllBugIds() ([]string, error) {
-	return bug.ListLocalIds(c.repo)
-}
-
-func (c *RepoCache) ClearAllBugs() {
-	c.bugs = make(map[string]BugCacher)
+	c.repos[""] = NewRepoCache(repo)
+	return nil
 }
 
-func (c *RepoCache) NewBug(title string, message string) (BugCacher, error) {
-	return c.NewBugWithFiles(title, message, nil)
-}
+func (c *RootCache) lockRepository(repo repository.Repo) error {
+	lockPath := repoLockFilePath(repo)
 
-func (c *RepoCache) NewBugWithFiles(title string, message string, files []util.Hash) (BugCacher, error) {
-	author, err := bug.GetUser(c.repo)
+	err := RepoIsAvailable(repo)
 	if err != nil {
-		return nil, err
+		return err
 	}
 
-	b, err := operations.CreateWithFiles(author, title, message, files)
+	f, err := os.Create(lockPath)
 	if err != nil {
-		return nil, err
+		return err
 	}
 
-	err = b.Commit(c.repo)
+	pid := fmt.Sprintf("%d", os.Getpid())
+	_, err = f.WriteString(pid)
 	if err != nil {
-		return nil, err
+		return err
 	}
 
-	cached := NewBugCache(c.repo, b)
-	c.bugs[b.Id()] = cached
-
-	return cached, nil
-}
-
-func (c *RepoCache) Fetch(remote string) (string, error) {
-	return bug.Fetch(c.repo, remote)
-}
-
-func (c *RepoCache) MergeAll(remote string) <-chan bug.MergeResult {
-	return bug.MergeAll(c.repo, remote)
-}
-
-func (c *RepoCache) Pull(remote string, out io.Writer) error {
-	return bug.Pull(c.repo, out, remote)
+	return f.Close()
 }
 
-func (c *RepoCache) Push(remote string) (string, error) {
-	return bug.Push(c.repo, remote)
-}
-
-// Bug ------------------------
-
-type BugCache struct {
-	repo repository.Repo
-	bug  *bug.Bug
-	snap *bug.Snapshot
-}
-
-func NewBugCache(repo repository.Repo, b *bug.Bug) BugCacher {
-	return &BugCache{
-		repo: repo,
-		bug:  b,
+// ResolveRepo retrieve a repository by name
+func (c *RootCache) DefaultRepo() (RepoCacher, error) {
+	if len(c.repos) != 1 {
+		return nil, fmt.Errorf("repository is not unique")
 	}
-}
 
-func (c *BugCache) Snapshot() *bug.Snapshot {
-	if c.snap == nil {
-		snap := c.bug.Compile()
-		c.snap = &snap
+	for _, r := range c.repos {
+		return r, nil
 	}
-	return c.snap
-}
 
-func (c *BugCache) ClearSnapshot() {
-	c.snap = nil
-}
-
-func (c *BugCache) AddComment(message string) error {
-	return c.AddCommentWithFiles(message, nil)
+	panic("unreachable")
 }
 
-func (c *BugCache) AddCommentWithFiles(message string, files []util.Hash) error {
-	author, err := bug.GetUser(c.repo)
-	if err != nil {
-		return err
+// DefaultRepo retrieve the default repository
+func (c *RootCache) ResolveRepo(ref string) (RepoCacher, error) {
+	r, ok := c.repos[ref]
+	if !ok {
+		return nil, fmt.Errorf("unknown repo")
 	}
-
-	operations.CommentWithFiles(c.bug, author, message, files)
-
-	// TODO: perf --> the snapshot could simply be updated with the new op
-	c.ClearSnapshot()
-
-	return nil
+	return r, nil
 }
 
-func (c *BugCache) ChangeLabels(added []string, removed []string) error {
-	author, err := bug.GetUser(c.repo)
-	if err != nil {
-		return err
-	}
-
-	err = operations.ChangeLabels(nil, c.bug, author, added, removed)
-	if err != nil {
-		return err
+func (c *RootCache) Close() error {
+	for _, cachedRepo := range c.repos {
+		lockPath := repoLockFilePath(cachedRepo.Repository())
+		err := os.Remove(lockPath)
+		if err != nil {
+			return err
+		}
 	}
-
-	// TODO: perf --> the snapshot could simply be updated with the new op
-	c.ClearSnapshot()
-
 	return nil
 }
 
-func (c *BugCache) Open() error {
-	author, err := bug.GetUser(c.repo)
-	if err != nil {
-		return err
-	}
+func RepoIsAvailable(repo repository.Repo) error {
+	lockPath := repoLockFilePath(repo)
 
-	operations.Open(c.bug, author)
+	// Todo: this leave way for a racey access to the repo between the test
+	// if the file exist and the actual write. It's probably not a problem in
+	// practice because using a repository will be done from user interaction
+	// or in a context where a single instance of git-bug is already guaranteed
+	// (say, a server with the web UI running). But still, that might be nice to
+	// have a mutex or something to guard that.
 
-	// TODO: perf --> the snapshot could simply be updated with the new op
-	c.ClearSnapshot()
+	// Todo: this will fail if somehow the filesystem is shared with another
+	// computer. Should add a configuration that prevent the cleaning of the
+	// lock file
 
-	return nil
-}
+	f, err := os.Open(lockPath)
 
-func (c *BugCache) Close() error {
-	author, err := bug.GetUser(c.repo)
-	if err != nil {
+	if err != nil && !os.IsNotExist(err) {
 		return err
 	}
 
-	operations.Close(c.bug, author)
+	if err == nil {
+		// lock file already exist
+		buf, err := ioutil.ReadAll(io.LimitReader(f, 10))
+		if err != nil {
+			return err
+		}
+		if len(buf) == 10 {
+			return fmt.Errorf("The lock file should be < 10 bytes")
+		}
 
-	// TODO: perf --> the snapshot could simply be updated with the new op
-	c.ClearSnapshot()
+		pid, err := strconv.Atoi(string(buf))
+		if err != nil {
+			return err
+		}
 
-	return nil
-}
+		if util.ProcessIsRunning(pid) {
+			return fmt.Errorf("The repository you want to access is already locked by the process pid %d", pid)
+		}
 
-func (c *BugCache) SetTitle(title string) error {
-	author, err := bug.GetUser(c.repo)
-	if err != nil {
-		return err
-	}
+		// The lock file is just laying there after a crash, clean it
 
-	operations.SetTitle(c.bug, author, title)
+		fmt.Println("A lock file is present but the corresponding process is not, removing it.")
+		err = f.Close()
+		if err != nil {
+			return err
+		}
 
-	// TODO: perf --> the snapshot could simply be updated with the new op
-	c.ClearSnapshot()
+		os.Remove(lockPath)
+		if err != nil {
+			return err
+		}
+	}
 
 	return nil
 }
 
-func (c *BugCache) Commit() error {
-	return c.bug.Commit(c.repo)
-}
-
-func (c *BugCache) CommitAsNeeded() error {
-	if c.bug.HasPendingOp() {
-		return c.bug.Commit(c.repo)
-	}
-	return nil
+func repoLockFilePath(repo repository.Repo) string {
+	return path.Join(repo.GetPath(), ".git", "git-bug", lockfile)
 }

cache/repo_cache.go 🔗

@@ -0,0 +1,144 @@
+package cache
+
+import (
+	"fmt"
+	"io"
+	"strings"
+
+	"github.com/MichaelMure/git-bug/bug"
+	"github.com/MichaelMure/git-bug/bug/operations"
+	"github.com/MichaelMure/git-bug/repository"
+	"github.com/MichaelMure/git-bug/util"
+)
+
+type RepoCacher interface {
+	Repository() repository.Repo
+	ResolveBug(id string) (BugCacher, error)
+	ResolveBugPrefix(prefix string) (BugCacher, error)
+	AllBugIds() ([]string, error)
+	ClearAllBugs()
+
+	// Mutations
+	NewBug(title string, message string) (BugCacher, error)
+	NewBugWithFiles(title string, message string, files []util.Hash) (BugCacher, error)
+	Fetch(remote string) (string, error)
+	MergeAll(remote string) <-chan bug.MergeResult
+	Pull(remote string, out io.Writer) error
+	Push(remote string) (string, error)
+}
+
+type RepoCache struct {
+	repo repository.Repo
+	bugs map[string]BugCacher
+}
+
+func NewRepoCache(r repository.Repo) RepoCacher {
+	return &RepoCache{
+		repo: r,
+		bugs: make(map[string]BugCacher),
+	}
+}
+
+func (c *RepoCache) Repository() repository.Repo {
+	return c.repo
+}
+
+func (c *RepoCache) ResolveBug(id string) (BugCacher, error) {
+	cached, ok := c.bugs[id]
+	if ok {
+		return cached, nil
+	}
+
+	b, err := bug.ReadLocalBug(c.repo, id)
+	if err != nil {
+		return nil, err
+	}
+
+	cached = NewBugCache(c.repo, b)
+	c.bugs[id] = cached
+
+	return cached, nil
+}
+
+func (c *RepoCache) ResolveBugPrefix(prefix string) (BugCacher, error) {
+	// preallocate but empty
+	matching := make([]string, 0, 5)
+
+	for id := range c.bugs {
+		if strings.HasPrefix(id, prefix) {
+			matching = append(matching, id)
+		}
+	}
+
+	// TODO: should check matching bug in the repo as well
+
+	if len(matching) > 1 {
+		return nil, fmt.Errorf("Multiple matching bug found:\n%s", strings.Join(matching, "\n"))
+	}
+
+	if len(matching) == 1 {
+		b := c.bugs[matching[0]]
+		return b, nil
+	}
+
+	b, err := bug.FindLocalBug(c.repo, prefix)
+
+	if err != nil {
+		return nil, err
+	}
+
+	cached := NewBugCache(c.repo, b)
+	c.bugs[b.Id()] = cached
+
+	return cached, nil
+}
+
+func (c *RepoCache) AllBugIds() ([]string, error) {
+	return bug.ListLocalIds(c.repo)
+}
+
+func (c *RepoCache) ClearAllBugs() {
+	c.bugs = make(map[string]BugCacher)
+}
+
+func (c *RepoCache) NewBug(title string, message string) (BugCacher, error) {
+	return c.NewBugWithFiles(title, message, nil)
+}
+
+func (c *RepoCache) NewBugWithFiles(title string, message string, files []util.Hash) (BugCacher, error) {
+	author, err := bug.GetUser(c.repo)
+	if err != nil {
+		return nil, err
+	}
+
+	b, err := operations.CreateWithFiles(author, title, message, files)
+	if err != nil {
+		return nil, err
+	}
+
+	err = b.Commit(c.repo)
+	if err != nil {
+		return nil, err
+	}
+
+	cached := NewBugCache(c.repo, b)
+	c.bugs[b.Id()] = cached
+
+	return cached, nil
+}
+
+func (c *RepoCache) Fetch(remote string) (string, error) {
+	return bug.Fetch(c.repo, remote)
+}
+
+func (c *RepoCache) MergeAll(remote string) <-chan bug.MergeResult {
+	return bug.MergeAll(c.repo, remote)
+}
+
+func (c *RepoCache) Pull(remote string, out io.Writer) error {
+	return bug.Pull(c.repo, out, remote)
+}
+
+func (c *RepoCache) Push(remote string) (string, error) {
+	return bug.Push(c.repo, remote)
+}

commands/root.go 🔗

@@ -5,6 +5,7 @@ import (
 	"os"
 
 	"github.com/MichaelMure/git-bug/bug"
+	"github.com/MichaelMure/git-bug/cache"
 	"github.com/MichaelMure/git-bug/repository"
 	"github.com/spf13/cobra"
 )
@@ -68,5 +69,13 @@ func loadRepo(cmd *cobra.Command, args []string) error {
 		return err
 	}
 
+	// Prevent the command from running when the cache has locked the repo
+	// Todo: make it more fine-grained at first
+	// Todo: make the running cache available for other processes
+	err = cache.RepoIsAvailable(repo)
+	if err != nil {
+		return err
+	}
+
 	return nil
 }

commands/webui.go 🔗

@@ -2,11 +2,14 @@ package commands
 
 import (
 	"bytes"
+	"context"
 	"encoding/json"
 	"fmt"
 	"io/ioutil"
 	"log"
 	"net/http"
+	"os"
+	"os/signal"
 	"time"
 
 	"github.com/MichaelMure/git-bug/graphql"
@@ -34,23 +37,66 @@ func runWebUI(cmd *cobra.Command, args []string) error {
 	addr := fmt.Sprintf("127.0.0.1:%d", port)
 	webUiAddr := fmt.Sprintf("http://%s", addr)
 
-	fmt.Printf("Web UI: %s\n", webUiAddr)
-	fmt.Printf("Graphql API: http://%s/graphql\n", addr)
-	fmt.Printf("Graphql Playground: http://%s/playground\n", addr)
-
 	router := mux.NewRouter()
 
+	graphqlHandler, err := graphql.NewHandler(repo)
+	if err != nil {
+		return err
+	}
+
 	// Routes
 	router.Path("/playground").Handler(handler.Playground("git-bug", "/graphql"))
-	router.Path("/graphql").Handler(graphql.NewHandler(repo))
+	router.Path("/graphql").Handler(graphqlHandler)
 	router.Path("/gitfile/{hash}").Handler(newGitFileHandler(repo))
 	router.Path("/upload").Methods("POST").Handler(newGitUploadFileHandler(repo))
 	router.PathPrefix("/").Handler(http.FileServer(webui.WebUIAssets))
 
+	srv := &http.Server{
+		Addr:    addr,
+		Handler: router,
+	}
+
+	done := make(chan bool)
+	quit := make(chan os.Signal, 1)
+
+	// register as handler of the interrupt signal to trigger the teardown
+	signal.Notify(quit, os.Interrupt)
+
+	go func() {
+		<-quit
+		fmt.Println("WebUI is shutting down...")
+
+		ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
+		defer cancel()
+
+		srv.SetKeepAlivesEnabled(false)
+		if err := srv.Shutdown(ctx); err != nil {
+			log.Fatalf("Could not gracefully shutdown the WebUI: %v\n", err)
+		}
+
+		// Teardown
+		err := graphqlHandler.Close()
+		if err != nil {
+			fmt.Println(err)
+		}
+
+		close(done)
+	}()
+
+	fmt.Printf("Web UI: %s\n", webUiAddr)
+	fmt.Printf("Graphql API: http://%s/graphql\n", addr)
+	fmt.Printf("Graphql Playground: http://%s/playground\n", addr)
+
 	open.Run(webUiAddr)
 
-	log.Fatal(http.ListenAndServe(addr, router))
+	err = srv.ListenAndServe()
+	if err != nil && err != http.ErrServerClosed {
+		return err
+	}
+
+	<-done
 
+	fmt.Println("WebUI stopped")
 	return nil
 }
 

graphql/handler.go 🔗

@@ -10,10 +10,22 @@ import (
 	"net/http"
 )
 
-func NewHandler(repo repository.Repo) http.Handler {
-	backend := resolvers.NewBackend()
+type Handler struct {
+	http.HandlerFunc
+	*resolvers.Backend
+}
+
+func NewHandler(repo repository.Repo) (Handler, error) {
+	h := Handler{
+		Backend: resolvers.NewBackend(),
+	}
+
+	err := h.Backend.RegisterDefaultRepository(repo)
+	if err != nil {
+		return Handler{}, err
+	}
 
-	backend.RegisterDefaultRepository(repo)
+	h.HandlerFunc = handler.GraphQL(graph.NewExecutableSchema(h.Backend))
 
-	return handler.GraphQL(graph.NewExecutableSchema(backend))
+	return h, nil
 }

util/process.go 🔗

@@ -0,0 +1,23 @@
+package util
+
+import (
+	"os"
+	"syscall"
+)
+
+// ProcessIsRunning tell is a process is running
+func ProcessIsRunning(pid int) bool {
+	// never return no error in a unix system
+	process, err := os.FindProcess(pid)
+
+	if err != nil {
+		return false
+	}
+
+	// Signal 0 doesn't do anything but allow testing the process
+	err = process.Signal(syscall.Signal(0))
+
+	// Todo: distinguish "you don't have access" and "process doesn't exist"
+
+	return err == nil
+}