Merge pull request #3822 from dmaluka/fix-spurious-backups

Fix spurious backups of unmodified files
This commit is contained in:
Dmytro Maluka
2025-08-11 22:30:25 +02:00
committed by GitHub
6 changed files with 143 additions and 133 deletions

View File

@@ -352,9 +352,11 @@ func main() {
} else { } else {
fmt.Println("Micro encountered an error:", errors.Wrap(err, 2).ErrorStack(), "\nIf you can reproduce this error, please report it at https://github.com/zyedidia/micro/issues") fmt.Println("Micro encountered an error:", errors.Wrap(err, 2).ErrorStack(), "\nIf you can reproduce this error, please report it at https://github.com/zyedidia/micro/issues")
} }
// backup all open buffers // immediately backup all buffers with unsaved changes
for _, b := range buffer.OpenBuffers { for _, b := range buffer.OpenBuffers {
b.Backup() if b.Modified() {
b.Backup()
}
} }
exit(1) exit(1)
} }
@@ -489,8 +491,6 @@ func DoEvent() {
} }
case f := <-timerChan: case f := <-timerChan:
f() f()
case b := <-buffer.BackupCompleteChan:
b.RequestedBackup = false
case <-sighup: case <-sighup:
exit(0) exit(0)
case <-util.Sigterm: case <-util.Sigterm:

View File

