diff --git a/api.c b/api.c index f52c307a..3dac1ade 100644 --- a/api.c +++ b/api.c @@ -29,12 +29,6 @@ #include "miner.h" #include "util.h" -// Big enough for largest API request -// data is truncated at the end of the last record that fits -// but still closed correctly for JSON -// Current code assumes it can socket send this size + JSON_CLOSE + JSON_END -#define SOCKBUFSIZ 65432 - // BUFSIZ varies on Windows and Linux #define TMPBUFSIZ 8192 @@ -126,7 +120,11 @@ static const char SEPARATOR = '|'; #define SEPSTR "|" static const char GPUSEP = ','; -static const char *APIVERSION = "3.0"; +#define CMDJOIN '+' +#define JOIN_CMD "CMD=" +#define BETWEEN_JOIN SEPSTR + +static const char *APIVERSION = "3.1"; static const char *DEAD = "Dead"; static const char *SICK = "Sick"; static const char *NOSTART = "NoStart"; @@ -215,9 +213,10 @@ static const char ISJSON = '{'; #define JSON_MINECOIN JSON1 _MINECOIN JSON2 #define JSON_DEBUGSET JSON1 _DEBUGSET JSON2 #define JSON_SETCONFIG JSON1 _SETCONFIG JSON2 -#define JSON_USBSTATS JSON1 _USBSTATS JSON2 + #define JSON_END JSON4 JSON5 #define JSON_END_TRUNCATED JSON4_TRUNCATED JSON5 +#define JSON_BETWEEN_JOIN "," static const char *JSON_COMMAND = "command"; static const char *JSON_PARAMETER = "parameter"; @@ -296,7 +295,7 @@ static const char *JSON_PARAMETER = "parameter"; #define MSG_INVNUM 84 #define MSG_CONPAR 85 #define MSG_CONVAL 86 -#define MSG_USBSTA 87 + #define MSG_NOUSTA 88 #define MSG_ZERMIS 94 @@ -418,7 +417,6 @@ struct CODES { { SEVERITY_SUCC, MSG_SETQUOTA,PARAM_SET, "Set pool '%s' to quota %d'" }, { SEVERITY_ERR, MSG_CONPAR, PARAM_NONE, "Missing config parameters 'name,N'" }, { SEVERITY_ERR, MSG_CONVAL, PARAM_STR, "Missing config value N for '%s,N'" }, - { SEVERITY_SUCC, MSG_USBSTA, PARAM_NONE, "USB Statistics" }, { SEVERITY_INFO, MSG_NOUSTA, PARAM_NONE, "No USB Statistics" }, { SEVERITY_ERR, MSG_ZERMIS, PARAM_NONE, "Missing zero parameters" }, { SEVERITY_ERR, MSG_ZERINV, PARAM_STR, "Invalid zero parameter '%s'" }, @@ -1061,10 +1059,8 @@ static void message(struct io_data *io_data, int messageid, int paramid, char *p int i; - io_reinit(io_data); - if (isjson) - io_put(io_data, JSON_START JSON_STATUS); + io_add(io_data, JSON_START JSON_STATUS); for (i = 0; codes[i].severity != SEVERITY_FAIL; i++) { if (codes[i].code == messageid) { @@ -3045,44 +3041,45 @@ struct CMDS { char *name; void (*func)(struct io_data *, SOCKETTYPE, char *, bool, char); bool iswritemode; + bool joinable; } cmds[] = { - { "version", apiversion, false }, - { "config", minerconfig, false }, - { "devs", devstatus, false }, - { "pools", poolstatus, false }, - { "summary", summary, false }, - { "gpuenable", gpuenable, true }, - { "gpudisable", gpudisable, true }, - { "gpurestart", gpurestart, true }, - { "gpu", gpudev, false }, - { "gpucount", gpucount, false }, - { "switchpool", switchpool, true }, - { "addpool", addpool, true }, - { "poolpriority", poolpriority, true }, - { "poolquota", poolquota, true }, - { "enablepool", enablepool, true }, - { "disablepool", disablepool, true }, - { "removepool", removepool, true }, - { "gpuintensity", gpuintensity, true }, - { "gpumem", gpumem, true }, - { "gpuengine", gpuengine, true }, - { "gpufan", gpufan, true }, - { "gpuvddc", gpuvddc, true }, - { "save", dosave, true }, - { "quit", doquit, true }, - { "privileged", privileged, true }, - { "notify", notify, false }, - { "devdetails", devdetails, false }, - { "restart", dorestart, true }, - { "stats", minerstats, false }, - { "check", checkcommand, false }, - { "failover-only", failoveronly, true }, - { "coin", minecoin, false }, - { "debug", debugstate, true }, - { "setconfig", setconfig, true }, - { "zero", dozero, true }, - { "lockstats", lockstats, true }, - { NULL, NULL, false } + { "version", apiversion, false, true }, + { "config", minerconfig, false, true }, + { "devs", devstatus, false, true }, + { "pools", poolstatus, false, true }, + { "summary", summary, false, true }, + { "gpuenable", gpuenable, true, false }, + { "gpudisable", gpudisable, true, false }, + { "gpurestart", gpurestart, true, false }, + { "gpu", gpudev, false, false }, + { "gpucount", gpucount, false, true }, + { "switchpool", switchpool, true, false }, + { "addpool", addpool, true, false }, + { "poolpriority", poolpriority, true, false }, + { "poolquota", poolquota, true, false }, + { "enablepool", enablepool, true, false }, + { "disablepool", disablepool, true, false }, + { "removepool", removepool, true, false }, + { "gpuintensity", gpuintensity, true, false }, + { "gpumem", gpumem, true, false }, + { "gpuengine", gpuengine, true, false }, + { "gpufan", gpufan, true, false }, + { "gpuvddc", gpuvddc, true, false }, + { "save", dosave, true, false }, + { "quit", doquit, true, false }, + { "privileged", privileged, true, false }, + { "notify", notify, false, true }, + { "devdetails", devdetails, false, true }, + { "restart", dorestart, true, false }, + { "stats", minerstats, false, true }, + { "check", checkcommand, false, false }, + { "failover-only", failoveronly, true, false }, + { "coin", minecoin, false, true }, + { "debug", debugstate, true, false }, + { "setconfig", setconfig, true, false }, + { "zero", dozero, true, false }, + { "lockstats", lockstats, true, true }, + { NULL, NULL, false, false } }; static void checkcommand(struct io_data *io_data, __maybe_unused SOCKETTYPE c, char *param, bool isjson, char group) @@ -3125,6 +3122,49 @@ static void checkcommand(struct io_data *io_data, __maybe_unused SOCKETTYPE c, c io_close(io_data); } +static void head_join(struct io_data *io_data, char *cmdptr, bool isjson, bool *firstjoin) +{ + char *ptr; + + if (*firstjoin) { + if (isjson) + io_add(io_data, JSON0); + *firstjoin = false; + } else { + if (isjson) + io_add(io_data, JSON_BETWEEN_JOIN); + } + + // External supplied string + ptr = escape_string(cmdptr, isjson); + + if (isjson) { + io_add(io_data, JSON1); + io_add(io_data, ptr); + io_add(io_data, JSON2); + } else { + io_add(io_data, JOIN_CMD); + io_add(io_data, ptr); + io_add(io_data, BETWEEN_JOIN); + } + + if (ptr != cmdptr) + free(ptr); +} + +static void tail_join(struct io_data *io_data, bool isjson) +{ + if (io_data->close) { + io_add(io_data, JSON_CLOSE); + io_data->close = false; + } + + if (isjson) { + io_add(io_data, JSON_END); + io_add(io_data, JSON3); + } +} + static void send_result(struct io_data *io_data, SOCKETTYPE c, bool isjson) { int count, sendc, res, tosend, len, n; @@ -3689,7 +3729,7 @@ void api(int api_thr_id) struct sockaddr_in cli; socklen_t clisiz; char cmdbuf[100]; - char *cmd = NULL; + char *cmd = NULL, *cmdptr, *cmdsbuf; char *param; bool addrok; char group; @@ -3697,7 +3737,7 @@ void api(int api_thr_id) json_t *json_config = NULL; json_t *json_val; bool isjson; - bool did; + bool did, isjoin, firstjoin; int i; SOCKETTYPE *apisock; @@ -3898,27 +3938,80 @@ void api(int api_thr_id) } if (!did) { - for (i = 0; cmds[i].name != NULL; i++) { - if (strcmp(cmd, cmds[i].name) == 0) { - sprintf(cmdbuf, "|%s|", cmd); - if (ISPRIVGROUP(group) || strstr(COMMANDS(group), cmdbuf)) - (cmds[i].func)(io_data, c, param, isjson, group); - else { - message(io_data, MSG_ACCDENY, 0, cmds[i].name, isjson); - applog(LOG_DEBUG, "API: access denied to '%s' for '%s' command", connectaddr, cmds[i].name); + if (strchr(cmd, CMDJOIN)) { + firstjoin = isjoin = true; + // cmd + leading '|' + '\0' + cmdsbuf = malloc(strlen(cmd) + 2); + if (!cmdsbuf) + quithere(1, "OOM cmdsbuf"); + strcpy(cmdsbuf, "|"); + param = NULL; + } else + firstjoin = isjoin = false; + + cmdptr = cmd; + do { + did = false; + if (isjoin) { + cmd = strchr(cmdptr, CMDJOIN); + if (cmd) + *(cmd++) = '\0'; + if (!*cmdptr) + goto inochi; + } + + for (i = 0; cmds[i].name != NULL; i++) { + if (strcmp(cmdptr, cmds[i].name) == 0) { + sprintf(cmdbuf, "|%s|", cmdptr); + if (isjoin) { + if (strstr(cmdsbuf, cmdbuf)) { + did = true; + break; + } + strcat(cmdsbuf, cmdptr); + strcat(cmdsbuf, "|"); + head_join(io_data, cmdptr, isjson, &firstjoin); + if (!cmds[i].joinable) { + message(io_data, MSG_ACCDENY, 0, cmds[i].name, isjson); + did = true; + tail_join(io_data, isjson); + break; + } + } + if (ISPRIVGROUP(group) || strstr(COMMANDS(group), cmdbuf)) + (cmds[i].func)(io_data, c, param, isjson, group); + else { + message(io_data, MSG_ACCDENY, 0, cmds[i].name, isjson); + applog(LOG_DEBUG, "API: access denied to '%s' for '%s' command", connectaddr, cmds[i].name); + } + + did = true; + if (!isjoin) + send_result(io_data, c, isjson); + else + tail_join(io_data, isjson); + break; } + } - send_result(io_data, c, isjson); - did = true; - break; + if (!did) { + if (isjoin) + head_join(io_data, cmdptr, isjson, &firstjoin); + message(io_data, MSG_INVCMD, 0, NULL, isjson); + if (isjoin) + tail_join(io_data, isjson); + else + send_result(io_data, c, isjson); } - } +inochi: + if (isjoin) + cmdptr = cmd; + } while (isjoin && cmdptr); } - if (!did) { - message(io_data, MSG_INVCMD, 0, NULL, isjson); + if (isjoin) send_result(io_data, c, isjson); - } + if (isjson && json_is_object(json_config)) json_decref(json_config); } diff --git a/doc/API b/doc/API index 9de48223..ae7587d0 100644 --- a/doc/API +++ b/doc/API @@ -109,6 +109,36 @@ The STATUS section is: This defaults to the sgminer version but is the value of --api-description if it was specified at runtime. +With API V3.1 you can also request multiple report replies in a single command +request +e.g. to request both summary and devs, the command would be summary+devs + +This is only available for report commands that don't need parameters, +and is not available for commands that change anything +Any parameters supplied will be ignored + +The extra formatting of the result is to have a section for each command +e.g. summary|STATUS=....|devs|STATUS=... +With JSON, each result is within a section of the command name +e.g. {"summary":{"STATUS":[{"STATUS":"S"...}],"SUMMARY":[...],"id":1}, + "devs":{"STATUS":[{"STATUS:"S"...}],"DEVS":[...],"id":1},"id":1} + +As before, if you supply bad JSON you'll just get a single 'E' STATUS section +in the old format, since it doesn't switch to using the new format until it +correctly processes the JSON and can match a '+' in the command + +If you request a command multiple times, e.g. devs+devs +you'll just get it once +If this results in only one command, it will still use the new layout +with just the one command + +If you request a command that can't be used due to requiring parameters, +a command that isn't a report, or an invalid command, you'll get an 'E' STATUS +for that one but it will still attempt to process all other commands supplied + +Blank/missing commands are ignore e.g. +devs++ +will just show 'devs' using the new layout + For API version 1.10 and later: The list of requests - a (*) means it requires privileged access - and replies: @@ -497,7 +527,13 @@ miner.php - an example web page to access the API Feature Changelog for external applications using the API: -API V3.0 (cgminer v3.9.1) +API V3.1 (cgminer v3.12.1) + +Multiple report request command with '+' e.g. summary+devs + +--------- + +API V3.0 (cgminer v3.11.0) Allow unlimited size replies