RunEvery() and better Error-handling added
1. symbolyze.Scanner now implements the RunEvery(time.Duration) method that will call scanner.Run() periodically. It should be called to run in its own goroutine. The loop can be stopped by calling scanner.Stop(). 2. The scanner now collects all errors from all observers in a private error type `errors []error`. It's Error() returns a cumulated list of errors, seperated by a newline.
This commit is contained in:
parent
baf998232a
commit
d800683dce
@ -4,6 +4,7 @@ import (
|
|||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/optimyze-interviews/OezguerKesim/GetRuntimeAddresses/ebpf"
|
"github.com/optimyze-interviews/OezguerKesim/GetRuntimeAddresses/ebpf"
|
||||||
"github.com/optimyze-interviews/OezguerKesim/GetRuntimeAddresses/symbolyze"
|
"github.com/optimyze-interviews/OezguerKesim/GetRuntimeAddresses/symbolyze"
|
||||||
@ -28,9 +29,12 @@ func main() {
|
|||||||
|
|
||||||
scanner := symbolyze.NewScanner(*symbol, *glob)
|
scanner := symbolyze.NewScanner(*symbol, *glob)
|
||||||
scanner.OnFound(mapFD.Set)
|
scanner.OnFound(mapFD.Set)
|
||||||
if *debug {
|
scanner.Debug(*debug)
|
||||||
scanner.DebugOn()
|
|
||||||
}
|
go scanner.RunEvery(time.Second)
|
||||||
|
|
||||||
|
time.Sleep(10 * time.Second)
|
||||||
|
scanner.Stop()
|
||||||
|
|
||||||
err = scanner.Run()
|
err = scanner.Run()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -10,6 +10,7 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Scanner represents an engine for scanning for a specific symbol in all
|
// Scanner represents an engine for scanning for a specific symbol in all
|
||||||
@ -24,25 +25,51 @@ import (
|
|||||||
// /proc. Whenever a match is found, all observers will be called with the
|
// /proc. Whenever a match is found, all observers will be called with the
|
||||||
// (pid, offset), concurrently.
|
// (pid, offset), concurrently.
|
||||||
type Scanner struct {
|
type Scanner struct {
|
||||||
|
rwmutex
|
||||||
|
|
||||||
symbol string
|
symbol string
|
||||||
pathglob string
|
pathglob string
|
||||||
cache map[string]uint64 // Contains (pathname, offset)
|
cache map[string]uint64 // Contains (pathname, offset)
|
||||||
observers []Observer // Callbacks
|
observers []Observer // Callbacks
|
||||||
|
|
||||||
logger // Embedded logger
|
|
||||||
|
|
||||||
// Instead of using a boolean to indicate debugging, we use function
|
// Instead of using a boolean to indicate debugging, we use function
|
||||||
// members. This way we can populate them with noop-functions in the
|
// members. This way we can populate them with noop-functions in the
|
||||||
// non-debug case and not polute the code with if-statements.
|
// non-debug case and not polute the code with if-statements.
|
||||||
debugf func(format string, v ...interface{})
|
debugf func(format string, v ...interface{})
|
||||||
debugln func(v ...interface{})
|
debugln func(v ...interface{})
|
||||||
|
logger // Embedded logger
|
||||||
|
|
||||||
err error // error state of the scanner.
|
errors errors
|
||||||
|
|
||||||
|
ticker *time.Ticker // Used to run the scanner repeatedly
|
||||||
|
}
|
||||||
|
|
||||||
|
type errors []error
|
||||||
|
|
||||||
|
func (e errors) Error() string {
|
||||||
|
switch len(e) {
|
||||||
|
case 0:
|
||||||
|
return "nil"
|
||||||
|
case 1:
|
||||||
|
return e[0].Error()
|
||||||
|
default:
|
||||||
|
list := make([]string, len(e))
|
||||||
|
for i := range e {
|
||||||
|
list[i] = e[i].Error()
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("multiple errors:\n%s", strings.Join(list, "\n"))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// We use a lowercase type alias for *log.Logger so that we can embedd it in
|
// We use a lowercase type alias for *log.Logger so that we can embedd it in
|
||||||
// Scanner without exporting it.
|
// Scanner without exporting it.
|
||||||
type logger = *log.Logger
|
type logger = *log.Logger
|
||||||
|
type rwmutex = sync.RWMutex
|
||||||
|
|
||||||
|
var (
|
||||||
|
nodebugf = func(format string, v ...interface{}) {}
|
||||||
|
nodebugln = func(v ...interface{}) {}
|
||||||
|
)
|
||||||
|
|
||||||
// An Observer is a callback that can be registered with Scanner.OnFound. It
|
// An Observer is a callback that can be registered with Scanner.OnFound. It
|
||||||
// will be called with a pid and an offset. Observers are called concurrently.
|
// will be called with a pid and an offset. Observers are called concurrently.
|
||||||
@ -62,41 +89,76 @@ func NewScanner(symbol, pathglob string) *Scanner {
|
|||||||
logger: log.New(os.Stderr, "[symbolyze] ", log.Ltime|log.Lmicroseconds),
|
logger: log.New(os.Stderr, "[symbolyze] ", log.Ltime|log.Lmicroseconds),
|
||||||
|
|
||||||
// debugging is off per default.
|
// debugging is off per default.
|
||||||
debugf: func(string, ...interface{}) {},
|
debugf: nodebugf,
|
||||||
debugln: func(...interface{}) {},
|
debugln: nodebugln,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Debug sets the scanner into debugging mode. It must be called only once
|
// Debug sets the scanner into debugging mode. It must be called only once
|
||||||
// before a call to Scanner.Run().
|
// before a call to Scanner.Run().
|
||||||
func (S *Scanner) DebugOn() {
|
func (S *Scanner) Debug(on bool) {
|
||||||
|
S.Lock()
|
||||||
|
defer S.Unlock()
|
||||||
|
|
||||||
|
if on {
|
||||||
// Use the embedded *log.Logger for debugging.
|
// Use the embedded *log.Logger for debugging.
|
||||||
S.debugf = S.Printf
|
S.debugf = S.Printf
|
||||||
S.debugln = S.Println
|
S.debugln = S.Println
|
||||||
S.debugln("starting in debug-mode")
|
S.debugln("starting in debug-mode")
|
||||||
|
} else {
|
||||||
|
S.debugf = nodebugf
|
||||||
|
S.debugln = nodebugln
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// setErrorf puts the Scanner into an error state with the given error
|
// setErrorf puts the Scanner into an error state with the given error
|
||||||
// statement. It also logs the error. setErrorf is not thread-safe.
|
// statement. It also logs the error. setErrorf is not thread-safe.
|
||||||
func (S *Scanner) setErrorf(format string, a ...interface{}) {
|
func (S *Scanner) setErrorf(format string, a ...interface{}) {
|
||||||
S.err = fmt.Errorf(format, a...)
|
S.Lock()
|
||||||
|
S.errors = append(S.errors, fmt.Errorf(format, a...))
|
||||||
|
S.Unlock()
|
||||||
S.Printf(format, a...)
|
S.Printf(format, a...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// OnFound puts an Observer function into the internal queue. The functions
|
func (S *Scanner) HasErrors() bool {
|
||||||
// are called in sequence in their own goroutine whenever the scanner finds the
|
S.RLock()
|
||||||
// symbol in a running program. That implies that an Observer has to be
|
defer S.RUnlock()
|
||||||
// thread-safe. Errors from the observers will be logged.
|
return len(S.errors) > 0
|
||||||
//
|
}
|
||||||
// Calling OnFound is not thread-safe.
|
|
||||||
func (S *Scanner) OnFound(fun Observer) {
|
func (S *Scanner) Errors() error {
|
||||||
S.observers = append(S.observers, fun)
|
S.RLock()
|
||||||
|
defer S.RUnlock()
|
||||||
|
if len(S.errors) == 0 {
|
||||||
|
return nil
|
||||||
|
} else {
|
||||||
|
e2 := make(errors, len(S.errors))
|
||||||
|
copy(e2, S.errors)
|
||||||
|
return e2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (S *Scanner) addError(err error) {
|
||||||
|
S.Lock()
|
||||||
|
defer S.Unlock()
|
||||||
|
S.errors = append(S.errors, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// OnFound puts Observer functions at the end of the internal queue. All
|
||||||
|
// Observer functions are called in sequence in their own goroutine whenever
|
||||||
|
// the scanner finds the symbol in a running program. That implies that an
|
||||||
|
// Observer has to be thread-safe. Errors from the observers will be logged.
|
||||||
|
func (S *Scanner) OnFound(fun ...Observer) {
|
||||||
|
S.Lock()
|
||||||
|
defer S.Unlock()
|
||||||
|
|
||||||
|
S.observers = append(S.observers, fun...)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Run starts the scanning process. It scans the entries of all /proc/NNN/maps
|
// Run starts the scanning process. It scans the entries of all /proc/NNN/maps
|
||||||
// files for pathnames that match the provided path-glob and are executables or
|
// files for pathnames that match the provided path-glob and are executables or
|
||||||
// shared libraries in ELF formmat. It searches for the provided symbol in
|
// shared libraries in ELF format. It searches for the provided symbol in
|
||||||
// those files and calls the registered Observer functions, concurrently, with
|
// those files and calls the registered Observer functions, concurrently, with
|
||||||
// the pid and offset of the symbol.
|
// the pid and offset of the symbol.
|
||||||
//
|
//
|
||||||
@ -104,20 +166,20 @@ func (S *Scanner) OnFound(fun Observer) {
|
|||||||
// it will try to continue to loop over all pids, writing potential errors to
|
// it will try to continue to loop over all pids, writing potential errors to
|
||||||
// the console. Errors from the observer functions are logged.
|
// the console. Errors from the observer functions are logged.
|
||||||
func (S *Scanner) Run() error {
|
func (S *Scanner) Run() error {
|
||||||
if S.err != nil {
|
if S.HasErrors() {
|
||||||
return S.err
|
return S.Errors()
|
||||||
}
|
}
|
||||||
|
|
||||||
proc, err := os.Open("/proc")
|
proc, err := os.Open("/proc")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
S.setErrorf("Failed to open /proc: %v\n", err)
|
S.setErrorf("Failed to open /proc: %v\n", err)
|
||||||
return S.err
|
return S.Errors()
|
||||||
}
|
}
|
||||||
|
|
||||||
infos, err := proc.Readdir(-1)
|
infos, err := proc.Readdir(-1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
S.setErrorf("Failed to read /proc: %v\n", err)
|
S.setErrorf("Failed to read /proc: %v\n", err)
|
||||||
return S.err
|
return S.Errors()
|
||||||
}
|
}
|
||||||
|
|
||||||
proc.Close()
|
proc.Close()
|
||||||
@ -132,7 +194,7 @@ func (S *Scanner) Run() error {
|
|||||||
continue
|
continue
|
||||||
} else if pid, err := strconv.Atoi(pid_s); err != nil {
|
} else if pid, err := strconv.Atoi(pid_s); err != nil {
|
||||||
continue
|
continue
|
||||||
} else if offset, found := S.searchSymbolIn(pid); !found {
|
} else if offset, found := S.searchSymbolInPid(pid); !found {
|
||||||
continue
|
continue
|
||||||
} else {
|
} else {
|
||||||
// Call the observers with (pid, offset), in the
|
// Call the observers with (pid, offset), in the
|
||||||
@ -143,8 +205,7 @@ func (S *Scanner) Run() error {
|
|||||||
go func() {
|
go func() {
|
||||||
err = observer(pid, offset)
|
err = observer(pid, offset)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
S.Printf("S.observer[%d](%d, %d) error: %v", n, pid, offset, err)
|
S.addError(fmt.Errorf("S.observer[%d](%d, %d) error: %v", n, pid, offset, err))
|
||||||
// TODO: accumulate errors from all Observers.
|
|
||||||
}
|
}
|
||||||
wg.Done()
|
wg.Done()
|
||||||
}()
|
}()
|
||||||
@ -154,10 +215,31 @@ func (S *Scanner) Run() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
wg.Wait() // Wait for all observers to finish
|
wg.Wait() // Wait for all observers to finish
|
||||||
return S.err
|
return S.Errors()
|
||||||
}
|
}
|
||||||
|
|
||||||
// searchSymbolIn loops over the entries in /proc/<pid>/maps and searches for
|
// RunEvery() starts a scanning process and repeats at the given time step.
|
||||||
|
func (S *Scanner) RunEvery(step time.Duration) {
|
||||||
|
S.ticker = time.NewTicker(step)
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-S.ticker.C:
|
||||||
|
err := S.Run()
|
||||||
|
if err != nil {
|
||||||
|
S.Println(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (S *Scanner) Stop() {
|
||||||
|
if S.ticker != nil {
|
||||||
|
S.debugln("Stopping ticker")
|
||||||
|
S.ticker.Stop()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// searchSymbolInPid loops over the entries in /proc/<pid>/maps and searches for
|
||||||
// the symbol in the mapped files.
|
// the symbol in the mapped files.
|
||||||
//
|
//
|
||||||
// The current implementation makes the following assumptions:
|
// The current implementation makes the following assumptions:
|
||||||
@ -167,12 +249,12 @@ func (S *Scanner) Run() error {
|
|||||||
// 4. The symbol is present at most in one mapped file at the same time.
|
// 4. The symbol is present at most in one mapped file at the same time.
|
||||||
//
|
//
|
||||||
// It returns the offsets in memory of the running program, if found.
|
// It returns the offsets in memory of the running program, if found.
|
||||||
func (S *Scanner) searchSymbolIn(pid int) (offset uint64, found bool) {
|
func (S *Scanner) searchSymbolInPid(pid int) (offset uint64, found bool) {
|
||||||
|
|
||||||
path := filepath.Join("/proc", strconv.Itoa(pid), "maps")
|
path := filepath.Join("/proc", strconv.Itoa(pid), "maps")
|
||||||
maps, err := os.Open(path)
|
maps, err := os.Open(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
S.Printf("%v\n", err)
|
S.debugf("%v\n", err)
|
||||||
return 0, false
|
return 0, false
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -230,7 +312,7 @@ func (S *Scanner) searchSymbolIn(pid int) (offset uint64, found bool) {
|
|||||||
// Finally, find the symbol in the binary. If found,
|
// Finally, find the symbol in the binary. If found,
|
||||||
// findSymbol returns the offset of the symbol in memory,
|
// findSymbol returns the offset of the symbol in memory,
|
||||||
// taking alignment into account.
|
// taking alignment into account.
|
||||||
memOffset, found := S.findSymbol(pathname)
|
memOffset, found := S.findSymbolInELF(pathname)
|
||||||
if !found {
|
if !found {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@ -242,15 +324,15 @@ func (S *Scanner) searchSymbolIn(pid int) (offset uint64, found bool) {
|
|||||||
return 0, false
|
return 0, false
|
||||||
}
|
}
|
||||||
|
|
||||||
// findSymbol searches for the provided symbol in the given pathname to an
|
// findSymbolInELF searches for the provided symbol in the given pathname to an
|
||||||
// ELF-file. If found, it returns the offset of the symbol in the virtual
|
// ELF-file. If found, it returns the offset of the symbol in the virtual
|
||||||
// memory according to the fomula:
|
// memory according to the fomula:
|
||||||
//
|
//
|
||||||
// vmOffset = alignedOffset(section) + offsetInSection(symbol)
|
// vmOffset = alignedOffset(section) + offsetInSection(symbol)
|
||||||
//
|
//
|
||||||
// The result will be cached so that subsequent calls to findSymbol with the
|
// The result will be cached so that subsequent calls to findSymbolInELF with
|
||||||
// same pathname can quickly return.
|
// the same pathname can quickly return.
|
||||||
func (S *Scanner) findSymbol(pathname string) (offset uint64, found bool) {
|
func (S *Scanner) findSymbolInELF(pathname string) (offset uint64, found bool) {
|
||||||
|
|
||||||
// 0. Return the value from the cache, if found.
|
// 0. Return the value from the cache, if found.
|
||||||
if offset, found = S.cache[pathname]; found {
|
if offset, found = S.cache[pathname]; found {
|
||||||
|
Loading…
Reference in New Issue
Block a user