@@ -55,9 +55,11 @@ func startup(args []string) (tcell.SimulationScreen, error) {
if err := recover(); err != nil { if err := recover(); err != nil {
screen.Screen.Fini() screen.Screen.Fini()
fmt.Println("Micro encountered an error:", err) fmt.Println("Micro encountered an error:", err)
// backup all open buffers // immediately backup all buffers with unsaved changes
for _, b := range buffer.OpenBuffers { for _, b := range buffer.OpenBuffers {
b.Backup() if b.Modified() {
b.Backup()
}
} }
// Print the stack trace too // Print the stack trace too
log.Fatalf(errors.Wrap(err, 2).ErrorStack()) log.Fatalf(errors.Wrap(err, 2).ErrorStack())

View File

@@ -34,24 +34,53 @@ Options: [r]ecover, [i]gnore, [a]bort: `
const backupSeconds = 8 const backupSeconds = 8
var BackupCompleteChan chan *Buffer type backupRequestType int
func init() { const (
BackupCompleteChan = make(chan *Buffer, 10) backupCreate = iota
backupRemove
)
type backupRequest struct {
buf *SharedBuffer
reqType backupRequestType
} }
func (b *Buffer) RequestBackup() { var requestedBackups map[*SharedBuffer]bool
if !b.RequestedBackup {
select { func init() {
case backupRequestChan <- b: requestedBackups = make(map[*SharedBuffer]bool)
default: }
// channel is full
} func (b *SharedBuffer) RequestBackup() {
b.RequestedBackup = true backupRequestChan <- backupRequest{buf: b, reqType: backupCreate}
}
func (b *SharedBuffer) CancelBackup() {
backupRequestChan <- backupRequest{buf: b, reqType: backupRemove}
}
func handleBackupRequest(br backupRequest) {
switch br.reqType {
case backupCreate:
// schedule periodic backup
requestedBackups[br.buf] = true
case backupRemove:
br.buf.RemoveBackup()
delete(requestedBackups, br.buf)
} }
} }
func (b *Buffer) backupDir() string { func periodicBackup() {
for buf := range requestedBackups {
err := buf.Backup()
if err == nil {
delete(requestedBackups, buf)
}
}
}
func (b *SharedBuffer) backupDir() string {
backupdir, err := util.ReplaceHome(b.Settings["backupdir"].(string)) backupdir, err := util.ReplaceHome(b.Settings["backupdir"].(string))
if backupdir == "" || err != nil { if backupdir == "" || err != nil {
backupdir = filepath.Join(config.ConfigDir, "backups") backupdir = filepath.Join(config.ConfigDir, "backups")
@@ -59,50 +88,51 @@ func (b *Buffer) backupDir() string {
return backupdir return backupdir
} }
func (b *Buffer) keepBackup() bool { func (b *SharedBuffer) keepBackup() bool {
return b.forceKeepBackup || b.Settings["permbackup"].(bool) return b.forceKeepBackup || b.Settings["permbackup"].(bool)
} }
// Backup saves the current buffer to the backups directory func (b *SharedBuffer) writeBackup(path string) (string, error) {
func (b *Buffer) Backup() error {
if !b.Settings["backup"].(bool) || b.Path == "" || b.Type != BTDefault {
return nil
}
backupdir := b.backupDir() backupdir := b.backupDir()
if _, err := os.Stat(backupdir); errors.Is(err, fs.ErrNotExist) { if _, err := os.Stat(backupdir); err != nil {
os.Mkdir(backupdir, os.ModePerm) if !errors.Is(err, fs.ErrNotExist) {
} return "", err
}
name := util.DetermineEscapePath(backupdir, b.AbsPath) if err = os.Mkdir(backupdir, os.ModePerm); err != nil {
if _, err := os.Stat(name); errors.Is(err, fs.ErrNotExist) { return "", err
_, err = b.overwriteFile(name)
if err == nil {
BackupCompleteChan <- b
} }
return err
} }
name := util.DetermineEscapePath(backupdir, path)
tmp := util.AppendBackupSuffix(name) tmp := util.AppendBackupSuffix(name)
_, err := b.overwriteFile(tmp) _, err := b.overwriteFile(tmp)
if err != nil { if err != nil {
os.Remove(tmp) os.Remove(tmp)
return err return name, err
} }
err = os.Rename(tmp, name) err = os.Rename(tmp, name)
if err != nil { if err != nil {
os.Remove(tmp) os.Remove(tmp)
return err return name, err
} }
BackupCompleteChan <- b return name, nil
}
// Backup saves the buffer to the backups directory
func (b *SharedBuffer) Backup() error {
if !b.Settings["backup"].(bool) || b.Path == "" || b.Type != BTDefault {
return nil
}
_, err := b.writeBackup(b.AbsPath)
return err return err
} }
// RemoveBackup removes any backup file associated with this buffer // RemoveBackup removes any backup file associated with this buffer
func (b *Buffer) RemoveBackup() { func (b *SharedBuffer) RemoveBackup() {
if !b.Settings["backup"].(bool) || b.keepBackup() || b.Path == "" || b.Type != BTDefault { if b.keepBackup() || b.Path == "" || b.Type != BTDefault {
return return
} }
f := util.DetermineEscapePath(b.backupDir(), b.AbsPath) f := util.DetermineEscapePath(b.backupDir(), b.AbsPath)
@@ -111,7 +141,7 @@ func (b *Buffer) RemoveBackup() {
// ApplyBackup applies the corresponding backup file to this buffer (if one exists) // ApplyBackup applies the corresponding backup file to this buffer (if one exists)
// Returns true if a backup was applied // Returns true if a backup was applied
func (b *Buffer) ApplyBackup(fsize int64) (bool, bool) { func (b *SharedBuffer) ApplyBackup(fsize int64) (bool, bool) {
if b.Settings["backup"].(bool) && !b.Settings["permbackup"].(bool) && len(b.Path) > 0 && b.Type == BTDefault { if b.Settings["backup"].(bool) && !b.Settings["permbackup"].(bool) && len(b.Path) > 0 && b.Type == BTDefault {
backupfile := util.DetermineEscapePath(b.backupDir(), b.AbsPath) backupfile := util.DetermineEscapePath(b.backupDir(), b.AbsPath)
if info, err := os.Stat(backupfile); err == nil { if info, err := os.Stat(backupfile); err == nil {
@@ -125,7 +155,7 @@ func (b *Buffer) ApplyBackup(fsize int64) (bool, bool) {
if choice%3 == 0 { if choice%3 == 0 {
// recover // recover
b.LineArray = NewLineArray(uint64(fsize), FFAuto, backup) b.LineArray = NewLineArray(uint64(fsize), FFAuto, backup)
b.isModified = true b.setModified()
return true, true return true, true
} else if choice%3 == 1 { } else if choice%3 == 1 {
// delete // delete

View File

@@ -13,7 +13,6 @@ import (
"strconv" "strconv"
"strings" "strings"
"sync" "sync"
"sync/atomic"
"time" "time"
luar "layeh.com/gopher-luar" luar "layeh.com/gopher-luar"
@@ -101,7 +100,6 @@ type SharedBuffer struct {
diffLock sync.RWMutex diffLock sync.RWMutex
diff map[int]DiffStatus diff map[int]DiffStatus
RequestedBackup bool
forceKeepBackup bool forceKeepBackup bool
// ReloadDisabled allows the user to disable reloads if they // ReloadDisabled allows the user to disable reloads if they
@@ -126,20 +124,62 @@ type SharedBuffer struct {
} }
func (b *SharedBuffer) insert(pos Loc, value []byte) { func (b *SharedBuffer) insert(pos Loc, value []byte) {
b.isModified = true
b.HasSuggestions = false b.HasSuggestions = false
b.LineArray.insert(pos, value) b.LineArray.insert(pos, value)
b.setModified()
inslines := bytes.Count(value, []byte{'\n'}) inslines := bytes.Count(value, []byte{'\n'})
b.MarkModified(pos.Y, pos.Y+inslines) b.MarkModified(pos.Y, pos.Y+inslines)
} }
func (b *SharedBuffer) remove(start, end Loc) []byte { func (b *SharedBuffer) remove(start, end Loc) []byte {
b.isModified = true
b.HasSuggestions = false b.HasSuggestions = false
defer b.setModified()
defer b.MarkModified(start.Y, end.Y) defer b.MarkModified(start.Y, end.Y)
return b.LineArray.remove(start, end) return b.LineArray.remove(start, end)
} }
func (b *SharedBuffer) setModified() {
if b.Type.Scratch {
return
}
if b.Settings["fastdirty"].(bool) {
b.isModified = true
} else {
var buff [md5.Size]byte
b.calcHash(&buff)
b.isModified = buff != b.origHash
}
if b.isModified {
b.RequestBackup()
} else {
b.CancelBackup()
}
}
// calcHash calculates md5 hash of all lines in the buffer
func (b *SharedBuffer) calcHash(out *[md5.Size]byte) {
h := md5.New()
if len(b.lines) > 0 {
h.Write(b.lines[0].data)
for _, l := range b.lines[1:] {
if b.Endings == FFDos {
h.Write([]byte{'\r', '\n'})
} else {
h.Write([]byte{'\n'})
}
h.Write(l.data)
}
}
h.Sum((*out)[:0])
}
// MarkModified marks the buffer as modified for this frame // MarkModified marks the buffer as modified for this frame
// and performs rehighlighting if syntax highlighting is enabled // and performs rehighlighting if syntax highlighting is enabled
func (b *SharedBuffer) MarkModified(start, end int) { func (b *SharedBuffer) MarkModified(start, end int) {
@@ -187,7 +227,6 @@ type Buffer struct {
*EventHandler *EventHandler
*SharedBuffer *SharedBuffer
fini int32
cursors []*Cursor cursors []*Cursor
curCursor int curCursor int
StartCursor Loc StartCursor Loc
@@ -416,7 +455,7 @@ func NewBuffer(r io.Reader, size int64, path string, startcursor Loc, btype BufT
} else if !hasBackup { } else if !hasBackup {
// since applying a backup does not save the applied backup to disk, we should // since applying a backup does not save the applied backup to disk, we should
// not calculate the original hash based on the backup data // not calculate the original hash based on the backup data
calcHash(b, &b.origHash) b.calcHash(&b.origHash)
} }
} }
@@ -458,13 +497,11 @@ func (b *Buffer) Fini() {
if !b.Modified() { if !b.Modified() {
b.Serialize() b.Serialize()
} }
b.RemoveBackup() b.CancelBackup()
if b.Type == BTStdout { if b.Type == BTStdout {
fmt.Fprint(util.Stdout, string(b.Bytes())) fmt.Fprint(util.Stdout, string(b.Bytes()))
} }
atomic.StoreInt32(&(b.fini), int32(1))
} }
// GetName returns the name that should be displayed in the statusline // GetName returns the name that should be displayed in the statusline
@@ -494,8 +531,6 @@ func (b *Buffer) Insert(start Loc, text string) {
b.EventHandler.cursors = b.cursors b.EventHandler.cursors = b.cursors
b.EventHandler.active = b.curCursor b.EventHandler.active = b.curCursor
b.EventHandler.Insert(start, text) b.EventHandler.Insert(start, text)
b.RequestBackup()
} }
} }
@@ -505,8 +540,6 @@ func (b *Buffer) Remove(start, end Loc) {
b.EventHandler.cursors = b.cursors b.EventHandler.cursors = b.cursors
b.EventHandler.active = b.curCursor b.EventHandler.active = b.curCursor
b.EventHandler.Remove(start, end) b.EventHandler.Remove(start, end)
b.RequestBackup()
} }
} }
@@ -558,7 +591,7 @@ func (b *Buffer) ReOpen() error {
if len(data) > LargeFileThreshold { if len(data) > LargeFileThreshold {
b.Settings["fastdirty"] = true b.Settings["fastdirty"] = true
} else { } else {
calcHash(b, &b.origHash) b.calcHash(&b.origHash)
} }
} }
b.isModified = false b.isModified = false
@@ -633,18 +666,7 @@ func (b *Buffer) Shared() bool {
// Modified returns if this buffer has been modified since // Modified returns if this buffer has been modified since
// being opened // being opened
func (b *Buffer) Modified() bool { func (b *Buffer) Modified() bool {
if b.Type.Scratch { return b.isModified
return false
}
if b.Settings["fastdirty"].(bool) {
return b.isModified
}
var buff [md5.Size]byte
calcHash(b, &buff)
return buff != b.origHash
} }
// Size returns the number of bytes in the current buffer // Size returns the number of bytes in the current buffer
@@ -663,26 +685,6 @@ func (b *Buffer) Size() int {
return nb return nb
} }
// calcHash calculates md5 hash of all lines in the buffer
func calcHash(b *Buffer, out *[md5.Size]byte) {
h := md5.New()
if len(b.lines) > 0 {
h.Write(b.lines[0].data)
for _, l := range b.lines[1:] {
if b.Endings == FFDos {
h.Write([]byte{'\r', '\n'})
} else {
h.Write([]byte{'\n'})
}
h.Write(l.data)
}
}
h.Sum((*out)[:0])
}
func parseDefFromFile(f config.RuntimeFile, header *highlight.Header) *highlight.Def { func parseDefFromFile(f config.RuntimeFile, header *highlight.Header) *highlight.Def {
data, err := f.Data() data, err := f.Data()
if err != nil { if err != nil {
@@ -1233,7 +1235,6 @@ func (b *Buffer) FindMatchingBrace(start Loc) (Loc, bool, bool) {
func (b *Buffer) Retab() { func (b *Buffer) Retab() {
toSpaces := b.Settings["tabstospaces"].(bool) toSpaces := b.Settings["tabstospaces"].(bool)
tabsize := util.IntOpt(b.Settings["tabsize"]) tabsize := util.IntOpt(b.Settings["tabsize"])
dirty := false
for i := 0; i < b.LinesNum(); i++ { for i := 0; i < b.LinesNum(); i++ {
l := b.LineBytes(i) l := b.LineBytes(i)
@@ -1254,10 +1255,9 @@ func (b *Buffer) Retab() {
b.Unlock() b.Unlock()
b.MarkModified(i, i) b.MarkModified(i, i)
dirty = true
} }
b.isModified = dirty b.setModified()
} }
// ParseCursorLocation turns a cursor location like 10:5 (LINE:COL) // ParseCursorLocation turns a cursor location like 10:5 (LINE:COL)

View File

@@ -11,7 +11,6 @@ import (
"os/signal" "os/signal"
"path/filepath" "path/filepath"
"runtime" "runtime"
"sync/atomic"
"time" "time"
"unicode" "unicode"
@@ -47,11 +46,14 @@ type saveRequest struct {
} }
var saveRequestChan chan saveRequest var saveRequestChan chan saveRequest
var backupRequestChan chan *Buffer var backupRequestChan chan backupRequest
func init() { func init() {
saveRequestChan = make(chan saveRequest, 10) // Both saveRequestChan and backupRequestChan need to be non-buffered
backupRequestChan = make(chan *Buffer, 10) // 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() { go func() {
duration := backupSeconds * float64(time.Second) duration := backupSeconds * float64(time.Second)
@@ -62,14 +64,10 @@ func init() {
case sr := <-saveRequestChan: case sr := <-saveRequestChan:
size, err := sr.buf.safeWrite(sr.path, sr.withSudo, sr.newFile) size, err := sr.buf.safeWrite(sr.path, sr.withSudo, sr.newFile)
sr.saveResponseChan <- saveResponse{size, err} sr.saveResponseChan <- saveResponse{size, err}
case br := <-backupRequestChan:
handleBackupRequest(br)
case <-backupTicker.C: case <-backupTicker.C:
for len(backupRequestChan) > 0 { periodicBackup()
b := <-backupRequestChan
bfini := atomic.LoadInt32(&(b.fini)) != 0
if !bfini {
b.Backup()
}
}
} }
} }
}() }()
@@ -116,7 +114,7 @@ func openFile(name string, withSudo bool) (wrappedFile, error) {
return wrappedFile{writeCloser, withSudo, screenb, cmd, sigChan}, nil return wrappedFile{writeCloser, withSudo, screenb, cmd, sigChan}, nil
} }
func (wf wrappedFile) Write(b *Buffer) (int, error) { func (wf wrappedFile) Write(b *SharedBuffer) (int, error) {
file := bufio.NewWriter(transform.NewWriter(wf.writeCloser, b.encoding.NewEncoder())) file := bufio.NewWriter(transform.NewWriter(wf.writeCloser, b.encoding.NewEncoder()))
b.Lock() b.Lock()
@@ -184,7 +182,7 @@ func (wf wrappedFile) Close() error {
return err return err
} }
func (b *Buffer) overwriteFile(name string) (int, error) { func (b *SharedBuffer) overwriteFile(name string) (int, error) {
file, err := openFile(name, false) file, err := openFile(name, false)
if err != nil { if err != nil {
return 0, err return 0, err
@@ -206,9 +204,7 @@ func (b *Buffer) Save() error {
// AutoSave saves the buffer to its default path // AutoSave saves the buffer to its default path
func (b *Buffer) AutoSave() error { func (b *Buffer) AutoSave() error {
// Doing full b.Modified() check every time would be costly, due to the hash if !b.Modified() {
// calculation. So use just isModified even if fastdirty is not set.
if !b.isModified {
return nil return nil
} }
return b.saveToFile(b.Path, false, true) return b.saveToFile(b.Path, false, true)
@@ -318,7 +314,7 @@ func (b *Buffer) saveToFile(filename string, withSudo bool, autoSave bool) error
// For large files 'fastdirty' needs to be on // For large files 'fastdirty' needs to be on
b.Settings["fastdirty"] = true b.Settings["fastdirty"] = true
} else { } else {
calcHash(b, &b.origHash) b.calcHash(&b.origHash)
} }
} }
@@ -337,32 +333,11 @@ func (b *Buffer) saveToFile(filename string, withSudo bool, autoSave bool) error
return err return err
} }
func (b *Buffer) 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 // 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. // 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 // This means that the file is not overwritten directly but by writing to the
// backup file first. // backup file first.
func (b *Buffer) safeWrite(path string, withSudo bool, newFile bool) (int, error) { func (b *SharedBuffer) safeWrite(path string, withSudo bool, newFile bool) (int, error) {
file, err := openFile(path, withSudo) file, err := openFile(path, withSudo)
if err != nil { if err != nil {
return 0, err return 0, err
@@ -382,6 +357,9 @@ func (b *Buffer) safeWrite(path string, withSudo bool, newFile bool) (int, error
return 0, err return 0, err
} }
// Backup saved, so cancel pending periodic backup, if any
delete(requestedBackups, b)
b.forceKeepBackup = true b.forceKeepBackup = true
size := 0 size := 0
{ {

View File

@@ -73,7 +73,7 @@ func (b *Buffer) DoSetOptionNative(option string, nativeValue interface{}) {
b.Settings["fastdirty"] = true b.Settings["fastdirty"] = true
} else { } else {
if !b.isModified { if !b.isModified {
calcHash(b, &b.origHash) b.calcHash(&b.origHash)
} else { } else {
// prevent using an old stale origHash value // prevent using an old stale origHash value
b.origHash = [md5.Size]byte{} b.origHash = [md5.Size]byte{}
@@ -91,7 +91,7 @@ func (b *Buffer) DoSetOptionNative(option string, nativeValue interface{}) {
case "dos": case "dos":
b.Endings = FFDos b.Endings = FFDos
} }
b.isModified = true b.setModified()
} else if option == "syntax" { } else if option == "syntax" {
if !nativeValue.(bool) { if !nativeValue.(bool) {
b.ClearMatches() b.ClearMatches()
@@ -105,7 +105,7 @@ func (b *Buffer) DoSetOptionNative(option string, nativeValue interface{}) {
b.Settings["encoding"] = "utf-8" b.Settings["encoding"] = "utf-8"
} }
b.encoding = enc b.encoding = enc
b.isModified = true b.setModified()
} else if option == "readonly" && b.Type.Kind == BTDefault.Kind { } else if option == "readonly" && b.Type.Kind == BTDefault.Kind {
b.Type.Readonly = nativeValue.(bool) b.Type.Readonly = nativeValue.(bool)
} else if option == "hlsearch" { } else if option == "hlsearch" {