From 2756bbf4093da21c71067312fb334fcb0e287dab Mon Sep 17 00:00:00 2001 From: Kale Blankenship Date: Fri, 25 Nov 2016 17:21:12 -0800 Subject: [PATCH 1/2] refactor agent cache --- agent/cache/cache.go | 48 ++++---- agent/cache/keyvalue.go | 121 +++++++------------- agent/cache/testruns.go | 179 +++++++----------------------- agent/tasks/deletetestdata.go | 5 +- agent/tasks/downloadasset.go | 3 +- agent/tasks/downloadasset_test.go | 2 +- agent/tasks/executetestrun.go | 3 +- agent/tasks/installtestrun.go | 3 +- agent/tasks/keyvalue.go | 5 +- agent/tasks/setgroup.go | 12 +- agent/tasks/tasks.go | 4 +- cmd/todd-agent/main.go | 26 +++-- comms/comms.go | 18 ++- comms/rabbitmq.go | 54 ++++++--- 14 files changed, 191 insertions(+), 292 deletions(-) diff --git a/agent/cache/cache.go b/agent/cache/cache.go index e4aef8e..5dc1596 100644 --- a/agent/cache/cache.go +++ b/agent/cache/cache.go @@ -23,50 +23,52 @@ import ( "database/sql" "fmt" "os" + "path/filepath" - log "github.com/Sirupsen/logrus" _ "github.com/mattn/go-sqlite3" // This look strange but is necessary - the sqlite package is used indirectly by database/sql + "github.com/pkg/errors" "github.com/toddproject/todd/config" ) -func NewAgentCache(cfg config.Config) *AgentCache { - var ac AgentCache - ac.dbLoc = fmt.Sprintf("%s/agent_cache.db", cfg.LocalResources.OptDir) - return &ac -} - +// AgentCache provides methods for interacting with the on disk cache. type AgentCache struct { // Need similar abstractions to what you did in the tasks package - dbLoc string + db *sql.DB } -// TODO(mierdin): Handling errors in this package? +// New returns an initialized instance of AgentCache. +func New(cfg config.Config) (*AgentCache, error) { -// Init will set up the sqlite database to serve as this agent cache. -func (ac AgentCache) Init() { + dbLoc := filepath.Join(cfg.LocalResources.OptDir, "agent_cache.db") // Clean up any old cache data - os.Remove(ac.dbLoc) + err := os.Remove(dbLoc) + if err != nil && !os.IsNotExist(err) { + return nil, errors.Wrap(err, "removing existing DB file") + } // Open connection - db, err := sql.Open("sqlite3", ac.dbLoc) + db, err := sql.Open("sqlite3", dbLoc) if err != nil { - log.Fatal(err) + return nil, errors.Wrap(err, "opening DB file") } - defer db.Close() // Initialize database - sqlStmt := ` - create table testruns (id integer not null primary key, uuid text, testlet text, args text, targets text, results text); - delete from testruns; - create table keyvalue (id integer not null primary key, key text, value text); - delete from keyvalue; - ` + const sqlStmt = ` + CREATE TABLE testruns (id INTEGER NOT NULL PRIMARY KEY, uuid TEXT, testlet TEXT, args TEXT, targets TEXT, results TEXT); + CREATE TABLE keyvalue (id INTEGER NOT NULL PRIMARY KEY, key TEXT, value TEXT); + ` _, err = db.Exec(sqlStmt) if err != nil { - log.Errorf("%q: %s\n", err, sqlStmt) - return + return nil, fmt.Errorf("%q: %s", err, sqlStmt) } + + return &AgentCache{db: db}, nil +} + +// Close closes the underlying database connection. +func (ac *AgentCache) Close() error { + return ac.db.Close() } diff --git a/agent/cache/keyvalue.go b/agent/cache/keyvalue.go index f65c561..a547b1a 100644 --- a/agent/cache/keyvalue.go +++ b/agent/cache/keyvalue.go @@ -9,116 +9,77 @@ package cache import ( - "database/sql" - "fmt" - log "github.com/Sirupsen/logrus" - _ "github.com/mattn/go-sqlite3" // This look strange but is necessary - the sqlite package is used indirectly by database/sql + "github.com/pkg/errors" ) // GetKeyValue will retrieve a value from the agent cache using a key string -func (ac AgentCache) GetKeyValue(key string) string { - // Open connection - db, err := sql.Open("sqlite3", ac.dbLoc) - if err != nil { - log.Fatal(err) - } - defer db.Close() - - log.Debugf("Retrieving value of key - %s", key) +func (ac *AgentCache) GetKeyValue(key string) (string, error) { + log.Debugf("Retrieving value of key - %s\n", key) // First, see if the key exists. - rows, err := db.Query(fmt.Sprintf("select value from keyvalue where key = \"%s\" ", key)) + rows, err := ac.db.Query("SELECT value FROM keyvalue WHERE key = ?", key) if err != nil { - log.Fatal(err) + return "", errors.Wrap(err, "querying DB") } - value := "" defer rows.Close() + + var value string for rows.Next() { - rows.Scan(&value) + err = rows.Scan(&value) + if err != nil { + return "", errors.Wrap(err, "scanning values retrieved from DB") + } } - return value + return value, nil } // SetKeyValue sets a KeyValue pair within the agent cache -func (ac AgentCache) SetKeyValue(key, value string) error { - - // Open connection - db, err := sql.Open("sqlite3", ac.dbLoc) - if err != nil { - log.Fatal(err) - } - defer db.Close() - - log.Debugf("Writing keyvalue pair to agent cache - %s:%s", key, value) +func (ac *AgentCache) SetKeyValue(key, value string) error { + log.Debugf("Writing keyvalue pair to agent cache - %s:%s\n", key, value) // First, see if the key exists. - rows, err := db.Query(fmt.Sprintf("select key, value FROM keyvalue WHERE KEY = \"%s\";", key)) + rows, err := ac.db.Query("SELECT count(1) FROM keyvalue WHERE KEY = ?", key) if err != nil { - log.Fatal(err) + return errors.Wrap(err, "querying count from DB") } - rowcount := 0 defer rows.Close() - for rows.Next() { - rowcount++ - } - - if rowcount != 1 { - // If there is MORE than one row, we should delete the extras first - // TODO(mierdin): Is this really necessary? - if rowcount > 1 { - log.Warn("Extra keyvalue pair detected. Deleting and inserting new record.") - tx, err := db.Begin() - if err != nil { - log.Fatal(err) - } - stmt, err := tx.Prepare(fmt.Sprintf("DELETE FROM keyvalue WHERE KEY = \"%s\";", key)) - if err != nil { - log.Fatal(err) - } - defer stmt.Close() - _, err = stmt.Exec() - if err != nil { - log.Fatal(err) - } - } - - // Begin Insert - tx, err := db.Begin() - if err != nil { - log.Fatal(err) - } - stmt, err := tx.Prepare("insert into keyvalue(key, value) values(?, ?)") + var rowCount int + for rows.Next() { + err = rows.Scan(&rowCount) if err != nil { - log.Fatal(err) + return errors.Wrap(err, "scanning rowCount from DB") } - defer stmt.Close() - _, err = stmt.Exec(key, value) + } + + if rowCount != 1 { + tx, err := ac.db.Begin() if err != nil { - log.Fatal(err) + return errors.Wrap(err, "starting DB transaction") } - tx.Commit() - } else { + if rowCount > 1 { + // If there is MORE than one row, we should delete the extras first + // TODO(mierdin): Is this really necessary? + log.Warn("Extra keyvalue pair detected. Deleting and inserting new record.") - // Begin Update - tx, err := db.Begin() - if err != nil { - log.Fatal(err) + _, err = tx.Exec("DELETE FROM keyvalue WHERE KEY = ?", key) + if err != nil { + tx.Rollback() + return errors.Wrap(err, "deleteing keyvalues") + } } - stmt, err := tx.Prepare(fmt.Sprintf("update keyvalue set value = \"%s\" where key = \"%s\" ", value, key)) - if err != nil { - log.Fatal(err) - } - defer stmt.Close() - _, err = stmt.Exec() + _, err = tx.Exec("INSERT INTO keyvalue(key, value) values(?, ?)", key, value) if err != nil { - log.Fatal(err) + tx.Rollback() + return errors.Wrap(err, "inserting keyvalue into DB") } - tx.Commit() + return errors.Wrap(tx.Commit(), "commmitting transaction") } - return nil + + _, err = ac.db.Exec("UPDATE keyvalue SET value = ? WHERE key = ?", value, key) + return errors.Wrap(err, "updating keyvalue") } diff --git a/agent/cache/testruns.go b/agent/cache/testruns.go index a29b41b..ce8edf5 100644 --- a/agent/cache/testruns.go +++ b/agent/cache/testruns.go @@ -9,207 +9,106 @@ package cache import ( - "database/sql" "encoding/json" - "errors" - "fmt" log "github.com/Sirupsen/logrus" - _ "github.com/mattn/go-sqlite3" // This looks strange but is necessary - the sqlite package is used indirectly by database/sql + "github.com/pkg/errors" "github.com/toddproject/todd/agent/defs" ) // InsertTestRun places a new test run into the agent cache -func (ac AgentCache) InsertTestRun(tr defs.TestRun) error { - - // Open connection - db, err := sql.Open("sqlite3", ac.dbLoc) - if err != nil { - log.Error(err) - return errors.New("Error accessing sqlite cache") - } - defer db.Close() - - // Begin Insert - tx, err := db.Begin() - if err != nil { - log.Error(err) - return errors.New("Error beginning new InsertTestRun action") - } - stmt, err := tx.Prepare("insert into testruns(uuid, testlet, args, targets) values(?, ?, ?, ?)") - if err != nil { - log.Error(err) - return errors.New("Error preparing new InsertTestRun action") - } - defer stmt.Close() - +func (ac *AgentCache) InsertTestRun(tr defs.TestRun) error { // Marshal our string slices to be stored in the database jsonTargets, err := json.Marshal(tr.Targets) if err != nil { - log.Error(err) - return errors.New("Error marshaling testrun data into JSON") + return errors.Wrap(err, "marshaling testrun targets into JSON") } jsonArgs, err := json.Marshal(tr.Args) if err != nil { - log.Error(err) - return errors.New("Error marshaling testrun data into JSON") + return errors.Wrap(err, "marshaling testrun args into JSON") } - _, err = stmt.Exec(tr.UUID, tr.Testlet, string(jsonArgs), string(jsonTargets)) + _, err = ac.db.Exec("INSERT INTO testruns(uuid, testlet, args, targets) VALUES(?, ?, ?, ?)", + tr.UUID, tr.Testlet, string(jsonArgs), string(jsonTargets)) if err != nil { - log.Error(err) - return errors.New("Error executing new testrun insert") + return errors.Wrap(err, "executing new testrun insert") } - tx.Commit() - log.Info("Inserted new testrun into agent cache - ", tr.UUID) return nil } // GetTestRun retrieves a testrun from the agent cache by its UUID -func (ac AgentCache) GetTestRun(uuid string) (defs.TestRun, error) { - - var tr defs.TestRun - - // Open connection - db, err := sql.Open("sqlite3", ac.dbLoc) - if err != nil { - log.Error(err) - return tr, errors.New("Error accessing sqlite cache") - } - defer db.Close() - - rows, err := db.Query(fmt.Sprintf("select testlet, args, targets from testruns where uuid = \"%s\" ", uuid)) +func (ac *AgentCache) GetTestRun(uuid string) (*defs.TestRun, error) { + rows, err := ac.db.Query("SELECT testlet, args, targets FROM testruns WHERE uuid = ?", uuid) if err != nil { - log.Error(err) - return tr, errors.New("Error creating query for selecting testrun") + return nil, errors.Wrap(err, "querying for selecting testrun") } - defer rows.Close() - for rows.Next() { - // TODO(mierdin): This may be unnecessary - rows.Scan() might allow you to pass this in as a byteslice. Experiment with this - var argsJSON, targetsJSON string + tr := &defs.TestRun{UUID: uuid} + // TODO(mierdin): This may be unnecessary - rows.Scan() might allow you to pass this in as a byteslice. Experiment with this + var argsJSON, targetsJSON string + for rows.Next() { + err = rows.Scan(&tr.Testlet, &argsJSON, &targetsJSON) + if err != nil { + return tr, errors.Wrap(err, "scanning testrun data from database") + } - rows.Scan(&tr.Testlet, &argsJSON, &targetsJSON) err = json.Unmarshal([]byte(argsJSON), &tr.Args) if err != nil { - log.Error(err) - return tr, errors.New("Error unmarshaling testrun data from JSON") + return tr, errors.Wrap(err, "unmarshaling testrun args from JSON") } + err = json.Unmarshal([]byte(targetsJSON), &tr.Targets) if err != nil { - log.Error(err) - return tr, errors.New("Error unmarshaling testrun data from JSON") + return tr, errors.Wrap(err, "unmarshaling testrun targets from JSON") } } - tr.UUID = uuid - - log.Info("Found testrun ", tr.UUID, " running testlet ", tr.Testlet) + log.Infof("Found testrun %q running testlet %q\n", tr.UUID, tr.Testlet) return tr, nil } // UpdateTestRunData will update an existing testrun entry in the agent cache with the post-test // metrics dataset that corresponds to that testrun (by testrun UUID) -func (ac AgentCache) UpdateTestRunData(uuid string, testData string) error { - - // Open connection - db, err := sql.Open("sqlite3", ac.dbLoc) +func (ac *AgentCache) UpdateTestRunData(uuid string, testData string) error { + _, err := ac.db.Exec("UPDATE testruns SET results = ? WHERE uuid = ?", testData, uuid) if err != nil { - log.Error(err) - return errors.New("Error accessing sqlite cache for testrun update") + return errors.Wrap(err, "updating testrun data") } - defer db.Close() - // Begin Update - tx, err := db.Begin() - if err != nil { - log.Error(err) - return errors.New("Error beginning new UpdateTestRunData action") - } - - stmt, err := tx.Prepare(fmt.Sprintf("update testruns set results = '%s' where uuid = '%s' ", testData, uuid)) - if err != nil { - log.Error(err) - return errors.New("Error preparing new UpdateTestRunData action") - } - defer stmt.Close() - _, err = stmt.Exec() - if err != nil { - log.Error(err) - return errors.New("Error executing new UpdateTestRunData action") - } - tx.Commit() - - log.Infof("Inserted test data for %s into cache", uuid) + log.Infof("Inserted test data for %q into cache\n", uuid) return nil } // DeleteTestRun will remove an entire testrun entry from teh agent cache by UUID -func (ac AgentCache) DeleteTestRun(uuid string) error { - - // Open connection - db, err := sql.Open("sqlite3", ac.dbLoc) - if err != nil { - log.Error(err) - return errors.New("Error accessing sqlite cache for DeleteTestRun") - } - defer db.Close() - - // Begin Update - tx, err := db.Begin() - if err != nil { - log.Fatal(err) - return errors.New("Error beginning new DeleteTestRun action") - } - - stmt, err := tx.Prepare(fmt.Sprintf("delete from testruns where uuid = \"%s\" ", uuid)) - if err != nil { - log.Fatal(err) - return errors.New("Error preparing new DeleteTestRun action") - } - defer stmt.Close() - _, err = stmt.Exec() - if err != nil { - log.Fatal(err) - return errors.New("Error executing new DeleteTestRun action") - } - tx.Commit() - - return nil +func (ac *AgentCache) DeleteTestRun(uuid string) error { + _, err := ac.db.Exec("DELETE FROM testruns WHERE uuid = ?", uuid) + return errors.Wrap(err, "deleting testrun") } // GetFinishedTestRuns returns a map of test UUIDS (keys) and the corresponding post-test metric data for those UUIDs (values) // The metric data is stored as a string containing JSON text, so this is what's placed into this map (meaning JSON parsing is // not performed in this function) -func (ac AgentCache) GetFinishedTestRuns() (map[string]string, error) { - - retmap := make(map[string]string) - - // Open connection - db, err := sql.Open("sqlite3", ac.dbLoc) +func (ac *AgentCache) GetFinishedTestRuns() (map[string]string, error) { + rows, err := ac.db.Query(`SELECT uuid, results FROM testruns WHERE results != ""`) if err != nil { - log.Error(err) - return retmap, errors.New("Error accessing sqlite cache for finished testruns") + return nil, errors.Wrap(err, "selecting finished testruns") } - defer db.Close() - - rows, err := db.Query(fmt.Sprint("select uuid, results from testruns where results != \"\" ")) - if err != nil { - log.Error(err) - return retmap, errors.New("Error creating query for selecting finished testruns") - } - defer rows.Close() + + retmap := make(map[string]string) + var uuid, testdata string for rows.Next() { - var uuid, testdata string - rows.Scan(&uuid, &testdata) + err = rows.Scan(&uuid, &testdata) + if err != nil { + return nil, errors.Wrap(err, "scanning testrun data from database") + } log.Debug("Found ripe testrun: ", uuid) retmap[uuid] = testdata } diff --git a/agent/tasks/deletetestdata.go b/agent/tasks/deletetestdata.go index 9eb56a3..3e50480 100644 --- a/agent/tasks/deletetestdata.go +++ b/agent/tasks/deletetestdata.go @@ -26,10 +26,7 @@ type DeleteTestDataTask struct { } // Run contains the logic necessary to perform this task on the agent. -func (dtdt DeleteTestDataTask) Run() error { - - var ac = cache.NewAgentCache(dtdt.Config) - +func (dtdt DeleteTestDataTask) Run(ac *cache.AgentCache) error { err := ac.DeleteTestRun(dtdt.TestUUID) if err != nil { return fmt.Errorf("DeleteTestDataTask failed - %s", dtdt.TestUUID) diff --git a/agent/tasks/downloadasset.go b/agent/tasks/downloadasset.go index a700706..8aaa20c 100644 --- a/agent/tasks/downloadasset.go +++ b/agent/tasks/downloadasset.go @@ -15,6 +15,7 @@ import ( "strings" log "github.com/Sirupsen/logrus" + "github.com/toddproject/todd/agent/cache" ) // DownloadAssetTask defines this particular task. It contains definitions not only for the task message, but @@ -31,7 +32,7 @@ type DownloadAssetTask struct { // Run contains the logic necessary to perform this task on the agent. This particular task will download all required assets, // copy them into the appropriate directory, and ensure that the execute permission is given to each collector file. -func (dat DownloadAssetTask) Run() error { +func (dat DownloadAssetTask) Run(*cache.AgentCache) error { // Iterate over the slice of collectors and download them. for x := range dat.Assets { diff --git a/agent/tasks/downloadasset_test.go b/agent/tasks/downloadasset_test.go index eab40d7..e2b6b9f 100644 --- a/agent/tasks/downloadasset_test.go +++ b/agent/tasks/downloadasset_test.go @@ -53,7 +53,7 @@ func TestTaskRun(t *testing.T) { } // Run task - err := task.Run() + err := task.Run(nil) if err != nil { t.Fatalf("DownloadCollectors failed in some way and wasn't supposed to") diff --git a/agent/tasks/executetestrun.go b/agent/tasks/executetestrun.go index bf3adab..e2d57f2 100644 --- a/agent/tasks/executetestrun.go +++ b/agent/tasks/executetestrun.go @@ -36,7 +36,7 @@ type ExecuteTestRunTask struct { // Run contains the logic necessary to perform this task on the agent. This particular task will execute a // testrun that has already been installed into the local agent cache. In this context (single agent), // a testrun will be executed once per target, all in parallel. -func (ett ExecuteTestRunTask) Run() error { +func (ett ExecuteTestRunTask) Run(ac *cache.AgentCache) error { // gatheredData represents test data from this agent for all targets. // Key is target name, value is JSON output from testlet for that target @@ -54,7 +54,6 @@ func (ett ExecuteTestRunTask) Run() error { time.Sleep(3000 * time.Millisecond) // Retrieve test from cache by UUID - var ac = cache.NewAgentCache(ett.Config) tr, err := ac.GetTestRun(ett.TestUUID) if err != nil { log.Error(err) diff --git a/agent/tasks/installtestrun.go b/agent/tasks/installtestrun.go index a99f48c..a123cd8 100644 --- a/agent/tasks/installtestrun.go +++ b/agent/tasks/installtestrun.go @@ -34,7 +34,7 @@ type InstallTestRunTask struct { // The installation procedure will first run the referenced testlet in check mode // to help ensure that the actual testrun execution can take place. If that // succeeds, the testrun is installed in the agent cache. -func (itt InstallTestRunTask) Run() error { +func (itt InstallTestRunTask) Run(ac *cache.AgentCache) error { if itt.Tr.Testlet == "" { log.Error("Testlet parameter for this testrun is null") @@ -67,7 +67,6 @@ func (itt InstallTestRunTask) Run() error { } // Insert testrun into agent cache - var ac = cache.NewAgentCache(itt.Config) err = ac.InsertTestRun(itt.Tr) if err != nil { log.Error(err) diff --git a/agent/tasks/keyvalue.go b/agent/tasks/keyvalue.go index 3a996e2..f619868 100644 --- a/agent/tasks/keyvalue.go +++ b/agent/tasks/keyvalue.go @@ -27,10 +27,7 @@ type KeyValueTask struct { // Run contains the logic necessary to perform this task on the agent. This particular task // will simply pass a key/value pair to the agent cache to be set -func (kvt KeyValueTask) Run() error { - - var ac = cache.NewAgentCache(kvt.Config) - +func (kvt KeyValueTask) Run(ac *cache.AgentCache) error { err := ac.SetKeyValue(kvt.Key, kvt.Value) if err != nil { return fmt.Errorf("KeyValueTask failed - %s:%s", kvt.Key, kvt.Value) diff --git a/agent/tasks/setgroup.go b/agent/tasks/setgroup.go index 170e821..d813dfb 100644 --- a/agent/tasks/setgroup.go +++ b/agent/tasks/setgroup.go @@ -27,12 +27,14 @@ type SetGroupTask struct { // TODO (mierdin): Could this not be condensed with the generic "keyvalue" task? // Run contains the logic necessary to perform this task on the agent. -func (sgt SetGroupTask) Run() error { - - var ac = cache.NewAgentCache(sgt.Config) - +func (sgt SetGroupTask) Run(ac *cache.AgentCache) error { // First, see what the current group is. If it matches what this task is instructing, we don't need to do anything. - if ac.GetKeyValue("group") != sgt.GroupName { + groupName, err := ac.GetKeyValue("group") + if err != nil { + return err + } + + if groupName != sgt.GroupName { err := ac.SetKeyValue("group", sgt.GroupName) if err != nil { return fmt.Errorf("Failed to set keyvalue pair - %s:%s", "group", sgt.GroupName) diff --git a/agent/tasks/tasks.go b/agent/tasks/tasks.go index 6679c1e..c66df6c 100644 --- a/agent/tasks/tasks.go +++ b/agent/tasks/tasks.go @@ -11,6 +11,8 @@ package tasks import ( "io" "os" + + "github.com/toddproject/todd/agent/cache" ) // Task is an interface to define task behavior This is used for functions like those in comms @@ -20,7 +22,7 @@ type Task interface { // TODO(mierdin): This works but is a little "meh". Basically, each task has to have a "Run()" function, as enforced // by this interface. If the task needs some additional data, it gets these through struct properties. This works but // doesn't quite feel right. Come back to this and see if there's a better way. - Run() error + Run(*cache.AgentCache) error } // BaseTask is a struct that is intended to be embedded by specific task structs. Both of these in conjunction diff --git a/cmd/todd-agent/main.go b/cmd/todd-agent/main.go index 4784a97..7ca143b 100644 --- a/cmd/todd-agent/main.go +++ b/cmd/todd-agent/main.go @@ -57,20 +57,26 @@ func main() { } // Set up cache - var ac = cache.NewAgentCache(cfg) - ac.Init() + ac, err := cache.New(cfg) + if err != nil { + log.Fatal(err) + } + defer ac.Close() // Generate UUID uuid := hostresources.GenerateUUID() - ac.SetKeyValue("uuid", uuid) + err = ac.SetKeyValue("uuid", uuid) + if err != nil { + log.Fatal(err) + } log.Infof("ToDD Agent Activated: %s", uuid) // Start test data reporting service - go watchForFinishedTestRuns(cfg) + go watchForFinishedTestRuns(cfg, ac) // Construct comms package - tc, err := comms.NewToDDComms(cfg) + tc, err := comms.NewAgentComms(cfg, ac) if err != nil { os.Exit(1) } @@ -126,11 +132,11 @@ func main() { // It will periodically look at the table and send any present test data back to the server as a response. // When the server has successfully received this data, it will send a task back to this specific agent // to delete this row from the cache. -func watchForFinishedTestRuns(cfg config.Config) error { - - var ac = cache.NewAgentCache(cfg) - - agentUUID := ac.GetKeyValue("uuid") +func watchForFinishedTestRuns(cfg config.Config, ac *cache.AgentCache) error { + agentUUID, err := ac.GetKeyValue("uuid") + if err != nil { + return err + } for { diff --git a/comms/comms.go b/comms/comms.go index ccd4cc0..dcee2fb 100644 --- a/comms/comms.go +++ b/comms/comms.go @@ -15,6 +15,7 @@ import ( log "github.com/Sirupsen/logrus" + "github.com/toddproject/todd/agent/cache" "github.com/toddproject/todd/agent/defs" "github.com/toddproject/todd/agent/responses" "github.com/toddproject/todd/agent/tasks" @@ -47,12 +48,16 @@ type Package interface { SendTask(string, tasks.Task) error // watches for new group membership instructions in the cache and reregisters - WatchForGroup() + WatchForGroup() error ListenForGroupTasks(string, chan bool) error ListenForResponses(*chan bool) error SendResponse(responses.Response) error + + // adds a cache agent to the comms package. temporary until + // comms package is refactored + setAgentCache(*cache.AgentCache) } // toddComms is a struct to hold anything that satisfies the CommsPackage interface @@ -78,3 +83,14 @@ func NewToDDComms(cfg config.Config) (*toddComms, error) { // TODO: Return Packa return &tc, nil } + +// NewAgentComms returns a comms instance configured for agent usages. +// +// TODO: accept an interface for cache instead of concrete type +func NewAgentComms(cfg config.Config, ac *cache.AgentCache) (*toddComms, error) { + comms, err := NewToDDComms(cfg) + if err == nil { + comms.setAgentCache(ac) + } + return comms, err +} diff --git a/comms/rabbitmq.go b/comms/rabbitmq.go index 9c5e50a..ce16f55 100644 --- a/comms/rabbitmq.go +++ b/comms/rabbitmq.go @@ -50,6 +50,7 @@ func newRabbitMQComms(cfg config.Config) *rabbitMQComms { type rabbitMQComms struct { config config.Config queueURL string + ac *cache.AgentCache } // connectRabbitMQ wraps the amqp.Dial function in order to provide connection retry functionality @@ -463,7 +464,7 @@ func (rmq rabbitMQComms) ListenForTasks(uuid string) error { err = json.Unmarshal(d.Body, &downloadAssetTask) // TODO(mierdin): Need to handle this error - err = downloadAssetTask.Run() + err = downloadAssetTask.Run(rmq.ac) if err != nil { log.Warning("The KeyValue task failed to initialize") } @@ -477,7 +478,7 @@ func (rmq rabbitMQComms) ListenForTasks(uuid string) error { err = json.Unmarshal(d.Body, &kvTask) // TODO(mierdin): Need to handle this error - err = kvTask.Run() + err = kvTask.Run(rmq.ac) if err != nil { log.Warning("The KeyValue task failed to initialize") } @@ -491,7 +492,7 @@ func (rmq rabbitMQComms) ListenForTasks(uuid string) error { err = json.Unmarshal(d.Body, &sgTask) // TODO(mierdin): Need to handle this error - err = sgTask.Run() + err = sgTask.Run(rmq.ac) if err != nil { log.Warning("The SetGroup task failed to initialize") } @@ -505,7 +506,7 @@ func (rmq rabbitMQComms) ListenForTasks(uuid string) error { err = json.Unmarshal(d.Body, &dtdtTask) // TODO(mierdin): Need to handle this error - err = dtdtTask.Run() + err = dtdtTask.Run(rmq.ac) if err != nil { log.Warning("The DeleteTestData task failed to initialize") } @@ -513,8 +514,11 @@ func (rmq rabbitMQComms) ListenForTasks(uuid string) error { case "InstallTestRun": // Retrieve UUID - var ac = cache.NewAgentCache(rmq.config) - uuid := ac.GetKeyValue("uuid") + uuid, err := rmq.ac.GetKeyValue("uuid") + if err != nil { + log.Errorf("unable to retrieve UUID: %v", err) + continue + } itrTask := tasks.InstallTestRunTask{ Config: rmq.config, @@ -528,7 +532,7 @@ func (rmq rabbitMQComms) ListenForTasks(uuid string) error { response.AgentUUID = uuid response.TestUUID = itrTask.Tr.UUID - err = itrTask.Run() + err = itrTask.Run(rmq.ac) if err != nil { log.Warning("The InstallTestRun task failed to initialize") response.Status = "fail" @@ -540,8 +544,11 @@ func (rmq rabbitMQComms) ListenForTasks(uuid string) error { case "ExecuteTestRun": // Retrieve UUID - var ac = cache.NewAgentCache(rmq.config) - uuid := ac.GetKeyValue("uuid") + uuid, err := rmq.ac.GetKeyValue("uuid") + if err != nil { + log.Errorf("unable to retrieve UUID: %v", err) + continue + } etrTask := tasks.ExecuteTestRunTask{ Config: rmq.config, @@ -559,7 +566,7 @@ func (rmq rabbitMQComms) ListenForTasks(uuid string) error { response.Type = "AgentStatus" //TODO(mierdin): This is an extra step. Maybe a factory function for the task could help here? rmq.SendResponse(response) - err = etrTask.Run() + err = etrTask.Run(rmq.ac) if err != nil { log.Warning("The ExecuteTestRun task failed to initialize") response.Status = "fail" @@ -580,15 +587,15 @@ func (rmq rabbitMQComms) ListenForTasks(uuid string) error { // WatchForGroup should be run as a goroutine, like other background services. This is because it will itself spawn a goroutine to // listen for tasks that are sent to groups, and this goroutine can be restarted when group membership changes -func (rmq rabbitMQComms) WatchForGroup() { - - var ac = cache.NewAgentCache(rmq.config) - +func (rmq rabbitMQComms) WatchForGroup() error { // dereg is a channel that allows us to instruct the goroutine that's listening for tests to stop. This allows us to re-register to a new command dereg := make(chan bool) rereg: - group := ac.GetKeyValue("group") + group, err := rmq.ac.GetKeyValue("group") + if err != nil { + return err + } // if the group is nothing, rewrite to "mull". This is being done for now so that we don't have to worry if the goroutine was started or not // This way, it's always running, but if the agent is not in a group, it's listening on the "null" queue, which never has anything on it. @@ -614,18 +621,25 @@ rereg: time.Sleep(2 * time.Second) // The key "unackedGroup" stores a "true" or "false" to indicate that there has been a group change that we need to acknowledge (handle) - if ac.GetKeyValue("unackedGroup") == "true" { + unackedGroup, err := rmq.ac.GetKeyValue("unackedGroup") + if err != nil { + log.Warnf("unable to retrieve unackedGroup: %v\n", err) + continue + } + if unackedGroup == "true" { // This will kill the underlying goroutine, and in effect stop listening to the old queue. dereg <- true // Finally, set the "unackedGroup" to indicate that we've acknowledged the group change, and go back to the "rereg" label // to re-register onto the new group name - ac.SetKeyValue("unackedGroup", "false") + err := rmq.ac.SetKeyValue("unackedGroup", "false") + if err != nil { + log.Errorf("logging setting unackedGroup: %v\n", err) + } goto rereg } } - } // ListenForGroupTasks is a method that recieves tasks from the server that are intended for groups @@ -918,3 +932,7 @@ func (rmq rabbitMQComms) ListenForResponses(stopListeningForResponses *chan bool return nil } + +func (rmq *rabbitMQComms) setAgentCache(ac *cache.AgentCache) { + rmq.ac = ac +} From fd11344f5aa8ebdf28c79e5c4f15b6cf53479da4 Mon Sep 17 00:00:00 2001 From: Kale Blankenship Date: Mon, 26 Dec 2016 14:21:13 -0800 Subject: [PATCH 2/2] Add gitub.com/pkg/errors to vendor --- Godeps/Godeps.json | 7 +- vendor/github.com/pkg/errors/.gitignore | 24 ++ vendor/github.com/pkg/errors/.travis.yml | 11 + vendor/github.com/pkg/errors/LICENSE | 23 ++ vendor/github.com/pkg/errors/README.md | 52 +++++ vendor/github.com/pkg/errors/appveyor.yml | 32 +++ vendor/github.com/pkg/errors/errors.go | 269 ++++++++++++++++++++++ vendor/github.com/pkg/errors/stack.go | 178 ++++++++++++++ 8 files changed, 595 insertions(+), 1 deletion(-) create mode 100644 vendor/github.com/pkg/errors/.gitignore create mode 100644 vendor/github.com/pkg/errors/.travis.yml create mode 100644 vendor/github.com/pkg/errors/LICENSE create mode 100644 vendor/github.com/pkg/errors/README.md create mode 100644 vendor/github.com/pkg/errors/appveyor.yml create mode 100644 vendor/github.com/pkg/errors/errors.go create mode 100644 vendor/github.com/pkg/errors/stack.go diff --git a/Godeps/Godeps.json b/Godeps/Godeps.json index dcc78f3..2155dfe 100644 --- a/Godeps/Godeps.json +++ b/Godeps/Godeps.json @@ -1,7 +1,7 @@ { "ImportPath": "github.com/toddproject/todd", "GoVersion": "go1.6", - "GodepVersion": "v63", + "GodepVersion": "v75", "Packages": [ "./..." ], @@ -65,6 +65,11 @@ "Comment": "v1.1.0-67-g7204887", "Rev": "7204887cf3a42df1cfaa5505dc3a3427f6dded8b" }, + { + "ImportPath": "github.com/pkg/errors", + "Comment": "v0.8.0-2-g248dadf", + "Rev": "248dadf4e9068a0b3e79f02ed0a610d935de5302" + }, { "ImportPath": "github.com/streadway/amqp", "Rev": "6a378341a305bfbc252e8a6638f16cf443221997" diff --git a/vendor/github.com/pkg/errors/.gitignore b/vendor/github.com/pkg/errors/.gitignore new file mode 100644 index 0000000..daf913b --- /dev/null +++ b/vendor/github.com/pkg/errors/.gitignore @@ -0,0 +1,24 @@ +# Compiled Object files, Static and Dynamic libs (Shared Objects) +*.o +*.a +*.so + +# Folders +_obj +_test + +# Architecture specific extensions/prefixes +*.[568vq] +[568vq].out + +*.cgo1.go +*.cgo2.c +_cgo_defun.c +_cgo_gotypes.go +_cgo_export.* + +_testmain.go + +*.exe +*.test +*.prof diff --git a/vendor/github.com/pkg/errors/.travis.yml b/vendor/github.com/pkg/errors/.travis.yml new file mode 100644 index 0000000..567ccdb --- /dev/null +++ b/vendor/github.com/pkg/errors/.travis.yml @@ -0,0 +1,11 @@ +language: go +go_import_path: github.com/pkg/errors +go: + - 1.4.3 + - 1.5.4 + - 1.6.3 + - 1.7.3 + - tip + +script: + - go test -v ./... diff --git a/vendor/github.com/pkg/errors/LICENSE b/vendor/github.com/pkg/errors/LICENSE new file mode 100644 index 0000000..835ba3e --- /dev/null +++ b/vendor/github.com/pkg/errors/LICENSE @@ -0,0 +1,23 @@ +Copyright (c) 2015, Dave Cheney +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/vendor/github.com/pkg/errors/README.md b/vendor/github.com/pkg/errors/README.md new file mode 100644 index 0000000..273db3c --- /dev/null +++ b/vendor/github.com/pkg/errors/README.md @@ -0,0 +1,52 @@ +# errors [![Travis-CI](https://travis-ci.org/pkg/errors.svg)](https://travis-ci.org/pkg/errors) [![AppVeyor](https://ci.appveyor.com/api/projects/status/b98mptawhudj53ep/branch/master?svg=true)](https://ci.appveyor.com/project/davecheney/errors/branch/master) [![GoDoc](https://godoc.org/github.com/pkg/errors?status.svg)](http://godoc.org/github.com/pkg/errors) [![Report card](https://goreportcard.com/badge/github.com/pkg/errors)](https://goreportcard.com/report/github.com/pkg/errors) + +Package errors provides simple error handling primitives. + +`go get github.com/pkg/errors` + +The traditional error handling idiom in Go is roughly akin to +```go +if err != nil { + return err +} +``` +which applied recursively up the call stack results in error reports without context or debugging information. The errors package allows programmers to add context to the failure path in their code in a way that does not destroy the original value of the error. + +## Adding context to an error + +The errors.Wrap function returns a new error that adds context to the original error. For example +```go +_, err := ioutil.ReadAll(r) +if err != nil { + return errors.Wrap(err, "read failed") +} +``` +## Retrieving the cause of an error + +Using `errors.Wrap` constructs a stack of errors, adding context to the preceding error. Depending on the nature of the error it may be necessary to reverse the operation of errors.Wrap to retrieve the original error for inspection. Any error value which implements this interface can be inspected by `errors.Cause`. +```go +type causer interface { + Cause() error +} +``` +`errors.Cause` will recursively retrieve the topmost error which does not implement `causer`, which is assumed to be the original cause. For example: +```go +switch err := errors.Cause(err).(type) { +case *MyError: + // handle specifically +default: + // unknown error +} +``` + +[Read the package documentation for more information](https://godoc.org/github.com/pkg/errors). + +## Contributing + +We welcome pull requests, bug fixes and issue reports. With that said, the bar for adding new symbols to this package is intentionally set high. + +Before proposing a change, please discuss your change by raising an issue. + +## Licence + +BSD-2-Clause diff --git a/vendor/github.com/pkg/errors/appveyor.yml b/vendor/github.com/pkg/errors/appveyor.yml new file mode 100644 index 0000000..a932ead --- /dev/null +++ b/vendor/github.com/pkg/errors/appveyor.yml @@ -0,0 +1,32 @@ +version: build-{build}.{branch} + +clone_folder: C:\gopath\src\github.com\pkg\errors +shallow_clone: true # for startup speed + +environment: + GOPATH: C:\gopath + +platform: + - x64 + +# http://www.appveyor.com/docs/installed-software +install: + # some helpful output for debugging builds + - go version + - go env + # pre-installed MinGW at C:\MinGW is 32bit only + # but MSYS2 at C:\msys64 has mingw64 + - set PATH=C:\msys64\mingw64\bin;%PATH% + - gcc --version + - g++ --version + +build_script: + - go install -v ./... + +test_script: + - set PATH=C:\gopath\bin;%PATH% + - go test -v ./... + +#artifacts: +# - path: '%GOPATH%\bin\*.exe' +deploy: off diff --git a/vendor/github.com/pkg/errors/errors.go b/vendor/github.com/pkg/errors/errors.go new file mode 100644 index 0000000..842ee80 --- /dev/null +++ b/vendor/github.com/pkg/errors/errors.go @@ -0,0 +1,269 @@ +// Package errors provides simple error handling primitives. +// +// The traditional error handling idiom in Go is roughly akin to +// +// if err != nil { +// return err +// } +// +// which applied recursively up the call stack results in error reports +// without context or debugging information. The errors package allows +// programmers to add context to the failure path in their code in a way +// that does not destroy the original value of the error. +// +// Adding context to an error +// +// The errors.Wrap function returns a new error that adds context to the +// original error by recording a stack trace at the point Wrap is called, +// and the supplied message. For example +// +// _, err := ioutil.ReadAll(r) +// if err != nil { +// return errors.Wrap(err, "read failed") +// } +// +// If additional control is required the errors.WithStack and errors.WithMessage +// functions destructure errors.Wrap into its component operations of annotating +// an error with a stack trace and an a message, respectively. +// +// Retrieving the cause of an error +// +// Using errors.Wrap constructs a stack of errors, adding context to the +// preceding error. Depending on the nature of the error it may be necessary +// to reverse the operation of errors.Wrap to retrieve the original error +// for inspection. Any error value which implements this interface +// +// type causer interface { +// Cause() error +// } +// +// can be inspected by errors.Cause. errors.Cause will recursively retrieve +// the topmost error which does not implement causer, which is assumed to be +// the original cause. For example: +// +// switch err := errors.Cause(err).(type) { +// case *MyError: +// // handle specifically +// default: +// // unknown error +// } +// +// causer interface is not exported by this package, but is considered a part +// of stable public API. +// +// Formatted printing of errors +// +// All error values returned from this package implement fmt.Formatter and can +// be formatted by the fmt package. The following verbs are supported +// +// %s print the error. If the error has a Cause it will be +// printed recursively +// %v see %s +// %+v extended format. Each Frame of the error's StackTrace will +// be printed in detail. +// +// Retrieving the stack trace of an error or wrapper +// +// New, Errorf, Wrap, and Wrapf record a stack trace at the point they are +// invoked. This information can be retrieved with the following interface. +// +// type stackTracer interface { +// StackTrace() errors.StackTrace +// } +// +// Where errors.StackTrace is defined as +// +// type StackTrace []Frame +// +// The Frame type represents a call site in the stack trace. Frame supports +// the fmt.Formatter interface that can be used for printing information about +// the stack trace of this error. For example: +// +// if err, ok := err.(stackTracer); ok { +// for _, f := range err.StackTrace() { +// fmt.Printf("%+s:%d", f) +// } +// } +// +// stackTracer interface is not exported by this package, but is considered a part +// of stable public API. +// +// See the documentation for Frame.Format for more details. +package errors + +import ( + "fmt" + "io" +) + +// New returns an error with the supplied message. +// New also records the stack trace at the point it was called. +func New(message string) error { + return &fundamental{ + msg: message, + stack: callers(), + } +} + +// Errorf formats according to a format specifier and returns the string +// as a value that satisfies error. +// Errorf also records the stack trace at the point it was called. +func Errorf(format string, args ...interface{}) error { + return &fundamental{ + msg: fmt.Sprintf(format, args...), + stack: callers(), + } +} + +// fundamental is an error that has a message and a stack, but no caller. +type fundamental struct { + msg string + *stack +} + +func (f *fundamental) Error() string { return f.msg } + +func (f *fundamental) Format(s fmt.State, verb rune) { + switch verb { + case 'v': + if s.Flag('+') { + io.WriteString(s, f.msg) + f.stack.Format(s, verb) + return + } + fallthrough + case 's': + io.WriteString(s, f.msg) + case 'q': + fmt.Fprintf(s, "%q", f.msg) + } +} + +// WithStack annotates err with a stack trace at the point WithStack was called. +// If err is nil, WithStack returns nil. +func WithStack(err error) error { + if err == nil { + return nil + } + return &withStack{ + err, + callers(), + } +} + +type withStack struct { + error + *stack +} + +func (w *withStack) Cause() error { return w.error } + +func (w *withStack) Format(s fmt.State, verb rune) { + switch verb { + case 'v': + if s.Flag('+') { + fmt.Fprintf(s, "%+v", w.Cause()) + w.stack.Format(s, verb) + return + } + fallthrough + case 's': + io.WriteString(s, w.Error()) + case 'q': + fmt.Fprintf(s, "%q", w.Error()) + } +} + +// Wrap returns an error annotating err with a stack trace +// at the point Wrap is called, and the supplied message. +// If err is nil, Wrap returns nil. +func Wrap(err error, message string) error { + if err == nil { + return nil + } + err = &withMessage{ + cause: err, + msg: message, + } + return &withStack{ + err, + callers(), + } +} + +// Wrapf returns an error annotating err with a stack trace +// at the point Wrapf is call, and the format specifier. +// If err is nil, Wrapf returns nil. +func Wrapf(err error, format string, args ...interface{}) error { + if err == nil { + return nil + } + err = &withMessage{ + cause: err, + msg: fmt.Sprintf(format, args...), + } + return &withStack{ + err, + callers(), + } +} + +// WithMessage annotates err with a new message. +// If err is nil, WithMessage returns nil. +func WithMessage(err error, message string) error { + if err == nil { + return nil + } + return &withMessage{ + cause: err, + msg: message, + } +} + +type withMessage struct { + cause error + msg string +} + +func (w *withMessage) Error() string { return w.msg + ": " + w.cause.Error() } +func (w *withMessage) Cause() error { return w.cause } + +func (w *withMessage) Format(s fmt.State, verb rune) { + switch verb { + case 'v': + if s.Flag('+') { + fmt.Fprintf(s, "%+v\n", w.Cause()) + io.WriteString(s, w.msg) + return + } + fallthrough + case 's', 'q': + io.WriteString(s, w.Error()) + } +} + +// Cause returns the underlying cause of the error, if possible. +// An error value has a cause if it implements the following +// interface: +// +// type causer interface { +// Cause() error +// } +// +// If the error does not implement Cause, the original error will +// be returned. If the error is nil, nil will be returned without further +// investigation. +func Cause(err error) error { + type causer interface { + Cause() error + } + + for err != nil { + cause, ok := err.(causer) + if !ok { + break + } + err = cause.Cause() + } + return err +} diff --git a/vendor/github.com/pkg/errors/stack.go b/vendor/github.com/pkg/errors/stack.go new file mode 100644 index 0000000..6b1f289 --- /dev/null +++ b/vendor/github.com/pkg/errors/stack.go @@ -0,0 +1,178 @@ +package errors + +import ( + "fmt" + "io" + "path" + "runtime" + "strings" +) + +// Frame represents a program counter inside a stack frame. +type Frame uintptr + +// pc returns the program counter for this frame; +// multiple frames may have the same PC value. +func (f Frame) pc() uintptr { return uintptr(f) - 1 } + +// file returns the full path to the file that contains the +// function for this Frame's pc. +func (f Frame) file() string { + fn := runtime.FuncForPC(f.pc()) + if fn == nil { + return "unknown" + } + file, _ := fn.FileLine(f.pc()) + return file +} + +// line returns the line number of source code of the +// function for this Frame's pc. +func (f Frame) line() int { + fn := runtime.FuncForPC(f.pc()) + if fn == nil { + return 0 + } + _, line := fn.FileLine(f.pc()) + return line +} + +// Format formats the frame according to the fmt.Formatter interface. +// +// %s source file +// %d source line +// %n function name +// %v equivalent to %s:%d +// +// Format accepts flags that alter the printing of some verbs, as follows: +// +// %+s path of source file relative to the compile time GOPATH +// %+v equivalent to %+s:%d +func (f Frame) Format(s fmt.State, verb rune) { + switch verb { + case 's': + switch { + case s.Flag('+'): + pc := f.pc() + fn := runtime.FuncForPC(pc) + if fn == nil { + io.WriteString(s, "unknown") + } else { + file, _ := fn.FileLine(pc) + fmt.Fprintf(s, "%s\n\t%s", fn.Name(), file) + } + default: + io.WriteString(s, path.Base(f.file())) + } + case 'd': + fmt.Fprintf(s, "%d", f.line()) + case 'n': + name := runtime.FuncForPC(f.pc()).Name() + io.WriteString(s, funcname(name)) + case 'v': + f.Format(s, 's') + io.WriteString(s, ":") + f.Format(s, 'd') + } +} + +// StackTrace is stack of Frames from innermost (newest) to outermost (oldest). +type StackTrace []Frame + +func (st StackTrace) Format(s fmt.State, verb rune) { + switch verb { + case 'v': + switch { + case s.Flag('+'): + for _, f := range st { + fmt.Fprintf(s, "\n%+v", f) + } + case s.Flag('#'): + fmt.Fprintf(s, "%#v", []Frame(st)) + default: + fmt.Fprintf(s, "%v", []Frame(st)) + } + case 's': + fmt.Fprintf(s, "%s", []Frame(st)) + } +} + +// stack represents a stack of program counters. +type stack []uintptr + +func (s *stack) Format(st fmt.State, verb rune) { + switch verb { + case 'v': + switch { + case st.Flag('+'): + for _, pc := range *s { + f := Frame(pc) + fmt.Fprintf(st, "\n%+v", f) + } + } + } +} + +func (s *stack) StackTrace() StackTrace { + f := make([]Frame, len(*s)) + for i := 0; i < len(f); i++ { + f[i] = Frame((*s)[i]) + } + return f +} + +func callers() *stack { + const depth = 32 + var pcs [depth]uintptr + n := runtime.Callers(3, pcs[:]) + var st stack = pcs[0:n] + return &st +} + +// funcname removes the path prefix component of a function's name reported by func.Name(). +func funcname(name string) string { + i := strings.LastIndex(name, "/") + name = name[i+1:] + i = strings.Index(name, ".") + return name[i+1:] +} + +func trimGOPATH(name, file string) string { + // Here we want to get the source file path relative to the compile time + // GOPATH. As of Go 1.6.x there is no direct way to know the compiled + // GOPATH at runtime, but we can infer the number of path segments in the + // GOPATH. We note that fn.Name() returns the function name qualified by + // the import path, which does not include the GOPATH. Thus we can trim + // segments from the beginning of the file path until the number of path + // separators remaining is one more than the number of path separators in + // the function name. For example, given: + // + // GOPATH /home/user + // file /home/user/src/pkg/sub/file.go + // fn.Name() pkg/sub.Type.Method + // + // We want to produce: + // + // pkg/sub/file.go + // + // From this we can easily see that fn.Name() has one less path separator + // than our desired output. We count separators from the end of the file + // path until it finds two more than in the function name and then move + // one character forward to preserve the initial path segment without a + // leading separator. + const sep = "/" + goal := strings.Count(name, sep) + 2 + i := len(file) + for n := 0; n < goal; n++ { + i = strings.LastIndex(file[:i], sep) + if i == -1 { + // not enough separators found, set i so that the slice expression + // below leaves file unmodified + i = -len(sep) + break + } + } + // get back to 0 or trim the leading separator + file = file[i+len(sep):] + return file +}