1 module monitor;
2 
3 import core.memory;
4 
5 import std.array;
6 import std.conv;
7 import std.datetime;
8 import std.regex;
9 import std.socket;
10 import std.string;
11 import std.stdio;
12 
13 
14 alias Now = Clock.currTime;
15 
16 
17 struct URL {
18 	static URL parse(string url) {
19 		auto matches = matchFirst(url, ctRegex!(r"^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?$", "i"));
20 
21 		URL result;
22 		if (!matches.empty) {
23 			result.protocol = matches[2].toLower;
24 			auto host = matches[4].toLower;
25 			auto index = host.indexOf(':');
26 
27 			if (index != -1) {
28 				result.port = host[index + 1..$].to!ushort;
29 				result.host = host[0..index];
30 			} else {
31 				switch(result.protocol) {
32 				case "http":
33 					result.port = 80;
34 					break;
35 				case "https":
36 					result.port = 443;
37 					break;
38 				default:
39 					break;
40 				}
41 				result.host = host;
42 			}
43 
44 			result.path = matches[5];
45 			result.query = matches[7];
46 			result.anchor = matches[9];
47 		}
48 
49 		return result;
50 	}
51 
52 	string protocol;
53 	string host;
54 	ushort port;
55 	string path;
56 	string query;
57 	string anchor;
58 }
59 
60 
61 struct HTTPMonitor {
62 	enum State : uint {
63 		Reset,
64 		Ready,
65 		Idle,
66 		Connect,
67 		Connecting,
68 		Connected,
69 		Request,
70 		Requesting,
71 		Requested,
72 		Success,
73 		Failure,
74 		GraceFailure,
75 	}
76 
77 	~this() {
78 		reset();
79 	}
80 
81 	void reset() {
82 		close();
83 		try_ = 0;
84 
85 		changeState(State.Reset);
86 	}
87 
88 	void start(string url, size_t intervalMS, size_t timeoutMS, size_t graceMS, size_t retries) {
89 		url_ = URL.parse(url);
90 		timeoutMS_ = timeoutMS;
91 		intervalMS_ = intervalMS;
92 		graceMS_  = graceMS;
93 		retryCount_ = retries;
94 
95 		close();
96 		try_ = 0;
97 
98 		changeState(State.Ready);
99 		monitorStart_ = Now;
100 	}
101 
102 	private void close() {
103 		if (socket_ !is null) {
104 			socket_.shutdown(SocketShutdown.SEND);
105 			socket_.close();
106 
107 			destroy(socket_);
108 			socket_ = null;
109 
110 			GC.collect();
111 		}
112 	}
113 
114 	private void changeState(State state) {
115 		state_ = state;
116 	}
117 
118 	void update() {
119 		final switch (state_) with (State) {
120 		case Reset:
121 			break;
122 
123 		case Ready:
124 			changeState(Connect);
125 			goto case Connect;
126 
127 		case Idle:
128 			if (((Now - testStart_).total!"msecs" >= intervalMS_)) {
129 				changeState(Connect);
130 				goto case Connect;
131 			}
132 			break;
133 
134 		case Connect:
135 			assert(socket_ is null);
136 			try {
137 				testStart_ = Now;
138 				socket_ = new TcpSocket();
139 				socket_.blocking = false;
140 
141 				socket_.connect(new InternetAddress(url_.host, url_.port));
142 
143 				changeState(Connecting);
144 				goto case Connecting;
145 			} catch (Throwable e) {
146 				writeln(e.toString);
147 				changeState(Failure);
148 				goto case Failure;
149 			}
150 
151 		case Connecting:
152 			changeState(Connected);
153 			goto case Connected;
154 
155 		case Connected:
156 			changeState(Request);
157 			goto case Request;
158 
159 		case Request:
160 			if (request_.empty) {
161 				auto app = appender!string;
162 
163 				app.put("GET ");
164 				app.put(url_.path.empty ? "/" : url_.path);
165 				if (!url_.query.empty) {
166 					app.put("?");
167 					app.put(url_.query);
168 				}
169 				app.put(" HTTP/1.1\r\nHost: ");
170 				app.put(url_.host);
171 				app.put("\r\n\r\n");
172 
173 				request_ = cast(ubyte[])app.data.idup;
174 			}
175 
176 			try {
177 				auto result = socket_.send(request_);
178 
179 				if (result == Socket.ERROR) {
180 					if ((Now - testStart_).total!"msecs" >= timeoutMS_) {
181 						changeState(Failure);
182 						goto case Failure;
183 					}
184 					break;
185 				}
186 
187 				received_.length = 0;
188 
189 				changeState(Requesting);
190 				goto case Requesting;
191 			} catch {
192 				changeState(Failure);
193 				goto case Failure;
194 			}
195 
196 		case Requesting:
197 			ubyte[32] buf;
198 
199 			auto result = socket_.receive(buf);
200 			if (result > 0) {
201 				received_ ~= buf[0..result];
202 				if (received_.length > 12) {
203 					changeState(Requested);
204 					goto case Requested;
205 				}
206 			} else if (result == Socket.ERROR) {
207 				if ((Now - testStart_).total!"msecs" >= timeoutMS_) {
208 					changeState(Failure);
209 					goto case Failure;
210 				}
211 				break;
212 			} else if (result == 0) {
213 				changeState(Requested);
214 				goto case Requested;
215 			}
216 			break;
217 
218 		case Requested:
219 			if (statusOK()) {
220 				changeState(Success);
221 				goto case Success;
222 			}
223 			changeState(Failure);
224 			goto case Failure;
225 
226 		case Success:
227 			close();
228 			try_ = 0;
229 
230 			changeState(Idle);
231 			goto case Idle;
232 
233 		case Failure:
234 			close();
235 			++try_;
236 
237 			if ((try_ <= retryCount_) || ((Now - monitorStart_).total!"msecs" <= graceMS_)) {
238 				changeState(GraceFailure);
239 				goto case GraceFailure;
240 			}
241 			break;
242 
243 		case GraceFailure:
244 			if ((Now - monitorStart_).total!"msecs" <= graceMS_)
245 				try_ = 0;
246 
247 			changeState(Idle);
248 			goto case Idle;
249 		}
250 	}
251 
252 	private bool statusOK() {
253 		if (received_.length <= 12)
254 			return false;
255 
256 
257 		if ((received_.ptr[0] != 'H') || (received_.ptr[1] != 'T') || (received_.ptr[2] != 'T') || (received_.ptr[3] != 'P') || (received_.ptr[4] != '/'))
258 			return false;
259 
260 		if (received_.ptr[8] != ' ')
261 			return false;
262 
263 		if (received_.ptr[9] == '5')
264 			return false;
265 		return true;
266 	}
267 
268 	@property State state() {
269 		return state_;
270 	}
271 
272 private:
273 	URL url_;
274 
275 	size_t timeoutMS_;
276 	size_t intervalMS_;
277 	size_t graceMS_;
278 	size_t retryCount_;
279 	size_t try_;
280 
281 	ubyte[] request_;
282 	ubyte[] received_;
283 
284 	SysTime monitorStart_;
285 	SysTime testStart_;
286 
287 	State state_ = State.Ready;
288 	Socket socket_;
289 }