mirror of
https://github.com/zyedidia/micro.git
synced 2026-03-10 06:40:24 +09:00
Micro's logic for periodic backup creation is racy and may cause spurious backups of unmodified buffers, at least for the following reasons: 1. When a buffer is closed, its backup is removed by the main goroutine, without any synchronization with the backup/save goroutine which creates periodic backups in the background. A part of the problem here is that the main goroutine removes the backup before setting b.fini to true, not after it, so the backup/save goroutine may start creating a new backup even after it has been removed by the main goroutine. But even if we move the b.RemoveBackup() call after setting b.fini, it will not solve the problem, since the backup/save goroutine may have already started creating a new periodic backup just before b.fini was set to true. 2. When a buffer is successfully saved and thus its backup is removed, if there was a periodic backup for this buffer requested by the main goroutine but not saved by the backup/save goroutine yet (i.e. this request is still pending in backupRequestChan), micro doesn't cancel this pending request, so a backup is unexpectedly saved a couple of seconds after the file itself was saved. Although usually this erroneous backup is removed later, when the buffer is closed. But if micro terminates abnormally and the buffer is not properly closed, this backup is not removed. Also if this issue occurs in combination with the race issue #1 described above, this backup may not be successfully removed either. So, to fix these issues: 1. Do the backup removal in the backup/save goroutine (at requests from the main goroutine), not directly in the main goroutine. 2. Make the communication between these goroutines fully synchronous: 2a. Instead of using the buffered channel backupRequestChan as a storage for pending requests for periodic backups, let the backup/save goroutine itself store this information, in the requestesBackups map. Then, backupRequestChan can be made non-buffered. 2b. Make saveRequestChan a non-buffered channel as well. (There was no point in making it buffered in the first place, actually.) Once both channels are non-buffered, the backup/save goroutine receives both backup and save requests from the main goroutine in exactly the same order as the main goroutine sends them, so we can guarantee that saving the buffer will cancel the previous pending backup request for this buffer.
407 lines
9.1 KiB
Go
407 lines
9.1 KiB
Go
package buffer
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"errors"
|
|
"io"
|
|
"io/fs"
|
|
"os"
|
|
"os/exec"
|
|
"os/signal"
|
|
"path/filepath"
|
|
"runtime"
|
|
"time"
|
|
"unicode"
|
|
|
|
"github.com/zyedidia/micro/v2/internal/config"
|
|
"github.com/zyedidia/micro/v2/internal/screen"
|
|
"github.com/zyedidia/micro/v2/internal/util"
|
|
"golang.org/x/text/transform"
|
|
)
|
|
|
|
// LargeFileThreshold is the number of bytes when fastdirty is forced
|
|
// because hashing is too slow
|
|
const LargeFileThreshold = 50000
|
|
|
|
type wrappedFile struct {
|
|
writeCloser io.WriteCloser
|
|
withSudo bool
|
|
screenb bool
|
|
cmd *exec.Cmd
|
|
sigChan chan os.Signal
|
|
}
|
|
|
|
type saveResponse struct {
|
|
size int
|
|
err error
|
|
}
|
|
|
|
type saveRequest struct {
|
|
buf *Buffer
|
|
path string
|
|
withSudo bool
|
|
newFile bool
|
|
saveResponseChan chan saveResponse
|
|
}
|
|
|
|
var saveRequestChan chan saveRequest
|
|
var backupRequestChan chan backupRequest
|
|
|
|
func init() {
|
|
// Both saveRequestChan and backupRequestChan need to be non-buffered
|
|
// so the save/backup goroutine receives both save and backup requests
|
|
// in the same order the main goroutine sends them.
|
|
saveRequestChan = make(chan saveRequest)
|
|
backupRequestChan = make(chan backupRequest)
|
|
|
|
go func() {
|
|
duration := backupSeconds * float64(time.Second)
|
|
backupTicker := time.NewTicker(time.Duration(duration))
|
|
|
|
for {
|
|
select {
|
|
case sr := <-saveRequestChan:
|
|
size, err := sr.buf.safeWrite(sr.path, sr.withSudo, sr.newFile)
|
|
sr.saveResponseChan <- saveResponse{size, err}
|
|
case br := <-backupRequestChan:
|
|
handleBackupRequest(br)
|
|
case <-backupTicker.C:
|
|
periodicBackup()
|
|
}
|
|
}
|
|
}()
|
|
}
|
|
|
|
func openFile(name string, withSudo bool) (wrappedFile, error) {
|
|
var err error
|
|
var writeCloser io.WriteCloser
|
|
var screenb bool
|
|
var cmd *exec.Cmd
|
|
var sigChan chan os.Signal
|
|
|
|
if withSudo {
|
|
cmd = exec.Command(config.GlobalSettings["sucmd"].(string), "dd", "bs=4k", "of="+name)
|
|
writeCloser, err = cmd.StdinPipe()
|
|
if err != nil {
|
|
return wrappedFile{}, err
|
|
}
|
|
|
|
sigChan = make(chan os.Signal, 1)
|
|
signal.Reset(os.Interrupt)
|
|
signal.Notify(sigChan, os.Interrupt)
|
|
|
|
screenb = screen.TempFini()
|
|
// need to start the process now, otherwise when we flush the file
|
|
// contents to its stdin it might hang because the kernel's pipe size
|
|
// is too small to handle the full file contents all at once
|
|
err = cmd.Start()
|
|
if err != nil {
|
|
screen.TempStart(screenb)
|
|
|
|
signal.Notify(util.Sigterm, os.Interrupt)
|
|
signal.Stop(sigChan)
|
|
|
|
return wrappedFile{}, err
|
|
}
|
|
} else {
|
|
writeCloser, err = os.OpenFile(name, os.O_WRONLY|os.O_CREATE, util.FileMode)
|
|
if err != nil {
|
|
return wrappedFile{}, err
|
|
}
|
|
}
|
|
|
|
return wrappedFile{writeCloser, withSudo, screenb, cmd, sigChan}, nil
|
|
}
|
|
|
|
func (wf wrappedFile) Write(b *SharedBuffer) (int, error) {
|
|
file := bufio.NewWriter(transform.NewWriter(wf.writeCloser, b.encoding.NewEncoder()))
|
|
|
|
b.Lock()
|
|
defer b.Unlock()
|
|
|
|
if len(b.lines) == 0 {
|
|
return 0, nil
|
|
}
|
|
|
|
// end of line
|
|
var eol []byte
|
|
if b.Endings == FFDos {
|
|
eol = []byte{'\r', '\n'}
|
|
} else {
|
|
eol = []byte{'\n'}
|
|
}
|
|
|
|
if !wf.withSudo {
|
|
f := wf.writeCloser.(*os.File)
|
|
err := f.Truncate(0)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
}
|
|
|
|
// write lines
|
|
size, err := file.Write(b.lines[0].data)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
for _, l := range b.lines[1:] {
|
|
if _, err = file.Write(eol); err != nil {
|
|
return 0, err
|
|
}
|
|
if _, err = file.Write(l.data); err != nil {
|
|
return 0, err
|
|
}
|
|
size += len(eol) + len(l.data)
|
|
}
|
|
|
|
err = file.Flush()
|
|
if err == nil && !wf.withSudo {
|
|
// Call Sync() on the file to make sure the content is safely on disk.
|
|
f := wf.writeCloser.(*os.File)
|
|
err = f.Sync()
|
|
}
|
|
return size, err
|
|
}
|
|
|
|
func (wf wrappedFile) Close() error {
|
|
err := wf.writeCloser.Close()
|
|
if wf.withSudo {
|
|
// wait for dd to finish and restart the screen if we used sudo
|
|
err := wf.cmd.Wait()
|
|
screen.TempStart(wf.screenb)
|
|
|
|
signal.Notify(util.Sigterm, os.Interrupt)
|
|
signal.Stop(wf.sigChan)
|
|
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return err
|
|
}
|
|
|
|
func (b *SharedBuffer) overwriteFile(name string) (int, error) {
|
|
file, err := openFile(name, false)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
size, err := file.Write(b)
|
|
|
|
err2 := file.Close()
|
|
if err2 != nil && err == nil {
|
|
err = err2
|
|
}
|
|
return size, err
|
|
}
|
|
|
|
// Save saves the buffer to its default path
|
|
func (b *Buffer) Save() error {
|
|
return b.SaveAs(b.Path)
|
|
}
|
|
|
|
// AutoSave saves the buffer to its default path
|
|
func (b *Buffer) AutoSave() error {
|
|
if !b.Modified() {
|
|
return nil
|
|
}
|
|
return b.saveToFile(b.Path, false, true)
|
|
}
|
|
|
|
// SaveAs saves the buffer to a specified path (filename), creating the file if it does not exist
|
|
func (b *Buffer) SaveAs(filename string) error {
|
|
return b.saveToFile(filename, false, false)
|
|
}
|
|
|
|
func (b *Buffer) SaveWithSudo() error {
|
|
return b.SaveAsWithSudo(b.Path)
|
|
}
|
|
|
|
func (b *Buffer) SaveAsWithSudo(filename string) error {
|
|
return b.saveToFile(filename, true, false)
|
|
}
|
|
|
|
func (b *Buffer) saveToFile(filename string, withSudo bool, autoSave bool) error {
|
|
var err error
|
|
if b.Type.Readonly {
|
|
return errors.New("Cannot save readonly buffer")
|
|
}
|
|
if b.Type.Scratch {
|
|
return errors.New("Cannot save scratch buffer")
|
|
}
|
|
if withSudo && runtime.GOOS == "windows" {
|
|
return errors.New("Save with sudo not supported on Windows")
|
|
}
|
|
|
|
if !autoSave && b.Settings["rmtrailingws"].(bool) {
|
|
for i, l := range b.lines {
|
|
leftover := util.CharacterCount(bytes.TrimRightFunc(l.data, unicode.IsSpace))
|
|
|
|
linelen := util.CharacterCount(l.data)
|
|
b.Remove(Loc{leftover, i}, Loc{linelen, i})
|
|
}
|
|
|
|
b.RelocateCursors()
|
|
}
|
|
|
|
if b.Settings["eofnewline"].(bool) {
|
|
end := b.End()
|
|
if b.RuneAt(Loc{end.X - 1, end.Y}) != '\n' {
|
|
b.insert(end, []byte{'\n'})
|
|
}
|
|
}
|
|
|
|
filename, err = util.ReplaceHome(filename)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
newFile := false
|
|
fileInfo, err := os.Stat(filename)
|
|
if err != nil {
|
|
if !errors.Is(err, fs.ErrNotExist) {
|
|
return err
|
|
}
|
|
newFile = true
|
|
}
|
|
if err == nil && fileInfo.IsDir() {
|
|
return errors.New("Error: " + filename + " is a directory and cannot be saved")
|
|
}
|
|
if err == nil && !fileInfo.Mode().IsRegular() {
|
|
return errors.New("Error: " + filename + " is not a regular file and cannot be saved")
|
|
}
|
|
|
|
absFilename, err := filepath.Abs(filename)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Get the leading path to the file | "." is returned if there's no leading path provided
|
|
if dirname := filepath.Dir(absFilename); dirname != "." {
|
|
// Check if the parent dirs don't exist
|
|
if _, statErr := os.Stat(dirname); errors.Is(statErr, fs.ErrNotExist) {
|
|
// Prompt to make sure they want to create the dirs that are missing
|
|
if b.Settings["mkparents"].(bool) {
|
|
// Create all leading dir(s) since they don't exist
|
|
if mkdirallErr := os.MkdirAll(dirname, os.ModePerm); mkdirallErr != nil {
|
|
// If there was an error creating the dirs
|
|
return mkdirallErr
|
|
}
|
|
} else {
|
|
return errors.New("Parent dirs don't exist, enable 'mkparents' for auto creation")
|
|
}
|
|
}
|
|
}
|
|
|
|
saveResponseChan := make(chan saveResponse)
|
|
saveRequestChan <- saveRequest{b, absFilename, withSudo, newFile, saveResponseChan}
|
|
result := <-saveResponseChan
|
|
err = result.err
|
|
if err != nil {
|
|
if errors.Is(err, util.ErrOverwrite) {
|
|
screen.TermMessage(err)
|
|
err = errors.Unwrap(err)
|
|
|
|
b.UpdateModTime()
|
|
}
|
|
return err
|
|
}
|
|
|
|
if !b.Settings["fastdirty"].(bool) {
|
|
if result.size > LargeFileThreshold {
|
|
// For large files 'fastdirty' needs to be on
|
|
b.Settings["fastdirty"] = true
|
|
} else {
|
|
b.calcHash(&b.origHash)
|
|
}
|
|
}
|
|
|
|
newPath := b.Path != filename
|
|
b.Path = filename
|
|
b.AbsPath = absFilename
|
|
b.isModified = false
|
|
b.UpdateModTime()
|
|
|
|
if newPath {
|
|
// need to update glob-based and filetype-based settings
|
|
b.ReloadSettings(true)
|
|
}
|
|
|
|
err = b.Serialize()
|
|
return err
|
|
}
|
|
|
|
func (b *SharedBuffer) writeBackup(path string) (string, error) {
|
|
backupDir := b.backupDir()
|
|
if _, err := os.Stat(backupDir); err != nil {
|
|
if !errors.Is(err, fs.ErrNotExist) {
|
|
return "", err
|
|
}
|
|
if err = os.Mkdir(backupDir, os.ModePerm); err != nil {
|
|
return "", err
|
|
}
|
|
}
|
|
|
|
backupName := util.DetermineEscapePath(backupDir, path)
|
|
_, err := b.overwriteFile(backupName)
|
|
if err != nil {
|
|
os.Remove(backupName)
|
|
return "", err
|
|
}
|
|
|
|
return backupName, nil
|
|
}
|
|
|
|
// safeWrite writes the buffer to a file in a "safe" way, preventing loss of the
|
|
// contents of the file if it fails to write the new contents.
|
|
// This means that the file is not overwritten directly but by writing to the
|
|
// backup file first.
|
|
func (b *SharedBuffer) safeWrite(path string, withSudo bool, newFile bool) (int, error) {
|
|
file, err := openFile(path, withSudo)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
defer func() {
|
|
if newFile && err != nil {
|
|
os.Remove(path)
|
|
}
|
|
}()
|
|
|
|
|
|
// Try to backup first before writing
|
|
backupName, err := b.writeBackup(path)
|
|
if err != nil {
|
|
file.Close()
|
|
return 0, err
|
|
}
|
|
|
|
// Backup saved, so cancel pending periodic backup, if any
|
|
delete(requestedBackups, b)
|
|
|
|
b.forceKeepBackup = true
|
|
size := 0
|
|
{
|
|
// If we failed to write or close, keep the backup we made
|
|
size, err = file.Write(b)
|
|
if err != nil {
|
|
file.Close()
|
|
return 0, util.OverwriteError{err, backupName}
|
|
}
|
|
|
|
err = file.Close()
|
|
if err != nil {
|
|
return 0, util.OverwriteError{err, backupName}
|
|
}
|
|
}
|
|
b.forceKeepBackup = false
|
|
|
|
if !b.keepBackup() {
|
|
os.Remove(backupName)
|
|
}
|
|
|
|
return size, err
|
|
}
|