diff --git a/client/backend.go b/client/backend.go index bd3f79a..2774331 100644 --- a/client/backend.go +++ b/client/backend.go @@ -1,17 +1,25 @@ package client import ( + "bytes" + "encoding/json" + "errors" "github.com/sqshq/sampler/metadata" + "io/ioutil" "net/http" ) const ( - backendUrl = "http://localhost:8080/api/v1" - registrationPath = "/registration" - reportInstallationPath = "/report/installation" - reportCrashPath = "repost/crash" + backendUrl = "http://localhost/api/v1" + installationPath = "/telemetry/installation" + statisticsPath = "/telemetry/statistics" + crashPath = "/telemetry/crash" + registrationPath = "/license/registration" + jsonContentType = "application/json" ) +// Backend client is used to verify license and to send telemetry reports +// for analyses (anonymous usage data statistics and crash reports) type BackendClient struct { client http.Client } @@ -23,13 +31,57 @@ func NewBackendClient() *BackendClient { } func (c *BackendClient) ReportInstallation(statistics *metadata.Statistics) { + + buf := new(bytes.Buffer) + err := json.NewEncoder(buf).Encode(statistics) + if err != nil { + c.ReportCrash(err.Error(), statistics) + } + + _, err = http.Post(backendUrl+installationPath, jsonContentType, buf) + if err != nil { + c.ReportCrash(err.Error(), statistics) + } +} + +func (c *BackendClient) ReportUsageStatistics(error string, statistics *metadata.Statistics) { // TODO } -func (c *BackendClient) ReportCrash() { +func (c *BackendClient) ReportCrash(error string, statistics *metadata.Statistics) { // TODO } -func (c *BackendClient) Register(key string) { - // TODO +func (c *BackendClient) RegisterLicenseKey(licenseKey string, statistics *metadata.Statistics) (*metadata.License, error) { + + req := struct { + LicenseKey string + Statistics *metadata.Statistics + }{ + licenseKey, + statistics, + } + + buf := new(bytes.Buffer) + err := json.NewEncoder(buf).Encode(req) + if err != nil { + c.ReportCrash(err.Error(), statistics) + } + + response, err := http.Post( + backendUrl+registrationPath, jsonContentType, buf) + + if err != nil { + return nil, err + } + + if response.StatusCode != 200 { + body, _ := ioutil.ReadAll(response.Body) + return nil, errors.New(string(body)) + } + + var license metadata.License + json.NewDecoder(response.Body).Decode(&license) + + return &license, nil } diff --git a/component/statusbar.go b/component/statusbar.go index 3175da6..c983dc5 100644 --- a/component/statusbar.go +++ b/component/statusbar.go @@ -19,12 +19,12 @@ type StatusBar struct { } func NewStatusLine(configFileName string, palette console.Palette, license *metadata.License) *StatusBar { + block := *ui.NewBlock() block.Border = false - text := fmt.Sprintf(" %s %s | ", console.AppTitle, console.AppVersion) - if license == nil || !license.Purchased || !license.Valid { + if license == nil || !license.Valid { text += console.AppLicenseWarning } else if license.Username != nil { text += fmt.Sprintf("%s | licensed to %s", configFileName, *license.Username) diff --git a/config/config.go b/config/config.go index 2470d75..01fad02 100644 --- a/config/config.go +++ b/config/config.go @@ -27,20 +27,18 @@ func LoadConfig() (*Config, Options) { _, err := flags.Parse(&opt) if err != nil { - panic(err) + console.Exit("") } if opt.Version == true { - println(console.AppVersion) - os.Exit(0) + console.Exit(console.AppVersion) } - if opt.ConfigFile == nil && opt.License == nil { - println("Please specify config file using --config flag. Example: sampler --config example.yml") - os.Exit(0) + if opt.ConfigFile == nil && opt.LicenseKey == nil { + console.Exit("Please specify config file using --config flag. Example: sampler --config example.yml") } - if opt.License != nil { + if opt.LicenseKey != nil { return nil, opt } diff --git a/config/options.go b/config/options.go index d04277d..14cd61b 100644 --- a/config/options.go +++ b/config/options.go @@ -1,8 +1,9 @@ package config type Options struct { - ConfigFile *string `short:"c" long:"config" required:"false" description:"set path to YAML config file"` - License *string `short:"l" long:"license" required:"false" description:"provide license key. visit www.sampler.dev for details"` - Environment []string `short:"e" long:"env" required:"false" description:"specify name=value variable to use in script placeholder as $name. This flag takes precedence over the same name variables, specified in config yml"` - Version bool `short:"v" long:"version" required:"false" description:"print version"` + ConfigFile *string `short:"c" long:"config" required:"true" description:"Path to YAML config file"` + LicenseKey *string `short:"l" long:"license" description:"License key. Visit www.sampler.dev for details"` + Environment []string `short:"e" long:"env" description:"Specify name=value variable to use in script placeholder as $name. This flag takes precedence over the same name variables, specified in config yml"` + Version bool `short:"v" long:"version" description:"Print version"` + DisableTelemetry bool `long:"disable-telemetry" description:"Disable anonymous usage statistics and errors to be sent to Sampler online service for analyses"` } diff --git a/console/console.go b/console/console.go index 09c1be1..f502739 100644 --- a/console/console.go +++ b/console/console.go @@ -4,6 +4,7 @@ import ( "fmt" ui "github.com/gizak/termui/v3" "log" + "os" "time" ) @@ -33,10 +34,17 @@ func Init() { fmt.Printf("\033]0;%s\007", AppTitle) if err := ui.Init(); err != nil { - log.Fatalf("failed to initialize termui: %v", err) + log.Fatalf("Failed to initialize ui: %v", err) } } func Close() { ui.Close() } + +func Exit(message string) { + if len(message) > 0 { + println(message) + } + os.Exit(0) +} diff --git a/main.go b/main.go index 4224d07..23110b6 100644 --- a/main.go +++ b/main.go @@ -21,10 +21,38 @@ import ( ) type Starter struct { - lout *layout.Layout - player *asset.AudioPlayer - opt config.Options - cfg config.Config + player *asset.AudioPlayer + lout *layout.Layout + palette console.Palette + opt config.Options + cfg config.Config +} + +func (s *Starter) startAll() { + for _, c := range s.cfg.RunCharts { + cpt := runchart.NewRunChart(c, s.palette) + s.start(cpt, cpt.Consumer, c.ComponentConfig, c.Items, c.Triggers) + } + for _, c := range s.cfg.SparkLines { + cpt := sparkline.NewSparkLine(c, s.palette) + s.start(cpt, cpt.Consumer, c.ComponentConfig, []config.Item{c.Item}, c.Triggers) + } + for _, c := range s.cfg.BarCharts { + cpt := barchart.NewBarChart(c, s.palette) + s.start(cpt, cpt.Consumer, c.ComponentConfig, c.Items, c.Triggers) + } + for _, c := range s.cfg.Gauges { + cpt := gauge.NewGauge(c, s.palette) + s.start(cpt, cpt.Consumer, c.ComponentConfig, []config.Item{c.Cur, c.Min, c.Max}, c.Triggers) + } + for _, c := range s.cfg.AsciiBoxes { + cpt := asciibox.NewAsciiBox(c, s.palette) + s.start(cpt, cpt.Consumer, c.ComponentConfig, []config.Item{c.Item}, c.Triggers) + } + for _, c := range s.cfg.TextBoxes { + cpt := textbox.NewTextBox(c, s.palette) + s.start(cpt, cpt.Consumer, c.ComponentConfig, []config.Item{c.Item}, c.Triggers) + } } func (s *Starter) start(drawable ui.Drawable, consumer *data.Consumer, componentConfig config.ComponentConfig, itemsConfig []config.Item, triggersConfig []config.TriggerConfig) { @@ -39,6 +67,14 @@ func (s *Starter) start(drawable ui.Drawable, consumer *data.Consumer, component func main() { cfg, opt := config.LoadConfig() + bc := client.NewBackendClient() + + statistics := metadata.GetStatistics(cfg) + license := metadata.GetLicense() + + if opt.LicenseKey != nil { + registerLicense(statistics, opt, bc) + } console.Init() defer console.Close() @@ -46,58 +82,35 @@ func main() { player := asset.NewAudioPlayer() defer player.Close() - license := metadata.GetLicense() - statistics := metadata.PersistStatistics(cfg) - - if opt.License != nil { - // validate license - // save to storage on success - // exit with info - return - } - palette := console.GetPalette(*cfg.Theme) - lout := layout.NewLayout(component.NewStatusLine(*opt.ConfigFile, palette, license), component.NewMenu(palette), component.NewIntro(palette)) - bc := client.NewBackendClient() + lout := layout.NewLayout( + component.NewStatusLine(*opt.ConfigFile, palette, license), component.NewMenu(palette), component.NewIntro(palette)) - if license == nil { + if statistics.LaunchCount == 0 { + if !opt.DisableTelemetry { + go bc.ReportInstallation(statistics) + } lout.RunIntro() - metadata.InitLicense() - bc.ReportInstallation(statistics) - } else if !license.Purchased /* && random */ { - // TODO lout.showNagWindow() with timeout and OK button + } else /* with random */ { + // TODO if license == nil lout.showNagWindow() with timeout and OK button + // TODO if license != nil, verify license + // TODO report statistics } - starter := &Starter{lout, player, opt, *cfg} - startComponents(starter, cfg, palette) + metadata.PersistStatistics(cfg) + starter := &Starter{player, lout, palette, opt, *cfg} + starter.startAll() handler := event.NewHandler(lout, opt) handler.HandleEvents() } -func startComponents(starter *Starter, cfg *config.Config, palette console.Palette) { - for _, c := range cfg.RunCharts { - cpt := runchart.NewRunChart(c, palette) - starter.start(cpt, cpt.Consumer, c.ComponentConfig, c.Items, c.Triggers) - } - for _, c := range cfg.SparkLines { - cpt := sparkline.NewSparkLine(c, palette) - starter.start(cpt, cpt.Consumer, c.ComponentConfig, []config.Item{c.Item}, c.Triggers) - } - for _, c := range cfg.BarCharts { - cpt := barchart.NewBarChart(c, palette) - starter.start(cpt, cpt.Consumer, c.ComponentConfig, c.Items, c.Triggers) - } - for _, c := range cfg.Gauges { - cpt := gauge.NewGauge(c, palette) - starter.start(cpt, cpt.Consumer, c.ComponentConfig, []config.Item{c.Cur, c.Min, c.Max}, c.Triggers) - } - for _, c := range cfg.AsciiBoxes { - cpt := asciibox.NewAsciiBox(c, palette) - starter.start(cpt, cpt.Consumer, c.ComponentConfig, []config.Item{c.Item}, c.Triggers) - } - for _, c := range cfg.TextBoxes { - cpt := textbox.NewTextBox(c, palette) - starter.start(cpt, cpt.Consumer, c.ComponentConfig, []config.Item{c.Item}, c.Triggers) +func registerLicense(statistics *metadata.Statistics, opt config.Options, bc *client.BackendClient) { + lc, err := bc.RegisterLicenseKey(*opt.LicenseKey, statistics) + if err != nil { + console.Exit("License registration failed: " + err.Error()) + } else { + metadata.SaveLicense(*lc) + console.Exit("License successfully verified, Sampler can be restarted without --license flag now. Thank you.") } } diff --git a/metadata/license.go b/metadata/license.go index 06afa98..f5a67e6 100644 --- a/metadata/license.go +++ b/metadata/license.go @@ -6,11 +6,10 @@ import ( ) type License struct { - Purchased bool - Valid bool - Key *string - Username *string - Company *string + Key *string `yaml:"k"` + Username *string `yaml:"u"` + Company *string `yaml:"c"` + Valid bool `yaml:"v"` } const licenseFileName = "license.yml" @@ -32,18 +31,12 @@ func GetLicense() *License { } } -func InitLicense() { - - license := License{ - Purchased: false, - Valid: false, - } +func SaveLicense(license License) { file, err := yaml.Marshal(license) if err != nil { - log.Fatalf("Failed to marshal config file: %v", err) + log.Fatalf("Failed to marshal license file: %v", err) } - initStorage() - saveStorageFile(file, getPlatformStoragePath(licenseFileName)) + saveStorageFile(file, licenseFileName) } diff --git a/metadata/statistics.go b/metadata/statistics.go index 8a417d4..d64afc4 100644 --- a/metadata/statistics.go +++ b/metadata/statistics.go @@ -9,6 +9,8 @@ import ( "runtime" ) +// Anonymous usage data, which we collect for analyses and improvements +// User can disable it, along with crash reports, using --telemetry flag type Statistics struct { Version string OS string @@ -38,7 +40,7 @@ func PersistStatistics(config *config.Config) *Statistics { } statistics.WindowWidth = w - statistics.WindowWidth = h + statistics.WindowHeight = h statistics.LaunchCount += 1 } else { @@ -50,6 +52,7 @@ func PersistStatistics(config *config.Config) *Statistics { WindowHeight: h, ComponentsCount: countComponentsPerType(config), } + initStorage() } file, err := yaml.Marshal(statistics) @@ -62,6 +65,27 @@ func PersistStatistics(config *config.Config) *Statistics { return statistics } +func GetStatistics(cfg *config.Config) *Statistics { + if !fileExists(statisticsFileName) { + return &Statistics{ + Version: console.AppVersion, + OS: runtime.GOOS, + LaunchCount: 0, + WindowWidth: 0, + WindowHeight: 0, + ComponentsCount: countComponentsPerType(cfg), + } + } else { + file := readStorageFile(getPlatformStoragePath(statisticsFileName)) + license := new(Statistics) + err := yaml.Unmarshal(file, license) + if err != nil { + log.Fatalf("Failed to read statistics file: %v", err) + } + return license + } +} + func countComponentsPerType(config *config.Config) map[string]int { m := make(map[string]int) diff --git a/metadata/storage.go b/metadata/storage.go index beb62d4..a0db7b7 100644 --- a/metadata/storage.go +++ b/metadata/storage.go @@ -51,6 +51,6 @@ func readStorageFile(path string) []byte { func saveStorageFile(file []byte, fileName string) { err := ioutil.WriteFile(getPlatformStoragePath(fileName), file, os.ModePerm) if err != nil { - log.Fatalf("Failed to save the storage file: %v", err) + log.Fatalf("Failed to save the storage file: %s %v", fileName, err) } }