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) {
89 		url_ = URL.parse(url);
90 
91 		close();
92 		try_ = 0;
93 
94 		changeState(State.Ready);
95 		monitorStart_ = Now;
96 	}
97 
98 	private void close() {
99 		if (socket_ !is null) {
100 			socket_.shutdown(SocketShutdown.SEND);
101 			socket_.close();
102 
103 			destroy(socket_);
104 			socket_ = null;
105 
106 			GC.collect();
107 		}
108 	}
109 
110 	private void changeState(State state) {
111 		state_ = state;
112 	}
113 
114 	void update() {
115 		final switch (state_) with (State) {
116 		case Reset:
117 			break;
118 
119 		case Ready:
120 			changeState(Connect);
121 			goto case Connect;
122 
123 		case Idle:
124 			if (((Now - testStart_).total!"msecs" >= intervalMS_)) {
125 				changeState(Connect);
126 				goto case Connect;
127 			}
128 			break;
129 
130 		case Connect:
131 			assert(socket_ is null);
132 			try {
133 				testStart_ = Now;
134 				socket_ = new TcpSocket();
135 				socket_.blocking = false;
136 
137 				socket_.connect(new InternetAddress(url_.host, url_.port));
138 
139 				changeState(Connecting);
140 				goto case Connecting;
141 			} catch (Throwable e) {
142 				writeln(e.toString);
143 				changeState(Failure);
144 				goto case Failure;
145 			}
146 
147 		case Connecting:
148 			changeState(Connected);
149 			goto case Connected;
150 
151 		case Connected:
152 			changeState(Request);
153 			goto case Request;
154 
155 		case Request:
156 			if (request_.empty) {
157 				auto app = appender!string;
158 
159 				app.put("GET ");
160 				app.put(url_.path.empty ? "/" : url_.path);
161 				if (!url_.query.empty) {
162 					app.put("?");
163 					app.put(url_.query);
164 				}
165 				app.put(" HTTP/1.1\r\n\r\n");
166 
167 				request_ = cast(ubyte[])app.data.idup;
168 			}
169 
170 			try {
171 				auto result = socket_.send(request_);
172 
173 				if (result == Socket.ERROR) {
174 					if ((Now - testStart_).total!"msecs" >= timeoutMS_) {
175 						changeState(Failure);
176 						goto case Failure;
177 					}
178 					break;
179 				}
180 
181 				received_.length = 0;
182 
183 				changeState(Requesting);
184 				goto case Requesting;
185 			} catch {
186 				changeState(Failure);
187 				goto case Failure;
188 			}
189 
190 		case Requesting:
191 			ubyte[32] buf;
192 
193 			auto result = socket_.receive(buf);
194 			if (result > 0) {
195 				received_ ~= buf[0..result];
196 				if (received_.length > 12) {
197 					changeState(Requested);
198 					goto case Requested;
199 				}
200 			} else if (result == Socket.ERROR) {
201 				if ((Now - testStart_).total!"msecs" >= timeoutMS_) {
202 					changeState(Failure);
203 					goto case Failure;
204 				}
205 				break;
206 			} else if (result == 0) {
207 				changeState(Requested);
208 				goto case Requested;
209 			}
210 			break;
211 
212 		case Requested:
213 			if (statusOK()) {
214 				changeState(Success);
215 				goto case Success;
216 			}
217 			changeState(Failure);
218 			goto case Failure;
219 
220 		case Success:
221 			close();
222 			try_ = 0;
223 
224 			changeState(Idle);
225 			goto case Idle;
226 
227 		case Failure:
228 			close();
229 			++try_;
230 
231 			if ((try_ <= retryCount_) || ((Now - monitorStart_).total!"msecs" <= graceMS_)) {
232 				changeState(GraceFailure);
233 				goto case GraceFailure;
234 			}
235 			break;
236 
237 		case GraceFailure:
238 			if ((Now - monitorStart_).total!"msecs" <= graceMS_)
239 				try_ = 0;
240 
241 			changeState(Idle);
242 			goto case Idle;
243 		}
244 	}
245 
246 	private bool statusOK() {
247 		if (received_.length <= 12)
248 			return false;
249 
250 		if ((received_.ptr[0] != 'H') || (received_.ptr[1] != 'T') || (received_.ptr[2] != 'T') || (received_.ptr[3] != 'P') || (received_.ptr[4] != '/'))
251 			return false;
252 
253 		if (received_.ptr[8] != ' ')
254 			return false;
255 
256 		if (received_.ptr[9] == '5')
257 			return false;
258 		return true;
259 	}
260 
261 	@property State state() {
262 		return state_;
263 	}
264 
265 private:
266 	URL url_;
267 
268 	size_t timeoutMS_ = 2500;
269 	size_t intervalMS_ = 5000;
270 	size_t graceMS_  = 25000;
271 	size_t retryCount_ = 3;
272 	size_t try_ = 0;
273 
274 	ubyte[] request_;
275 	ubyte[] received_;
276 
277 	SysTime monitorStart_;
278 	SysTime testStart_;
279 
280 	State state_ = State.Ready;
281 	Socket socket_;
282 }