Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion cmd/micro/initlua.go
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
16 changes: 15 additions & 1 deletion cmd/micro/micro.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions cmd/micro/micro_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"log"
"os"
"testing"
"time"

"github.com/go-errors/errors"
"github.com/micro-editor/micro/v2/internal/action"
Expand Down Expand Up @@ -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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I really do not like this line

var buf *buffer.Buffer
file = util.ResolvePath(file)
for _, b := range buffer.OpenBuffers {
Expand Down
82 changes: 58 additions & 24 deletions internal/action/command.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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)
Expand Down
146 changes: 135 additions & 11 deletions internal/buffer/buffer.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,10 @@ import (
"io"
"io/fs"
"os"
"os/exec"
"os/signal"
"path/filepath"
"runtime"
"strconv"
"strings"
"sync"
Expand Down Expand Up @@ -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 {
Expand All @@ -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() {
Expand All @@ -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
Expand All @@ -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)
}
Expand All @@ -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
Expand Down