Remove all backend interaction

This commit is contained in:
sqshq 2019-12-21 21:05:01 -05:00
parent accb12e515
commit 76e9140cad
12 changed files with 17 additions and 695 deletions

View File

@ -1,153 +0,0 @@
package client
import (
"bytes"
"encoding/json"
"errors"
"github.com/sqshq/sampler/metadata"
"io/ioutil"
"net/http"
)
const (
backendUrl = "http://localhost/api/v1"
installationPath = "/telemetry/installation"
statisticsPath = "/telemetry/statistics"
crashPath = "/telemetry/crash"
registrationPath = "/license/registration"
verificationPath = "/license/verification"
)
// BackendClient is used to verify license and to send telemetry
// for analyses (anonymous usage data statistics and crash reports)
type BackendClient struct {
client http.Client
}
func NewBackendClient() *BackendClient {
return &BackendClient{
client: http.Client{},
}
}
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 = sendRequest(backendUrl+installationPath, buf)
if err != nil {
c.ReportCrash(err.Error(), statistics)
}
}
func (c *BackendClient) ReportStatistics(statistics *metadata.Statistics) {
buf := new(bytes.Buffer)
err := json.NewEncoder(buf).Encode(statistics)
if err != nil {
c.ReportCrash(err.Error(), statistics)
}
_, err = sendRequest(backendUrl+statisticsPath, buf)
if err != nil {
c.ReportCrash(err.Error(), statistics)
}
}
func (c *BackendClient) ReportCrash(error string, statistics *metadata.Statistics) {
req := struct {
Error string
Statistics *metadata.Statistics
}{
error,
statistics,
}
buf := new(bytes.Buffer)
err := json.NewEncoder(buf).Encode(req)
if err != nil {
return
}
_, _ = sendRequest(backendUrl+crashPath, buf)
}
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 := sendRequest(backendUrl+registrationPath, 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
}
func (c *BackendClient) VerifyLicenseKey(licenseKey string) (*metadata.License, error) {
req := struct {
LicenseKey string
}{
licenseKey,
}
buf := new(bytes.Buffer)
err := json.NewEncoder(buf).Encode(req)
if err != nil {
c.ReportCrash(err.Error(), nil)
}
response, err := sendRequest(backendUrl+verificationPath, 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
}
func sendRequest(url string, body *bytes.Buffer) (resp *http.Response, err error) {
c := http.DefaultClient
req, err := http.NewRequest("POST", url, body)
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", "application/json")
return c.Do(req)
}

View File

@ -1,142 +0,0 @@
package component
import (
ui "github.com/gizak/termui/v3"
"github.com/sqshq/sampler/component/util"
"github.com/sqshq/sampler/console"
)
type Intro struct {
*ui.Block
page IntroPage
option introOption
palette console.Palette
}
type IntroPage rune
const (
IntroPageWelcome IntroPage = 0
IntroPageCommercial IntroPage = 1
IntroPagePersonal IntroPage = 2
)
type introOption rune
const (
introOptionCommercial introOption = 0
introOptionPersonal introOption = 1
)
const (
buttonCommercial string = " COMMERCIAL USE "
buttonPersonal string = " PERSONAL USE "
buttonOk string = " OK "
)
func (intro *Intro) Up() {
intro.option = introOptionCommercial
}
func (intro *Intro) Down() {
intro.option = introOptionPersonal
}
func (intro *Intro) NextPage() {
if intro.option == introOptionCommercial {
intro.page = IntroPageCommercial
} else {
intro.page = IntroPagePersonal
}
}
func (intro *Intro) GetSelectedPage() IntroPage {
return intro.page
}
func NewIntro(palette console.Palette) *Intro {
return &Intro{
Block: NewBlock("", false, palette),
palette: palette,
}
}
func (intro *Intro) Draw(buffer *ui.Buffer) {
introText := append(util.AsciiLogo, []string{
"", "", "",
"Welcome.",
"",
"Sampler is an OSS project, and it needs funding to be alive and keep developing",
"Before the first start, please explore our licensing options below. For more details, visit WWW.SAMPLER.DEV",
"", "", "",
"How do you plan to use Sampler?",
}...)
commericalText := append(util.AsciiLogo, []string{
"", "", "", "",
"With Sampler, you can easily save time and solve some of your business problems.",
"That's why support of the project is in the interest of your organization.",
"",
"",
"We are offering commercial licenses which provide priority support and technical assistance.",
"After entering the licence key, your company name will appear in the status bar.",
"",
"",
"To make a purchase, please visit WWW.SAMPLER.DEV",
}...)
personalText := append(util.AsciiLogo, []string{
"", "", "", "",
"Sampler is always free to use, but you can support the project and donate any amount to get a personal license.",
"Once it is activated, your name will appear in the status bar.",
"",
"",
"To become a sponsor, please visit WWW.SAMPLER.DEV",
}...)
text := introText
switch intro.page {
case IntroPageWelcome:
text = introText
case IntroPageCommercial:
text = commericalText
case IntroPagePersonal:
text = personalText
}
for i, a := range text {
util.PrintString(
a,
ui.NewStyle(intro.palette.BaseColor),
util.GetMiddlePoint(intro.Block.Rectangle, a, i-15),
buffer)
}
highlightedStyle := ui.NewStyle(intro.palette.ReverseColor, intro.palette.BaseColor)
regularStyle := ui.NewStyle(intro.palette.BaseColor, intro.palette.ReverseColor)
if intro.page == IntroPageWelcome {
commercialButtonStyle := highlightedStyle
if intro.option == introOptionPersonal {
commercialButtonStyle = regularStyle
}
personalButtonStyle := highlightedStyle
if intro.option == introOptionCommercial {
personalButtonStyle = regularStyle
}
buffer.SetString(buttonCommercial, commercialButtonStyle,
util.GetMiddlePoint(intro.Block.Rectangle, buttonCommercial, 5))
buffer.SetString(buttonPersonal, personalButtonStyle,
util.GetMiddlePoint(intro.Block.Rectangle, buttonPersonal, 7))
} else {
buffer.SetString(buttonOk, highlightedStyle,
util.GetMiddlePoint(intro.Block.Rectangle, buttonOk, 7))
}
intro.Block.Draw(buffer)
}

View File

@ -19,8 +19,6 @@ type Layout struct {
Components []*component.Component
statusbar *component.StatusBar
menu *component.Menu
intro *component.Intro
nag *component.NagWindow
ChangeModeEvents chan Mode
mode Mode
selection int
@ -33,28 +31,24 @@ type Mode rune
const (
ModeDefault Mode = 0
ModeIntro Mode = 1
ModeNag Mode = 2
ModePause Mode = 3
ModeComponentSelect Mode = 4
ModeMenuOptionSelect Mode = 5
ModeComponentMove Mode = 6
ModeComponentResize Mode = 7
ModeChartPinpoint Mode = 8
ModePause Mode = 2
ModeComponentSelect Mode = 3
ModeMenuOptionSelect Mode = 4
ModeComponentMove Mode = 5
ModeComponentResize Mode = 6
ModeChartPinpoint Mode = 7
)
const (
minDimension = 3
statusbarHeight = 1
nagWindowDurationSec = 5
minDimension = 3
statusbarHeight = 1
)
func NewLayout(statusline *component.StatusBar, menu *component.Menu, intro *component.Intro, nag *component.NagWindow) *Layout {
func NewLayout(statusline *component.StatusBar, menu *component.Menu) *Layout {
width, height := ui.TerminalDimensions()
block := *ui.NewBlock()
block.SetRect(0, 0, width, height)
intro.SetRect(0, 0, width, height)
nag.SetRect(0, 0, width, height)
statusline.SetRect(0, height-statusbarHeight, width, height)
return &Layout{
@ -62,8 +56,6 @@ func NewLayout(statusline *component.StatusBar, menu *component.Menu, intro *com
Components: make([]*component.Component, 0),
statusbar: statusline,
menu: menu,
intro: intro,
nag: nag,
mode: ModeDefault,
selection: 0,
ChangeModeEvents: make(chan Mode, 10),
@ -79,10 +71,6 @@ func (l *Layout) StartWithIntro() {
l.mode = ModeIntro
}
func (l *Layout) StartWithNagWindow() {
l.mode = ModeNag
}
func (l *Layout) changeMode(m Mode) {
if m == ModeComponentResize || m == ModeComponentMove {
l.positionsChanged = true
@ -92,7 +80,7 @@ func (l *Layout) changeMode(m Mode) {
}
func (l *Layout) HandleMouseClick(x int, y int) {
if l.mode == ModeIntro || l.mode == ModeNag {
if l.mode == ModeIntro {
return
}
l.menu.Idle()
@ -151,15 +139,6 @@ func (l *Layout) HandleKeyboardEvent(e string) {
l.menu.Idle()
l.changeMode(ModeDefault)
break
case ModeIntro:
page := l.intro.GetSelectedPage()
if page == component.IntroPageWelcome {
l.intro.NextPage()
} else {
l.changeMode(ModeDefault)
}
case ModeNag:
l.nag.Accept()
}
case console.KeyEsc:
l.resetAlerts()
@ -222,8 +201,6 @@ func (l *Layout) HandleKeyboardEvent(e string) {
selected.Move(0, -1)
case ModeComponentResize:
selected.Resize(0, -1)
case ModeIntro:
l.intro.Up()
}
case console.KeyDown:
switch l.mode {
@ -239,8 +216,6 @@ func (l *Layout) HandleKeyboardEvent(e string) {
selected.Move(0, 1)
case ModeComponentResize:
selected.Resize(0, 1)
case ModeIntro:
l.intro.Down()
}
}
}
@ -330,22 +305,6 @@ func (l *Layout) Draw(buffer *ui.Buffer) {
c.SetRect(rectangle.Min.X, rectangle.Min.Y, rectangle.Max.X, rectangle.Max.Y)
}
if l.mode == ModeIntro {
l.intro.SetRect(l.Min.X, l.Min.Y, l.Max.X, l.Max.Y)
l.intro.Draw(buffer)
return
}
if l.mode == ModeNag {
if l.nag.IsAccepted() && time.Since(l.startupTime).Seconds() > nagWindowDurationSec {
l.mode = ModeDefault
} else {
l.nag.SetRect(l.Min.X, l.Min.Y, l.Max.X, l.Max.Y)
l.nag.Draw(buffer)
return
}
}
for _, c := range l.Components {
c.Draw(buffer)
}

View File

@ -1,54 +0,0 @@
package component
import (
ui "github.com/gizak/termui/v3"
"github.com/sqshq/sampler/component/util"
"github.com/sqshq/sampler/console"
)
type NagWindow struct {
*ui.Block
palette console.Palette
accepted bool
}
func NewNagWindow(palette console.Palette) *NagWindow {
return &NagWindow{
Block: NewBlock("", false, palette),
palette: palette,
accepted: false,
}
}
func (n *NagWindow) Accept() {
n.accepted = true
}
func (n *NagWindow) IsAccepted() bool {
return n.accepted
}
func (n *NagWindow) Draw(buffer *ui.Buffer) {
text := append(util.AsciiLogo, []string{
"", "", "",
"Thank you for using Sampler.",
"It is always free, but you can sponsor the project and buy the personal or commercial license",
"for priority support and technical assistance.",
"",
"Please visit www.sampler.dev",
}...)
for i, a := range text {
util.PrintString(
a,
ui.NewStyle(n.palette.BaseColor),
util.GetMiddlePoint(n.Block.Rectangle, a, i-15),
buffer)
}
buffer.SetString(buttonOk, ui.NewStyle(n.palette.ReverseColor, n.palette.BaseColor),
util.GetMiddlePoint(n.Block.Rectangle, buttonOk, 4))
n.Block.Draw(buffer)
}

View File

@ -4,7 +4,6 @@ import (
"fmt"
ui "github.com/gizak/termui/v3"
"github.com/sqshq/sampler/console"
"github.com/sqshq/sampler/metadata"
"image"
)
@ -20,24 +19,11 @@ type StatusBar struct {
pause bool
}
func NewStatusBar(configFileName string, palette console.Palette, license *metadata.License) *StatusBar {
func NewStatusBar(configFileName string, palette console.Palette) *StatusBar {
block := *ui.NewBlock()
block.Border = false
text := fmt.Sprintf(" %s %s | ", console.AppTitle, console.AppVersion)
if license == nil || !license.Valid || license.Type == nil {
text += console.AppLicenseWarning
} else if *license.Type == metadata.TypePersonal {
text += fmt.Sprintf("%s | personal license: %s", configFileName, *license.Username)
} else if license.Username != nil {
text += fmt.Sprintf("%s | licensed to %s", configFileName, *license.Username)
if license.Company != nil {
text += fmt.Sprintf(", %s", *license.Company)
}
} else {
text += fmt.Sprintf("%s | licensed to %s", configFileName, *license.Company)
}
text := fmt.Sprintf(" %s %s | %s", console.AppTitle, console.AppVersion, configFileName)
return &StatusBar{
Block: NewBlock("", false, palette),

View File

@ -34,14 +34,10 @@ func LoadConfig() (*Config, Options) {
console.Exit(console.AppVersion)
}
if opt.ConfigFile == nil && opt.LicenseKey == nil {
if opt.ConfigFile == nil {
console.Exit("Please specify config file using --config flag. Example: sampler --config example.yml")
}
if opt.LicenseKey != nil {
return nil, opt
}
cfg := readFile(opt.ConfigFile)
cfg.validate()
cfg.setDefaults()

View File

@ -2,9 +2,7 @@ package config
// Options with cli flags
type Options struct {
ConfigFile *string `short:"c" long:"config" 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"`
ConfigFile *string `short:"c" long:"config" description:"Path to YAML config file"`
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"`
}

View File

@ -16,7 +16,6 @@ const (
RowsCount = 40
AppTitle = "sampler"
AppVersion = "1.0.3"
AppLicenseWarning = "NOT ACTIVATED. PLEASE CONSIDER TO PURCHASE PERSONAL OR COMMERCIAL LICENSE. WWW.SAMPLER.DEV "
)
const (

65
main.go
View File

@ -1,10 +1,8 @@
package main
import (
"fmt"
ui "github.com/gizak/termui/v3"
"github.com/sqshq/sampler/asset"
"github.com/sqshq/sampler/client"
"github.com/sqshq/sampler/component"
"github.com/sqshq/sampler/component/asciibox"
"github.com/sqshq/sampler/component/barchart"
@ -17,8 +15,6 @@ import (
"github.com/sqshq/sampler/console"
"github.com/sqshq/sampler/data"
"github.com/sqshq/sampler/event"
"github.com/sqshq/sampler/metadata"
"runtime/debug"
"time"
)
@ -71,14 +67,6 @@ 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()
@ -88,28 +76,8 @@ func main() {
defer player.Close()
}
defer handleCrash(statistics, opt, bc)
defer updateStatistics(cfg, time.Now())
palette := console.GetPalette(*cfg.Theme)
lout := layout.NewLayout(component.NewStatusBar(*opt.ConfigFile, palette, license),
component.NewMenu(palette), component.NewIntro(palette), component.NewNagWindow(palette))
if statistics.LaunchCount == 0 {
if !opt.DisableTelemetry {
go bc.ReportInstallation(statistics)
}
lout.StartWithIntro()
} else if statistics.LaunchCount%20 == 0 { // once in a while
if license == nil || !license.Valid {
lout.StartWithNagWindow()
} else {
go verifyLicense(license, bc)
}
if !opt.DisableTelemetry {
go bc.ReportStatistics(statistics)
}
}
lout := layout.NewLayout(component.NewStatusBar(*opt.ConfigFile, palette), component.NewMenu(palette))
starter := &Starter{player, lout, palette, opt, *cfg}
samplers := starter.startAll()
@ -117,34 +85,3 @@ func main() {
handler := event.NewHandler(samplers, opt, lout)
handler.HandleEvents()
}
func handleCrash(statistics *metadata.Statistics, opt config.Options, bc *client.BackendClient) {
if rec := recover(); rec != nil {
err := rec.(error)
if !opt.DisableTelemetry {
bc.ReportCrash(fmt.Sprintf("%s\n%s", err.Error(), string(debug.Stack())), statistics)
}
panic(err)
}
}
func updateStatistics(cfg *config.Config, startTime time.Time) {
metadata.PersistStatistics(cfg, time.Since(startTime))
}
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.")
}
}
func verifyLicense(license *metadata.License, bc *client.BackendClient) {
verifiedLicense, _ := bc.VerifyLicenseKey(*license.Key)
if verifiedLicense != nil {
metadata.SaveLicense(*verifiedLicense)
}
}

View File

@ -1,53 +0,0 @@
package metadata
import (
"gopkg.in/yaml.v3"
"log"
)
type License struct {
Key *string `yaml:"k"`
Username *string `yaml:"u"`
Company *string `yaml:"c"`
Type *LicenseType `yaml:"t"`
Valid bool `yaml:"v"`
}
type LicenseType rune
const (
TypePersonal LicenseType = 0
TypeCommercial LicenseType = 1
)
const licenseFileName = "license.yml"
func GetLicense() *License {
if !fileExists(licenseFileName) {
return nil
}
file := readStorageFile(getPlatformStoragePath(licenseFileName))
license := new(License)
err := yaml.Unmarshal(file, license)
if err != nil {
log.Fatalf("Failed to read license file: %v", err)
}
return license
}
func SaveLicense(license License) {
initStorage()
file, err := yaml.Marshal(license)
if err != nil {
log.Fatalf("Failed to marshal license file: %v", err)
}
saveStorageFile(file, licenseFileName)
}

View File

@ -1,38 +0,0 @@
package metadata
import (
"os"
"testing"
)
func Test_getEmptyLicense(t *testing.T) {
cleanupPlatformStorage()
license := GetLicense()
if license != nil {
t.Errorf("expected to be nil")
}
}
func Test_saveAndGetExistingLicense(t *testing.T) {
cleanupPlatformStorage()
original := License{
Valid: true,
}
SaveLicense(original)
retrieved := *GetLicense()
if original != retrieved {
t.Errorf("read file != saved file")
}
}
func cleanupPlatformStorage() {
_ = os.RemoveAll(getPlatformStoragePath(""))
_ = os.Remove(getPlatformStoragePath(""))
}

View File

@ -1,113 +0,0 @@
package metadata
import (
ui "github.com/gizak/termui/v3"
"github.com/sqshq/sampler/config"
"github.com/sqshq/sampler/console"
"gopkg.in/yaml.v3"
"log"
"runtime"
"time"
)
// Statistics represents anonymous usage data, which we collect for analyses and improvements
// User can disable it, along with crash reports, using --disable-telemetry flag
type Statistics struct {
Version string
OS string
WindowWidth int `yaml:"ww"`
WindowHeight int `yaml:"wh"`
LaunchCount int `yaml:"lc"`
UsageTime int `yaml:"ut"`
ComponentsCount map[string]int `yaml:"cc"`
}
const statisticsFileName = "statistics.yml"
func PersistStatistics(config *config.Config, uptime time.Duration) *Statistics {
statistics := new(Statistics)
w, h := ui.TerminalDimensions()
if fileExists(statisticsFileName) {
file := readStorageFile(getPlatformStoragePath(statisticsFileName))
err := yaml.Unmarshal(file, statistics)
if err != nil {
log.Fatalf("Failed to read statistics file: %v", err)
}
if config != nil {
statistics.ComponentsCount = countComponentsPerType(config)
}
statistics.Version = console.AppVersion
statistics.WindowWidth = w
statistics.WindowHeight = h
statistics.LaunchCount += 1
statistics.UsageTime += int(uptime.Seconds())
} else {
statistics = &Statistics{
Version: console.AppVersion,
OS: runtime.GOOS,
WindowWidth: w,
WindowHeight: h,
LaunchCount: 1,
UsageTime: 0,
ComponentsCount: countComponentsPerType(config),
}
initStorage()
}
file, err := yaml.Marshal(statistics)
if err != nil {
log.Fatalf("Failed to marshal statistics file: %v", err)
}
saveStorageFile(file, statisticsFileName)
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),
}
}
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)
if config == nil {
return m
}
m["runcharts"] = len(config.RunCharts)
m["sparkLines"] = len(config.SparkLines)
m["barcharts"] = len(config.BarCharts)
m["gauges"] = len(config.Gauges)
m["asciiboxes"] = len(config.AsciiBoxes)
m["textboxes"] = len(config.TextBoxes)
return m
}