package main /* #include #include #include #include "extensionCallback.h" */ import "C" // This is required to import the C code import ( "bytes" "context" "encoding/json" "errors" "fmt" "io/ioutil" "log" "os" "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.1" 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 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 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 "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 "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 "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 "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 "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 "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 "UniqueUsers" ( "steamid64" varchar(100) NOT NULL, CONSTRAINT "UniqueUsers_pk" PRIMARY KEY ("steamid64") ) 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 "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 "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 "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 "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 "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 "Environment" ADD CONSTRAINT "Environment_fk0" FOREIGN KEY ("mission_id") REFERENCES "Missions"("id");`, `ALTER TABLE "StaticObjects" ADD CONSTRAINT "StaticObjects_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 "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 } _, 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() {}