From 4b1829026eb28c898475da0774a44ae02ab03799 Mon Sep 17 00:00:00 2001 From: SollyBunny Date: Wed, 8 Jul 2026 09:52:45 +0000 Subject: [PATCH 1/2] Use sudo if required to open a file --- cmd/micro/initlua.go | 3 +- cmd/micro/micro.go | 16 +++- internal/action/command.go | 82 +++++++++++++++------ internal/buffer/buffer.go | 146 ++++++++++++++++++++++++++++++++++--- 4 files changed, 210 insertions(+), 37 deletions(-) diff --git a/cmd/micro/initlua.go b/cmd/micro/initlua.go index 2f7b2c625b..61d015b82c 100644 --- a/cmd/micro/initlua.go +++ b/cmd/micro/initlua.go @@ -136,7 +136,8 @@ func luaImportMicroBuffer() *lua.LTable { return buffer.NewBufferFromString(text, path, buffer.BTDefault) })) ulua.L.SetField(pkg, "NewBufferFromFile", luar.New(ulua.L, func(path string) (*buffer.Buffer, error) { - return buffer.NewBufferFromFile(path, buffer.BTDefault) + // TODO: Somehow suport sudo here + return buffer.NewBufferFromFile(path, buffer.BTDefault, false) })) ulua.L.SetField(pkg, "ByteOffset", luar.New(ulua.L, buffer.ByteOffset)) ulua.L.SetField(pkg, "Log", luar.New(ulua.L, buffer.WriteLog)) diff --git a/cmd/micro/micro.go b/cmd/micro/micro.go index c8a99b2517..c0f2d82be0 100644 --- a/cmd/micro/micro.go +++ b/cmd/micro/micro.go @@ -214,7 +214,21 @@ func LoadInput(args []string) []*buffer.Buffer { // Option 1 // We go through each file and load it for i := 0; i < len(files); i++ { - buf, err := buffer.NewBufferFromFileWithCommand(files[i], buffer.BTDefault, command) + autoSu := config.GlobalSettings["autosu"].(bool) + buf, err := buffer.NewBufferFromFileWithCommand(files[i], buffer.BTDefault, command, autoSu) + // Don't retry if sudo was already used + // os.isPermission is used instead of errors.is as it is based of the + // builtin errors package instead of the custom one included in this file + if !autoSu && os.IsPermission(err) { + msg := fmt.Sprintf("Permission denied trying to read %s\nDo you want to load this file using %s? (y,n)", files[i], config.GlobalSettings["sucmd"].(string)) + retryWithSudo := screen.TermPrompt(msg, []string{"y", "n"}, true) == 0 + if (retryWithSudo) { + buf, err = buffer.NewBufferFromFileWithCommand(files[i], buffer.BTDefault, command, true) + // fallthrough + } else { + continue + } + } if err != nil { screen.TermMessage(err) continue diff --git a/internal/action/command.go b/internal/action/command.go index e60d60ad6c..1085b9c39d 100644 --- a/internal/action/command.go +++ b/internal/action/command.go @@ -300,21 +300,56 @@ func (h *BufPane) PwdCmd(args []string) { } } +// Returns the value of buffer.NewBufferFromFile(path, buffer.BTDefault, false) +// retrying with sudo if it fails due to lacking permissions +// If an error occurs, the error is displayed on the info bar and nil is returned +func newBufferFromFileWithSudo(path string) <-chan *buffer.Buffer { + ch := make(chan *buffer.Buffer, 1) + + autoSu := config.GlobalSettings["autosu"].(bool) + buf, err := buffer.NewBufferFromFile(path, buffer.BTDefault, autoSu) + // Don't retry if sudo was already used + if (!autoSu && errors.Is(err, os.ErrPermission)) { + InfoBar.YNPrompt( + fmt.Sprintf("Permission denied. Do you want to load this file using %s? (y,n)", config.GlobalSettings["sucmd"].(string)), + func(yes, canceled bool) { + if yes && !canceled { + buf, err := buffer.NewBufferFromFile(path, buffer.BTDefault, true) + if (err != nil) { + InfoBar.Error(err) + } + ch <- buf + } else { + ch <- nil + } + }, + ) + } else { + if (err != nil) { + InfoBar.Error(err) + } + ch <- buf + } + + return ch +} + // OpenCmd opens a new buffer with a given filename func (h *BufPane) OpenCmd(args []string) { if len(args) > 0 { open := func() { - b, err := buffer.NewBufferFromFile(args[0], buffer.BTDefault) - if err != nil { - InfoBar.Error(err) + b := <-newBufferFromFileWithSudo(args[0]) + if b == nil { return } h.OpenBuffer(b) } if h.Buf.Modified() && !h.Buf.Shared() { - h.closePrompt("Save", open) + h.closePrompt("Save", func() { + go open() + }) } else { - open() + go open() } } else { InfoBar.Error("No filename") @@ -512,15 +547,15 @@ func (h *BufPane) VSplitCmd(args []string) { return } - for _, a := range args { - buf, err := buffer.NewBufferFromFile(a, buffer.BTDefault) - if err != nil { - InfoBar.Error(err) - return + go func() { + for _, a := range args { + b := <-newBufferFromFileWithSudo(a) + if b == nil { + return + } + h.VSplitBuf(b) } - - h.VSplitBuf(buf) - } + }() } // HSplitCmd opens one or more horizontal splits with the files given as arguments @@ -532,15 +567,15 @@ func (h *BufPane) HSplitCmd(args []string) { return } - for _, a := range args { - buf, err := buffer.NewBufferFromFile(a, buffer.BTDefault) - if err != nil { - InfoBar.Error(err) - return + go func() { + for _, a := range args { + b := <-newBufferFromFileWithSudo(a) + if b == nil { + return + } + h.HSplitBuf(b) } - - h.HSplitBuf(buf) - } + }() } // EvalCmd evaluates a lua expression @@ -555,9 +590,8 @@ func (h *BufPane) NewTabCmd(args []string) { iOffset := config.GetInfoBarOffset() if len(args) > 0 { for _, a := range args { - b, err := buffer.NewBufferFromFile(a, buffer.BTDefault) - if err != nil { - InfoBar.Error(err) + b := <-newBufferFromFileWithSudo(a) + if b == nil { return } tp := NewTabFromBuffer(0, 0, width, height-1-iOffset, b) diff --git a/internal/buffer/buffer.go b/internal/buffer/buffer.go index d1f8db4b2b..ffe02dc902 100644 --- a/internal/buffer/buffer.go +++ b/internal/buffer/buffer.go @@ -9,7 +9,10 @@ import ( "io" "io/fs" "os" + "os/exec" + "os/signal" "path/filepath" + "runtime" "strconv" "strings" "sync" @@ -268,11 +271,101 @@ type Buffer struct { OverwriteMode bool } +// Reads a file with root +// Similar to os.Open(path) for the purposes of reading +// Requires the screen to be closed +func readerFromFileWithSudo(path string) (io.ReadCloser, error) { + args := []string{"dd", "bs=4k", "if=" + path} + // TODO: both platforms do not support dd with conv=fsync yet + if !(runtime.GOOS == "illumos" || runtime.GOOS == "netbsd") { + args = append(args, "conv=fsync") + } + + cmd := exec.Command(config.GlobalSettings["sucmd"].(string), args...) + writeCloser, err := cmd.StdinPipe() + if err != nil { + return nil, err + } + defer writeCloser.Close() + + var readCloser io.ReadCloser + readCloser, err = cmd.StdoutPipe() + if err != nil { + return nil, err + } + + sigChan := make(chan os.Signal, 1) + signal.Reset(os.Interrupt) + signal.Notify(sigChan, os.Interrupt) + + // 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 { + readCloser.Close() + signal.Notify(util.Sigterm, os.Interrupt) + signal.Stop(sigChan) + return nil, err + } + + return readCloser, nil +} + +// Gets the size in bytes of a file +// Requires the screen to be closed +func sizeFromFileWithSudo(path string) (int64, error) { + args := []string{"stat", "-c%s", path} + + cmd := exec.Command(config.GlobalSettings["sucmd"].(string), args...) + writeCloser, err := cmd.StdinPipe() + if err != nil { + return 0, err + } + defer writeCloser.Close() + + var readCloser io.ReadCloser + readCloser, err = cmd.StdoutPipe() + if err != nil { + return 0, err + } + defer readCloser.Close() + + sigChan := make(chan os.Signal, 1) + signal.Reset(os.Interrupt) + signal.Notify(sigChan, os.Interrupt) + + err = cmd.Start() + if err != nil { + signal.Notify(util.Sigterm, os.Interrupt) + signal.Stop(sigChan) + return 0, err + } + + output := make([]byte, 65) // size of the int (negatives allowed) and a newline + outputSize, err := readCloser.Read(output) + if (err != nil) { + return 0, err + } + if (outputSize <= 1) { + return 0, errors.New("Failed to read file size with stat") + } + var size int64 + size, err = strconv.ParseInt(strings.TrimSpace(string(output[:outputSize])), 10, 64) + if (err != nil) { + return 0, err + } + if (size < 0) { + return 0, errors.New("Got negative size with stat") + } + return size, nil +} + // NewBufferFromFileWithCommand opens a new buffer with a given command // If cmd.StartCursor is {-1, -1} the location does not overwrite what the cursor location // would otherwise be (start of file, or saved cursor position if `savecursor` is // enabled) -func NewBufferFromFileWithCommand(path string, btype BufType, cmd Command) (*Buffer, error) { +func NewBufferFromFileWithCommand(path string, btype BufType, cmd Command, withSudo bool) (*Buffer, error) { var err error filename := path if config.GetGlobalOption("parsecursor").(bool) && cmd.StartCursor.X == -1 && cmd.StartCursor.Y == -1 { @@ -290,7 +383,7 @@ func NewBufferFromFileWithCommand(path string, btype BufType, cmd Command) (*Buf } fileInfo, serr := os.Stat(filename) - if serr != nil && !errors.Is(serr, fs.ErrNotExist) { + if serr != nil && !errors.Is(serr, fs.ErrNotExist) && !errors.Is(serr, fs.ErrPermission) { return nil, serr } if serr == nil && fileInfo.IsDir() { @@ -300,13 +393,44 @@ func NewBufferFromFileWithCommand(path string, btype BufType, cmd Command) (*Buf return nil, errors.New("Error: " + filename + " is not a regular file and cannot be opened") } - f, err := os.OpenFile(filename, os.O_WRONLY, 0) - readonly := errors.Is(err, fs.ErrPermission) + // TODO this is more nuanced some files may be write only, some write only, some in unreadable dirs, etc + f, err := os.OpenFile(filename, os.O_RDWR, 0) + sudoRequired := errors.Is(err, fs.ErrPermission) || errors.Is(serr, fs.ErrPermission) f.Close() - file, err := os.Open(filename) - if err == nil { - defer file.Close() + var reader io.ReadCloser + var size int64 + if (sudoRequired) { + if (!withSudo) { + return nil, err + } + _, err = exec.LookPath(config.GlobalSettings["sucmd"].(string)) + if err != nil { + return nil, err + } + screenb := screen.TempFini() + if (fileInfo != nil) { + size = fileInfo.Size() + } else { + // TODO get the size with only one call to sudo + size, err = sizeFromFileWithSudo(filename) + if (errors.Is(err, io.EOF)) { + // If read failed, it probably doesn't exist + err = fs.ErrNotExist + } else if (err != nil) { + screen.TempStart(screenb) + return nil, err + } + } + // Preserve value of readonly, we don't deal with elevating on save here + reader, err = readerFromFileWithSudo(filename) + screen.TempStart(screenb) + } else if (!errors.Is(err, fs.ErrNotExist)) { + size = fileInfo.Size() + reader, err = os.Open(filename) + } + if reader != nil { + defer reader.Close() } var buf *Buffer @@ -316,13 +440,13 @@ func NewBufferFromFileWithCommand(path string, btype BufType, cmd Command) (*Buf } else if err != nil { return nil, err } else { - buf = NewBuffer(file, util.FSize(file), filename, btype, cmd) + buf = NewBuffer(reader, size, filename, btype, cmd) if buf == nil { return nil, errors.New("could not open file") } } - if readonly && prompt != nil { + if sudoRequired && prompt != nil { prompt.Message(fmt.Sprintf("Warning: file is readonly - %s will be attempted when saving", config.GlobalSettings["sucmd"].(string))) // buf.SetOptionNative("readonly", true) } @@ -334,8 +458,8 @@ func NewBufferFromFileWithCommand(path string, btype BufType, cmd Command) (*Buf // It will also automatically handle `~`, and line/column with filename:l:c // It will return an empty buffer if the path does not exist // and an error if the file is a directory -func NewBufferFromFile(path string, btype BufType) (*Buffer, error) { - return NewBufferFromFileWithCommand(path, btype, emptyCommand) +func NewBufferFromFile(path string, btype BufType, withSudo bool) (*Buffer, error) { + return NewBufferFromFileWithCommand(path, btype, emptyCommand, withSudo) } // NewBufferFromStringWithCommand creates a new buffer containing the given string From 6cc3a9d1c09ca682606164ca0a07982d121d5155 Mon Sep 17 00:00:00 2001 From: SollyBunny Date: Wed, 8 Jul 2026 14:32:21 +0000 Subject: [PATCH 2/2] Bodge tests --- cmd/micro/micro_test.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/cmd/micro/micro_test.go b/cmd/micro/micro_test.go index 185b296e5b..b711d79b0c 100644 --- a/cmd/micro/micro_test.go +++ b/cmd/micro/micro_test.go @@ -5,6 +5,7 @@ import ( "log" "os" "testing" + "time" "github.com/go-errors/errors" "github.com/micro-editor/micro/v2/internal/action" @@ -157,6 +158,7 @@ func openFile(file string) { } func findBuffer(file string) *buffer.Buffer { + time.Sleep(time.Second / 10) // TODO wait for open command to be done instead of waiting var buf *buffer.Buffer file = util.ResolvePath(file) for _, b := range buffer.OpenBuffers {