1 /// Authors: Thibaut CHARLES (Crom) cromfr@gmail.com
2 /// License: GPL-3.0
3 /// Copyright: Copyright Thibaut CHARLES 2016
4 
5 module tools.nwnsrv;
6 
7 import std.stdio;
8 import std.conv: to, ConvException;
9 import std.traits;
10 import std.meta;
11 import std.string;
12 import std.file;
13 import std.typecons: Tuple, Nullable;
14 import std.exception: enforce;
15 import std.path;
16 import std.algorithm;
17 import core.thread;
18 alias writeFile = std.file.write;
19 version(unittest) import std.exception: assertThrown, assertNotThrown;
20 
21 import tools.common.getopt;
22 import nwn.constants;
23 import nwn.nwnserver;
24 
25 
26 int main(string[] args){
27 	if(args.length >= 2 && (args[1] == "-h" || args[1] == "--help")){
28 		writeln("Send requests to NWN2 servers.");
29 		writefln("Usage: %s (ping|bnes|bnds|bnxi|bnlm)", args[0].baseName);
30 		return 0;
31 	}
32 	if(args.any!(a => a == "--version")){
33 		import nwn.ver: NWN_LIB_D_VERSION;
34 		writeln(NWN_LIB_D_VERSION);
35 		return 0;
36 	}
37 
38 	enforce(args.length > 1, "No subcommand provided");
39 	immutable command = args[1];
40 	args = args[0] ~ args[2..$];
41 
42 	cmdswitch:
43 	switch(command){
44 		case "ping":
45 			{
46 				int count = 1;
47 				auto res = getopt(args,
48 					"n", "Number of pings to issue. 0 for infinite. Default 1", &count);
49 				if(res.helpWanted){
50 					improvedGetoptPrinter(
51 						"Ping server and measure latency\n"
52 						~"Usage: "~args[0].baseName~" ping [args] hostname[:port]\n"
53 						~"Example: "~args[0].baseName~" ping lcda-nwn2.fr",
54 						res.options);
55 					return 0;
56 				}
57 				enforce(args.length > 1, "No hostname provided");
58 				enforce(args.length <= 2, "Too many arguments provided");
59 
60 				auto serv = connectServer(args[1]);
61 
62 				double avg = double.nan;
63 				double min = double.max;
64 				double max = 0.0;
65 				for(auto i = 0 ; i < count || count == 0 ; i++){
66 					if(i > 0)
67 						Thread.sleep(1.dur!"seconds");
68 					auto ping = serv.ping();
69 					writeln(i, ": ", ping, "ms");
70 					if(i == 0) avg = ping;
71 					else       avg = (avg * i + ping) / cast(double)(i + 1);
72 
73 					if(ping < min) min = ping;
74 					if(ping > max) max = ping;
75 				}
76 				writeln("Average: ", avg, "ms");
77 				writeln("Min: ", min, "ms");
78 				writeln("Max: ", max, "ms");
79 			}
80 			break;
81 
82 		foreach(Req ; AliasSeq!("bnes", "bnds", "bnxi", "bnlm")){
83 			case Req:
84 				{
85 					string format = "text";
86 					auto res = getopt(args,
87 						"f|format", "Output format. 'text' or 'json'. Default: 'text'.", &format
88 					);
89 					if(res.helpWanted){
90 						improvedGetoptPrinter(
91 							"Send a "~Req~" request to the server\n"
92 							~"Usage: "~args[0].baseName~" "~Req~" [args] hostname[:port]\n"
93 							~"Example: "~args[0].baseName~" "~Req~" lcda-nwn2.fr",
94 							res.options);
95 						return 0;
96 					}
97 					enforce(args.length > 1, "No hostname provided");
98 					enforce(args.length <= 2, "Too many arguments provided");
99 
100 					auto serv = connectServer(args[1]);
101 
102 					switch(format){
103 						case "text": writeln(mixin("serv.query"~Req.toUpper~"()").serializeStruct!"text"); break;
104 						case "json": writeln(mixin("serv.query"~Req.toUpper~"()").serializeStruct!"json"); break;
105 						default: assert(0, "Bad output format");
106 					}
107 				}
108 				break cmdswitch;
109 		}
110 
111 		default:
112 			writefln("Unknown command '%s'. Try %s --help", command, args[0].baseName);
113 			return -1;
114 	}
115 	return 0;
116 }
117 
118 auto connectServer(string url){
119 	auto urlSplit = url.split(":");
120 	auto address = urlSplit[0];
121 	ushort port = urlSplit.length > 1 ? urlSplit[1].to!ushort : 5121;
122 	return new NWNServer(address, port);
123 }
124 
125 string serializeStruct(string format, T)(in T s){
126 	static if(format == "json"){
127 		import std.json;
128 		auto ret = JSONValue();
129 		ret.object = null;
130 	}
131 	else static if(format == "text"){
132 		string ret;
133 	}
134 	else static assert(0);
135 
136 	static foreach(Member ; FieldNameTuple!T){
137 		static if(format == "json"){
138 			ret[Member] = mixin("s."~Member).to!string;
139 		}
140 		else static if(format == "text"){
141 			ret ~= Member ~ ": " ~ mixin("s."~Member).to!string ~ "\n";
142 		}
143 	}
144 	static if(format == "json"){
145 		return ret.toJSON();
146 	}
147 	else{
148 		return ret;
149 	}
150 }
151 
152 unittest {
153 	auto stdout_ = stdout;
154 	auto tmpOut = buildPath(tempDir, "unittest-nwn-lib-d-"~__MODULE__);
155 	stdout = File(tmpOut, "w");
156 	scope(success) tmpOut.remove();
157 	scope(exit) stdout = stdout_;
158 
159 	assertThrown(main(["nwn-srv"]));
160 	assert(main(["nwn-srv","--help"]) == 0);
161 	assert(main(["nwn-srv","--version"]) == 0);
162 
163 	assertThrown(main(["nwn-srv","bnxi"]));
164 	assert(main(["nwn-srv","bnxi", "--help"]) == 0);
165 }