package lsof // Copyright 2015 Keybase, Inc. All rights reserved. Use of // this source code is governed by the included BSD license. // CREDIT: https://github.com/keybase/client/tree/master/go/lsof import ( "fmt" "os/exec" "strings" ) // Process defines a process using an open file. Properties here are strings // for compatibility with different platforms. type Process struct { PID string Command string UserID string FileDescriptors []FileDescriptor } func (p Process) String() string { var fds []string for _, fd := range p.FileDescriptors { fds = append(fds, fmt.Sprintf("\n %s", fd)) } return fmt.Sprintf( "pid=%s, uid=%s '%s'%s", p.PID, p.UserID, p.Command, strings.Join(fds, ""), ) } // FileType defines the type of file in use by a process type FileType string const ( FileTypeUnknown FileType = "" FileTypeDir FileType = "DIR" FileTypeFile FileType = "REG" ) // FileDescriptor defines a file in use by a process type FileDescriptor struct { FD string Type FileType Name string } func (f FileDescriptor) String() string { return fmt.Sprintf("fd=%s, type=%s, name=%s", f.FD, f.Type, f.Name) } // ExecError is an error running lsof type ExecError struct { command string args []string output string err error } func (e ExecError) Error() string { return fmt.Sprintf("error running %s %s: %s (%s)", e.command, e.args, e.err, e.output) } // MountPoint returns processes using the mountpoint "lsof /dir" func MountPoint(dir string) ([]Process, error) { // TODO: Fix lsof to not return error on exit status 1 since it isn't // really any error, only an indication that there was no use of the // mount. return run([]string{"-F", "pcuftn", dir}) } func fileTypeFromString(s string) FileType { switch s { case "DIR": return FileTypeDir case "REG": return FileTypeFile default: return FileTypeUnknown } } func (p *Process) fillField(s string) error { if s == "" { return fmt.Errorf("empty field") } // See Output for Other Programs at http://linux.die.net/man/8/lsof key := s[0] value := s[1:] switch key { case 'p': p.PID = value case 'c': p.Command = value case 'u': p.UserID = value default: // Skip unhandled field } return nil } func (f *FileDescriptor) fillField(s string) error { // See Output for Other Programs at http://linux.die.net/man/8/lsof key := s[0] value := s[1:] switch key { case 't': f.Type = fileTypeFromString(value) case 'f': f.FD = value case 'n': f.Name = value default: // Skip unhandled field } return nil } func (p *Process) parseFileLines(lines []string) error { file := FileDescriptor{} for _, line := range lines { if strings.HasPrefix(line, "f") && file.FD != "" { // New file p.FileDescriptors = append(p.FileDescriptors, file) file = FileDescriptor{} } err := file.fillField(line) if err != nil { return err } } if file.FD != "" { p.FileDescriptors = append(p.FileDescriptors, file) } return nil } func parseProcessLines(lines []string) (Process, error) { p := Process{} for index, line := range lines { if strings.HasPrefix(line, "f") { err := p.parseFileLines(lines[index:]) if err != nil { return p, err } break } else { err := p.fillField(line) if err != nil { return p, err } } } return p, nil } func parseAppendProcessLines(processes []Process, linesChunk []string) ([]Process, []string, error) { if len(linesChunk) == 0 { return processes, linesChunk, nil } process, err := parseProcessLines(linesChunk) if err != nil { return processes, linesChunk, err } processesAfter := processes processesAfter = append(processesAfter, process) linesChunkAfter := []string{} return processesAfter, linesChunkAfter, nil } func parse(s string) ([]Process, error) { lines := strings.Split(s, "\n") linesChunk := []string{} processes := []Process{} var err error for _, line := range lines { if strings.TrimSpace(line) == "" { continue } // End of process, let's parse those lines if strings.HasPrefix(line, "p") && len(linesChunk) > 0 { processes, linesChunk, err = parseAppendProcessLines(processes, linesChunk) if err != nil { return nil, err } } linesChunk = append(linesChunk, line) } processes, _, err = parseAppendProcessLines(processes, linesChunk) if err != nil { return nil, err } return processes, nil } func run(args []string) ([]Process, error) { // Some systems (Arch, Debian) install lsof in /usr/bin and others (centos) // install it in /usr/sbin, even though regular users can use it too. FreeBSD, // on the other hand, puts it in /usr/local/sbin. So do not specify absolute path. command := "lsof" args = append([]string{"-w"}, args...) output, err := exec.Command(command, args...).Output() if err != nil { return nil, ExecError{command: command, args: args, output: string(output), err: err} } return parse(string(output)) }