github.com/keybase/client/go@v0.0.0-20240309051027-028f7c731f8b/lsof/lsof.go (about)

     1  // Copyright 2015 Keybase, Inc. All rights reserved. Use of
     2  // this source code is governed by the included BSD license.
     3  
     4  package lsof
     5  
     6  import (
     7  	"fmt"
     8  	"os/exec"
     9  	"strings"
    10  )
    11  
    12  // Process defines a process using an open file. Properties here are strings
    13  // for compatibility with different platforms.
    14  type Process struct {
    15  	PID             string
    16  	Command         string
    17  	UserID          string
    18  	FileDescriptors []FileDescriptor
    19  }
    20  
    21  // FileType defines the type of file in use by a process
    22  type FileType string
    23  
    24  const (
    25  	FileTypeUnknown FileType = ""
    26  	FileTypeDir     FileType = "DIR"
    27  	FileTypeFile    FileType = "REG"
    28  )
    29  
    30  // FileDescriptor defines a file in use by a process
    31  type FileDescriptor struct {
    32  	FD   string
    33  	Type FileType
    34  	Name string
    35  }
    36  
    37  // ExecError is an error running lsof
    38  type ExecError struct {
    39  	command string
    40  	args    []string
    41  	output  string
    42  	err     error
    43  }
    44  
    45  func (e ExecError) Error() string {
    46  	return fmt.Sprintf("Error running %s %s: %s (%s)", e.command, e.args, e.err, e.output)
    47  }
    48  
    49  // MountPoint returns processes using the mountpoint "lsof /dir"
    50  func MountPoint(dir string) ([]Process, error) {
    51  	// TODO: Fix lsof to not return error on exit status 1 since it isn't
    52  	// really any error, only an indication that there was no use of the
    53  	// mount.
    54  	return run([]string{"-F", "pcuftn", dir})
    55  }
    56  
    57  func fileTypeFromString(s string) FileType {
    58  	switch s {
    59  	case "DIR":
    60  		return FileTypeDir
    61  	case "REG":
    62  		return FileTypeFile
    63  	default:
    64  		return FileTypeUnknown
    65  	}
    66  }
    67  
    68  func (p *Process) fillField(s string) error {
    69  	if s == "" {
    70  		return fmt.Errorf("Empty field")
    71  	}
    72  	// See Output for Other Programs at http://linux.die.net/man/8/lsof
    73  	key := s[0]
    74  	value := s[1:]
    75  	switch key {
    76  	case 'p':
    77  		p.PID = value
    78  	case 'c':
    79  		p.Command = value
    80  	case 'u':
    81  		p.UserID = value
    82  	default:
    83  		// Skip unhandled field
    84  	}
    85  	return nil
    86  }
    87  
    88  func (f *FileDescriptor) fillField(s string) error {
    89  	// See Output for Other Programs at http://linux.die.net/man/8/lsof
    90  	key := s[0]
    91  	value := s[1:]
    92  	switch key {
    93  	case 't':
    94  		f.Type = fileTypeFromString(value)
    95  	case 'f':
    96  		f.FD = value
    97  	case 'n':
    98  		f.Name = value
    99  	default:
   100  		// Skip unhandled field
   101  	}
   102  
   103  	return nil
   104  }
   105  
   106  func (p *Process) parseFileLines(lines []string) error {
   107  	file := FileDescriptor{}
   108  	for _, line := range lines {
   109  		if strings.HasPrefix(line, "f") && file.FD != "" {
   110  			// New file
   111  			p.FileDescriptors = append(p.FileDescriptors, file)
   112  			file = FileDescriptor{}
   113  		}
   114  		err := file.fillField(line)
   115  		if err != nil {
   116  			return err
   117  		}
   118  	}
   119  	if file.FD != "" {
   120  		p.FileDescriptors = append(p.FileDescriptors, file)
   121  	}
   122  	return nil
   123  }
   124  
   125  func parseProcessLines(lines []string) (Process, error) {
   126  	p := Process{}
   127  	for index, line := range lines {
   128  		if strings.HasPrefix(line, "f") {
   129  			err := p.parseFileLines(lines[index:])
   130  			if err != nil {
   131  				return p, err
   132  			}
   133  			break
   134  		} else {
   135  			err := p.fillField(line)
   136  			if err != nil {
   137  				return p, err
   138  			}
   139  		}
   140  	}
   141  	return p, nil
   142  }
   143  
   144  func parseAppendProcessLines(processes []Process, linesChunk []string) ([]Process, []string, error) {
   145  	if len(linesChunk) == 0 {
   146  		return processes, linesChunk, nil
   147  	}
   148  	process, err := parseProcessLines(linesChunk)
   149  	if err != nil {
   150  		return processes, linesChunk, err
   151  	}
   152  	processesAfter := processes
   153  	processesAfter = append(processesAfter, process)
   154  	linesChunkAfter := []string{}
   155  	return processesAfter, linesChunkAfter, nil
   156  }
   157  
   158  func parse(s string) ([]Process, error) {
   159  	lines := strings.Split(s, "\n")
   160  	linesChunk := []string{}
   161  	processes := []Process{}
   162  	var err error
   163  	for _, line := range lines {
   164  		if strings.TrimSpace(line) == "" {
   165  			continue
   166  		}
   167  
   168  		// End of process, let's parse those lines
   169  		if strings.HasPrefix(line, "p") && len(linesChunk) > 0 {
   170  			processes, linesChunk, err = parseAppendProcessLines(processes, linesChunk)
   171  			if err != nil {
   172  				return nil, err
   173  			}
   174  		}
   175  		linesChunk = append(linesChunk, line)
   176  	}
   177  	processes, _, err = parseAppendProcessLines(processes, linesChunk)
   178  	if err != nil {
   179  		return nil, err
   180  	}
   181  	return processes, nil
   182  }
   183  
   184  func run(args []string) ([]Process, error) {
   185  	// Some systems (Arch, Debian) install lsof in /usr/bin and others (centos)
   186  	// install it in /usr/sbin, even though regular users can use it too. FreeBSD,
   187  	// on the other hand, puts it in /usr/local/sbin. So do not specify absolute path.
   188  	command := "lsof"
   189  	args = append([]string{"-w"}, args...)
   190  	output, err := exec.Command(command, args...).Output()
   191  	if err != nil {
   192  		return nil, ExecError{command: command, args: args, output: string(output), err: err}
   193  	}
   194  	return parse(string(output))
   195  }