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 }