package main /* #include #include #include #include "extensionCallback.h" */ import "C" // This is required to import the C code import ( "encoding/json" "fmt" "io" "log" "os" "strings" "time" "unsafe" influxdb2 "github.com/influxdata/influxdb-client-go/v2" ) var EXTENSION_VERSION string = "0.0.1" var extensionCallbackFnc C.extensionCallback var influxConnectionSettings influxSettings var a3Settings arma3Settings // InfluxDB variables var DB_CLIENT influxdb2.Client // file paths var ADDON_FOLDER string = "./@RangerMetrics" var LOG_FILE string = ADDON_FOLDER + "/rangermetrics.log" var SETTINGS_FILE string = ADDON_FOLDER + "/settings.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)) } // func RVExtensionContext(output *C.char, argc *C.int) { // } type influxSettings struct { Host string `json:"host"` Token string `json:"token"` Org string `json:"org"` } type arma3Settings struct { RefreshRateMs int `json:"refreshRateMs"` } type settingsJson struct { Influx influxSettings `json:"influxdb"` Arma3 arma3Settings `json:"arma3"` } func connectToInflux() string { if influxConnectionSettings.Host == "" { logLine("connectToInflux", `["influxConnectionSettings.Host is empty", "ERROR"]`) // 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" return "Error connecting to Influx." } DB_CLIENT = influxdb2.NewClientWithOptions(influxConnectionSettings.Host, influxConnectionSettings.Token, influxdb2.DefaultOptions().SetBatchSize(500).SetFlushInterval(2000)) logLine("connectToInflux", `["DB_CLIENT created", "INFO"]`) return "Connected to Influx successfully" } func deinitialize() { logLine("deinitialize", `["deinitialize called", "INFO"]`) DB_CLIENT.Close() } // 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 } func loadSettings() (dir string, result string, host string) { logLine("loadSettings", fmt.Sprintf(`["ADDON_FOLDER: %s", "INFO"]`, ADDON_FOLDER)) logLine("loadSettings", fmt.Sprintf(`["LOG_FILE: %s", "INFO"]`, LOG_FILE)) logLine("loadSettings", fmt.Sprintf(`["SETTINGS_FILE: %s", "INFO"]`, SETTINGS_FILE)) // 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 log.Println("settings.json does not exist") // create the file file, err = os.Create(SETTINGS_FILE) if err != nil { log.Fatal(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, } defaultSettings := map[string]interface{}{ "influxdb": ifSet, "arma3": a3Set, } encoder := json.NewEncoder(file) err = encoder.Encode(defaultSettings) if err != nil { log.Fatal(err) } result = `["settings.json created - please modify!", "WARN"]` host = ifSet.Host return dir, result, host } else { // file exists log.Println("settings.json exists") // read the file file, err = os.Open(SETTINGS_FILE) if err != nil { log.Fatal(err) } defer file.Close() decoder := json.NewDecoder(file) var settings settingsJson err = decoder.Decode(&settings) if err != nil { log.Fatal(err) } // set the settings influxConnectionSettings = settings.Influx a3Settings = settings.Arma3 // set the result result = `["settings.json loaded", "INFO"]` host = influxConnectionSettings.Host } return dir, result, host } 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 sendToInflux(&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 sendToInflux(a3DataRaw *[]string) string { // convert to string array a3Data := *a3DataRaw logLine("sendToInflux", fmt.Sprintf(`["Received %d params", "DEBUG"]`, len(a3Data))) MIN_PARAMS_COUNT := 1 var logData string functionName := "sendToInflux" 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") 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) log.Println(data) } //export goRVExtension func goRVExtension(output *C.char, outputsize C.size_t, input *C.char) { var temp string 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 := loadSettings() log.Println("CWD:", cwd) log.Println("RESULT:", result) log.Println("INFLUX HOST:", influxHost) 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))) result := connectToInflux() temp = fmt.Sprintf(`["%s", "INFO"]`, result) 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", "ERR"]`, 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() {}