up.go 3.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137
  1. /*
  2. Copyright 2020 Docker Compose CLI authors
  3. Licensed under the Apache License, Version 2.0 (the "License");
  4. you may not use this file except in compliance with the License.
  5. You may obtain a copy of the License at
  6. http://www.apache.org/licenses/LICENSE-2.0
  7. Unless required by applicable law or agreed to in writing, software
  8. distributed under the License is distributed on an "AS IS" BASIS,
  9. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  10. See the License for the specific language governing permissions and
  11. limitations under the License.
  12. */
  13. package compose
  14. import (
  15. "context"
  16. "fmt"
  17. "os"
  18. "os/signal"
  19. "sync"
  20. "syscall"
  21. "github.com/compose-spec/compose-go/types"
  22. "github.com/docker/cli/cli"
  23. "github.com/docker/compose/v2/internal/tracing"
  24. "github.com/docker/compose/v2/pkg/api"
  25. "github.com/docker/compose/v2/pkg/progress"
  26. "github.com/hashicorp/go-multierror"
  27. )
  28. func (s *composeService) Up(ctx context.Context, project *types.Project, options api.UpOptions) error {
  29. err := progress.Run(ctx, tracing.SpanWrapFunc("project/up", tracing.ProjectOptions(project), func(ctx context.Context) error {
  30. err := s.create(ctx, project, options.Create)
  31. if err != nil {
  32. return err
  33. }
  34. if options.Start.Attach == nil {
  35. return s.start(ctx, project.Name, options.Start, nil)
  36. }
  37. return nil
  38. }), s.stdinfo())
  39. if err != nil {
  40. return err
  41. }
  42. if options.Start.Attach == nil {
  43. return err
  44. }
  45. if s.dryRun {
  46. fmt.Fprintln(s.stdout(), "end of 'compose up' output, interactive run is not supported in dry-run mode")
  47. return err
  48. }
  49. // if we get a second signal during shutdown, we kill the services
  50. // immediately, so the channel needs to have sufficient capacity or
  51. // we might miss a signal while setting up the second channel read
  52. // (this is also why signal.Notify is used vs signal.NotifyContext)
  53. signalChan := make(chan os.Signal, 2)
  54. signal.Notify(signalChan, syscall.SIGINT, syscall.SIGTERM)
  55. signalCancel := sync.OnceFunc(func() {
  56. signal.Stop(signalChan)
  57. close(signalChan)
  58. })
  59. defer signalCancel()
  60. printer := newLogPrinter(options.Start.Attach)
  61. stopFunc := func() error {
  62. fmt.Fprintln(s.stdinfo(), "Aborting on container exit...")
  63. ctx := context.Background()
  64. return progress.Run(ctx, func(ctx context.Context) error {
  65. // race two goroutines - one that blocks until another signal is received
  66. // and then does a Kill() and one that immediately starts a friendly Stop()
  67. errCh := make(chan error, 1)
  68. go func() {
  69. if _, ok := <-signalChan; !ok {
  70. // channel closed, so the outer function is done, which
  71. // means the other goroutine (calling Stop()) finished
  72. return
  73. }
  74. errCh <- s.Kill(ctx, project.Name, api.KillOptions{
  75. Services: options.Create.Services,
  76. Project: project,
  77. })
  78. }()
  79. go func() {
  80. errCh <- s.Stop(ctx, project.Name, api.StopOptions{
  81. Services: options.Create.Services,
  82. Project: project,
  83. })
  84. }()
  85. return <-errCh
  86. }, s.stdinfo())
  87. }
  88. var isTerminated bool
  89. var eg multierror.Group
  90. eg.Go(func() error {
  91. if _, ok := <-signalChan; !ok {
  92. // function finished without receiving a signal
  93. return nil
  94. }
  95. isTerminated = true
  96. printer.Cancel()
  97. fmt.Fprintln(s.stdinfo(), "Gracefully stopping... (press Ctrl+C again to force)")
  98. return stopFunc()
  99. })
  100. var exitCode int
  101. eg.Go(func() error {
  102. code, err := printer.Run(options.Start.CascadeStop, options.Start.ExitCodeFrom, stopFunc)
  103. exitCode = code
  104. return err
  105. })
  106. err = s.start(ctx, project.Name, options.Start, printer.HandleEvent)
  107. if err != nil && !isTerminated { // Ignore error if the process is terminated
  108. return err
  109. }
  110. // signal for the goroutines to stop & wait for them to finish any remaining work
  111. signalCancel()
  112. printer.Stop()
  113. err = eg.Wait().ErrorOrNil()
  114. if exitCode != 0 {
  115. errMsg := ""
  116. if err != nil {
  117. errMsg = err.Error()
  118. }
  119. return cli.StatusError{StatusCode: exitCode, Status: errMsg}
  120. }
  121. return err
  122. }