big rework for settings. plus fixes found during testing
This commit is contained in:
552
arma.go
552
arma.go
@@ -9,13 +9,16 @@ package main
|
|||||||
import "C" // This is required to import the C code
|
import "C" // This is required to import the C code
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io/ioutil"
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
"unsafe"
|
"unsafe"
|
||||||
@@ -27,31 +30,87 @@ import (
|
|||||||
|
|
||||||
// declare list of functions available for call
|
// declare list of functions available for call
|
||||||
var AVAILABLE_FUNCTIONS = map[string]interface{}{
|
var AVAILABLE_FUNCTIONS = map[string]interface{}{
|
||||||
|
"initExtension": initExtension,
|
||||||
|
"deinitExtension": deinitExtension,
|
||||||
"loadSettings": loadSettings,
|
"loadSettings": loadSettings,
|
||||||
"connectToInflux": connectToInflux,
|
"connectToInflux": connectToInflux,
|
||||||
"writeToInflux": writeToInflux,
|
"writeToInflux": writeToInflux,
|
||||||
"connectToTimescale": connectToTimescale,
|
"connectToTimescale": connectToTimescale,
|
||||||
"initTimescale": initTimescale,
|
"initTimescale": initTimescale,
|
||||||
"writeToTimescale": writeToTimescale,
|
"writeToTimescale": writeToTimescale,
|
||||||
"deinitialize": deinitialize,
|
|
||||||
"getDir": getDir,
|
"getDir": getDir,
|
||||||
"sanitizeLineProtocol": sanitizeLineProtocol,
|
"sanitizeLineProtocol": sanitizeLineProtocol,
|
||||||
|
"version": version,
|
||||||
|
"getUnixTimeNano": getUnixTimeNano,
|
||||||
}
|
}
|
||||||
|
|
||||||
var EXTENSION_VERSION string = "0.0.1"
|
var EXTENSION_VERSION string = "0.0.1"
|
||||||
var extensionCallbackFnc C.extensionCallback
|
var extensionCallbackFnc C.extensionCallback
|
||||||
var influxConnectionSettings influxSettings
|
|
||||||
var a3Settings arma3Settings
|
type ServerPollSetting struct {
|
||||||
var timescaleConnectionSettings timescaleSettings
|
Name string `json:"name"`
|
||||||
var timescaleDbPool *pgxpool.Pool
|
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
|
// InfluxDB variables
|
||||||
var DB_CLIENT influxdb2.Client
|
var influxClient influxdb2.Client
|
||||||
|
|
||||||
|
// TimescaleDB variables
|
||||||
|
var timescaleDbPool *pgxpool.Pool
|
||||||
|
|
||||||
// file paths
|
// file paths
|
||||||
var ADDON_FOLDER string = "./@RangerMetrics"
|
var ADDON_FOLDER string = getDir() + "\\@RangerMetrics"
|
||||||
var LOG_FILE string = ADDON_FOLDER + "/rangermetrics.log"
|
var LOG_FILE string = ADDON_FOLDER + "\\rangermetrics.log"
|
||||||
var SETTINGS_FILE string = ADDON_FOLDER + "/settings.json"
|
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_FILE_PATH string = ADDON_FOLDER + "/local_backup.log.gzip"
|
||||||
// var BACKUP_WRITER *gzip.Writer
|
// var BACKUP_WRITER *gzip.Writer
|
||||||
@@ -65,36 +124,72 @@ func init() {
|
|||||||
log.Fatalf("error opening file: %v", err)
|
log.Fatalf("error opening file: %v", err)
|
||||||
}
|
}
|
||||||
// log to console as well
|
// 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) {
|
// func RVExtensionContext(output *C.char, argc *C.int) {
|
||||||
|
|
||||||
// }
|
// }
|
||||||
|
|
||||||
type influxSettings struct {
|
func version() {
|
||||||
Host string `json:"host"`
|
functionName := "version"
|
||||||
Token string `json:"token"`
|
logLine(functionName, fmt.Sprintf(`["RangerMetrics Extension Version:%s", "INFO"]`, EXTENSION_VERSION), false)
|
||||||
Org string `json:"org"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type timescaleSettings struct {
|
// return db client and error
|
||||||
ConnectionUrl string `json:"connectionUrl"`
|
func connectToInflux() (influxdb2.Client, error) {
|
||||||
}
|
if activeSettings.Influx.Host == "" ||
|
||||||
|
activeSettings.Influx.Host == "http://host:8086" {
|
||||||
|
|
||||||
type arma3Settings struct {
|
return nil, errors.New("influxConnectionSettings.Host is empty")
|
||||||
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"]`)
|
|
||||||
// logLine("connectToInflux", `["Creating backup file", "INFO"]`)
|
// logLine("connectToInflux", `["Creating backup file", "INFO"]`)
|
||||||
// file, err := os.Open(BACKUP_FILE_PATH)
|
// file, err := os.Open(BACKUP_FILE_PATH)
|
||||||
// if err != nil {
|
// if err != nil {
|
||||||
@@ -109,42 +204,70 @@ func connectToInflux() {
|
|||||||
// return "Error connecting to Influx. Using local backup"
|
// 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
|
// TIMESCALE
|
||||||
//////////////////////////////////
|
//////////////////////////////////
|
||||||
|
|
||||||
func connectToTimescale() {
|
func connectToTimescale() (*pgxpool.Pool, error) {
|
||||||
functionName := "connectToTimescale"
|
functionName := "connectToTimescale"
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
// urlExample := "postgres://username:password@localhost:5432/database_name"
|
// urlExample := "postgres://username:password@localhost:5432/database_name"
|
||||||
// logLine("connectToTimescale", fmt.Sprintf(`["timescaleConnectionSettings.ConnectionUrl: %s", "INFO"]`, timescaleConnectionSettings.ConnectionUrl))
|
// 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 {
|
if err != nil {
|
||||||
logLine(
|
logLine(
|
||||||
functionName,
|
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;")
|
// ensure database exists
|
||||||
logLine("connectToTimescale", `["Connected to Timescale successfully", "INFO"]`)
|
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() {
|
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
|
functionName := "initTimescale"
|
||||||
timescaleDbPool.Exec(context.Background(), `CREATE TABLE "public.Missions" (
|
var err error
|
||||||
|
|
||||||
|
// schema init sql
|
||||||
|
var tableCreationSql string = `
|
||||||
|
CREATE TABLE "Missions" (
|
||||||
"id" serial NOT NULL UNIQUE,
|
"id" serial NOT NULL UNIQUE,
|
||||||
"world_name" VARCHAR(255) NOT NULL,
|
"world_name" VARCHAR(255) NOT NULL,
|
||||||
"briefing_name" VARCHAR(255) NOT NULL,
|
"briefing_name" VARCHAR(255) NOT NULL,
|
||||||
@@ -155,8 +278,8 @@ func initTimescale() {
|
|||||||
"ace_medical" BOOLEAN NOT NULL,
|
"ace_medical" BOOLEAN NOT NULL,
|
||||||
"radio_tfar" BOOLEAN NOT NULL,
|
"radio_tfar" BOOLEAN NOT NULL,
|
||||||
"radio_acre" BOOLEAN NOT NULL,
|
"radio_acre" BOOLEAN NOT NULL,
|
||||||
"start_game" DATETIME NOT NULL,
|
"start_game" TIMESTAMP NOT NULL,
|
||||||
"start_utc" DATETIME NOT NULL,
|
"start_utc" TIMESTAMP NOT NULL,
|
||||||
"frame_count" FLOAT NOT NULL,
|
"frame_count" FLOAT NOT NULL,
|
||||||
"capture_delay_s" FLOAT NOT NULL,
|
"capture_delay_s" FLOAT NOT NULL,
|
||||||
"addon_ver_major" integer NOT NULL,
|
"addon_ver_major" integer NOT NULL,
|
||||||
@@ -173,7 +296,7 @@ func initTimescale() {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
CREATE TABLE "public.Worlds" (
|
CREATE TABLE "Worlds" (
|
||||||
"world_name" VARCHAR(255) NOT NULL,
|
"world_name" VARCHAR(255) NOT NULL,
|
||||||
"display_name" VARCHAR(255) NOT NULL,
|
"display_name" VARCHAR(255) NOT NULL,
|
||||||
"world_size_m" integer NOT NULL,
|
"world_size_m" integer NOT NULL,
|
||||||
@@ -184,7 +307,7 @@ func initTimescale() {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
CREATE TABLE "public.Units" (
|
CREATE TABLE "Units" (
|
||||||
"mission_id" integer NOT NULL,
|
"mission_id" integer NOT NULL,
|
||||||
"unit_id" integer NOT NULL,
|
"unit_id" integer NOT NULL,
|
||||||
"frame" integer NOT NULL,
|
"frame" integer NOT NULL,
|
||||||
@@ -213,7 +336,7 @@ func initTimescale() {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
CREATE TABLE "public.Vehicles" (
|
CREATE TABLE "Vehicles" (
|
||||||
"mission_id" integer NOT NULL,
|
"mission_id" integer NOT NULL,
|
||||||
"frame" integer NOT NULL,
|
"frame" integer NOT NULL,
|
||||||
"vehicle_id" integer NOT NULL,
|
"vehicle_id" integer NOT NULL,
|
||||||
@@ -233,7 +356,7 @@ func initTimescale() {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
CREATE TABLE "public.Markers" (
|
CREATE TABLE "Markers" (
|
||||||
"mission_id" integer NOT NULL,
|
"mission_id" integer NOT NULL,
|
||||||
"frame" integer NOT NULL,
|
"frame" integer NOT NULL,
|
||||||
"marker_name" VARCHAR(255) NOT NULL,
|
"marker_name" VARCHAR(255) NOT NULL,
|
||||||
@@ -245,7 +368,7 @@ func initTimescale() {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
CREATE TABLE "public.Chat" (
|
CREATE TABLE "Chat" (
|
||||||
"id" serial NOT NULL,
|
"id" serial NOT NULL,
|
||||||
"mission_id" integer NOT NULL,
|
"mission_id" integer NOT NULL,
|
||||||
"sender_id" integer NOT NULL,
|
"sender_id" integer NOT NULL,
|
||||||
@@ -260,7 +383,7 @@ func initTimescale() {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
CREATE TABLE "public.UniqueUsers" (
|
CREATE TABLE "UniqueUsers" (
|
||||||
"steamid64" varchar(100) NOT NULL,
|
"steamid64" varchar(100) NOT NULL,
|
||||||
CONSTRAINT "UniqueUsers_pk" PRIMARY KEY ("steamid64")
|
CONSTRAINT "UniqueUsers_pk" PRIMARY KEY ("steamid64")
|
||||||
) WITH (
|
) WITH (
|
||||||
@@ -269,11 +392,11 @@ func initTimescale() {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
CREATE TABLE "public.Environment" (
|
CREATE TABLE "Environment" (
|
||||||
"mission_id" integer NOT NULL,
|
"mission_id" integer NOT NULL,
|
||||||
"frame" integer NOT NULL,
|
"frame" integer NOT NULL,
|
||||||
"date_game" DATETIME NOT NULL,
|
"date_game" TIMESTAMP NOT NULL,
|
||||||
"date_utc" DATETIME NOT NULL,
|
"date_utc" TIMESTAMP NOT NULL,
|
||||||
"overcast" FLOAT NOT NULL,
|
"overcast" FLOAT NOT NULL,
|
||||||
"rain" FLOAT NOT NULL,
|
"rain" FLOAT NOT NULL,
|
||||||
"humidity" FLOAT NOT NULL,
|
"humidity" FLOAT NOT NULL,
|
||||||
@@ -290,7 +413,7 @@ func initTimescale() {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
CREATE TABLE "public.StaticObjects" (
|
CREATE TABLE "StaticObjects" (
|
||||||
"mission_id" integer NOT NULL,
|
"mission_id" integer NOT NULL,
|
||||||
"frame" integer NOT NULL,
|
"frame" integer NOT NULL,
|
||||||
"building_id" integer NOT NULL,
|
"building_id" integer NOT NULL,
|
||||||
@@ -308,7 +431,7 @@ func initTimescale() {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
CREATE TABLE "public.Campaigns" (
|
CREATE TABLE "Campaigns" (
|
||||||
"id" serial NOT NULL,
|
"id" serial NOT NULL,
|
||||||
"name" VARCHAR(255) NOT NULL,
|
"name" VARCHAR(255) NOT NULL,
|
||||||
"description" TEXT NOT NULL,
|
"description" TEXT NOT NULL,
|
||||||
@@ -320,38 +443,75 @@ func initTimescale() {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
CREATE TABLE "public.Missions_In_Campaigns" (
|
CREATE TABLE "Missions_In_Campaigns" (
|
||||||
"mission" integer NOT NULL,
|
"mission" integer NOT NULL,
|
||||||
"campaign" integer NOT NULL,
|
"campaign" integer NOT NULL,
|
||||||
CONSTRAINT "Missions_In_Campaigns_pk" PRIMARY KEY ("mission","campaign")
|
CONSTRAINT "Missions_In_Campaigns_pk" PRIMARY KEY ("mission","campaign")
|
||||||
) WITH (
|
) WITH (
|
||||||
OIDS=FALSE
|
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 "Chat" ADD CONSTRAINT "Chat_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_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 "Missions_In_Campaigns" ADD CONSTRAINT "Missions_In_Campaigns_fk0" FOREIGN KEY ("mission") 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_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");
|
err = tx.Commit(context.Background())
|
||||||
ALTER TABLE "Missions_In_Campaigns" ADD CONSTRAINT "Missions_In_Campaigns_fk1" FOREIGN KEY ("campaign") REFERENCES "Campaigns"("id");
|
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"
|
functionName := "writeToTimescale"
|
||||||
_, err := timescaleDbPool.Exec(context.Background(), "INSERT INTO %1 (time, line) VALUES (NOW(), $2);", table, line)
|
_, err := timescaleDbPool.Exec(context.Background(), "INSERT INTO %1 (time, line) VALUES (NOW(), $2);", table, line)
|
||||||
if err != nil {
|
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() {
|
func writeToInflux(a3DataRaw *[]string) string {
|
||||||
logLine("deinitialize", `["deinitialize called", "INFO"]`)
|
|
||||||
DB_CLIENT.Close()
|
// convert to string array
|
||||||
timescaleDbPool.Close()
|
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
|
// sanitize line protocol for influx
|
||||||
@@ -392,11 +609,14 @@ func getDir() string {
|
|||||||
return dir
|
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"
|
functionName := "loadSettings"
|
||||||
logLine(functionName, fmt.Sprintf(`["ADDON_FOLDER: %s", "INFO"]`, ADDON_FOLDER))
|
logLine(functionName, fmt.Sprintf(`["ADDON_FOLDER: %s", "INFO"]`, ADDON_FOLDER), false)
|
||||||
logLine(functionName, fmt.Sprintf(`["LOG_FILE: %s", "INFO"]`, LOG_FILE))
|
logLine(functionName, fmt.Sprintf(`["LOG_FILE: %s", "INFO"]`, LOG_FILE), false)
|
||||||
logLine(functionName, fmt.Sprintf(`["SETTINGS_FILE: %s", "INFO"]`, SETTINGS_FILE))
|
logLine(functionName, fmt.Sprintf(`["SETTINGS_FILE: %s", "INFO"]`, SETTINGS_FILE), false)
|
||||||
|
|
||||||
|
settings := settingsJson{}
|
||||||
|
|
||||||
// print the current working directory
|
// print the current working directory
|
||||||
var file *os.File
|
var file *os.File
|
||||||
@@ -406,66 +626,65 @@ func loadSettings() (dir string, result string, influxHost string, timescaleUrl
|
|||||||
// see if the file exists
|
// see if the file exists
|
||||||
if _, err = os.Stat(SETTINGS_FILE); os.IsNotExist(err) {
|
if _, err = os.Stat(SETTINGS_FILE); os.IsNotExist(err) {
|
||||||
// file does not exist
|
// file does not exist
|
||||||
log.Println("settings.json does not exist")
|
logLine(
|
||||||
// create the file
|
functionName,
|
||||||
file, err = os.Create(SETTINGS_FILE)
|
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 {
|
if err != nil {
|
||||||
log.Fatal(err)
|
return settings, err
|
||||||
}
|
}
|
||||||
defer file.Close()
|
// write contents to settings.json
|
||||||
// write the default settings to the file
|
err = ioutil.WriteFile(SETTINGS_FILE, fileContents, 0644)
|
||||||
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)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
return settings, err
|
||||||
}
|
}
|
||||||
result = `["settings.json created - please modify!", "WARN"]`
|
|
||||||
influxHost = ifSet.Host
|
// Exit false to discontinue initialization since settings are defaults
|
||||||
timescaleUrl = tsSettings.ConnectionUrl
|
logLine(functionName, `["CREATED SETTINGS"]`, false)
|
||||||
return dir, result, influxHost, timescaleUrl
|
// return a new error
|
||||||
|
return settings, errors.New("settings.json does not exist")
|
||||||
} else {
|
} else {
|
||||||
// file exists
|
// file exists
|
||||||
log.Println("settings.json exists")
|
logLine(functionName, `["settings.json found", "DEBUG"]`, true)
|
||||||
// read the file
|
// read the file
|
||||||
file, err = os.Open(SETTINGS_FILE)
|
file, err = os.Open(SETTINGS_FILE)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
return settings, err
|
||||||
}
|
}
|
||||||
defer file.Close()
|
defer file.Close()
|
||||||
decoder := json.NewDecoder(file)
|
decoder := json.NewDecoder(file)
|
||||||
var settings settingsJson
|
|
||||||
err = decoder.Decode(&settings)
|
err = decoder.Decode(&settings)
|
||||||
if err != nil {
|
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return dir, result, influxHost, timescaleUrl
|
// 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 settings, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func runExtensionCallback(name *C.char, function *C.char, data *C.char) C.int {
|
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)
|
return strings.Replace(s, `""`, `"`, -1)
|
||||||
}
|
}
|
||||||
|
|
||||||
func writeToInflux(a3DataRaw *[]string) string {
|
func logLine(functionName string, data string, isDebug bool) {
|
||||||
|
|
||||||
// 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) {
|
|
||||||
statusName := C.CString("RangerMetrics")
|
statusName := C.CString("RangerMetrics")
|
||||||
defer C.free(unsafe.Pointer(statusName))
|
defer C.free(unsafe.Pointer(statusName))
|
||||||
statusFunction := C.CString(functionName)
|
statusFunction := C.CString(functionName)
|
||||||
@@ -614,7 +770,11 @@ func logLine(functionName string, data string) {
|
|||||||
defer C.free(unsafe.Pointer(statusParam))
|
defer C.free(unsafe.Pointer(statusParam))
|
||||||
runExtensionCallback(statusName, statusFunction, statusParam)
|
runExtensionCallback(statusName, statusFunction, statusParam)
|
||||||
|
|
||||||
|
if activeSettings.Arma3.Debug && isDebug {
|
||||||
log.Println(data)
|
log.Println(data)
|
||||||
|
} else if !isDebug {
|
||||||
|
log.Println(data)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//export goRVExtension
|
//export goRVExtension
|
||||||
@@ -622,14 +782,28 @@ func goRVExtension(output *C.char, outputsize C.size_t, input *C.char) {
|
|||||||
|
|
||||||
var temp string
|
var temp string
|
||||||
|
|
||||||
|
// 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
|
// check if input is in AVAILABLE_FUNCTIONS
|
||||||
// if not, return error
|
// if not, return error
|
||||||
// if yes, continue
|
// if yes, continue
|
||||||
if _, ok := AVAILABLE_FUNCTIONS[C.GoString(input)]; !ok {
|
if _, ok := AVAILABLE_FUNCTIONS[C.GoString(input)]; !ok {
|
||||||
temp = fmt.Sprintf("Function: %s not found!", C.GoString(input))
|
temp = fmt.Sprintf(`["Function: %s not found!", "ERROR"]`, C.GoString(input))
|
||||||
} else {
|
} else {
|
||||||
// call the function by name
|
// call the function by name
|
||||||
reflect.ValueOf(AVAILABLE_FUNCTIONS[C.GoString(input)]).Call([]reflect.Value{})
|
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) {
|
// switch C.GoString(input) {
|
||||||
|
|||||||
27
testsqf.sqf
27
testsqf.sqf
@@ -1,11 +1,26 @@
|
|||||||
freeExtension "RangerMetrics";
|
freeExtension "RangerMetrics";
|
||||||
// sleep 0.5;
|
// sleep 0.5;
|
||||||
"RangerMetrics" callExtension "loadSettings";
|
// "RangerMetrics" callExtension "loadSettings";
|
||||||
"RangerMetrics" callExtension "version";
|
// "RangerMetrics" callExtension "version";
|
||||||
"RangerMetrics" callExtension "connectToInflux";
|
// "RangerMetrics" callExtension "connectToInflux";
|
||||||
"RangerMetrics" callExtension "connectToTimescale";
|
// "RangerMetrics" callExtension "connectToTimescale";
|
||||||
"RangerMetrics" callExtension "initTimescale";
|
// 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";
|
// freeExtension "RangerMetrics";
|
||||||
|
sleep 5;
|
||||||
exit;
|
exit;
|
||||||
Reference in New Issue
Block a user