log.Info("HTTP Listener: %s Closed", listenAddr)
graceful.Manager.WaitForServers()
graceful.Manager.WaitForTerminate()
+ log.Info("PID: %d Gitea Web Finished", os.Getpid())
log.Close()
return nil
}
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/log"
- "code.gitea.io/gitea/modules/process"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/sync"
headFile := pr.GetGitRefName()
// Check if a pull request is merged into BaseBranch
- _, stderr, err := process.GetManager().ExecDirEnv(-1, "", fmt.Sprintf("isMerged (git merge-base --is-ancestor): %d", pr.BaseRepo.ID),
- []string{"GIT_INDEX_FILE=" + indexTmpPath, "GIT_DIR=" + pr.BaseRepo.RepoPath()},
- git.GitExecutable, "merge-base", "--is-ancestor", headFile, pr.BaseBranch)
-
+ _, err := git.NewCommand("merge-base", "--is-ancestor", headFile, pr.BaseBranch).RunInDirWithEnv(pr.BaseRepo.RepoPath(), []string{"GIT_INDEX_FILE=" + indexTmpPath, "GIT_DIR=" + pr.BaseRepo.RepoPath()})
if err != nil {
// Errors are signaled by a non-zero status that is not 1
if strings.Contains(err.Error(), "exit status 1") {
return nil, nil
}
- return nil, fmt.Errorf("git merge-base --is-ancestor: %v %v", stderr, err)
+ return nil, fmt.Errorf("git merge-base --is-ancestor: %v", err)
}
commitIDBytes, err := ioutil.ReadFile(pr.BaseRepo.RepoPath() + "/" + headFile)
cmd := commitID[:40] + ".." + pr.BaseBranch
// Get the commit from BaseBranch where the pull request got merged
- mergeCommit, stderr, err := process.GetManager().ExecDirEnv(-1, "", fmt.Sprintf("isMerged (git rev-list --ancestry-path --merges --reverse): %d", pr.BaseRepo.ID),
- []string{"GIT_INDEX_FILE=" + indexTmpPath, "GIT_DIR=" + pr.BaseRepo.RepoPath()},
- git.GitExecutable, "rev-list", "--ancestry-path", "--merges", "--reverse", cmd)
+ mergeCommit, err := git.NewCommand("rev-list", "--ancestry-path", "--merges", "--reverse", cmd).RunInDirWithEnv("", []string{"GIT_INDEX_FILE=" + indexTmpPath, "GIT_DIR=" + pr.BaseRepo.RepoPath()})
if err != nil {
- return nil, fmt.Errorf("git rev-list --ancestry-path --merges --reverse: %v %v", stderr, err)
+ return nil, fmt.Errorf("git rev-list --ancestry-path --merges --reverse: %v", err)
} else if len(mergeCommit) < 40 {
// PR was fast-forwarded, so just use last commit of PR
mergeCommit = commitID[:40]
indexTmpPath := filepath.Join(os.TempDir(), "gitea-"+pr.BaseRepo.Name+"-"+strconv.Itoa(time.Now().Nanosecond()))
defer os.Remove(indexTmpPath)
- var stderr string
- _, stderr, err = process.GetManager().ExecDirEnv(-1, "", fmt.Sprintf("testPatch (git read-tree): %d", pr.BaseRepo.ID),
- []string{"GIT_DIR=" + pr.BaseRepo.RepoPath(), "GIT_INDEX_FILE=" + indexTmpPath},
- git.GitExecutable, "read-tree", pr.BaseBranch)
+ _, err = git.NewCommand("read-tree", pr.BaseBranch).RunInDirWithEnv("", []string{"GIT_DIR=" + pr.BaseRepo.RepoPath(), "GIT_INDEX_FILE=" + indexTmpPath})
if err != nil {
- return fmt.Errorf("git read-tree --index-output=%s %s: %v - %s", indexTmpPath, pr.BaseBranch, err, stderr)
+ return fmt.Errorf("git read-tree --index-output=%s %s: %v", indexTmpPath, pr.BaseBranch, err)
}
prUnit, err := pr.BaseRepo.getUnit(e, UnitTypePullRequests)
args = append(args, patchPath)
pr.ConflictedFiles = []string{}
- _, stderr, err = process.GetManager().ExecDirEnv(-1, "", fmt.Sprintf("testPatch (git apply --check): %d", pr.BaseRepo.ID),
+ stderrBuilder := new(strings.Builder)
+ err = git.NewCommand(args...).RunInDirTimeoutEnvPipeline(
[]string{"GIT_INDEX_FILE=" + indexTmpPath, "GIT_DIR=" + pr.BaseRepo.RepoPath()},
- git.GitExecutable, args...)
+ -1,
+ "",
+ nil,
+ stderrBuilder)
+ stderr := stderrBuilder.String()
+
if err != nil {
for i := range patchConflicts {
if strings.Contains(stderr, patchConflicts[i]) {
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/markup"
"code.gitea.io/gitea/modules/options"
- "code.gitea.io/gitea/modules/process"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/structs"
api "code.gitea.io/gitea/modules/structs"
"GIT_COMMITTER_DATE="+commitTimeStr,
)
- var stderr string
- if _, stderr, err = process.GetManager().ExecDir(-1,
- tmpPath, fmt.Sprintf("initRepoCommit (git add): %s", tmpPath),
- git.GitExecutable, "add", "--all"); err != nil {
- return fmt.Errorf("git add: %s", stderr)
+ if stdout, err := git.NewCommand("add", "--all").
+ SetDescription(fmt.Sprintf("initRepoCommit (git add): %s", tmpPath)).
+ RunInDir(tmpPath); err != nil {
+ log.Error("git add --all failed: Stdout: %s\nError: %v", stdout, err)
+ return fmt.Errorf("git add --all: %v", err)
}
binVersion, err := git.BinVersion()
}
}
- if _, stderr, err = process.GetManager().ExecDirEnv(-1,
- tmpPath, fmt.Sprintf("initRepoCommit (git commit): %s", tmpPath),
- env,
- git.GitExecutable, args...); err != nil {
- return fmt.Errorf("git commit: %s", stderr)
+ if stdout, err := git.NewCommand(args...).
+ SetDescription(fmt.Sprintf("initRepoCommit (git commit): %s", tmpPath)).
+ RunInDirWithEnv(tmpPath, env); err != nil {
+ log.Error("Failed to commit: %v: Stdout: %s\nError: %v", args, stdout, err)
+ return fmt.Errorf("git commit: %v", err)
}
- if _, stderr, err = process.GetManager().ExecDir(-1,
- tmpPath, fmt.Sprintf("initRepoCommit (git push): %s", tmpPath),
- git.GitExecutable, "push", "origin", "master"); err != nil {
- return fmt.Errorf("git push: %s", stderr)
+ if stdout, err := git.NewCommand("push", "origin", "master").
+ SetDescription(fmt.Sprintf("initRepoCommit (git push): %s", tmpPath)).
+ RunInDir(tmpPath); err != nil {
+ log.Error("Failed to push back to master: Stdout: %s\nError: %v", stdout, err)
+ return fmt.Errorf("git push: %v", err)
}
+
return nil
}
)
// Clone to temporary path and do the init commit.
- _, stderr, err := process.GetManager().ExecDirEnv(
- -1, "",
- fmt.Sprintf("initRepository(git clone): %s", repoPath),
- env,
- git.GitExecutable, "clone", repoPath, tmpDir,
- )
- if err != nil {
- return fmt.Errorf("git clone: %v - %s", err, stderr)
+ if stdout, err := git.NewCommand("clone", repoPath, tmpDir).
+ SetDescription(fmt.Sprintf("initRepository (git clone): %s to %s", repoPath, tmpDir)).
+ RunInDirWithEnv("", env); err != nil {
+ log.Error("Failed to clone from %v into %s: stdout: %s\nError: %v", repo, tmpDir, stdout, err)
+ return fmt.Errorf("git clone: %v", err)
}
// README
}
}
- _, stderr, err := process.GetManager().ExecDir(-1,
- repoPath, fmt.Sprintf("CreateRepository(git update-server-info): %s", repoPath),
- git.GitExecutable, "update-server-info")
- if err != nil {
- return nil, errors.New("CreateRepository(git update-server-info): " + stderr)
+ if stdout, err := git.NewCommand("update-server-info").
+ SetDescription(fmt.Sprintf("CreateRepository(git update-server-info): %s", repoPath)).
+ RunInDir(repoPath); err != nil {
+ log.Error("CreateRepitory(git update-server-info) in %v: Stdout: %s\nError: %v", repo, stdout, err)
+ return nil, fmt.Errorf("CreateRepository(git update-server-info): %v", err)
}
}
if err := repo.GetOwner(); err != nil {
return err
}
- _, stderr, err := process.GetManager().ExecDir(
- time.Duration(setting.Git.Timeout.GC)*time.Second,
- RepoPath(repo.Owner.Name, repo.Name), "Repository garbage collection",
- git.GitExecutable, args...)
- if err != nil {
- return fmt.Errorf("%v: %v", err, stderr)
+ if stdout, err := git.NewCommand(args...).
+ SetDescription(fmt.Sprintf("Repository Garbage Collection: %s", repo.FullName())).
+ RunInDirTimeout(
+ time.Duration(setting.Git.Timeout.GC)*time.Second,
+ RepoPath(repo.Owner.Name, repo.Name)); err != nil {
+ log.Error("Repository garbage collection failed for %v. Stdout: %s\nError: %v", repo, stdout, err)
+ return fmt.Errorf("Repository garbage collection failed: Error: %v", err)
}
return nil
})
}
repoPath := RepoPath(owner.Name, repo.Name)
- _, stderr, err := process.GetManager().ExecTimeout(10*time.Minute,
- fmt.Sprintf("ForkRepository(git clone): %s/%s", owner.Name, repo.Name),
- git.GitExecutable, "clone", "--bare", oldRepo.repoPath(sess), repoPath)
- if err != nil {
- return nil, fmt.Errorf("git clone: %v", stderr)
- }
-
- _, stderr, err = process.GetManager().ExecDir(-1,
- repoPath, fmt.Sprintf("ForkRepository(git update-server-info): %s", repoPath),
- git.GitExecutable, "update-server-info")
- if err != nil {
- return nil, fmt.Errorf("git update-server-info: %v", stderr)
+ if stdout, err := git.NewCommand(
+ "clone", "--bare", oldRepo.repoPath(sess), repoPath).
+ SetDescription(fmt.Sprintf("ForkRepository(git clone): %s to %s", oldRepo.FullName(), repo.FullName())).
+ RunInDirTimeout(10*time.Minute, ""); err != nil {
+ log.Error("Fork Repository (git clone) Failed for %v (from %v):\nStdout: %s\nError: %v", repo, oldRepo, stdout, err)
+ return nil, fmt.Errorf("git clone: %v", err)
+ }
+
+ if stdout, err := git.NewCommand("update-server-info").
+ SetDescription(fmt.Sprintf("ForkRepository(git update-server-info): %s", repo.FullName())).
+ RunInDir(repoPath); err != nil {
+ log.Error("Fork Repository (git update-server-info) failed for %v:\nStdout: %s\nError: %v", repo, stdout, err)
+ return nil, fmt.Errorf("git update-server-info: %v", err)
}
if err = createDelegateHooks(repoPath); err != nil {
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/log"
- "code.gitea.io/gitea/modules/process"
"code.gitea.io/gitea/modules/util"
"github.com/gobwas/glob"
}
repoPath := repo.repoPath(e)
- _, stderr, err := process.GetManager().ExecDirEnv(
- -1, tmpDir,
- fmt.Sprintf("generateRepoCommit(git remote add): %s", repoPath),
- env,
- git.GitExecutable, "remote", "add", "origin", repoPath,
- )
- if err != nil {
- return fmt.Errorf("git remote add: %v - %s", err, stderr)
+ if stdout, err := git.NewCommand("remote", "add", "origin", repoPath).
+ SetDescription(fmt.Sprintf("generateRepoCommit (git remote add): %s to %s", templateRepoPath, tmpDir)).
+ RunInDirWithEnv(tmpDir, env); err != nil {
+ log.Error("Unable to add %v as remote origin to temporary repo to %s: stdout %s\nError: %v", repo, tmpDir, stdout, err)
+ return fmt.Errorf("git remote add: %v", err)
}
return initRepoCommit(tmpDir, repo.Owner)
import (
"bufio"
+ "context"
"fmt"
"io"
"os"
output io.ReadCloser
scanner *bufio.Scanner
lastSha *string
+ cancel context.CancelFunc
}
var shaLineRegex = regexp.MustCompile("^([a-z0-9]{40})")
// Close BlameReader - don't run NextPart after invoking that
func (r *BlameReader) Close() error {
- process.GetManager().Remove(r.pid)
+ defer process.GetManager().Remove(r.pid)
+ defer r.cancel()
if err := r.cmd.Wait(); err != nil {
return fmt.Errorf("Wait: %v", err)
}
func createBlameReader(dir string, command ...string) (*BlameReader, error) {
- cmd := exec.Command(command[0], command[1:]...)
+ // FIXME: graceful: This should have a timeout
+ ctx, cancel := context.WithCancel(DefaultContext)
+ cmd := exec.CommandContext(ctx, command[0], command[1:]...)
cmd.Dir = dir
cmd.Stderr = os.Stderr
stdout, err := cmd.StdoutPipe()
if err != nil {
+ defer cancel()
return nil, fmt.Errorf("StdoutPipe: %v", err)
}
if err = cmd.Start(); err != nil {
+ defer cancel()
return nil, fmt.Errorf("Start: %v", err)
}
- pid := process.GetManager().Add(fmt.Sprintf("GetBlame [repo_path: %s]", dir), cmd)
+ pid := process.GetManager().Add(fmt.Sprintf("GetBlame [repo_path: %s]", dir), cancel)
scanner := bufio.NewScanner(stdout)
stdout,
scanner,
nil,
+ cancel,
}, nil
}
// Command represents a command with its subcommands or arguments.
type Command struct {
- name string
- args []string
+ name string
+ args []string
+ parentContext context.Context
+ desc string
}
func (c *Command) String() string {
cargs := make([]string, len(GlobalCommandArgs))
copy(cargs, GlobalCommandArgs)
return &Command{
- name: GitExecutable,
- args: append(cargs, args...),
+ name: GitExecutable,
+ args: append(cargs, args...),
+ parentContext: DefaultContext,
}
}
// NewCommandNoGlobals creates and returns a new Git Command based on given command and arguments only with the specify args and don't care global command args
func NewCommandNoGlobals(args ...string) *Command {
return &Command{
- name: GitExecutable,
- args: args,
+ name: GitExecutable,
+ args: args,
+ parentContext: DefaultContext,
}
}
+// SetParentContext sets the parent context for this command
+func (c *Command) SetParentContext(ctx context.Context) *Command {
+ c.parentContext = ctx
+ return c
+}
+
+// SetDescription sets the description for this command which be returned on
+// c.String()
+func (c *Command) SetDescription(desc string) *Command {
+ c.desc = desc
+ return c
+}
+
// AddArguments adds new argument(s) to the command.
func (c *Command) AddArguments(args ...string) *Command {
c.args = append(c.args, args...)
log("%s: %v", dir, c)
}
- ctx, cancel := context.WithTimeout(context.Background(), timeout)
+ ctx, cancel := context.WithTimeout(c.parentContext, timeout)
defer cancel()
cmd := exec.CommandContext(ctx, c.name, c.args...)
return err
}
- pid := process.GetManager().Add(fmt.Sprintf("%s %s %s [repo_path: %s]", GitExecutable, c.name, strings.Join(c.args, " "), dir), cmd)
+ desc := c.desc
+ if desc == "" {
+ desc = fmt.Sprintf("%s %s %s [repo_path: %s]", GitExecutable, c.name, strings.Join(c.args, " "), dir)
+ }
+ pid := process.GetManager().Add(desc, cancel)
defer process.GetManager().Remove(pid)
if fn != nil {
package git
import (
+ "context"
"fmt"
"os/exec"
"runtime"
// Could be updated to an absolute path while initialization
GitExecutable = "git"
+ // DefaultContext is the default context to run git commands in
+ DefaultContext = context.Background()
+
gitVersion string
)
--- /dev/null
+// Copyright 2019 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package graceful
+
+import (
+ "context"
+ "fmt"
+ "time"
+)
+
+// Errors for context.Err()
+var (
+ ErrShutdown = fmt.Errorf("Graceful Manager called Shutdown")
+ ErrHammer = fmt.Errorf("Graceful Manager called Hammer")
+ ErrTerminate = fmt.Errorf("Graceful Manager called Terminate")
+)
+
+// ChannelContext is a context that wraps a channel and error as a context
+type ChannelContext struct {
+ done <-chan struct{}
+ err error
+}
+
+// NewChannelContext creates a ChannelContext from a channel and error
+func NewChannelContext(done <-chan struct{}, err error) *ChannelContext {
+ return &ChannelContext{
+ done: done,
+ err: err,
+ }
+}
+
+// Deadline returns the time when work done on behalf of this context
+// should be canceled. There is no Deadline for a ChannelContext
+func (ctx *ChannelContext) Deadline() (deadline time.Time, ok bool) {
+ return
+}
+
+// Done returns the channel provided at the creation of this context.
+// When closed, work done on behalf of this context should be canceled.
+func (ctx *ChannelContext) Done() <-chan struct{} {
+ return ctx.done
+}
+
+// Err returns nil, if Done is not closed. If Done is closed,
+// Err returns the error provided at the creation of this context
+func (ctx *ChannelContext) Err() error {
+ select {
+ case <-ctx.done:
+ return ctx.err
+ default:
+ return nil
+ }
+}
+
+// Value returns nil for all calls as no values are or can be associated with this context
+func (ctx *ChannelContext) Value(key interface{}) interface{} {
+ return nil
+}
+
+// ShutdownContext returns a context.Context that is Done at shutdown
+// Callers using this context should ensure that they are registered as a running server
+// in order that they are waited for.
+func (g *gracefulManager) ShutdownContext() context.Context {
+ return &ChannelContext{
+ done: g.IsShutdown(),
+ err: ErrShutdown,
+ }
+}
+
+// HammerContext returns a context.Context that is Done at hammer
+// Callers using this context should ensure that they are registered as a running server
+// in order that they are waited for.
+func (g *gracefulManager) HammerContext() context.Context {
+ return &ChannelContext{
+ done: g.IsHammer(),
+ err: ErrHammer,
+ }
+}
+
+// TerminateContext returns a context.Context that is Done at terminate
+// Callers using this context should ensure that they are registered as a terminating server
+// in order that they are waited for.
+func (g *gracefulManager) TerminateContext() context.Context {
+ return &ChannelContext{
+ done: g.IsTerminate(),
+ err: ErrTerminate,
+ }
+}
package graceful
import (
+ "context"
"time"
+ "code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/log"
+ "code.gitea.io/gitea/modules/process"
"code.gitea.io/gitea/modules/setting"
)
var Manager *gracefulManager
func init() {
- Manager = newGracefulManager()
+ Manager = newGracefulManager(context.Background())
+ // Set the git default context to the HammerContext
+ git.DefaultContext = Manager.HammerContext()
+ // Set the process default context to the HammerContext
+ process.DefaultContext = Manager.HammerContext()
}
+// CallbackWithContext is combined runnable and context to watch to see if the caller has finished
+type CallbackWithContext func(ctx context.Context, callback func())
+
+// RunnableWithShutdownFns is a runnable with functions to run at shutdown and terminate
+// After the callback to atShutdown is called and is complete, the main function must return.
+// Similarly the callback function provided to atTerminate must return once termination is complete.
+// Please note that use of the atShutdown and atTerminate callbacks will create go-routines that will wait till their respective signals
+// - users must therefore be careful to only call these as necessary.
+// If run is not expected to run indefinitely RunWithShutdownChan is likely to be more appropriate.
+type RunnableWithShutdownFns func(atShutdown, atTerminate func(context.Context, func()))
+
+// RunWithShutdownFns takes a function that has both atShutdown and atTerminate callbacks
+// After the callback to atShutdown is called and is complete, the main function must return.
+// Similarly the callback function provided to atTerminate must return once termination is complete.
+// Please note that use of the atShutdown and atTerminate callbacks will create go-routines that will wait till their respective signals
+// - users must therefore be careful to only call these as necessary.
+// If run is not expected to run indefinitely RunWithShutdownChan is likely to be more appropriate.
+func (g *gracefulManager) RunWithShutdownFns(run RunnableWithShutdownFns) {
+ g.runningServerWaitGroup.Add(1)
+ defer g.runningServerWaitGroup.Done()
+ run(func(ctx context.Context, atShutdown func()) {
+ go func() {
+ select {
+ case <-g.IsShutdown():
+ atShutdown()
+ case <-ctx.Done():
+ return
+ }
+ }()
+ }, func(ctx context.Context, atTerminate func()) {
+ g.RunAtTerminate(ctx, atTerminate)
+ })
+}
+
+// RunnableWithShutdownChan is a runnable with functions to run at shutdown and terminate.
+// After the atShutdown channel is closed, the main function must return once shutdown is complete.
+// (Optionally IsHammer may be waited for instead however, this should be avoided if possible.)
+// The callback function provided to atTerminate must return once termination is complete.
+// Please note that use of the atTerminate function will create a go-routine that will wait till terminate - users must therefore be careful to only call this as necessary.
+type RunnableWithShutdownChan func(atShutdown <-chan struct{}, atTerminate CallbackWithContext)
+
+// RunWithShutdownChan takes a function that has channel to watch for shutdown and atTerminate callbacks
+// After the atShutdown channel is closed, the main function must return once shutdown is complete.
+// (Optionally IsHammer may be waited for instead however, this should be avoided if possible.)
+// The callback function provided to atTerminate must return once termination is complete.
+// Please note that use of the atTerminate function will create a go-routine that will wait till terminate - users must therefore be careful to only call this as necessary.
+func (g *gracefulManager) RunWithShutdownChan(run RunnableWithShutdownChan) {
+ g.runningServerWaitGroup.Add(1)
+ defer g.runningServerWaitGroup.Done()
+ run(g.IsShutdown(), func(ctx context.Context, atTerminate func()) {
+ g.RunAtTerminate(ctx, atTerminate)
+ })
+}
+
+// RunWithShutdownContext takes a function that has a context to watch for shutdown.
+// After the provided context is Done(), the main function must return once shutdown is complete.
+// (Optionally the HammerContext may be obtained and waited for however, this should be avoided if possible.)
+func (g *gracefulManager) RunWithShutdownContext(run func(context.Context)) {
+ g.runningServerWaitGroup.Add(1)
+ defer g.runningServerWaitGroup.Done()
+ run(g.ShutdownContext())
+}
+
+// RunAtTerminate adds to the terminate wait group and creates a go-routine to run the provided function at termination
+func (g *gracefulManager) RunAtTerminate(ctx context.Context, terminate func()) {
+ g.terminateWaitGroup.Add(1)
+ go func() {
+ select {
+ case <-g.IsTerminate():
+ terminate()
+ case <-ctx.Done():
+ }
+ g.terminateWaitGroup.Done()
+ }()
+}
+
+// RunAtShutdown creates a go-routine to run the provided function at shutdown
+func (g *gracefulManager) RunAtShutdown(ctx context.Context, shutdown func()) {
+ go func() {
+ select {
+ case <-g.IsShutdown():
+ shutdown()
+ case <-ctx.Done():
+ }
+ }()
+}
+
+// RunAtHammer creates a go-routine to run the provided function at shutdown
+func (g *gracefulManager) RunAtHammer(ctx context.Context, hammer func()) {
+ go func() {
+ select {
+ case <-g.IsHammer():
+ hammer()
+ case <-ctx.Done():
+ }
+ }()
+}
func (g *gracefulManager) doShutdown() {
if !g.setStateTransition(stateRunning, stateShuttingDown) {
return
}
go func() {
g.WaitForServers()
+ // Mop up any remaining unclosed events.
+ g.doHammerTime(0)
<-time.After(1 * time.Second)
g.doTerminate()
}()
package graceful
import (
+ "context"
"errors"
"os"
"os/signal"
terminateWaitGroup sync.WaitGroup
}
-func newGracefulManager() *gracefulManager {
+func newGracefulManager(ctx context.Context) *gracefulManager {
manager := &gracefulManager{
isChild: len(os.Getenv(listenFDs)) > 0 && os.Getppid() > 1,
lock: &sync.RWMutex{},
}
manager.createServerWaitGroup.Add(numberOfServersToCreate)
- manager.Run()
+ manager.Run(ctx)
return manager
}
-func (g *gracefulManager) Run() {
+func (g *gracefulManager) Run(ctx context.Context) {
g.setState(stateRunning)
- go g.handleSignals()
+ go g.handleSignals(ctx)
c := make(chan struct{})
go func() {
defer close(c)
}
}
-func (g *gracefulManager) handleSignals() {
- var sig os.Signal
-
+func (g *gracefulManager) handleSignals(ctx context.Context) {
signalChannel := make(chan os.Signal, 1)
signal.Notify(
pid := syscall.Getpid()
for {
- sig = <-signalChannel
- switch sig {
- case syscall.SIGHUP:
- if setting.GracefulRestartable {
- log.Info("PID: %d. Received SIGHUP. Forking...", pid)
- err := g.doFork()
- if err != nil && err.Error() != "another process already forked. Ignoring this one" {
- log.Error("Error whilst forking from PID: %d : %v", pid, err)
+ select {
+ case sig := <-signalChannel:
+ switch sig {
+ case syscall.SIGHUP:
+ if setting.GracefulRestartable {
+ log.Info("PID: %d. Received SIGHUP. Forking...", pid)
+ err := g.doFork()
+ if err != nil && err.Error() != "another process already forked. Ignoring this one" {
+ log.Error("Error whilst forking from PID: %d : %v", pid, err)
+ }
+ } else {
+ log.Info("PID: %d. Received SIGHUP. Not set restartable. Shutting down...", pid)
+
+ g.doShutdown()
}
- } else {
- log.Info("PID: %d. Received SIGHUP. Not set restartable. Shutting down...", pid)
-
+ case syscall.SIGUSR1:
+ log.Info("PID %d. Received SIGUSR1.", pid)
+ case syscall.SIGUSR2:
+ log.Warn("PID %d. Received SIGUSR2. Hammering...", pid)
+ g.doHammerTime(0 * time.Second)
+ case syscall.SIGINT:
+ log.Warn("PID %d. Received SIGINT. Shutting down...", pid)
g.doShutdown()
+ case syscall.SIGTERM:
+ log.Warn("PID %d. Received SIGTERM. Shutting down...", pid)
+ g.doShutdown()
+ case syscall.SIGTSTP:
+ log.Info("PID %d. Received SIGTSTP.", pid)
+ default:
+ log.Info("PID %d. Received %v.", pid, sig)
}
- case syscall.SIGUSR1:
- log.Info("PID %d. Received SIGUSR1.", pid)
- case syscall.SIGUSR2:
- log.Warn("PID %d. Received SIGUSR2. Hammering...", pid)
- g.doHammerTime(0 * time.Second)
- case syscall.SIGINT:
- log.Warn("PID %d. Received SIGINT. Shutting down...", pid)
- g.doShutdown()
- case syscall.SIGTERM:
- log.Warn("PID %d. Received SIGTERM. Shutting down...", pid)
+ case <-ctx.Done():
+ log.Warn("PID: %d. Background context for manager closed - %v - Shutting down...", pid, ctx.Err())
g.doShutdown()
- case syscall.SIGTSTP:
- log.Info("PID %d. Received SIGTSTP.", pid)
- default:
- log.Info("PID %d. Received %v.", pid, sig)
}
}
}
package graceful
import (
+ "context"
"os"
"strconv"
"sync"
)
type gracefulManager struct {
+ ctx context.Context
isChild bool
lock *sync.RWMutex
state state
terminateWaitGroup sync.WaitGroup
}
-func newGracefulManager() *gracefulManager {
+func newGracefulManager(ctx context.Context) *gracefulManager {
manager := &gracefulManager{
isChild: false,
lock: &sync.RWMutex{},
+ ctx: ctx,
}
manager.createServerWaitGroup.Add(numberOfServersToCreate)
manager.Run()
waitTime := 30 * time.Second
loop:
- for change := range changes {
- switch change.Cmd {
- case svc.Interrogate:
- status <- change.CurrentStatus
- case svc.Stop, svc.Shutdown:
+ for {
+ select {
+ case <-g.ctx.Done():
g.doShutdown()
waitTime += setting.GracefulHammerTime
break loop
- case hammerCode:
- g.doShutdown()
- g.doHammerTime(0 * time.Second)
- break loop
- default:
- log.Debug("Unexpected control request: %v", change.Cmd)
+ case change := <-changes:
+ switch change.Cmd {
+ case svc.Interrogate:
+ status <- change.CurrentStatus
+ case svc.Stop, svc.Shutdown:
+ g.doShutdown()
+ waitTime += setting.GracefulHammerTime
+ break loop
+ case hammerCode:
+ g.doShutdown()
+ g.doHammerTime(0 * time.Second)
+ break loop
+ default:
+ log.Debug("Unexpected control request: %v", change.Cmd)
+ }
}
}
-
status <- svc.Status{
State: svc.StopPending,
WaitHint: uint32(waitTime / time.Millisecond),
"fmt"
"io"
"os/exec"
+ "sort"
"sync"
"time"
)
// ErrExecTimeout represent a timeout error
ErrExecTimeout = errors.New("Process execution timeout")
manager *Manager
+
+ // DefaultContext is the default context to run processing commands in
+ DefaultContext = context.Background()
)
-// Process represents a working process inherit from Gogs.
+// Process represents a working process inheriting from Gitea.
type Process struct {
PID int64 // Process ID, not system one.
Description string
Start time.Time
- Cmd *exec.Cmd
+ Cancel context.CancelFunc
}
// Manager knows about all processes and counts PIDs.
mutex sync.Mutex
counter int64
- Processes map[int64]*Process
+ processes map[int64]*Process
}
// GetManager returns a Manager and initializes one as singleton if there's none yet
func GetManager() *Manager {
if manager == nil {
manager = &Manager{
- Processes: make(map[int64]*Process),
+ processes: make(map[int64]*Process),
}
}
return manager
}
// Add a process to the ProcessManager and returns its PID.
-func (pm *Manager) Add(description string, cmd *exec.Cmd) int64 {
+func (pm *Manager) Add(description string, cancel context.CancelFunc) int64 {
pm.mutex.Lock()
pid := pm.counter + 1
- pm.Processes[pid] = &Process{
+ pm.processes[pid] = &Process{
PID: pid,
Description: description,
Start: time.Now(),
- Cmd: cmd,
+ Cancel: cancel,
}
pm.counter = pid
pm.mutex.Unlock()
// Remove a process from the ProcessManager.
func (pm *Manager) Remove(pid int64) {
pm.mutex.Lock()
- delete(pm.Processes, pid)
+ delete(pm.processes, pid)
pm.mutex.Unlock()
}
+// Cancel a process in the ProcessManager.
+func (pm *Manager) Cancel(pid int64) {
+ pm.mutex.Lock()
+ process, ok := pm.processes[pid]
+ pm.mutex.Unlock()
+ if ok {
+ process.Cancel()
+ }
+}
+
+// Processes gets the processes in a thread safe manner
+func (pm *Manager) Processes() []*Process {
+ pm.mutex.Lock()
+ processes := make([]*Process, 0, len(pm.processes))
+ for _, process := range pm.processes {
+ processes = append(processes, process)
+ }
+ pm.mutex.Unlock()
+ sort.Sort(processList(processes))
+ return processes
+}
+
// Exec a command and use the default timeout.
func (pm *Manager) Exec(desc, cmdName string, args ...string) (string, string, error) {
return pm.ExecDir(-1, "", desc, cmdName, args...)
stdOut := new(bytes.Buffer)
stdErr := new(bytes.Buffer)
- ctx, cancel := context.WithTimeout(context.Background(), timeout)
+ ctx, cancel := context.WithTimeout(DefaultContext, timeout)
defer cancel()
cmd := exec.CommandContext(ctx, cmdName, args...)
return "", "", err
}
- pid := pm.Add(desc, cmd)
+ pid := pm.Add(desc, cancel)
err := cmd.Wait()
pm.Remove(pid)
return stdOut.String(), stdErr.String(), err
}
-// Kill and remove a process from list.
-func (pm *Manager) Kill(pid int64) error {
- if proc, exists := pm.Processes[pid]; exists {
- pm.mutex.Lock()
- if proc.Cmd != nil &&
- proc.Cmd.Process != nil &&
- proc.Cmd.ProcessState != nil &&
- !proc.Cmd.ProcessState.Exited() {
- if err := proc.Cmd.Process.Kill(); err != nil {
- return fmt.Errorf("failed to kill process(%d/%s): %v", pid, proc.Description, err)
- }
- }
- delete(pm.Processes, pid)
- pm.mutex.Unlock()
- }
+type processList []*Process
+
+func (l processList) Len() int {
+ return len(l)
+}
+
+func (l processList) Less(i, j int) bool {
+ return l[i].PID < l[j].PID
+}
- return nil
+func (l processList) Swap(i, j int) {
+ l[i], l[j] = l[j], l[i]
}
package process
import (
- "os/exec"
+ "context"
"testing"
"time"
)
func TestManager_Add(t *testing.T) {
- pm := Manager{Processes: make(map[int64]*Process)}
+ pm := Manager{processes: make(map[int64]*Process)}
- pid := pm.Add("foo", exec.Command("foo"))
+ pid := pm.Add("foo", nil)
assert.Equal(t, int64(1), pid, "expected to get pid 1 got %d", pid)
- pid = pm.Add("bar", exec.Command("bar"))
+ pid = pm.Add("bar", nil)
assert.Equal(t, int64(2), pid, "expected to get pid 2 got %d", pid)
}
+func TestManager_Cancel(t *testing.T) {
+ pm := Manager{processes: make(map[int64]*Process)}
+
+ ctx, cancel := context.WithCancel(context.Background())
+ pid := pm.Add("foo", cancel)
+
+ pm.Cancel(pid)
+
+ select {
+ case <-ctx.Done():
+ default:
+ assert.Fail(t, "Cancel should cancel the provided context")
+ }
+}
+
func TestManager_Remove(t *testing.T) {
- pm := Manager{Processes: make(map[int64]*Process)}
+ pm := Manager{processes: make(map[int64]*Process)}
- pid1 := pm.Add("foo", exec.Command("foo"))
+ pid1 := pm.Add("foo", nil)
assert.Equal(t, int64(1), pid1, "expected to get pid 1 got %d", pid1)
- pid2 := pm.Add("bar", exec.Command("bar"))
+ pid2 := pm.Add("bar", nil)
assert.Equal(t, int64(2), pid2, "expected to get pid 2 got %d", pid2)
pm.Remove(pid2)
- _, exists := pm.Processes[pid2]
+ _, exists := pm.processes[pid2]
assert.False(t, exists, "PID %d is in the list but shouldn't", pid2)
}
monitor.desc = Description
monitor.start = Start Time
monitor.execute_time = Execution Time
+monitor.process.cancel = Cancel process
+monitor.process.cancel_desc = Cancelling a process may cause data loss
+monitor.process.cancel_notices = Cancel: <strong>%s</strong>?
notices.system_notice_list = System Notices
notices.view_detail_header = View Notice Details
ctx.Data["Title"] = ctx.Tr("admin.monitor")
ctx.Data["PageIsAdmin"] = true
ctx.Data["PageIsAdminMonitor"] = true
- ctx.Data["Processes"] = process.GetManager().Processes
+ ctx.Data["Processes"] = process.GetManager().Processes()
ctx.Data["Entries"] = cron.ListTasks()
ctx.HTML(200, tplMonitor)
}
+
+// MonitorCancel cancels a process
+func MonitorCancel(ctx *context.Context) {
+ pid := ctx.ParamsInt64("pid")
+ process.GetManager().Cancel(pid)
+ ctx.JSON(200, map[string]interface{}{
+ "redirect": ctx.Repo.RepoLink + "/admin/monitor",
+ })
+}
import (
"bytes"
"compress/gzip"
+ gocontext "context"
"fmt"
"net/http"
"os"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/log"
+ "code.gitea.io/gitea/modules/process"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/timeutil"
)
// set this for allow pre-receive and post-receive execute
h.environ = append(h.environ, "SSH_ORIGINAL_COMMAND="+service)
+ ctx, cancel := gocontext.WithCancel(git.DefaultContext)
+ defer cancel()
var stderr bytes.Buffer
- cmd := exec.Command(git.GitExecutable, service, "--stateless-rpc", h.dir)
+ cmd := exec.CommandContext(ctx, git.GitExecutable, service, "--stateless-rpc", h.dir)
cmd.Dir = h.dir
if service == "receive-pack" {
cmd.Env = append(os.Environ(), h.environ...)
cmd.Stdout = h.w
cmd.Stdin = reqBody
cmd.Stderr = &stderr
+
+ pid := process.GetManager().Add(fmt.Sprintf("%s %s %s [repo_path: %s]", git.GitExecutable, service, "--stateless-rpc", h.dir), cancel)
+ defer process.GetManager().Remove(pid)
+
if err := cmd.Run(); err != nil {
log.Error("Fail to serve RPC(%s): %v - %s", service, err, stderr.String())
return
m.Get("/config", admin.Config)
m.Post("/config/test_mail", admin.SendTestMail)
m.Get("/monitor", admin.Monitor)
+ m.Post("/monitor/cancel/:pid", admin.MonitorCancel)
m.Group("/users", func() {
m.Get("", admin.Users)
import (
"bufio"
"bytes"
+ "context"
"fmt"
"html"
"html/template"
return nil, err
}
+ // FIXME: graceful: These commands should likely have a timeout
+ ctx, cancel := context.WithCancel(git.DefaultContext)
+ defer cancel()
var cmd *exec.Cmd
if len(beforeCommitID) == 0 && commit.ParentCount() == 0 {
- cmd = exec.Command(git.GitExecutable, "show", afterCommitID)
+ cmd = exec.CommandContext(ctx, git.GitExecutable, "show", afterCommitID)
} else {
actualBeforeCommitID := beforeCommitID
if len(actualBeforeCommitID) == 0 {
}
diffArgs = append(diffArgs, actualBeforeCommitID)
diffArgs = append(diffArgs, afterCommitID)
- cmd = exec.Command(git.GitExecutable, diffArgs...)
+ cmd = exec.CommandContext(ctx, git.GitExecutable, diffArgs...)
beforeCommitID = actualBeforeCommitID
}
cmd.Dir = repoPath
return nil, fmt.Errorf("Start: %v", err)
}
- pid := process.GetManager().Add(fmt.Sprintf("GetDiffRange [repo_path: %s]", repoPath), cmd)
+ pid := process.GetManager().Add(fmt.Sprintf("GetDiffRange [repo_path: %s]", repoPath), cancel)
defer process.GetManager().Remove(pid)
diff, err := ParsePatch(maxLines, maxLineCharacters, maxFiles, stdout)
if len(file) > 0 {
fileArgs = append(fileArgs, "--", file)
}
+ // FIXME: graceful: These commands should have a timeout
+ ctx, cancel := context.WithCancel(git.DefaultContext)
+ defer cancel()
+
var cmd *exec.Cmd
switch diffType {
case RawDiffNormal:
if len(startCommit) != 0 {
- cmd = exec.Command(git.GitExecutable, append([]string{"diff", "-M", startCommit, endCommit}, fileArgs...)...)
+ cmd = exec.CommandContext(ctx, git.GitExecutable, append([]string{"diff", "-M", startCommit, endCommit}, fileArgs...)...)
} else if commit.ParentCount() == 0 {
- cmd = exec.Command(git.GitExecutable, append([]string{"show", endCommit}, fileArgs...)...)
+ cmd = exec.CommandContext(ctx, git.GitExecutable, append([]string{"show", endCommit}, fileArgs...)...)
} else {
c, _ := commit.Parent(0)
- cmd = exec.Command(git.GitExecutable, append([]string{"diff", "-M", c.ID.String(), endCommit}, fileArgs...)...)
+ cmd = exec.CommandContext(ctx, git.GitExecutable, append([]string{"diff", "-M", c.ID.String(), endCommit}, fileArgs...)...)
}
case RawDiffPatch:
if len(startCommit) != 0 {
query := fmt.Sprintf("%s...%s", endCommit, startCommit)
- cmd = exec.Command(git.GitExecutable, append([]string{"format-patch", "--no-signature", "--stdout", "--root", query}, fileArgs...)...)
+ cmd = exec.CommandContext(ctx, git.GitExecutable, append([]string{"format-patch", "--no-signature", "--stdout", "--root", query}, fileArgs...)...)
} else if commit.ParentCount() == 0 {
- cmd = exec.Command(git.GitExecutable, append([]string{"format-patch", "--no-signature", "--stdout", "--root", endCommit}, fileArgs...)...)
+ cmd = exec.CommandContext(ctx, git.GitExecutable, append([]string{"format-patch", "--no-signature", "--stdout", "--root", endCommit}, fileArgs...)...)
} else {
c, _ := commit.Parent(0)
query := fmt.Sprintf("%s...%s", endCommit, c.ID.String())
- cmd = exec.Command(git.GitExecutable, append([]string{"format-patch", "--no-signature", "--stdout", query}, fileArgs...)...)
+ cmd = exec.CommandContext(ctx, git.GitExecutable, append([]string{"format-patch", "--no-signature", "--stdout", query}, fileArgs...)...)
}
default:
return fmt.Errorf("invalid diffType: %s", diffType)
cmd.Dir = repoPath
cmd.Stdout = writer
cmd.Stderr = stderr
+ pid := process.GetManager().Add(fmt.Sprintf("GetRawDiffForFile: [repo_path: %s]", repoPath), cancel)
+ defer process.GetManager().Remove(pid)
+
if err = cmd.Run(); err != nil {
return fmt.Errorf("Run: %v - %s", err, stderr)
}
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/notification"
- "code.gitea.io/gitea/modules/process"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/sync"
"code.gitea.io/gitea/modules/timeutil"
gitArgs = append(gitArgs, "--prune")
}
- _, stderr, err := process.GetManager().ExecDir(
- timeout, repoPath, fmt.Sprintf("Mirror.runSync: %s", repoPath),
- git.GitExecutable, gitArgs...)
- if err != nil {
+ stdoutBuilder := strings.Builder{}
+ stderrBuilder := strings.Builder{}
+ if err := git.NewCommand(gitArgs...).
+ SetDescription(fmt.Sprintf("Mirror.runSync: %s", m.Repo.FullName())).
+ RunInDirTimeoutPipeline(timeout, repoPath, &stdoutBuilder, &stderrBuilder); err != nil {
+ stdout := stdoutBuilder.String()
+ stderr := stderrBuilder.String()
// sanitize the output, since it may contain the remote address, which may
// contain a password
- message, err := sanitizeOutput(stderr, repoPath)
+ stderrMessage, sanitizeErr := sanitizeOutput(stderr, repoPath)
+ if sanitizeErr != nil {
+ log.Error("sanitizeOutput failed on stderr: %v", sanitizeErr)
+ log.Error("Failed to update mirror repository %v:\nStdout: %s\nStderr: %s\nErr: %v", m.Repo, stdout, stderr, err)
+ return nil, false
+ }
+ stdoutMessage, err := sanitizeOutput(stdout, repoPath)
if err != nil {
- log.Error("sanitizeOutput: %v", err)
+ log.Error("sanitizeOutput failed: %v", sanitizeErr)
+ log.Error("Failed to update mirror repository %v:\nStdout: %s\nStderr: %s\nErr: %v", m.Repo, stdout, stderrMessage, err)
return nil, false
}
- desc := fmt.Sprintf("Failed to update mirror repository '%s': %s", repoPath, message)
- log.Error(desc)
+
+ log.Error("Failed to update mirror repository %v:\nStdout: %s\nStderr: %s\nErr: %v", m.Repo, stdoutMessage, stderrMessage, err)
+ desc := fmt.Sprintf("Failed to update mirror repository '%s': %s", repoPath, stderrMessage)
if err = models.CreateRepositoryNotice(desc); err != nil {
log.Error("CreateRepositoryNotice: %v", err)
}
return nil, false
}
- output := stderr
+ output := stderrBuilder.String()
gitRepo, err := git.OpenRepository(repoPath)
if err != nil {
}
if m.Repo.HasWiki() {
- if _, stderr, err := process.GetManager().ExecDir(
- timeout, wikiPath, fmt.Sprintf("Mirror.runSync: %s", wikiPath),
- git.GitExecutable, "remote", "update", "--prune"); err != nil {
+ stderrBuilder.Reset()
+ stdoutBuilder.Reset()
+ if err := git.NewCommand("remote", "update", "--prune").
+ SetDescription(fmt.Sprintf("Mirror.runSync Wiki: %s ", m.Repo.FullName())).
+ RunInDirTimeoutPipeline(timeout, wikiPath, &stdoutBuilder, &stderrBuilder); err != nil {
+ stdout := stdoutBuilder.String()
+ stderr := stderrBuilder.String()
// sanitize the output, since it may contain the remote address, which may
// contain a password
- message, err := sanitizeOutput(stderr, wikiPath)
+ stderrMessage, sanitizeErr := sanitizeOutput(stderr, repoPath)
+ if sanitizeErr != nil {
+ log.Error("sanitizeOutput failed on stderr: %v", sanitizeErr)
+ log.Error("Failed to update mirror repository wiki %v:\nStdout: %s\nStderr: %s\nErr: %v", m.Repo, stdout, stderr, err)
+ return nil, false
+ }
+ stdoutMessage, err := sanitizeOutput(stdout, repoPath)
if err != nil {
- log.Error("sanitizeOutput: %v", err)
+ log.Error("sanitizeOutput failed: %v", sanitizeErr)
+ log.Error("Failed to update mirror repository wiki %v:\nStdout: %s\nStderr: %s\nErr: %v", m.Repo, stdout, stderrMessage, err)
return nil, false
}
- desc := fmt.Sprintf("Failed to update mirror wiki repository '%s': %s", wikiPath, message)
- log.Error(desc)
+
+ log.Error("Failed to update mirror repository wiki %v:\nStdout: %s\nStderr: %s\nErr: %v", m.Repo, stdoutMessage, stderrMessage, err)
+ desc := fmt.Sprintf("Failed to update mirror repository wiki '%s': %s", wikiPath, stderrMessage)
if err = models.CreateRepositoryNotice(desc); err != nil {
log.Error("CreateRepositoryNotice: %v", err)
}
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/notification"
- "code.gitea.io/gitea/modules/process"
"code.gitea.io/gitea/modules/timeutil"
)
}
if delTag {
- _, stderr, err := process.GetManager().ExecDir(-1, repo.RepoPath(),
- fmt.Sprintf("DeleteReleaseByID (git tag -d): %d", rel.ID),
- git.GitExecutable, "tag", "-d", rel.TagName)
- if err != nil && !strings.Contains(stderr, "not found") {
- return fmt.Errorf("git tag -d: %v - %s", err, stderr)
+ if stdout, err := git.NewCommand("tag", "-d", rel.TagName).
+ SetDescription(fmt.Sprintf("DeleteReleaseByID (git tag -d): %d", rel.ID)).
+ RunInDir(repo.RepoPath()); err != nil && !strings.Contains(err.Error(), "not found") {
+ log.Error("DeleteReleaseByID (git tag -d): %d in %v Failed:\nStdout: %s\nError: %v", rel.ID, repo, stdout, err)
+ return fmt.Errorf("git tag -d: %v", err)
}
if err := models.DeleteReleaseByID(id); err != nil {
<th>{{.i18n.Tr "admin.monitor.desc"}}</th>
<th>{{.i18n.Tr "admin.monitor.start"}}</th>
<th>{{.i18n.Tr "admin.monitor.execute_time"}}</th>
+ <th></th>
</tr>
</thead>
<tbody>
<td>{{.Description}}</td>
<td>{{DateFmtLong .Start}}</td>
<td>{{TimeSince .Start $.Lang}}</td>
+ <td><a class="delete-button" href="" data-url="{{$.Link}}/cancel/{{.PID}}" data-id="{{.PID}}" data-name="{{.Description}}"><i class="close icon text red"></i></a></td>
</tr>
{{end}}
</tbody>
</div>
</div>
</div>
+<div class="ui small basic delete modal">
+ <div class="ui icon header">
+ <i class="close icon"></i>
+ {{.i18n.Tr "admin.monitor.process.cancel"}}
+ </div>
+ <div class="content">
+ <p>{{$.i18n.Tr "admin.monitor.process.cancel_notices" `<span class="name"></span>` | Safe}}</p>
+ <p>{{$.i18n.Tr "admin.monitor.process.cancel_desc"}}</p>
+ </div>
+ {{template "base/delete_modal_actions" .}}
+</div>
{{template "base/footer" .}}