-
Notifications
You must be signed in to change notification settings - Fork 9
/
NodeController.cs
executable file
·330 lines (296 loc) · 12.4 KB
/
NodeController.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.IO;
using System.Linq;
namespace FreenetTray
{
public class NodeController
{
public enum CrashType
{
WrapperFileNotFound,
PathTooLong,
WrapperCrashed,
}
public class MissingConfigValueException : Exception
{
public readonly string Filename;
public readonly string Value;
public MissingConfigValueException(string filename, string value)
{
Filename = filename;
Value = value;
}
}
// System Error Codes
// See http://msdn.microsoft.com/en-us/library/windows/desktop/ms681382%28v=vs.85%29.aspx
// TODO: Is there a C# assembly with these?
private const int ERROR_FILE_NOT_FOUND = 0x2;
private const int ERROR_INSUFFICIENT_BUFFER = 0x7A;
private const int ERROR_ACCESS_DENIED = 0x5;
public delegate void CrashHandler(CrashType type);
public CrashHandler OnCrashed;
public EventHandler OnStarted;
public EventHandler OnStopped;
private Process _wrapper;
private readonly ProcessStartInfo _wrapperInfo = new ProcessStartInfo();
private readonly NodeConfig _config;
public string WrapperLogFilename { get { return _config.WrapperLogFilename; } }
public int FProxyPort { get { return _config.FProxyPort; } }
public string DownloadsDir { get { return _config.DownloadsDir; } }
private const string FreenetIniFilename = @"freenet.ini";
private const string WrapperConfFilename = "wrapper.conf";
public static string WrapperFilename() {
switch (MachineConfig.GetBestAvailableJRE) {
case JREType.JRE64Bit:
return @"wrapper\freenetwrapper-64.exe";
case JREType.JRE32Bit:
return @"wrapper\freenetwrapper.exe";
case JREType.None:
// there is no JRE installed at all
throw new MissingJRE();
default:
throw new MissingJRE();
}
}
// TODO: Where to document? Throws FileNotFound, DirectoryNotFound, MissingJRE
public NodeController()
{
if (Properties.Settings.Default.CustomLocation.Length != 0)
{
_config = new NodeConfig(Properties.Settings.Default.CustomLocation);
}
else
{
Exception configException = null;
foreach (var path in new[]
{
Directory.GetCurrentDirectory(),
Environment.ExpandEnvironmentVariables(@"%LocalAppData%\Freenet"),
})
{
// TODO: If the wrapper has problems with arguments with non-ASCII characters should
// this the wrapper invocation change the working directory? Won't work in the general
// case because the pidfile location could contain non-ASCII characters, but it
// works for the default configuration.
// http://sourceforge.net/p/wrapper/bugs/290/
try
{
_config = new NodeConfig(path);
configException = null;
break;
}
catch (Exception e)
{
configException = e;
}
}
if (configException != null)
{
FNLog.Error("Failed to detect Freenet installation.", configException);
throw configException;
}
}
// Search for an existing wrapper process.
try
{
using (var reader = new StreamReader(_config.PidFilename))
{
var line = reader.ReadLine();
if (line != null)
{
var pid = int.Parse(line);
_wrapper = Process.GetProcessById(pid);
_wrapper.EnableRaisingEvents = true;
_wrapper.Exited += Wrapper_Exited;
}
}
}
catch (ArgumentException)
{
FNLog.Debug("No process has the PID in the PID file.");
// The wrapper can refuse to start if there is a stale PID file - "strict".
try
{
File.Delete(_config.PidFilename);
}
catch (IOException)
{
// TODO: Be louder about this? Or will the wrapper fail to start and exit nonzero?
FNLog.Debug("Stale PID file is still held.");
}
}
catch (FormatException)
{
FNLog.Debug("PID file does not contain an integer.");
}
catch (OverflowException)
{
FNLog.Debug("PID file does not contain an integer.");
}
catch (FileNotFoundException)
{
FNLog.Debug("PID file not found.");
}
/*
* Hide the wrapper window when launching it. This prevents (or at least heavily complicates)
* stopping it with Process.CloseMainWindow() or by sending ctrl + C.
*/
_wrapperInfo.FileName = Path.Combine(_config.RelativeTo, WrapperFilename());
// TODO: Is it worthwhile to omit the pidfile here when it's in the config file?
_wrapperInfo.Arguments = "-c " + WrapperConfFilename + " wrapper.pidfile=" + _config.PidFilename;
_wrapperInfo.UseShellExecute = false;
_wrapperInfo.CreateNoWindow = true;
}
/*
* TODO: What are the function documentation comments supposed to be formatted like?
* Start the node if it is not already started.
*
* Throws FileNotFoundException
*/
public void Start()
{
if (IsRunning())
{
return;
}
try
{
// TODO: Under what circumstances will Process.Start() return null?
_wrapper = Process.Start(_wrapperInfo);
_wrapper.EnableRaisingEvents = true;
_wrapper.Exited += Wrapper_Exited;
}
catch (Win32Exception ex)
{
// http://msdn.microsoft.com/en-us/library/0w4h05yb%28v=vs.110%29.aspx
switch (ex.NativeErrorCode)
{
case ERROR_FILE_NOT_FOUND:
FNLog.Error("Cannot start Freenet: wrapper executable not found.");
OnCrashed(CrashType.WrapperFileNotFound);
return;
case ERROR_INSUFFICIENT_BUFFER:
case ERROR_ACCESS_DENIED:
FNLog.Error("Cannot start Freenet: the file path is too long.");
OnCrashed(CrashType.PathTooLong);
return;
default:
FNLog.ErrorException(ex, "Cannot start Freenet: Process.Start() gave an error code it is not documented as giving.");
throw;
}
}
OnStarted(this, null);
}
public void Stop()
{
if (IsRunning())
{
// TODO: Tolerate missing file.
File.Delete(_config.AnchorFilename);
}
}
public Boolean IsRunning()
{
return _wrapper != null && !_wrapper.HasExited;
}
private void Wrapper_Exited(object sender, EventArgs e)
{
// TODO: Is exit code enough to distinguish between stopping and crashing?
if (_wrapper.ExitCode == 0)
{
FNLog.Debug("Wrapper exited.");
OnStopped(sender, e);
}
else
{
FNLog.Error("Wrapper crashed. Exit code: {0}", _wrapper.ExitCode);
OnCrashed(CrashType.WrapperCrashed);
}
}
private class NodeConfig
{
public readonly string AnchorFilename;
public readonly string PidFilename;
public readonly string WrapperLogFilename;
public readonly int FProxyPort;
public readonly string RelativeTo;
public readonly string DownloadsDir;
public NodeConfig(string relativeTo)
{
RelativeTo = relativeTo;
/*
* Read wrapper config: wrapper log location, PID file location, anchor location.
* The PID file location is specified on the command line, so if none is read
* it will use a default. It's not in the default wrapper.conf and is defined on
* the command line in run.sh.
*/
PidFilename = "freenet.pid";
// wrapper.conf is relative to the wrapper's location.
var wrapperConfPath = Path.Combine(relativeTo, "wrapper", WrapperConfFilename);
// check JVM (version and architecture) and
// adjust launch parameters for used JVM version
MachineConfig.CheckConfig(wrapperConfPath);
foreach (var line in File.ReadAllLines(wrapperConfPath))
{
// TODO: Map between constants and variables to reduce repetition?
if (Defines(line, "wrapper.logfile"))
{
WrapperLogFilename = Path.Combine(relativeTo, Value(line));
}
else if (Defines(line, "wrapper.pidfile"))
{
PidFilename = Path.Combine(relativeTo, Value(line));
}
else if (Defines(line, "wrapper.anchorfile"))
{
AnchorFilename = Path.Combine(relativeTo, Value(line));
}
}
// TODO: A mapping between config location and variable would reduce verbosity here too.
if (WrapperLogFilename == null)
{
throw new MissingConfigValueException(WrapperConfFilename, "wrapper.logfile");
}
if (AnchorFilename == null)
{
throw new MissingConfigValueException(WrapperConfFilename, "wrapper.anchorfile");
}
// Read Freenet config: FProxy port TODO: Use ini-parser instead
// TODO: Does this need to wait until the node is running for the first run?
var freenetIniLines = File.ReadAllLines(Path.Combine(relativeTo, FreenetIniFilename));
var port = RequireValue(freenetIniLines, "fproxy.port");
var isValid = int.TryParse(port, out FProxyPort);
if (!isValid)
{
FNLog.Error("fproxy.port is not an integer.");
throw new MissingConfigValueException(FreenetIniFilename, "fproxy.port");
}
DownloadsDir = Path.Combine(RelativeTo,
RequireValue(freenetIniLines, "node.downloadsDir"));
}
private static bool Defines(string line, string key)
{
// TODO: Does this need to tolerate whitespace between the key and the =? Find an INI library somewhere maybe?
return line.StartsWith(key + "=");
}
private static string Value(string line)
{
return line.Split(new[] { '=' }, 2)[1];
}
private static string RequireValue(IEnumerable<string> lines, string key)
{
try
{
return Value(lines.First(line => Defines(line, key)));
}
catch (InvalidOperationException)
{
throw new MissingConfigValueException(FreenetIniFilename, key);
}
}
}
}
}