Files
Arma3-Influx/arma.go

970 lines
28 KiB
Go

package main
/*
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include "extensionCallback.h"
*/
import "C" // This is required to import the C code
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"log"
"os"
"path/filepath"
"reflect"
"strconv"
"strings"
"time"
"unsafe"
influxdb2 "github.com/influxdata/influxdb-client-go/v2"
"github.com/jackc/pgx/v4"
"github.com/jackc/pgx/v4/pgxpool"
)
// 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,
"getDir": getDir,
"sanitizeLineProtocol": sanitizeLineProtocol,
"version": version,
"getUnixTimeNano": getUnixTimeNano,
}
var EXTENSION_VERSION string = "0.0.2"
var extensionCallbackFnc C.extensionCallback
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 influxClient influxdb2.Client
// TimescaleDB variables
var timescaleDbPool *pgxpool.Pool
// file paths
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
// configure log output
func init() {
log.SetFlags(log.LstdFlags | log.Lshortfile)
// log to file
f, err := os.OpenFile(LOG_FILE, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666)
if err != nil {
log.Fatalf("error opening file: %v", err)
}
// log to console as well
// 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
// get location of this dll
dllPath, err := filepath.Abs(os.Args[0])
if err != nil {
logLine(functionName, fmt.Sprintf(`["Error getting DLL path: %v", "ERROR"]`, err), false)
return
}
// set the addon directory to the parent directory of the dll
ADDON_FOLDER = filepath.Dir(dllPath)
LOG_FILE = ADDON_FOLDER + "\\rangermetrics.log"
SETTINGS_FILE = ADDON_FOLDER + "\\settings.json"
SETTINGS_FILE_EXAMPLE = ADDON_FOLDER + "\\settings.example.json"
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("extensionReady", `["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 version() {
functionName := "version"
logLine(functionName, fmt.Sprintf(`["RangerMetrics Extension Version:%s", "INFO"]`, EXTENSION_VERSION), false)
}
// return db client and error
func connectToInflux() (influxdb2.Client, error) {
if activeSettings.Influx.Host == "" ||
activeSettings.Influx.Host == "http://host:8086" {
return nil, errors.New("influxConnectionSettings.Host is empty")
// logLine("connectToInflux", `["Creating backup file", "INFO"]`)
// file, err := os.Open(BACKUP_FILE_PATH)
// if err != nil {
// log.Fatal(err)
// logLine("connectToInflux", `["Error opening backup file", "ERROR"]`)
// }
// BACKUP_WRITER = gzip.NewWriter(file)
// if err != nil {
// log.Fatal(err)
// logLine("connectToInflux", `["Error creating gzip writer", "ERROR"]`)
// }
// return "Error connecting to Influx. Using local backup"
}
influxClient := influxdb2.NewClientWithOptions(activeSettings.Influx.Host, activeSettings.Influx.Token, influxdb2.DefaultOptions().SetBatchSize(1000).SetFlushInterval(1000))
return influxClient, nil
}
//////////////////////////////////
// TIMESCALE
//////////////////////////////////
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(), activeSettings.Timescale.ConnectionUrl+"/postgres")
if err != nil {
logLine(
functionName,
fmt.Sprintf(`["Error connecting to Timescale DB: %v", "ERROR"]`, err.Error()),
false,
)
return nil, err
}
// 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() {
functionName := "initTimescale"
var err error
// schema init sql
var tableCreationSql string = `
CREATE TABLE IF NOT EXISTS units."State"
(
"timestamp" timestamp with time zone NOT NULL,
mission_id integer NOT NULL,
net_id text COLLATE pg_catalog."default" NOT NULL,
player_uid text COLLATE pg_catalog."default" NOT NULL DEFAULT '-1'::integer,
is_alive boolean NOT NULL,
is_afk boolean,
is_speaking smallint,
unit_name text COLLATE pg_catalog."default" NOT NULL,
side text COLLATE pg_catalog."default" NOT NULL,
"position" point NOT NULL,
direction numeric(2, 0) NOT NULL,
health numeric(2, 0),
traits text[] COLLATE pg_catalog."default",
CONSTRAINT "UnitStates_pkey" PRIMARY KEY (net_id, "timestamp", player_uid, mission_id)
);
COMMENT ON TABLE units."State"
IS 'Reflects a unit''s (soldier''s) state during a mission. Can be AI (player_uid = -1) or a player.';
CREATE TABLE IF NOT EXISTS units."Identity"
(
"timestamp" timestamp with time zone NOT NULL,
mission_id integer NOT NULL,
net_id text COLLATE pg_catalog."default" NOT NULL,
player_uid text COLLATE pg_catalog."default" NOT NULL DEFAULT '-1'::integer,
class_name text COLLATE pg_catalog."default" NOT NULL,
display_name text,
side text,
role_description text COLLATE pg_catalog."default",
traits text[] COLLATE pg_catalog."default",
CONSTRAINT "Units_pkey" PRIMARY KEY (mission_id, net_id, "timestamp", player_uid)
);
COMMENT ON TABLE units."Identity"
IS 'This is polled periodically during the mission to gather static and less-dynamic traits of units (soldiers).';
CREATE TABLE IF NOT EXISTS events."Chat"
(
id bigserial NOT NULL,
"timestamp" timestamp with time zone NOT NULL,
mission_id integer NOT NULL,
player_uid text COLLATE pg_catalog."default" NOT NULL DEFAULT -1,
channel integer,
owner integer,
name text COLLATE pg_catalog."default",
"from" text COLLATE pg_catalog."default",
text text COLLATE pg_catalog."default",
"forcedDisplay" boolean,
"isPlayerMessage" boolean,
"sentenceType" integer,
"chatMessageType" integer,
CONSTRAINT "Chat_pkey" PRIMARY KEY ("timestamp", mission_id, player_uid, id),
UNIQUE (id)
);
CREATE TABLE IF NOT EXISTS missions."Played"
(
id serial NOT NULL,
start_time_utc timestamp with time zone NOT NULL,
start_time_game timestamp with time zone NOT NULL,
world_name text COLLATE pg_catalog."default" NOT NULL,
briefing_name text COLLATE pg_catalog."default" NOT NULL,
mission_name text COLLATE pg_catalog."default" NOT NULL,
mission_name_source text COLLATE pg_catalog."default" NOT NULL,
on_load_name text NOT NULL,
author text COLLATE pg_catalog."default" NOT NULL,
server_name text COLLATE pg_catalog."default" NOT NULL,
server_mods json,
ace_medical boolean,
radio_tfar boolean,
radio_acre boolean,
duration interval,
version_addon integer[] NOT NULL,
version_extension integer[] NOT NULL,
tags text[] COLLATE pg_catalog."default",
CONSTRAINT "MissionsRun_pkey" PRIMARY KEY (id, world_name, start_time_utc),
CONSTRAINT "MissionsRun_id_start_time_utc_key" UNIQUE (id, start_time_utc),
UNIQUE (id)
);
COMMENT ON TABLE missions."Played"
IS 'Contains core mission data, recorded at mission init. The IDs here are referenced by many other areas to "place" them.';
CREATE TABLE IF NOT EXISTS players."ConnectionsMission"
(
id bigserial NOT NULL,
"timestamp" timestamp with time zone NOT NULL,
mission_id integer NOT NULL,
player_uid text COLLATE pg_catalog."default" NOT NULL,
event_type text COLLATE pg_catalog."default" NOT NULL,
profile_name text COLLATE pg_catalog."default" NOT NULL,
display_name text COLLATE pg_catalog."default" NOT NULL,
steam_name text COLLATE pg_catalog."default" NOT NULL,
is_jip boolean,
CONSTRAINT "ConnectionsMission_pkey" PRIMARY KEY (player_uid, "timestamp", profile_name, mission_id, id),
UNIQUE ("timestamp", mission_id, player_uid, id)
);
CREATE TABLE IF NOT EXISTS units."Inventory"
(
"timestamp" timestamp with time zone NOT NULL,
mission_id integer NOT NULL,
net_id text NOT NULL,
player_uid text NOT NULL DEFAULT -1,
inventory jsonb NOT NULL,
PRIMARY KEY ("timestamp", net_id, mission_id, player_uid)
);
COMMENT ON TABLE units."Inventory"
IS 'This is used to track the inventory state of units.';
CREATE TABLE IF NOT EXISTS players."ConnectionsServer"
(
"timestamp" timestamp with time zone NOT NULL,
player_uid text COLLATE pg_catalog."default" NOT NULL,
event_type text COLLATE pg_catalog."default" NOT NULL,
profile_name text COLLATE pg_catalog."default" NOT NULL,
display_name text COLLATE pg_catalog."default" NOT NULL,
steam_name text COLLATE pg_catalog."default" NOT NULL,
is_jip boolean,
CONSTRAINT "PlayersObserved_pkey" PRIMARY KEY (player_uid, "timestamp", profile_name)
);
CREATE TABLE IF NOT EXISTS missions."Environment"
(
"timestamp" timestamp with time zone NOT NULL,
mission_id integer NOT NULL,
fog numeric(2),
overcast numeric(2),
rain numeric(2),
humidity numeric(2),
waves numeric(2),
"windDir" numeric(2),
"windStr" numeric(2),
gusts numeric(2),
lightnings numeric(2),
"moonIntensity" numeric(2),
"moonPhase" numeric(2),
"sunOrMoon" numeric(2),
PRIMARY KEY ("timestamp", mission_id)
);
COMMENT ON TABLE missions."Environment"
IS 'Contains periodically collected environmental information during missions.';
CREATE TABLE IF NOT EXISTS vehicles."Identity"
(
"timestamp" timestamp with time zone NOT NULL,
mission_id integer NOT NULL,
net_id text NOT NULL,
class_name text NOT NULL,
display_name text,
customization jsonb,
weapons jsonb,
PRIMARY KEY ("timestamp", mission_id, net_id)
);
COMMENT ON TABLE vehicles."Identity"
IS 'Periodically polled identity information about a vehicle.';
CREATE TABLE IF NOT EXISTS vehicles."State"
(
"timestamp" timestamp with time zone NOT NULL,
mission_id integer NOT NULL,
net_id text NOT NULL,
is_alive boolean,
side text,
"position" point NOT NULL,
direction numeric(2),
health numeric(2),
crew text[],
PRIMARY KEY ("timestamp", mission_id, net_id)
);
COMMENT ON TABLE vehicles."State"
IS 'State information about vehicles. Crew is an array of net_id to be referenced against the Unit tables.';
CREATE TABLE IF NOT EXISTS players."AllObserved"
(
id serial NOT NULL,
player_uid text NOT NULL,
PRIMARY KEY (id, player_uid),
UNIQUE (player_uid)
);
`
relationCreationSql := []string{
`ALTER TABLE IF EXISTS units."State"
ADD FOREIGN KEY (mission_id)
REFERENCES missions."Played" (id) MATCH SIMPLE
ON UPDATE NO ACTION
ON DELETE NO ACTION
NOT VALID;
`,
`ALTER TABLE IF EXISTS units."State"
ADD FOREIGN KEY (player_uid)
REFERENCES players."AllObserved" (player_uid) MATCH SIMPLE
ON UPDATE NO ACTION
ON DELETE NO ACTION
NOT VALID;
`,
`ALTER TABLE IF EXISTS units."Identity"
ADD FOREIGN KEY (mission_id)
REFERENCES missions."Played" (id) MATCH SIMPLE
ON UPDATE NO ACTION
ON DELETE NO ACTION
NOT VALID;
`,
`ALTER TABLE IF EXISTS units."Identity"
ADD FOREIGN KEY (player_uid)
REFERENCES players."AllObserved" (player_uid) MATCH SIMPLE
ON UPDATE NO ACTION
ON DELETE NO ACTION
NOT VALID;
`,
`ALTER TABLE IF EXISTS events."Chat"
ADD FOREIGN KEY (mission_id)
REFERENCES missions."Played" (id) MATCH SIMPLE
ON UPDATE NO ACTION
ON DELETE NO ACTION
NOT VALID;
`,
`ALTER TABLE IF EXISTS events."Chat"
ADD FOREIGN KEY (player_uid)
REFERENCES players."AllObserved" (player_uid) MATCH SIMPLE
ON UPDATE NO ACTION
ON DELETE NO ACTION
NOT VALID;
`,
`ALTER TABLE IF EXISTS players."ConnectionsMission"
ADD FOREIGN KEY (mission_id)
REFERENCES missions."Played" (id) MATCH SIMPLE
ON UPDATE NO ACTION
ON DELETE NO ACTION
NOT VALID;
`,
`ALTER TABLE IF EXISTS players."ConnectionsMission"
ADD FOREIGN KEY (player_uid)
REFERENCES players."AllObserved" (player_uid) MATCH SIMPLE
ON UPDATE NO ACTION
ON DELETE NO ACTION
NOT VALID;
`,
`ALTER TABLE IF EXISTS units."Inventory"
ADD FOREIGN KEY (mission_id)
REFERENCES missions."Played" (id) MATCH SIMPLE
ON UPDATE NO ACTION
ON DELETE NO ACTION
NOT VALID;
`,
`ALTER TABLE IF EXISTS units."Inventory"
ADD FOREIGN KEY (player_uid)
REFERENCES players."AllObserved" (player_uid) MATCH SIMPLE
ON UPDATE NO ACTION
ON DELETE NO ACTION
NOT VALID;
`,
`ALTER TABLE IF EXISTS missions."Environment"
ADD FOREIGN KEY (mission_id)
REFERENCES missions."Played" (id) MATCH SIMPLE
ON UPDATE NO ACTION
ON DELETE NO ACTION
NOT VALID;
`,
`ALTER TABLE IF EXISTS vehicles."Identity"
ADD FOREIGN KEY (mission_id)
REFERENCES missions."Played" (id) MATCH SIMPLE
ON UPDATE NO ACTION
ON DELETE NO ACTION
NOT VALID;
`,
`ALTER TABLE IF EXISTS vehicles."State"
ADD FOREIGN KEY (mission_id)
REFERENCES missions."Played" (id) MATCH SIMPLE
ON UPDATE NO ACTION
ON DELETE NO ACTION
NOT VALID;
`,
}
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
}
_, 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
}
// 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
}
}
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)
}
func writeToTimescale(table string, line string) {
// logLine("writeToTimescale", fmt.Sprintf(`["line: %s", "INFO"]`, line))
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()), false)
}
}
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
func sanitizeLineProtocol(line string) string {
// replace all spaces with underscores
// line = strings.ReplaceAll(line, ` `, `\ `)
// replace all commas with underscores
// line = strings.ReplaceAll(line, `,`, `\,`)
// replace all equals with underscores
// line = strings.ReplaceAll(line, "=", "_")
// replace all quotes with underscores
// line = strings.ReplaceAll(line, "\"", "_")
return line
}
func getDir() string {
dir, err := os.Getwd()
if err != nil {
log.Fatal(err)
}
return dir
}
// return true if the program should continue
func loadSettings() (settingsJson, error) {
functionName := "loadSettings"
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
var err error
// read settings from file
// settings.json should be in the same directory as the .dll
// see if the file exists
if _, err = os.Stat(SETTINGS_FILE); os.IsNotExist(err) {
// file does not exist
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 {
return settings, err
}
// write contents to settings.json
err = ioutil.WriteFile(SETTINGS_FILE, fileContents, 0644)
if err != nil {
return settings, err
}
// 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
logLine(functionName, `["settings.json found", "DEBUG"]`, true)
// read the file
file, err = os.Open(SETTINGS_FILE)
if err != nil {
return settings, err
}
defer file.Close()
decoder := json.NewDecoder(file)
err = decoder.Decode(&settings)
if err != nil {
return settings, err
}
// 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 {
return C.runExtensionCallback(extensionCallbackFnc, name, function, data)
}
//export goRVExtensionVersion
func goRVExtensionVersion(output *C.char, outputsize C.size_t) {
result := C.CString(EXTENSION_VERSION)
defer C.free(unsafe.Pointer(result))
var size = C.strlen(result) + 1
if size > outputsize {
size = outputsize
}
C.memmove(unsafe.Pointer(output), unsafe.Pointer(result), size)
}
//export goRVExtensionArgs
func goRVExtensionArgs(output *C.char, outputsize C.size_t, input *C.char, argv **C.char, argc C.int) {
var offset = unsafe.Sizeof(uintptr(0))
var out []string
for index := C.int(0); index < argc; index++ {
out = append(out, C.GoString(*argv))
argv = (**C.char)(unsafe.Pointer(uintptr(unsafe.Pointer(argv)) + offset))
}
var temp string
temp = fmt.Sprintf("Function: %s nb params: %d params: %s!", C.GoString(input), argc, out)
if C.GoString(input) == "sendToInflux" {
// start a goroutine to send the data to influx
// param string is argv[0] which is the data to send to influx
go writeToInflux(&out)
temp = fmt.Sprintf("Function: %s nb params: %d", C.GoString(input), argc)
}
// Return a result to Arma
result := C.CString(temp)
defer C.free(unsafe.Pointer(result))
var size = C.strlen(result) + 1
if size > outputsize {
size = outputsize
}
C.memmove(unsafe.Pointer(output), unsafe.Pointer(result), size)
}
func callBackExample() {
name := C.CString("arma")
defer C.free(unsafe.Pointer(name))
function := C.CString("funcToExecute")
defer C.free(unsafe.Pointer(function))
// Make a callback to Arma
for i := 0; i < 3; i++ {
time.Sleep(2 * time.Second)
param := C.CString(fmt.Sprintf("Loop: %d", i))
defer C.free(unsafe.Pointer(param))
runExtensionCallback(name, function, param)
}
}
func getUnixTimeNano() int64 {
// get the current unix timestamp in nanoseconds
return time.Now().UnixNano()
}
func trimQuotes(s string) string {
// trim the start and end quotes from a string
return strings.Trim(s, `"`)
}
func fixEscapeQuotes(s string) string {
// fix the escape quotes in a string
return strings.Replace(s, `""`, `"`, -1)
}
func logLine(functionName string, data string, isDebug bool) {
statusName := C.CString("RangerMetrics")
defer C.free(unsafe.Pointer(statusName))
statusFunction := C.CString(functionName)
defer C.free(unsafe.Pointer(statusFunction))
statusParam := C.CString(data)
defer C.free(unsafe.Pointer(statusParam))
runExtensionCallback(statusName, statusFunction, statusParam)
if activeSettings.Arma3.Debug && isDebug {
log.Println(data)
} else if !isDebug {
log.Println(data)
}
}
//export goRVExtension
func goRVExtension(output *C.char, outputsize C.size_t, input *C.char) {
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
// 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) {
// case "version":
// logLine("goRVExtension", fmt.Sprintf(`["Input: %s", "INFO"]`, C.GoString(input)))
// temp = EXTENSION_VERSION
// case "getDir":
// logLine("goRVExtension", fmt.Sprintf(`["Input: %s", "INFO"]`, C.GoString(input)))
// temp = getDir()
// case "loadSettings":
// logLine("goRVExtension", fmt.Sprintf(`["Input: %s", "INFO"]`, C.GoString(input)))
// cwd, result, influxHost, timescaleUrl := loadSettings()
// log.Println("CWD:", cwd)
// log.Println("RESULT:", result)
// log.Println("INFLUX HOST:", influxHost)
// log.Println("TIMESCALE URL:", timescaleUrl)
// if result != "" {
// logLine("goRVExtension", result)
// temp = fmt.Sprintf(
// `["%s", "%s", "%s", "%d"]`,
// EXTENSION_VERSION,
// influxConnectionSettings.Host,
// influxConnectionSettings.Org,
// a3Settings.RefreshRateMs,
// )
// }
// case "connectToInflux":
// // logLine("goRVExtension", fmt.Sprintf(`["Input: %s", "INFO"]`, C.GoString(input)))
// go connectToInflux()
// temp = `["Connecting to InfluxDB", "INFO"]`
// case "connectToTimescale":
// // logLine("goRVExtension", fmt.Sprintf(`["Input: %s", "INFO"]`, C.GoString(input)))
// go connectToTimescale()
// temp = `["Connecting to TimescaleDB", "INFO"]`
// case "getUnixTimeNano":
// temp = fmt.Sprintf(`["%d", "INFO"]`, getUnixTimeNano())
// case "deinitialize":
// logLine("goRVExtension", fmt.Sprintf(`["Input: %s", "INFO"]`, C.GoString(input)))
// deinitialize()
// temp = `["Deinitializing", "INFO"]`
// default:
// temp = fmt.Sprintf(`["Unknown command: %s", "ERROR"]`, C.GoString(input))
// }
result := C.CString(temp)
defer C.free(unsafe.Pointer(result))
var size = C.strlen(result) + 1
if size > outputsize {
size = outputsize
}
C.memmove(unsafe.Pointer(output), unsafe.Pointer(result), size)
// return
}
//export goRVExtensionRegisterCallback
func goRVExtensionRegisterCallback(fnc C.extensionCallback) {
extensionCallbackFnc = fnc
}
func main() {}