From ea897bf53c1d72c4df427aa83b32b3f18afeacdf Mon Sep 17 00:00:00 2001 From: sawka Date: Sun, 22 Jan 2023 23:10:18 -0800 Subject: [PATCH] send telemetry data to pcloud. pcloud dev settings (with PCLOUD_ENDPOINT). /client:show and /client:set commands (for no-telemetry) --- cmd/main-server.go | 20 +++- db/migrations/000002_activity.down.sql | 2 + db/migrations/000002_activity.up.sql | 2 + pkg/cmdrunner/cmdrunner.go | 66 ++++++++++++- pkg/pcloud/pcloud.go | 129 ++++++++++++++++++++++++- pkg/sstore/migrate.go | 7 ++ pkg/sstore/sstore.go | 16 +++ 7 files changed, 236 insertions(+), 6 deletions(-) diff --git a/cmd/main-server.go b/cmd/main-server.go index 25e29105..bf66c9d8 100644 --- a/cmd/main-server.go +++ b/cmd/main-server.go @@ -406,8 +406,13 @@ func test() error { return nil } -func doBeforeClose() { - pcloud.SendTelemetry() +func sendTelemetryWrapper() { + ctx, cancelFn := context.WithTimeout(context.Background(), 5*time.Second) + defer cancelFn() + err := pcloud.SendTelemetry(ctx) + if err != nil { + log.Printf("[error] sending telemetry: %v\n", err) + } } // watch stdin, kill server if stdin is closed @@ -417,7 +422,7 @@ func stdinReadWatch() { _, err := os.Stdin.Read(buf) if err != nil { log.Printf("stdin closed/error, shutting down: %v\n", err) - doBeforeClose() + sendTelemetryWrapper() time.Sleep(1 * time.Second) syscall.Kill(syscall.Getpid(), syscall.SIGINT) } @@ -491,6 +496,15 @@ func main() { log.Printf("[error] resetting window focus: %v\n", err) } + go func() { + log.Printf("PCLOUD_ENDPOINT=%s\n", pcloud.GetEndpoint()) + time.Sleep(1 * time.Minute) + for { + sendTelemetryWrapper() + // send new telemetry every 8-hours + time.Sleep(8 * time.Hour) + } + }() go stdinReadWatch() go runWebSocketServer() gr := mux.NewRouter() diff --git a/db/migrations/000002_activity.down.sql b/db/migrations/000002_activity.down.sql index 6a508d33..75ccae38 100644 --- a/db/migrations/000002_activity.down.sql +++ b/db/migrations/000002_activity.down.sql @@ -1 +1,3 @@ DROP TABLE activity; + +ALTER TABLE client DROP COLUMN clientopts; diff --git a/db/migrations/000002_activity.up.sql b/db/migrations/000002_activity.up.sql index a143305a..dbd82104 100644 --- a/db/migrations/000002_activity.up.sql +++ b/db/migrations/000002_activity.up.sql @@ -10,3 +10,5 @@ CREATE TABLE activity ( clientversion varchar(20) NOT NULL, clientarch varchar(20) NOT NULL ); + +ALTER TABLE client ADD COLUMN clientopts json NOT NULL DEFAULT ''; diff --git a/pkg/cmdrunner/cmdrunner.go b/pkg/cmdrunner/cmdrunner.go index 58d193b4..456aa29c 100644 --- a/pkg/cmdrunner/cmdrunner.go +++ b/pkg/cmdrunner/cmdrunner.go @@ -19,6 +19,7 @@ import ( "github.com/scripthaus-dev/mshell/pkg/packet" "github.com/scripthaus-dev/mshell/pkg/shexec" "github.com/scripthaus-dev/sh2-server/pkg/comp" + "github.com/scripthaus-dev/sh2-server/pkg/pcloud" "github.com/scripthaus-dev/sh2-server/pkg/remote" "github.com/scripthaus-dev/sh2-server/pkg/scbase" "github.com/scripthaus-dev/sh2-server/pkg/scpacket" @@ -159,6 +160,7 @@ func init() { registerCmdFn("client", ClientCommand) registerCmdFn("client:set", ClientSetCommand) + registerCmdFn("client:show", ClientShowCommand) registerCmdFn("history", HistoryCommand) @@ -2083,11 +2085,71 @@ func KillServerCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (s } func ClientCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { - return nil, fmt.Errorf("/client requires a subcommand: %s", formatStrs([]string{"set"}, "or", false)) + return nil, fmt.Errorf("/client requires a subcommand: %s", formatStrs([]string{"set", "show"}, "or", false)) +} + +func boolToStr(v bool, trueStr string, falseStr string) string { + if v { + return trueStr + } + return falseStr +} + +func ClientShowCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { + clientData, err := sstore.EnsureClientData(ctx) + if err != nil { + return nil, fmt.Errorf("cannot retrieve client data: %v\n", err) + } + var buf bytes.Buffer + buf.WriteString(fmt.Sprintf(" %-15s %s\n", "userid", clientData.UserId)) + buf.WriteString(fmt.Sprintf(" %-15s %s\n", "clientid", clientData.ClientId)) + buf.WriteString(fmt.Sprintf(" %-15s %s\n", "telemetry", boolToStr(clientData.ClientOpts.NoTelemetry, "off", "on"))) + update := sstore.ModelUpdate{ + Info: &sstore.InfoMsgType{ + InfoTitle: fmt.Sprintf("client info"), + InfoLines: splitLinesForInfo(buf.String()), + }, + } + return update, nil } func ClientSetCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { - return nil, fmt.Errorf("not implemented") + clientData, err := sstore.EnsureClientData(ctx) + if err != nil { + return nil, fmt.Errorf("cannot retrieve client data: %v\n", err) + } + var varsUpdated []string + if pk.Kwargs["telemetry"] != "" { + noTelemetry := !resolveBool(pk.Kwargs["telemetry"], true) + if clientData.ClientOpts.NoTelemetry != noTelemetry { + clientOpts := clientData.ClientOpts + clientOpts.NoTelemetry = noTelemetry + err = sstore.SetClientOpts(ctx, clientOpts) + if err != nil { + return nil, fmt.Errorf("error trying to update client telemetry: %v", err) + } + log.Printf("client telemetry setting updated to %v\n", !noTelemetry) + err = pcloud.SendNoTelemetryUpdate(ctx, noTelemetry) + if err != nil { + // ignore error, just log + log.Printf("[error] sending no-telemetry update: %v\n", err) + log.Printf("note that telemetry update has still taken effect locally, and will be respected by the client\n") + } + } else { + log.Printf("client telemetry setting unchanged, is %v\n", !noTelemetry) + } + varsUpdated = append(varsUpdated, "telemetry") + } + if len(varsUpdated) == 0 { + return nil, fmt.Errorf("/client:set no updates, can set %s", formatStrs([]string{"telemetry"}, "or", false)) + } + update := sstore.ModelUpdate{ + Info: &sstore.InfoMsgType{ + InfoMsg: fmt.Sprintf("client updated %s", formatStrs(varsUpdated, "and", false)), + TimeoutMs: 2000, + }, + } + return update, nil } func formatTermOpts(termOpts sstore.TermOpts) string { diff --git a/pkg/pcloud/pcloud.go b/pkg/pcloud/pcloud.go index 33df9a2b..ffb39d2c 100644 --- a/pkg/pcloud/pcloud.go +++ b/pkg/pcloud/pcloud.go @@ -1,5 +1,132 @@ package pcloud -func SendTelemetry() error { +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "log" + "net/http" + "os" + "strconv" + "strings" + + "github.com/scripthaus-dev/sh2-server/pkg/scbase" + "github.com/scripthaus-dev/sh2-server/pkg/sstore" +) + +const PCloudEndpoint = "https://api.getprompt.dev/central" +const PCloudEndpointVarName = "PCLOUD_ENDPOINT" +const APIVersion = 1 + +type NoTelemetryInputType struct { + ClientId string `json:"clientid"` + Value bool `json:"value"` +} + +type TelemetryInputType struct { + UserId string `json:"userid"` + ClientId string `json:"clientid"` + Activity []*sstore.ActivityType `json:"activity"` +} + +func GetEndpoint() string { + if !scbase.IsDevMode() { + return PCloudEndpoint + } + endpoint := os.Getenv(PCloudEndpointVarName) + if endpoint == "" || !strings.HasPrefix(endpoint, "https://") { + panic("Invalid PCloud dev endpoint, PCLOUD_ENDPOINT not set or invalid") + } + return endpoint +} + +func makePostReq(ctx context.Context, apiUrl string, data interface{}) (*http.Request, error) { + var dataReader io.Reader + if data != nil { + byteArr, err := json.Marshal(data) + if err != nil { + return nil, fmt.Errorf("error marshaling json for %s request: %v", apiUrl, err) + } + dataReader = bytes.NewReader(byteArr) + } + fullUrl := GetEndpoint() + apiUrl + req, err := http.NewRequestWithContext(ctx, "POST", fullUrl, dataReader) + if err != nil { + return nil, fmt.Errorf("error creating %s request: %v", apiUrl, err) + } + req.Header.Set("Content-Type", "application/json") + req.Header.Set("X-PromptAPIVersion", strconv.Itoa(APIVersion)) + req.Header.Set("X-PromptAPIUrl", apiUrl) + req.Close = true + return req, nil +} + +func doRequest(req *http.Request, outputObj interface{}) (*http.Response, error) { + apiUrl := req.Header.Get("X-PromptAPIUrl") + log.Printf("sending request %v\n", req.URL) + resp, err := http.DefaultClient.Do(req) + if err != nil { + return nil, fmt.Errorf("error contacting pcloud %q service: %v", apiUrl, err) + } + defer resp.Body.Close() + bodyBytes, err := io.ReadAll(resp.Body) + if err != nil { + return resp, fmt.Errorf("error reading %q response body: %v", apiUrl, err) + } + if resp.StatusCode != http.StatusOK { + return resp, fmt.Errorf("error contacting pcloud %q service: %s", apiUrl, resp.Status) + } + if outputObj != nil && resp.Header.Get("Content-Type") == "application/json" { + err = json.Unmarshal(bodyBytes, outputObj) + if err != nil { + return resp, fmt.Errorf("error decoding json: %v", err) + } + } + return resp, nil +} + +func SendTelemetry(ctx context.Context) error { + clientData, err := sstore.EnsureClientData(ctx) + if err != nil { + return fmt.Errorf("cannot retrieve client data: %v", err) + } + if clientData.ClientOpts.NoTelemetry { + return nil + } + activity, err := sstore.GetNonUploadedActivity(ctx) + if err != nil { + return fmt.Errorf("cannot get activity: %v", err) + } + if len(activity) == 0 { + return nil + } + log.Printf("sending telemetry data\n") + input := TelemetryInputType{UserId: clientData.UserId, ClientId: clientData.ClientId, Activity: activity} + req, err := makePostReq(ctx, "/telemetry", input) + if err != nil { + return err + } + _, err = doRequest(req, nil) + if err != nil { + return err + } + return nil +} + +func SendNoTelemetryUpdate(ctx context.Context, noTelemetryVal bool) error { + clientData, err := sstore.EnsureClientData(ctx) + if err != nil { + return fmt.Errorf("cannot retrieve client data: %v", err) + } + req, err := makePostReq(ctx, "/no-telemetry", NoTelemetryInputType{ClientId: clientData.ClientId, Value: noTelemetryVal}) + if err != nil { + return err + } + _, err = doRequest(req, nil) + if err != nil { + return err + } return nil } diff --git a/pkg/sstore/migrate.go b/pkg/sstore/migrate.go index db43816d..5dcff1c1 100644 --- a/pkg/sstore/migrate.go +++ b/pkg/sstore/migrate.go @@ -4,6 +4,7 @@ import ( "fmt" "log" "strconv" + "time" _ "github.com/golang-migrate/migrate/v4/database/sqlite3" _ "github.com/golang-migrate/migrate/v4/source/file" @@ -99,12 +100,18 @@ func MigratePrintVersion() error { func MigrateCommandOpts(opts []string) error { var err error if opts[0] == "--migrate-up" { + fmt.Printf("migrate-up %v\n", GetSessionDBName()) + time.Sleep(3 * time.Second) err = MigrateUp() } else if opts[0] == "--migrate-down" { + fmt.Printf("migrate-down %v\n", GetSessionDBName()) + time.Sleep(3 * time.Second) err = MigrateDown() } else if opts[0] == "--migrate-goto" { n, err := strconv.Atoi(opts[1]) if err == nil { + fmt.Printf("migrate-goto %v => %d\n", GetSessionDBName(), n) + time.Sleep(3 * time.Second) err = MigrateGoto(uint(n)) } } else { diff --git a/pkg/sstore/sstore.go b/pkg/sstore/sstore.go index be68bdd2..cff8f208 100644 --- a/pkg/sstore/sstore.go +++ b/pkg/sstore/sstore.go @@ -132,6 +132,10 @@ type ActivityType struct { ClientArch string `json:"clientarch"` } +type ClientOptsType struct { + NoTelemetry bool `json:"notelemetry,omitempty"` +} + type ClientData struct { ClientId string `json:"clientid"` UserId string `json:"userid"` @@ -141,6 +145,7 @@ type ClientData struct { UserPublicKey *ecdsa.PublicKey `json:"-"` ActiveSessionId string `json:"activesessionid"` WinSize ClientWinSizeType `json:"winsize"` + ClientOpts ClientOptsType `json:"clientopts"` } func (c *ClientData) ToMap() map[string]interface{} { @@ -151,6 +156,7 @@ func (c *ClientData) ToMap() map[string]interface{} { rtn["userpublickeybytes"] = c.UserPublicKeyBytes rtn["activesessionid"] = c.ActiveSessionId rtn["winsize"] = quickJson(c.WinSize) + rtn["clientopts"] = quickJson(c.ClientOpts) return rtn } @@ -165,6 +171,7 @@ func ClientDataFromMap(m map[string]interface{}) *ClientData { quickSetBytes(&c.UserPublicKeyBytes, m, "userpublickeybytes") quickSetStr(&c.ActiveSessionId, m, "activesessionid") quickSetJson(&c.WinSize, m, "winsize") + quickSetJson(&c.ClientOpts, m, "clientopts") return &c } @@ -1005,3 +1012,12 @@ func EnsureClientData(ctx context.Context) (*ClientData, error) { } return &rtn, nil } + +func SetClientOpts(ctx context.Context, clientOpts ClientOptsType) error { + txErr := WithTx(ctx, func(tx *TxWrap) error { + query := `UPDATE client SET clientopts = ?` + tx.ExecWrap(query, quickJson(clientOpts)) + return nil + }) + return txErr +}