send telemetry data to pcloud. pcloud dev settings (with PCLOUD_ENDPOINT). /client:show and /client:set commands (for no-telemetry)

This commit is contained in:
sawka 2023-01-22 23:10:18 -08:00
parent dc051beeb8
commit ea897bf53c
7 changed files with 236 additions and 6 deletions

View File

@ -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()

View File

@ -1 +1,3 @@
DROP TABLE activity;
ALTER TABLE client DROP COLUMN clientopts;

View File

@ -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 '';

View File

@ -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 {

View File

@ -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
}

View File

@ -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 {

View File

@ -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
}