@@ -3,12 +3,15 @@ package main
33import (
44 "bufio"
55 "bytes"
6+ "context"
67 "crypto/sha256"
78 "fmt"
89 "io"
910 "os"
11+ "os/signal"
1012 "path/filepath"
1113 "strings"
14+ "syscall"
1215
1316 "github.com/getsops/sops/v3"
1417 "github.com/getsops/sops/v3/cmd/sops/codes"
@@ -96,6 +99,24 @@ func edit(opts editOpts) ([]byte, error) {
9699 return editTree (opts , tree , dataKey )
97100}
98101
102+ type cancelError struct {}
103+
104+ func (err * cancelError ) Error () string {
105+ return "User canceled operation"
106+ }
107+
108+ type editTreeResult struct {
109+ value []byte
110+ err error
111+ }
112+
113+ func createError (err error ) editTreeResult {
114+ return editTreeResult {
115+ value : nil ,
116+ err : err ,
117+ }
118+ }
119+
99120func editTree (opts editOpts , tree * sops.Tree , dataKey []byte ) ([]byte , error ) {
100121 // Create temporary file for editing
101122 tmpdir , err := os .MkdirTemp ("" , "" )
@@ -117,33 +138,58 @@ func editTree(opts editOpts, tree *sops.Tree, dataKey []byte) ([]byte, error) {
117138
118139 tmpfileName := tmpfile .Name ()
119140
141+ // Catch when the user presses Ctrl+C, or kills SOPS.
142+ ctx , stop := signal .NotifyContext (context .Background (), os .Interrupt , syscall .SIGINT , syscall .SIGTERM , syscall .SIGKILL )
143+ defer stop ()
144+
145+ result := make (chan editTreeResult , 1 )
146+
147+ // This goroutine handles signals that exit SOPS, that usually lead to termination
148+ // before editTree() can clean up the temporary directory and file.
149+ go func () {
150+ <- ctx .Done ()
151+ result <- createError (& cancelError {})
152+ }()
153+
154+ // This goroutine handles regular execution of editing.
155+ go func () {
156+ result <- editTreeImpl (tmpfile , tmpfileName , opts , tree , dataKey )
157+ }()
158+
159+ // Wait until the first result shows up (either an exit is requested, or editTreeImpl returns).
160+ res := <- result
161+ return res .value , res .err
162+ }
163+
164+ func editTreeImpl (tmpfile * os.File , tmpfileName string , opts editOpts , tree * sops.Tree , dataKey []byte ) editTreeResult {
120165 // Write to temporary file
121166 var out []byte
167+ var err error
122168 if opts .ShowMasterKeys {
123169 out , err = opts .OutputStore .EmitEncryptedFile (* tree )
124170 } else {
125171 out , err = opts .OutputStore .EmitPlainFile (tree .Branches )
126172 }
127173 if err != nil {
128- return nil , common .NewExitError (fmt .Sprintf ("Could not marshal tree: %s" , err ), codes .ErrorDumpingTree )
174+ return createError ( common .NewExitError (fmt .Sprintf ("Could not marshal tree: %s" , err ), codes .ErrorDumpingTree ) )
129175 }
130176 _ , err = tmpfile .Write (out )
131177 if err != nil {
132- return nil , common .NewExitError (fmt .Sprintf ("Could not write output file: %s" , err ), codes .CouldNotWriteOutputFile )
178+ return createError ( common .NewExitError (fmt .Sprintf ("Could not write output file: %s" , err ), codes .CouldNotWriteOutputFile ) )
133179 }
134180
135181 // Compute file hash to detect if the file has been edited
136182 origHash , err := hashFile (tmpfileName )
137183 if err != nil {
138- return nil , common .NewExitError (fmt .Sprintf ("Could not hash file: %s" , err ), codes .CouldNotReadInputFile )
184+ return createError ( common .NewExitError (fmt .Sprintf ("Could not hash file: %s" , err ), codes .CouldNotReadInputFile ) )
139185 }
140186
141187 // Close the temporary file, so that an editor can open it.
142188 // We need to do this because some editors (e.g. VSCode) will refuse to
143189 // open a file on Windows due to the Go standard library not opening
144190 // files with shared delete access.
145191 if err := tmpfile .Close (); err != nil {
146- return nil , err
192+ return createError ( err )
147193 }
148194
149195 // Let the user edit the file
@@ -155,23 +201,26 @@ func editTree(opts editOpts, tree *sops.Tree, dataKey []byte) ([]byte, error) {
155201 ShowMasterKeys : opts .ShowMasterKeys ,
156202 Tree : tree })
157203 if err != nil {
158- return nil , err
204+ return createError ( err )
159205 }
160206
161207 // Encrypt the file
162208 err = common .EncryptTree (common.EncryptTreeOpts {
163209 DataKey : dataKey , Tree : tree , Cipher : opts .Cipher ,
164210 })
165211 if err != nil {
166- return nil , err
212+ return createError ( err )
167213 }
168214
169215 // Output the file
170216 encryptedFile , err := opts .OutputStore .EmitEncryptedFile (* tree )
171217 if err != nil {
172- return nil , common .NewExitError (fmt .Sprintf ("Could not marshal tree: %s" , err ), codes .ErrorDumpingTree )
218+ return createError (common .NewExitError (fmt .Sprintf ("Could not marshal tree: %s" , err ), codes .ErrorDumpingTree ))
219+ }
220+ return editTreeResult {
221+ value : encryptedFile ,
222+ err : nil ,
173223 }
174- return encryptedFile , nil
175224}
176225
177226const pressKeyMsg = "Press enter to return to the editor, or Ctrl+C to exit."
0 commit comments