From 8de6750dca6142b85bdee9552d977c0be44e74d3 Mon Sep 17 00:00:00 2001 From: IndigoFox Date: Tue, 18 Apr 2023 00:06:10 -0700 Subject: [PATCH] big rework for settings. plus fixes found during testing --- arma.go | 838 +++++++++++++++++++++++++++++++--------------------- testsqf.sqf | 27 +- 2 files changed, 527 insertions(+), 338 deletions(-) diff --git a/arma.go b/arma.go index 4376848..a03491c 100644 --- a/arma.go +++ b/arma.go @@ -9,13 +9,16 @@ package main import "C" // This is required to import the C code import ( + "bytes" "context" "encoding/json" + "errors" "fmt" - "io" + "io/ioutil" "log" "os" "reflect" + "strconv" "strings" "time" "unsafe" @@ -27,31 +30,87 @@ import ( // declare list of functions available for call var AVAILABLE_FUNCTIONS = map[string]interface{}{ + "initExtension": initExtension, + "deinitExtension": deinitExtension, "loadSettings": loadSettings, "connectToInflux": connectToInflux, "writeToInflux": writeToInflux, "connectToTimescale": connectToTimescale, "initTimescale": initTimescale, "writeToTimescale": writeToTimescale, - "deinitialize": deinitialize, "getDir": getDir, "sanitizeLineProtocol": sanitizeLineProtocol, + "version": version, + "getUnixTimeNano": getUnixTimeNano, } var EXTENSION_VERSION string = "0.0.1" var extensionCallbackFnc C.extensionCallback -var influxConnectionSettings influxSettings -var a3Settings arma3Settings -var timescaleConnectionSettings timescaleSettings -var timescaleDbPool *pgxpool.Pool + +type ServerPollSetting struct { + Name string `json:"name"` + Enabled bool `json:"enabled"` + ServerOnly bool `json:"serverOnly"` + IntervalMs int `json:"intervalMs"` + Bucket string `json:"bucket"` + Measurement string `json:"measurement"` + Description string `json:"description"` +} + +var ServerPollSettingProperties []string = []string{ + "Name", + "Enabled", + "ServerOnly", + "IntervalMs", + "Bucket", + "Measurement", + "Description", +} + +type CBAEventHandler struct { + Name string `json:"name"` + Enabled bool `json:"enabled"` + Description string `json:"description"` +} + +var CBAEventHandlerProperties []string = []string{ + "Name", + "Enabled", + "Description", +} + +type settingsJson struct { + Influx struct { + Enabled bool `json:"enabled"` + Host string `json:"host"` + Token string `json:"token"` + Org string `json:"org"` + } `json:"influxdb"` + Timescale struct { + Enabled bool `json:"enabled"` + ConnectionUrl string `json:"connectionUrl"` + DatabaseName string `json:"databaseName"` + } `json:"timescaledb"` + Arma3 struct { + RefreshRateMs int `json:"refreshRateMs"` + Debug bool `json:"debug"` + } `json:"arma3"` + RecordingSettings map[string]interface{} `json:"recordingSettings"` +} + +var activeSettings settingsJson // InfluxDB variables -var DB_CLIENT influxdb2.Client +var influxClient influxdb2.Client + +// TimescaleDB variables +var timescaleDbPool *pgxpool.Pool // file paths -var ADDON_FOLDER string = "./@RangerMetrics" -var LOG_FILE string = ADDON_FOLDER + "/rangermetrics.log" -var SETTINGS_FILE string = ADDON_FOLDER + "/settings.json" +var ADDON_FOLDER string = getDir() + "\\@RangerMetrics" +var LOG_FILE string = ADDON_FOLDER + "\\rangermetrics.log" +var SETTINGS_FILE string = ADDON_FOLDER + "\\settings.json" +var SETTINGS_FILE_EXAMPLE string = ADDON_FOLDER + "\\settings.example.json" // var BACKUP_FILE_PATH string = ADDON_FOLDER + "/local_backup.log.gzip" // var BACKUP_WRITER *gzip.Writer @@ -65,36 +124,72 @@ func init() { log.Fatalf("error opening file: %v", err) } // log to console as well - log.SetOutput(io.MultiWriter(f, os.Stdout)) + // log.SetOutput(io.MultiWriter(f, os.Stdout)) + // log only to file + log.SetOutput(f) +} + +func initExtension() { + functionName := "initExtension" + logLine(functionName, `["Initializing RangerMetrics extension", "INFO"]`, false) + logLine(functionName, fmt.Sprintf(`["RangerMetrics Extension Version: %v", "INFO"]`, EXTENSION_VERSION), false) + + var err error + + activeSettings, err = loadSettings() + if err != nil { + logLine(functionName, fmt.Sprintf(`["Error loading settings: %v", "ERROR"]`, err), false) + return + } + + if activeSettings.Influx.Enabled { + influxClient, err = connectToInflux() + if err != nil { + logLine(functionName, fmt.Sprintf(`["Error connecting to InfluxDB: %v", "ERROR"]`, err), false) + return + } + } + if activeSettings.Timescale.Enabled { + timescaleDbPool, err = connectToTimescale() + if err != nil { + logLine(functionName, fmt.Sprintf(`["Error connecting to TimescaleDB: %v", "ERROR"]`, err), false) + return + } + + initTimescale() + } + logLine(functionName, `["Extension ready", "INFO"]`, false) +} + +func deinitExtension() { + functionName := "deinitExtension" + logLine(functionName, `["Deinitializing RangerMetrics extension", "INFO"]`, false) + + if timescaleDbPool != nil { + logLine(functionName, `["Closing TimescaleDB connection", "INFO"]`, false) + timescaleDbPool.Close() + } else { + logLine(functionName, `["TimescaleDB connection not open", "INFO"]`, false) + } + + logLine(functionName, `[true]`, false) } // func RVExtensionContext(output *C.char, argc *C.int) { // } -type influxSettings struct { - Host string `json:"host"` - Token string `json:"token"` - Org string `json:"org"` +func version() { + functionName := "version" + logLine(functionName, fmt.Sprintf(`["RangerMetrics Extension Version:%s", "INFO"]`, EXTENSION_VERSION), false) } -type timescaleSettings struct { - ConnectionUrl string `json:"connectionUrl"` -} +// return db client and error +func connectToInflux() (influxdb2.Client, error) { + if activeSettings.Influx.Host == "" || + activeSettings.Influx.Host == "http://host:8086" { -type arma3Settings struct { - RefreshRateMs int `json:"refreshRateMs"` -} - -type settingsJson struct { - Influx influxSettings `json:"influxdb"` - Arma3 arma3Settings `json:"arma3"` - Timescale timescaleSettings `json:"timescaledb"` -} - -func connectToInflux() { - if influxConnectionSettings.Host == "" { - logLine("connectToInflux", `["influxConnectionSettings.Host is empty", "ERROR"]`) + return nil, errors.New("influxConnectionSettings.Host is empty") // logLine("connectToInflux", `["Creating backup file", "INFO"]`) // file, err := os.Open(BACKUP_FILE_PATH) // if err != nil { @@ -109,249 +204,314 @@ func connectToInflux() { // return "Error connecting to Influx. Using local backup" } - DB_CLIENT = influxdb2.NewClientWithOptions(influxConnectionSettings.Host, influxConnectionSettings.Token, influxdb2.DefaultOptions().SetBatchSize(500).SetFlushInterval(2000)) + influxClient := influxdb2.NewClientWithOptions(activeSettings.Influx.Host, activeSettings.Influx.Token, influxdb2.DefaultOptions().SetBatchSize(1000).SetFlushInterval(1000)) - logLine("connectToInflux", `["DB_CLIENT created", "INFO"]`) + return influxClient, nil } ////////////////////////////////// // TIMESCALE ////////////////////////////////// -func connectToTimescale() { +func connectToTimescale() (*pgxpool.Pool, error) { functionName := "connectToTimescale" var err error // urlExample := "postgres://username:password@localhost:5432/database_name" // logLine("connectToTimescale", fmt.Sprintf(`["timescaleConnectionSettings.ConnectionUrl: %s", "INFO"]`, timescaleConnectionSettings.ConnectionUrl)) - conn, err := pgx.Connect(context.Background(), timescaleConnectionSettings.ConnectionUrl) + conn, err := pgx.Connect(context.Background(), activeSettings.Timescale.ConnectionUrl+"/postgres") if err != nil { logLine( functionName, - fmt.Sprintf(`["Unable to connect to Timescale DB: %v", "ERROR"]`, err.Error()), + fmt.Sprintf(`["Error connecting to Timescale DB: %v", "ERROR"]`, err.Error()), + false, ) - // return fmt.Sprintf(`["Error connecting to timescale DB: %v\n"]`, err.Error()) + return nil, err } - conn.Exec(context.Background(), "CREATE DATABASE rangermetrics;") - logLine("connectToTimescale", `["Connected to Timescale successfully", "INFO"]`) + // ensure database exists + logLine( + functionName, + fmt.Sprintf(`["TimescaleDB: 'CREATE DATABASE %s'", "INFO"]`, activeSettings.Timescale.DatabaseName), + false, + ) + conn.Query(context.Background(), fmt.Sprintf(`CREATE DATABASE %s`, activeSettings.Timescale.DatabaseName)) + + // close connection + conn.Close(context.Background()) + + // create connection pool + var dbPool *pgxpool.Pool + dbPool, err = pgxpool.Connect( + context.Background(), + fmt.Sprintf(`%s/%s`, activeSettings.Timescale.ConnectionUrl, activeSettings.Timescale.DatabaseName), + ) + if err != nil { + logLine( + functionName, + fmt.Sprintf(`["Error connecting to Timescale DB: %v", "ERROR"]`, err.Error()), + false, + ) + return nil, err + } + + logLine("connectToTimescale", `["Connected to Timescale successfully", "INFO"]`, false) + return dbPool, nil } func initTimescale() { - // create a table - // CREATE TABLE IF NOT EXISTS rangermetrics (time timestamp, line text); - timescaleDbPool.Exec(context.Background(), "CREATE DATABASE rangermetrics;") - // create entities table - timescaleDbPool.Exec(context.Background(), `CREATE TABLE "public.Missions" ( - "id" serial NOT NULL UNIQUE, - "world_name" VARCHAR(255) NOT NULL, - "briefing_name" VARCHAR(255) NOT NULL, - "mission_name" VARCHAR(255) NOT NULL, - "mission_author" VARCHAR(255) NOT NULL, - "server_name" VARCHAR(255) NOT NULL, - "server_mods" TEXT, - "ace_medical" BOOLEAN NOT NULL, - "radio_tfar" BOOLEAN NOT NULL, - "radio_acre" BOOLEAN NOT NULL, - "start_game" DATETIME NOT NULL, - "start_utc" DATETIME NOT NULL, - "frame_count" FLOAT NOT NULL, - "capture_delay_s" FLOAT NOT NULL, - "addon_ver_major" integer NOT NULL, - "addon_ver_minor" integer NOT NULL, - "addon_ver_patch" integer NOT NULL, - "extension_ver_major" integer NOT NULL, - "extension_ver_minor" integer NOT NULL, - "extension_ver_patch" integer NOT NULL, - "tags" VARCHAR(255) NOT NULL, - CONSTRAINT "Missions_pk" PRIMARY KEY ("id") - ) WITH ( - OIDS=FALSE - ); + functionName := "initTimescale" + var err error + + // schema init sql + var tableCreationSql string = ` +CREATE TABLE "Missions" ( + "id" serial NOT NULL UNIQUE, + "world_name" VARCHAR(255) NOT NULL, + "briefing_name" VARCHAR(255) NOT NULL, + "mission_name" VARCHAR(255) NOT NULL, + "mission_author" VARCHAR(255) NOT NULL, + "server_name" VARCHAR(255) NOT NULL, + "server_mods" TEXT, + "ace_medical" BOOLEAN NOT NULL, + "radio_tfar" BOOLEAN NOT NULL, + "radio_acre" BOOLEAN NOT NULL, + "start_game" TIMESTAMP NOT NULL, + "start_utc" TIMESTAMP NOT NULL, + "frame_count" FLOAT NOT NULL, + "capture_delay_s" FLOAT NOT NULL, + "addon_ver_major" integer NOT NULL, + "addon_ver_minor" integer NOT NULL, + "addon_ver_patch" integer NOT NULL, + "extension_ver_major" integer NOT NULL, + "extension_ver_minor" integer NOT NULL, + "extension_ver_patch" integer NOT NULL, + "tags" VARCHAR(255) NOT NULL, + CONSTRAINT "Missions_pk" PRIMARY KEY ("id") +) WITH ( + OIDS=FALSE +); - CREATE TABLE "public.Worlds" ( - "world_name" VARCHAR(255) NOT NULL, - "display_name" VARCHAR(255) NOT NULL, - "world_size_m" integer NOT NULL, - CONSTRAINT "Worlds_pk" PRIMARY KEY ("world_name") - ) WITH ( - OIDS=FALSE - ); +CREATE TABLE "Worlds" ( + "world_name" VARCHAR(255) NOT NULL, + "display_name" VARCHAR(255) NOT NULL, + "world_size_m" integer NOT NULL, + CONSTRAINT "Worlds_pk" PRIMARY KEY ("world_name") +) WITH ( + OIDS=FALSE +); - CREATE TABLE "public.Units" ( - "mission_id" integer NOT NULL, - "unit_id" integer NOT NULL, - "frame" integer NOT NULL, - "steamid64" varchar(100), - "steam_name" VARCHAR(255) NOT NULL, - "a3_profile_name" VARCHAR(255) NOT NULL, - "is_human" BOOLEAN NOT NULL, - "is_afk" BOOLEAN NOT NULL, - "is_alive" BOOLEAN NOT NULL, - "unit_type" VARCHAR(255) NOT NULL, - "role_description" VARCHAR(255), - "side" integer NOT NULL, - "group_id" varchar(100) NOT NULL, - "name" VARCHAR(255), - "position" VARCHAR(255) NOT NULL, - "direction" FLOAT NOT NULL, - "anim_state" varchar(100), - "stance" VARCHAR(255), - "traits" VARCHAR(255), - "damage" FLOAT NOT NULL, - "is_speaking" integer NOT NULL, - CONSTRAINT "Units_pk" PRIMARY KEY ("mission_id","unit_id","frame") - ) WITH ( - OIDS=FALSE - ); +CREATE TABLE "Units" ( + "mission_id" integer NOT NULL, + "unit_id" integer NOT NULL, + "frame" integer NOT NULL, + "steamid64" varchar(100), + "steam_name" VARCHAR(255) NOT NULL, + "a3_profile_name" VARCHAR(255) NOT NULL, + "is_human" BOOLEAN NOT NULL, + "is_afk" BOOLEAN NOT NULL, + "is_alive" BOOLEAN NOT NULL, + "unit_type" VARCHAR(255) NOT NULL, + "role_description" VARCHAR(255), + "side" integer NOT NULL, + "group_id" varchar(100) NOT NULL, + "name" VARCHAR(255), + "position" VARCHAR(255) NOT NULL, + "direction" FLOAT NOT NULL, + "anim_state" varchar(100), + "stance" VARCHAR(255), + "traits" VARCHAR(255), + "damage" FLOAT NOT NULL, + "is_speaking" integer NOT NULL, + CONSTRAINT "Units_pk" PRIMARY KEY ("mission_id","unit_id","frame") +) WITH ( + OIDS=FALSE +); - CREATE TABLE "public.Vehicles" ( - "mission_id" integer NOT NULL, - "frame" integer NOT NULL, - "vehicle_id" integer NOT NULL, - "object_type" VARCHAR(255), - "weapons" varchar(3000), - "customization" varchar(1000), - "position" VARCHAR(255) NOT NULL, - "direction" FLOAT NOT NULL, - "vector_dir" varchar(70) NOT NULL, - "vector_up" varchar(70) NOT NULL, - "is_alive" BOOLEAN NOT NULL, - "damage" FLOAT NOT NULL, - CONSTRAINT "Vehicles_pk" PRIMARY KEY ("mission_id","frame","vehicle_id") - ) WITH ( - OIDS=FALSE - ); +CREATE TABLE "Vehicles" ( + "mission_id" integer NOT NULL, + "frame" integer NOT NULL, + "vehicle_id" integer NOT NULL, + "object_type" VARCHAR(255), + "weapons" varchar(3000), + "customization" varchar(1000), + "position" VARCHAR(255) NOT NULL, + "direction" FLOAT NOT NULL, + "vector_dir" varchar(70) NOT NULL, + "vector_up" varchar(70) NOT NULL, + "is_alive" BOOLEAN NOT NULL, + "damage" FLOAT NOT NULL, + CONSTRAINT "Vehicles_pk" PRIMARY KEY ("mission_id","frame","vehicle_id") +) WITH ( + OIDS=FALSE +); - CREATE TABLE "public.Markers" ( - "mission_id" integer NOT NULL, - "frame" integer NOT NULL, - "marker_name" VARCHAR(255) NOT NULL, - "data" VARCHAR(255) NOT NULL, - CONSTRAINT "Markers_pk" PRIMARY KEY ("mission_id","frame","marker_name") - ) WITH ( - OIDS=FALSE - ); +CREATE TABLE "Markers" ( + "mission_id" integer NOT NULL, + "frame" integer NOT NULL, + "marker_name" VARCHAR(255) NOT NULL, + "data" VARCHAR(255) NOT NULL, + CONSTRAINT "Markers_pk" PRIMARY KEY ("mission_id","frame","marker_name") +) WITH ( + OIDS=FALSE +); - CREATE TABLE "public.Chat" ( - "id" serial NOT NULL, - "mission_id" integer NOT NULL, - "sender_id" integer NOT NULL, - "frame" integer NOT NULL, - "timestamp_utc" TIMESTAMP NOT NULL, - "content" VARCHAR(255) NOT NULL, - "channel" integer NOT NULL, - CONSTRAINT "Chat_pk" PRIMARY KEY ("id") - ) WITH ( - OIDS=FALSE - ); +CREATE TABLE "Chat" ( + "id" serial NOT NULL, + "mission_id" integer NOT NULL, + "sender_id" integer NOT NULL, + "frame" integer NOT NULL, + "timestamp_utc" TIMESTAMP NOT NULL, + "content" VARCHAR(255) NOT NULL, + "channel" integer NOT NULL, + CONSTRAINT "Chat_pk" PRIMARY KEY ("id") +) WITH ( + OIDS=FALSE +); - CREATE TABLE "public.UniqueUsers" ( - "steamid64" varchar(100) NOT NULL, - CONSTRAINT "UniqueUsers_pk" PRIMARY KEY ("steamid64") - ) WITH ( - OIDS=FALSE - ); +CREATE TABLE "UniqueUsers" ( + "steamid64" varchar(100) NOT NULL, + CONSTRAINT "UniqueUsers_pk" PRIMARY KEY ("steamid64") +) WITH ( + OIDS=FALSE +); - CREATE TABLE "public.Environment" ( - "mission_id" integer NOT NULL, - "frame" integer NOT NULL, - "date_game" DATETIME NOT NULL, - "date_utc" DATETIME NOT NULL, - "overcast" FLOAT NOT NULL, - "rain" FLOAT NOT NULL, - "humidity" FLOAT NOT NULL, - "fog_value" FLOAT NOT NULL, - "fog_decay" FLOAT NOT NULL, - "fog_base" FLOAT NOT NULL, - "wind_vector" VARCHAR(255) NOT NULL, - "gusts" FLOAT NOT NULL, - "waves" FLOAT NOT NULL, - CONSTRAINT "Environment_pk" PRIMARY KEY ("mission_id","frame") - ) WITH ( - OIDS=FALSE - ); +CREATE TABLE "Environment" ( + "mission_id" integer NOT NULL, + "frame" integer NOT NULL, + "date_game" TIMESTAMP NOT NULL, + "date_utc" TIMESTAMP NOT NULL, + "overcast" FLOAT NOT NULL, + "rain" FLOAT NOT NULL, + "humidity" FLOAT NOT NULL, + "fog_value" FLOAT NOT NULL, + "fog_decay" FLOAT NOT NULL, + "fog_base" FLOAT NOT NULL, + "wind_vector" VARCHAR(255) NOT NULL, + "gusts" FLOAT NOT NULL, + "waves" FLOAT NOT NULL, + CONSTRAINT "Environment_pk" PRIMARY KEY ("mission_id","frame") +) WITH ( + OIDS=FALSE +); - CREATE TABLE "public.StaticObjects" ( - "mission_id" integer NOT NULL, - "frame" integer NOT NULL, - "building_id" integer NOT NULL, - "position" integer NOT NULL, - "direction" FLOAT NOT NULL, - "vector_dir" varchar(70) NOT NULL, - "vector_up" varchar(70) NOT NULL, - "object_type" VARCHAR(255) NOT NULL, - "classname" VARCHAR(255) NOT NULL, - "simple_object_data" varchar(1500) NOT NULL, - CONSTRAINT "StaticObjects_pk" PRIMARY KEY ("mission_id","frame","building_id") - ) WITH ( - OIDS=FALSE - ); +CREATE TABLE "StaticObjects" ( + "mission_id" integer NOT NULL, + "frame" integer NOT NULL, + "building_id" integer NOT NULL, + "position" integer NOT NULL, + "direction" FLOAT NOT NULL, + "vector_dir" varchar(70) NOT NULL, + "vector_up" varchar(70) NOT NULL, + "object_type" VARCHAR(255) NOT NULL, + "classname" VARCHAR(255) NOT NULL, + "simple_object_data" varchar(1500) NOT NULL, + CONSTRAINT "StaticObjects_pk" PRIMARY KEY ("mission_id","frame","building_id") +) WITH ( + OIDS=FALSE +); - CREATE TABLE "public.Campaigns" ( - "id" serial NOT NULL, - "name" VARCHAR(255) NOT NULL, - "description" TEXT NOT NULL, - "image" bytea NOT NULL, - CONSTRAINT "Campaigns_pk" PRIMARY KEY ("id") - ) WITH ( - OIDS=FALSE - ); +CREATE TABLE "Campaigns" ( + "id" serial NOT NULL, + "name" VARCHAR(255) NOT NULL, + "description" TEXT NOT NULL, + "image" bytea NOT NULL, + CONSTRAINT "Campaigns_pk" PRIMARY KEY ("id") +) WITH ( + OIDS=FALSE +); - CREATE TABLE "public.Missions_In_Campaigns" ( - "mission" integer NOT NULL, - "campaign" integer NOT NULL, - CONSTRAINT "Missions_In_Campaigns_pk" PRIMARY KEY ("mission","campaign") - ) WITH ( - OIDS=FALSE - ); +CREATE TABLE "Missions_In_Campaigns" ( + "mission" integer NOT NULL, + "campaign" integer NOT NULL, + CONSTRAINT "Missions_In_Campaigns_pk" PRIMARY KEY ("mission","campaign") +) WITH ( + OIDS=FALSE +); +` + relationCreationSql := []string{ + `ALTER TABLE "Missions" ADD CONSTRAINT "Missions_fk0" FOREIGN KEY ("world_name") REFERENCES "Worlds"("world_name");`, + `ALTER TABLE "Units" ADD CONSTRAINT "Units_fk0" FOREIGN KEY ("mission_id") REFERENCES "Missions"("id");`, + `ALTER TABLE "Units" ADD CONSTRAINT "Units_fk1" FOREIGN KEY ("steamid64") REFERENCES "UniqueUsers"("steamid64");`, - ALTER TABLE "Missions" ADD CONSTRAINT "Missions_fk0" FOREIGN KEY ("world_name") REFERENCES "Worlds"("world_name"); + `ALTER TABLE "Vehicles" ADD CONSTRAINT "Vehicles_fk0" FOREIGN KEY ("mission_id") REFERENCES "Missions"("id");`, + `ALTER TABLE "Markers" ADD CONSTRAINT "Markers_fk0" FOREIGN KEY ("mission_id") REFERENCES "Missions"("id");`, - ALTER TABLE "Units" ADD CONSTRAINT "Units_fk0" FOREIGN KEY ("mission_id") REFERENCES "Missions"("id"); - ALTER TABLE "Units" ADD CONSTRAINT "Units_fk1" FOREIGN KEY ("steamid64") REFERENCES "UniqueUsers"("steamid64"); + `ALTER TABLE "Chat" ADD CONSTRAINT "Chat_fk0" FOREIGN KEY ("mission_id") REFERENCES "Missions"("id");`, + `ALTER TABLE "Chat" ADD CONSTRAINT "Chat_fk1" FOREIGN KEY ("sender_id") REFERENCES "Units"("unit_id");`, - ALTER TABLE "Vehicles" ADD CONSTRAINT "Vehicles_fk0" FOREIGN KEY ("mission_id") REFERENCES "Missions"("id"); + `ALTER TABLE "Environment" ADD CONSTRAINT "Environment_fk0" FOREIGN KEY ("mission_id") REFERENCES "Missions"("id");`, - ALTER TABLE "Markers" ADD CONSTRAINT "Markers_fk0" FOREIGN KEY ("mission_id") REFERENCES "Missions"("id"); + `ALTER TABLE "StaticObjects" ADD CONSTRAINT "StaticObjects_fk0" FOREIGN KEY ("mission_id") REFERENCES "Missions"("id");`, - ALTER TABLE "Chat" ADD CONSTRAINT "Chat_fk0" FOREIGN KEY ("mission_id") REFERENCES "Missions"("id"); - ALTER TABLE "Chat" ADD CONSTRAINT "Chat_fk1" FOREIGN KEY ("sender_id") REFERENCES "Units"("unit_id"); + `ALTER TABLE "Missions_In_Campaigns" ADD CONSTRAINT "Missions_In_Campaigns_fk0" FOREIGN KEY ("mission") REFERENCES "Missions"("id");`, + `ALTER TABLE "Missions_In_Campaigns" ADD CONSTRAINT "Missions_In_Campaigns_fk1" FOREIGN KEY ("campaign") REFERENCES "Campaigns"("id");`, + } + var tx pgx.Tx + tx, err = timescaleDbPool.Begin(context.Background()) + if err != nil { + logLine(functionName, fmt.Sprintf(`["Error creating transaction: %v", "ERROR"]`, err.Error()), false) + return + } - ALTER TABLE "Environment" ADD CONSTRAINT "Environment_fk0" FOREIGN KEY ("mission_id") REFERENCES "Missions"("id"); + _, err = tx.Exec(context.Background(), tableCreationSql) + if err != nil { + logLine(functionName, fmt.Sprintf(`["Error creating tables: %v", "ERROR"]`, err.Error()), false) + tx.Rollback(context.Background()) + tx.Commit(context.Background()) + return + } + err = tx.Commit(context.Background()) + if err != nil { + logLine(functionName, fmt.Sprintf(`["Error committing table creation transaction: %v", "ERROR"]`, err.Error()), false) + return + } - ALTER TABLE "StaticObjects" ADD CONSTRAINT "StaticObjects_fk0" FOREIGN KEY ("mission_id") REFERENCES "Missions"("id"); + // run each relation creation sql statement + for _, sql := range relationCreationSql { + _, err = tx.Exec(context.Background(), sql) + if err != nil { + logLine(functionName, fmt.Sprintf(`["Error creating relation: %v", "ERROR"]`, err.Error()), false) + tx.Rollback(context.Background()) + tx.Commit(context.Background()) + return + } + } - ALTER TABLE "Missions_In_Campaigns" ADD CONSTRAINT "Missions_In_Campaigns_fk0" FOREIGN KEY ("mission") REFERENCES "Missions"("id"); - ALTER TABLE "Missions_In_Campaigns" ADD CONSTRAINT "Missions_In_Campaigns_fk1" FOREIGN KEY ("campaign") REFERENCES "Campaigns"("id"); -`) + err = tx.Commit(context.Background()) + if err != nil { + logLine(functionName, fmt.Sprintf(`["Error committing relation creation transaction: %v", "ERROR"]`, err.Error()), false) + return + } + + logLine(functionName, `["Timescale schema initialized", "INFO"]`, false) } @@ -360,14 +520,71 @@ func writeToTimescale(table string, line string) { functionName := "writeToTimescale" _, err := timescaleDbPool.Exec(context.Background(), "INSERT INTO %1 (time, line) VALUES (NOW(), $2);", table, line) if err != nil { - logLine(functionName, fmt.Sprintf(`["Error writing to timescale: %v", "ERROR"]`, err.Error())) + logLine(functionName, fmt.Sprintf(`["Error writing to timescale: %v", "ERROR"]`, err.Error()), false) } } -func deinitialize() { - logLine("deinitialize", `["deinitialize called", "INFO"]`) - DB_CLIENT.Close() - timescaleDbPool.Close() +func writeToInflux(a3DataRaw *[]string) string { + + // convert to string array + a3Data := *a3DataRaw + + logLine("writeToInflux", fmt.Sprintf(`["Received %d params", "DEBUG"]`, len(a3Data)), true) + + MIN_PARAMS_COUNT := 1 + + var logData string + functionName := "writeToInflux" + + if len(a3Data) < MIN_PARAMS_COUNT { + logData = fmt.Sprintf(`["Not all parameters present (got %d, expected at least %d)", "ERROR"]`, len(a3Data), MIN_PARAMS_COUNT) + logLine(functionName, logData, false) + return logData + } + + // use custom bucket or default + var bucket string = fixEscapeQuotes(trimQuotes(string(a3Data[0]))) + + // Get non-blocking write client + WRITE_API := influxClient.WriteAPI(activeSettings.Influx.Org, bucket) + + if WRITE_API == nil { + logData = `["Error creating write API", "ERROR"]` + logLine(functionName, logData, false) + return logData + } + + // Get errors channel + errorsCh := WRITE_API.Errors() + go func() { + for writeErr := range errorsCh { + logData = fmt.Sprintf(`["Error parsing line protocol: %s", "ERROR"]`, strings.Replace(writeErr.Error(), `"`, `'`, -1)) + logLine(functionName, logData, false) + } + }() + + // now we have our write client, we'll go through the rest of the receive array items in line protocol format and write them to influx + for i := 1; i < len(a3Data); i++ { + var p string = fixEscapeQuotes(trimQuotes(string(a3Data[i]))) + + // write the line to influx + WRITE_API.WriteRecord(p) + + // TODO: Add backup writer + // // append backup line to file if BACKUP_WRITER is set + // // + // if BACKUP_WRITER != nil { + // _, err = BACKUP_WRITER.Write([]byte(p + "\n")) + // } + + } + + // schedule cleanup + WRITE_API.Flush() + logData = fmt.Sprintf(`["Wrote %d lines to influx", "DEBUG"]`, len(a3Data)-1) + logLine(functionName, logData, true) + + return "Success" } // sanitize line protocol for influx @@ -392,11 +609,14 @@ func getDir() string { return dir } -func loadSettings() (dir string, result string, influxHost string, timescaleUrl string) { +// return true if the program should continue +func loadSettings() (settingsJson, error) { functionName := "loadSettings" - logLine(functionName, fmt.Sprintf(`["ADDON_FOLDER: %s", "INFO"]`, ADDON_FOLDER)) - logLine(functionName, fmt.Sprintf(`["LOG_FILE: %s", "INFO"]`, LOG_FILE)) - logLine(functionName, fmt.Sprintf(`["SETTINGS_FILE: %s", "INFO"]`, SETTINGS_FILE)) + logLine(functionName, fmt.Sprintf(`["ADDON_FOLDER: %s", "INFO"]`, ADDON_FOLDER), false) + logLine(functionName, fmt.Sprintf(`["LOG_FILE: %s", "INFO"]`, LOG_FILE), false) + logLine(functionName, fmt.Sprintf(`["SETTINGS_FILE: %s", "INFO"]`, SETTINGS_FILE), false) + + settings := settingsJson{} // print the current working directory var file *os.File @@ -406,66 +626,65 @@ func loadSettings() (dir string, result string, influxHost string, timescaleUrl // see if the file exists if _, err = os.Stat(SETTINGS_FILE); os.IsNotExist(err) { // file does not exist - log.Println("settings.json does not exist") - // create the file - file, err = os.Create(SETTINGS_FILE) + logLine( + functionName, + fmt.Sprintf(`["%s does not exist", "ERROR"]`, SETTINGS_FILE), + false, + ) + // copy settings.json.example to settings.json + // load contents + fileContents, err := ioutil.ReadFile(SETTINGS_FILE_EXAMPLE) if err != nil { - log.Fatal(err) + return settings, err } - defer file.Close() - // write the default settings to the file - ifSet := influxSettings{ - Host: "http://localhost:8086", - Token: "my-token", - Org: "my-org", - } - a3Set := arma3Settings{ - RefreshRateMs: 1000, - } - tsSettings := timescaleSettings{ - ConnectionUrl: "postgres://username:password@localhost:5432/database_name", - } - defaultSettings := map[string]interface{}{ - "influxdb": ifSet, - "arma3": a3Set, - "timescale": tsSettings, - } - encoder := json.NewEncoder(file) - err = encoder.Encode(defaultSettings) + // write contents to settings.json + err = ioutil.WriteFile(SETTINGS_FILE, fileContents, 0644) if err != nil { - log.Fatal(err) + return settings, err } - result = `["settings.json created - please modify!", "WARN"]` - influxHost = ifSet.Host - timescaleUrl = tsSettings.ConnectionUrl - return dir, result, influxHost, timescaleUrl + + // Exit false to discontinue initialization since settings are defaults + logLine(functionName, `["CREATED SETTINGS"]`, false) + // return a new error + return settings, errors.New("settings.json does not exist") } else { // file exists - log.Println("settings.json exists") + logLine(functionName, `["settings.json found", "DEBUG"]`, true) // read the file file, err = os.Open(SETTINGS_FILE) if err != nil { - log.Fatal(err) + return settings, err } defer file.Close() decoder := json.NewDecoder(file) - var settings settingsJson err = decoder.Decode(&settings) if err != nil { - log.Fatal(err) + return settings, err } - // set the settings - influxConnectionSettings = settings.Influx - a3Settings = settings.Arma3 - timescaleConnectionSettings = settings.Timescale - // set the result - result = `["settings.json loaded", "INFO"]` - influxHost = influxConnectionSettings.Host - timescaleUrl = timescaleConnectionSettings.ConnectionUrl + // send contents of settings file + // get the file contents + fileContents, err := ioutil.ReadFile(SETTINGS_FILE) + if err != nil { + return settings, err + } + + // compact the json + var jsonStr bytes.Buffer + err = json.Compact(&jsonStr, fileContents) + if err != nil { + return settings, err + } + + logLine( + "loadSettingsJSON", + jsonStr.String(), + false, + ) + } - return dir, result, influxHost, timescaleUrl + return settings, nil } func runExtensionCallback(name *C.char, function *C.char, data *C.char) C.int { @@ -542,70 +761,7 @@ func fixEscapeQuotes(s string) string { return strings.Replace(s, `""`, `"`, -1) } -func writeToInflux(a3DataRaw *[]string) string { - - // convert to string array - a3Data := *a3DataRaw - - logLine("writeToInflux", fmt.Sprintf(`["Received %d params", "DEBUG"]`, len(a3Data))) - - MIN_PARAMS_COUNT := 1 - - var logData string - functionName := "writeToInflux" - - if len(a3Data) < MIN_PARAMS_COUNT { - logData = fmt.Sprintf(`["Not all parameters present (got %d, expected at least %d)", "ERROR"]`, len(a3Data), MIN_PARAMS_COUNT) - logLine(functionName, logData) - return logData - } - - // use custom bucket or default - var bucket string = fixEscapeQuotes(trimQuotes(string(a3Data[0]))) - - // Get non-blocking write client - WRITE_API := DB_CLIENT.WriteAPI(influxConnectionSettings.Org, bucket) - - if WRITE_API == nil { - logData = `["Error creating write API", "ERROR"]` - logLine(functionName, logData) - return logData - } - - // Get errors channel - errorsCh := WRITE_API.Errors() - go func() { - for writeErr := range errorsCh { - logData = fmt.Sprintf(`["Error parsing line protocol: %s", "ERROR"]`, strings.Replace(writeErr.Error(), `"`, `'`, -1)) - logLine(functionName, logData) - } - }() - - // now we have our write client, we'll go through the rest of the receive array items in line protocol format and write them to influx - for i := 1; i < len(a3Data); i++ { - var p string = fixEscapeQuotes(trimQuotes(string(a3Data[i]))) - - // write the line to influx - WRITE_API.WriteRecord(p) - - // TODO: Add backup writer - // // append backup line to file if BACKUP_WRITER is set - // // - // if BACKUP_WRITER != nil { - // _, err = BACKUP_WRITER.Write([]byte(p + "\n")) - // } - - } - - // schedule cleanup - WRITE_API.Flush() - logData = fmt.Sprintf(`["Wrote %d lines to influx", "INFO"]`, len(a3Data)-1) - logLine(functionName, logData) - - return "Success" -} - -func logLine(functionName string, data string) { +func logLine(functionName string, data string, isDebug bool) { statusName := C.CString("RangerMetrics") defer C.free(unsafe.Pointer(statusName)) statusFunction := C.CString(functionName) @@ -614,7 +770,11 @@ func logLine(functionName string, data string) { defer C.free(unsafe.Pointer(statusParam)) runExtensionCallback(statusName, statusFunction, statusParam) - log.Println(data) + if activeSettings.Arma3.Debug && isDebug { + log.Println(data) + } else if !isDebug { + log.Println(data) + } } //export goRVExtension @@ -622,14 +782,28 @@ func goRVExtension(output *C.char, outputsize C.size_t, input *C.char) { var temp string - // check if input is in AVAILABLE_FUNCTIONS - // if not, return error - // if yes, continue - if _, ok := AVAILABLE_FUNCTIONS[C.GoString(input)]; !ok { - temp = fmt.Sprintf("Function: %s not found!", C.GoString(input)) - } else { - // call the function by name - reflect.ValueOf(AVAILABLE_FUNCTIONS[C.GoString(input)]).Call([]reflect.Value{}) + // logLine("goRVExtension", fmt.Sprintf(`["Input: %s", "DEBUG"]`, C.GoString(input)), true) + + switch C.GoString(input) { + case "version": + temp = EXTENSION_VERSION + case "getDir": + temp = getDir() + case "getUnixTimeNano": + time := getUnixTimeNano() + temp = fmt.Sprintf(`["%s"]`, strconv.FormatInt(time, 10)) + + default: + // check if input is in AVAILABLE_FUNCTIONS + // if not, return error + // if yes, continue + if _, ok := AVAILABLE_FUNCTIONS[C.GoString(input)]; !ok { + temp = fmt.Sprintf(`["Function: %s not found!", "ERROR"]`, C.GoString(input)) + } else { + // call the function by name + go reflect.ValueOf(AVAILABLE_FUNCTIONS[C.GoString(input)]).Call([]reflect.Value{}) + temp = fmt.Sprintf(`["Function: %s called successfully", "DEBUG"]`, C.GoString(input)) + } } // switch C.GoString(input) { diff --git a/testsqf.sqf b/testsqf.sqf index 9a0fc4b..cb858db 100644 --- a/testsqf.sqf +++ b/testsqf.sqf @@ -1,11 +1,26 @@ freeExtension "RangerMetrics"; // sleep 0.5; -"RangerMetrics" callExtension "loadSettings"; -"RangerMetrics" callExtension "version"; -"RangerMetrics" callExtension "connectToInflux"; -"RangerMetrics" callExtension "connectToTimescale"; -"RangerMetrics" callExtension "initTimescale"; +// "RangerMetrics" callExtension "loadSettings"; +// "RangerMetrics" callExtension "version"; +// "RangerMetrics" callExtension "connectToInflux"; +// "RangerMetrics" callExtension "connectToTimescale"; +// sleep 5; +// "RangerMetrics" callExtension "initTimescale"; -sleep 30; +// addMissionEventHandler ["ExtensionCallback", { +// params ["_extension", "_function", "_data"]; +// if ( +// _extension == "RangerMetrics" && _function isEqualTo "connectToTimescale" +// ) then { +// diag_log format ["RangerMetrics: %1", _data]; +// }; +// }]; +"RangerMetrics" callExtension "deinitExtension"; +sleep 1; +"RangerMetrics" callExtension "initExtension"; + +sleep 20; +"RangerMetrics" callExtension "deinitExtension"; // freeExtension "RangerMetrics"; +sleep 5; exit; \ No newline at end of file