commit 536852c4cdc485fa76999e782c359d52a5546c25 Author: Harald Wolff Date: Thu Feb 14 09:14:50 2019 +0100 Initial Commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..bf793ed --- /dev/null +++ b/.gitignore @@ -0,0 +1,41 @@ +# Autosave files +*~ + +# build +[Oo]bj/ +[Bb]in/ +packages/ +TestResults/ + +# globs +Makefile.in +*.DS_Store +*.sln.cache +*.suo +*.cache +*.pidb +*.userprefs +*.usertasks +config.log +config.make +config.status +aclocal.m4 +install-sh +autom4te.cache/ +*.user +*.tar.gz +tarballs/ +test-results/ +Thumbs.db +.vs/ + +# Mac bundle stuff +*.dmg +*.app + +# resharper +*_Resharper.* +*.Resharper + +# dotCover +*.dotCover diff --git a/HTTPServer.cs b/HTTPServer.cs new file mode 100644 index 0000000..aef50b4 --- /dev/null +++ b/HTTPServer.cs @@ -0,0 +1,145 @@ +using System; +using System.Collections.Generic; +using System.Net; +using System.Net.Sockets; +using System.Threading; +using ln.http.threads; +using System.IO; +using System.Text; + + +namespace ln.http +{ + public class HTTPServer + { + public static int backlog = 5; + public static int defaultPort = 8080; + public static bool exclusivePortListener = false; + + public HttpApplication DefaultApplication { get; set; } + + bool shutdown = false; + + Dictionary tcpListeners = new Dictionary(); + Dictionary applications = new Dictionary(); + + Pool threadPool = new Pool(); + + public HTTPServer() + { + } + + public void AddEndpoint(IPEndPoint endpoint) + { + if (this.tcpListeners.ContainsKey(endpoint)) + { + throw new ArgumentOutOfRangeException(nameof(endpoint), "EndPoint already added"); + } + + this.tcpListeners.Add(endpoint, new TcpListener(endpoint)); + } + public void RemoveEndpoint(IPEndPoint endpoint) + { + if (this.tcpListeners.ContainsKey(endpoint)) + { + this.tcpListeners[endpoint].Stop(); + this.tcpListeners.Remove(endpoint); + } + } + + public void AddApplication(Uri BaseURI,HttpApplication application) + { + applications[BaseURI] = application; + } + + + public void Start() + { + foreach (TcpListener tcpListener in this.tcpListeners.Values) + { + tcpListener.Start(backlog); + this.threadPool.Enqueue(() => AcceptConnections(tcpListener)); + } + + } + + public void Stop() + { + lock (this) + { + this.shutdown = true; + } + } + + private void AcceptConnections(TcpListener tcpListener) + { + while (true) + { + lock(this) + { + if (this.shutdown) { + return; + } + } + AcceptConnection(tcpListener); + } + } + + private void AcceptConnection(TcpListener tcpListener) + { + TcpClient tcpClient = tcpListener.AcceptTcpClient(); + this.threadPool.Enqueue(() => HandleConnection(tcpClient)); + } + + private void HandleConnection(TcpClient tcpClient) + { + HttpReader httpReader = new HttpReader(tcpClient.GetStream()); + httpReader.Read(); + + HttpRequest httpRequest = new HttpRequest(httpReader, (IPEndPoint)tcpClient.Client.LocalEndPoint); + HttpResponse response = null; + + try + { + HttpApplication application = DefaultApplication; + + if (applications.ContainsKey(httpRequest.BaseURI)) + application = applications[httpRequest.BaseURI]; + + response = application.GetResponse( httpRequest ); + } catch (Exception e) + { + response = new HttpResponse(httpRequest,"text/plain"); + response.StatusCode = 500; + response.ContentWriter.WriteLine("Exception caught: {0}",e); + } + + if (!response.HasCustomContentStream) + { + response.ContentWriter.Flush(); + MemoryStream cstream = (MemoryStream)response.ContentStream; + cstream.Position = 0; + + response.SetHeader("content-length", cstream.Length.ToString()); + } + + StreamWriter streamWriter = new StreamWriter(tcpClient.GetStream()); + + streamWriter.WriteLine("{0} {1} {2}", httpRequest.Protocol,response.StatusCode, response.StatusMessage); + foreach (String headerName in response.GetHeaderNames()) + { + streamWriter.WriteLine("{0}: {1}", headerName, response.GetHeader(headerName)); + } + + streamWriter.WriteLine(); + streamWriter.Flush(); + + response.ContentStream.CopyTo(tcpClient.GetStream()); + response.ContentStream.Close(); + response.ContentStream.Dispose(); + + tcpClient.Close(); + } + + } +} diff --git a/HttpApplication.cs b/HttpApplication.cs new file mode 100644 index 0000000..5d8683b --- /dev/null +++ b/HttpApplication.cs @@ -0,0 +1,13 @@ +using System; +using ln.http; +namespace ln.http +{ + public abstract class HttpApplication + { + public HttpApplication() + { + } + + public abstract HttpResponse GetResponse(HttpRequest httpRequest); + } +} diff --git a/HttpReader.cs b/HttpReader.cs new file mode 100644 index 0000000..57674bc --- /dev/null +++ b/HttpReader.cs @@ -0,0 +1,297 @@ +using System; +using System.IO; +using System.Text; +using System.Collections.Generic; +using System.Net; + +namespace ln.http +{ + public class HttpReader + { + delegate bool ReadCondition(int b); + + public Stream Stream { get; } + + private byte[] buffer = new byte[8192]; + private int hlen; + private int blen; + private int bptr; + + public IPEndPoint RemoteEndpoint { get; private set; } + + public String Method { get; private set; } + public String URL { get; private set; } + public String Protocol { get; private set; } + + public Dictionary Headers { get; } = new Dictionary(); + + public HttpReader(Stream stream) + { + Stream = stream; + } + public HttpReader(Stream stream,IPEndPoint remoteEndpoint) + { + Stream = stream; + RemoteEndpoint = remoteEndpoint; + } + + public void Read() + { + ReadRequestHead(); + + Method = ReadToken(); + SkipWhiteSpace(); + URL = ReadToken(); + SkipWhiteSpace(); + Protocol = ReadToken(); + ReadLine(); + + ReadHeaders(); + } + + public int ReadByte() + { + if (bptr >= blen) + return -1; + return buffer[bptr++]; + } + + public int Current + { + get + { + if (bptr >= blen) + return -1; + return buffer[bptr]; + } + } + + public void Reverse() + { + if ((bptr > 0) && (bptr < blen)) + bptr--; + } + + public void ReadRequestHead() + { + bptr = 0; + do + { + blen += Stream.Read(buffer, blen, buffer.Length - blen); + while (bptr <= (blen - 4)) + { + if ( + (buffer[bptr + 0] == '\r') && + (buffer[bptr + 1] == '\n') && + (buffer[bptr + 2] == '\r') && + (buffer[bptr + 3] == '\n') + ) + { + hlen = bptr; + bptr = 0; + return; + } + bptr++; + } + + byte[] nbuffer = new byte[buffer.Length << 1]; + Array.Copy(buffer, nbuffer, buffer.Length); + buffer = nbuffer; + } while (blen >= buffer.Length); + bptr = 0; + } + + private String ReadConditional(ReadCondition readCondition,bool reverse = true) + { + StringBuilder stringBuilder = new StringBuilder(); + int b = ReadByte(); + while ((b != -1) && (readCondition(b)) ) + { + stringBuilder.Append((char)b); + b = ReadByte(); + } + if (reverse) + Reverse(); + + return stringBuilder.ToString(); + } + + public void SkipWhiteSpace() + { + while (Char.IsWhiteSpace((char)ReadByte())) { }; + Reverse(); + } + + public String ReadToken() + { + return ReadConditional((b) => !Char.IsWhiteSpace((char)b)); + } + + public String ReadLine() + { + int p = bptr; + while (p < blen - 1) + { + if ((buffer[p] == '\r') && (buffer[p + 1] == '\n')) + { + break; + } + p++; + } + + string result = Encoding.ASCII.GetString(buffer, bptr, p - bptr); + bptr = p + 2; + return result; + } + + public String ReadHeaderName() + { + return ReadConditional((b) => b != ':', false).ToUpper(); + } + public String ReadHeaderValue() + { + String value = ReadLine(); + while (Char.IsWhiteSpace((char)Current)) + { + value = value + ReadLine(); + } + return value.Trim(); + } + + public void ReadHeaders() + { + while (bptr < hlen) + { + String name = ReadHeaderName(); + String value = ReadHeaderValue(); + + Headers.Add(name, value); + } + } + + public int ReadRequestBody(byte[] dst,int offset,int length) + { + int nRead = 0; + if (bptr < blen) + { + int len = Math.Min(length, blen - bptr); + Array.Copy(buffer, bptr, dst, offset, len); + bptr += len; + length -= len; + offset += len; + nRead += len; + } + if (length > 0) + { + nRead += Stream.Read(dst, offset, length); + } + return nRead; + } + + + + /* + byte[] buffer; + int blen = 0; + int bptr = 0; + + public HttpReader(Stream stream) + { + Stream = stream; + buffer = new byte[1024]; + } + public HttpReader(Stream stream, int buffersize) + { + Stream = stream; + buffer = new byte[buffersize]; + } + + private int read() + { + if (bptr >= blen) + { + bptr = 0; + blen = Stream.Read(buffer, 0, buffer.Length); + if (blen <= 0) + throw new EndOfStreamException(); + } + return buffer[bptr++]; + } + + private int ReadTo(byte[] b,int offset,byte[] mark) + { + int pm = 0; + int p = offset; + while (p < b.Length) + { + b[p] = (byte)read(); + if (b[p] == mark[pm]) + { + pm++; + if (pm >= mark.Length) + { + p++; + break; + } + } + else + { + pm = 0; + } + p++; + } + return p; + } + + + public String ReadRequestLine(int maxSize = 1024) + { + byte[] b = new byte[maxSize]; + int l = ReadTo(b, 0, new byte[] { 0x0d, 0x0a }); + return Encoding.ASCII.GetString(b, 0, l); + } + + public Dictionary ReadHTTPHeaders() + { + byte[] b = new byte[8192]; + int hlen = ReadTo(b, 0, new byte[] { 0x0d, 0x0a, 0x0d, 0x0a }); + + Dictionary headers = new Dictionary(); + + string rawHeaders = Encoding.ASCII.GetString(b, 0, hlen); + String[] rawLines = rawHeaders.Split(new String[] { "\r\n" }, StringSplitOptions.None); + + for (int n = rawLines.Length-1; n >= 0 ; n--) + { + if ((rawLines[n].Length > 0) && (Char.IsWhiteSpace(rawLines[n][0]))) + { + rawLines[n - 1] = rawLines[n - 1] + rawLines[n]; + rawLines[n] = null; + } + } + + foreach (String rawLine in rawLines) + { + if (rawLine != null) + { + int colon = rawLine.IndexOf(':'); + if (colon > 0) + { + String name = rawLine.Substring(0, colon).Trim().ToUpper(); + String value = rawLine.Substring(colon + 1).Trim(); + headers.Add(name, value); + } + } + } + + return headers; + } + + public byte[] ReadRequestBody(byte[] buffer) + { + return null; + } + */ + } + +} diff --git a/HttpRequest.cs b/HttpRequest.cs new file mode 100644 index 0000000..6d731f2 --- /dev/null +++ b/HttpRequest.cs @@ -0,0 +1,121 @@ +using System; +using System.IO; +using System.Text; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.Sockets; +using ln.http.exceptions; + +namespace ln.http +{ + public class HttpRequest + { + Dictionary requestHeaders; + + public IPEndPoint RemoteEndpoint { get; private set; } + public IPEndPoint LocalEndpoint { get; private set; } + + public Uri BaseURI { get; set; } + public Uri URI { get; private set; } + + public String Method { get; private set; } + public String RequestURL { get; private set; } + public String Protocol { get; private set; } + + public String Hostname { get; private set; } + public int Port { get; private set; } + + public QueryStringParameters Query { get; private set; } + + public HttpRequest(HttpReader httpReader,IPEndPoint localEndpoint) + { + LocalEndpoint = localEndpoint; + RemoteEndpoint = httpReader.RemoteEndpoint; + Method = httpReader.Method; + Protocol = httpReader.Protocol; + RequestURL = httpReader.URL; + + requestHeaders = new Dictionary(httpReader.Headers); + + Setup(); + } + + private void Setup() + { + SetupResourceURI(); + } + + /* + * SetupResourceURI() + * + * Setup the following fields: + * + * - Hostname + * - Port + * - BaseURI + * - URI + * - Query + * + */ + private void SetupResourceURI() + { + String host = GetRequestHeader("HOST"); + String[] hostTokens = host.Split(':'); + + Hostname = hostTokens[0]; + Port = (hostTokens.Length > 1) ? int.Parse(hostTokens[1]) : LocalEndpoint.Port; + BaseURI = new UriBuilder("http", Hostname, Port).Uri; + URI = new Uri(BaseURI, RequestURL); + Query = new QueryStringParameters(URI.Query); + } + + + public override string ToString() + { + //return string.Format("[HttpRequest: RemoteEndpoint={0}, Hostname={1} Port={2} URI={4}, Method={4}, RequestURL={5}, Protocol={6} Query={7}]", RemoteEndpoint, URI, Method, RequestURL, Protocol, Hostname, Port,Query); + return base.ToString(); + } + + public String GetRequestHeader(String name, String def = "") + { + name = name.ToUpper(); + + if (requestHeaders.ContainsKey(name)) + return requestHeaders[name]; + + return def; + } + + public HttpResponse Redirect(string location,int status = 301) + { + HttpResponse httpResponse = new HttpResponse(this); + httpResponse.AddHeader("location", location); + httpResponse.StatusCode = status; + httpResponse.AddHeader("content-type", "text/plain"); + httpResponse.ContentWriter.WriteLine("Redirect: {0}", location); + return httpResponse; + } + + + //private void SendResponse() + //{ + // using (StreamWriter writer = new StreamWriter(this.stream)) + // { + // ResponseStream.Position = 0; + // SetResponseHeader("Content-Length", responseStream.Length.ToString()); + + // writer.WriteLine("{0} {1} {2}", Protocol, StatusCode, HttpStatusCodes.GetStatusMessage(StatusCode)); + // foreach (String rhName in responseHeaders.Keys){ + // writer.WriteLine("{0}: {1}", rhName, responseHeaders[rhName]); + // } + // writer.WriteLine(); + // writer.Flush(); + + // responseStream.CopyTo(this.stream); + // } + //} + + + } +} diff --git a/HttpResponse.cs b/HttpResponse.cs new file mode 100644 index 0000000..b4a3c42 --- /dev/null +++ b/HttpResponse.cs @@ -0,0 +1,113 @@ +using System; +using System.IO; +using System.Collections.Generic; +using System.Linq; + +namespace ln.http +{ + public class HttpResponse + { + public HttpRequest HttpRequest { get; } + + public Stream ContentStream { get; private set; } + public TextWriter ContentWriter { get; private set; } + public bool HasCustomContentStream { get; private set; } + + int statusCode; + string statusMessage; + Dictionary> headers = new Dictionary>(); + + public HttpResponse(HttpRequest httpRequest) + { + HttpRequest = httpRequest; + ContentStream = new MemoryStream(); + ContentWriter = new StreamWriter(ContentStream); + + StatusCode = 200; + SetHeader("content-type", "text/html"); + } + public HttpResponse(HttpRequest httpRequest,string contentType) + :this(httpRequest) + { + SetHeader("content-type", contentType); + } + + public HttpResponse(HttpRequest httpRequest, Stream contentStream) + { + HttpRequest = httpRequest; + ContentStream = contentStream; + ContentWriter = null; + HasCustomContentStream = true; + + StatusCode = 200; + SetHeader("content-type", "text/html"); + } + public HttpResponse(HttpRequest httpRequest, Stream contentStream,string contentType) + :this(httpRequest,contentStream) + { + SetHeader("content-type", contentType); + } + + + public String GetHeader(string name) + { + return String.Join(",", headers[name.ToUpper()]); + } + public String[] GetHeaderValues(string name) + { + return headers[name.ToUpper()].ToArray(); + } + public String[] GetHeaderNames() + { + return headers.Keys.ToArray(); + } + + public void SetHeader(String name, String value) + { + name = name.ToUpper(); + + headers[name] = new List(); + headers[name].Add(value); + } + public void AddHeader(String name, String value) + { + name = name.ToUpper(); + + if (!headers.ContainsKey(name)) + headers[name] = new List(); + + headers[name].Add(value); + } + public void RemoveHeader(String name) + { + headers.Remove(name.ToUpper()); + } + + + public int StatusCode + { + get + { + return statusCode; + } + set { + statusCode = value; + statusMessage = HttpStatusCodes.GetStatusMessage(statusCode); + } + } + public String StatusMessage + { + get { + return statusMessage; + } + set { + statusMessage = value; + } + } + + + + + + } +} diff --git a/HttpStatusCodes.cs b/HttpStatusCodes.cs new file mode 100644 index 0000000..7b294b2 --- /dev/null +++ b/HttpStatusCodes.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; +using System.Runtime.Remoting.Messaging; +namespace ln.http +{ + public class HttpStatusCodes + { + static Dictionary statusMessages = new Dictionary() + { + { 200, "Ok" }, + { 403, "Access denied" }, + { 404, "Not Found" }, + { 500, "Internal Error" } + }; + + public static String GetStatusMessage(int code){ + if (statusMessages.ContainsKey(code)) + return statusMessages[code]; + return ""; + } + + } +} diff --git a/Properties/AssemblyInfo.cs b/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..8da6c0b --- /dev/null +++ b/Properties/AssemblyInfo.cs @@ -0,0 +1,26 @@ +using System.Reflection; +using System.Runtime.CompilerServices; + +// Information about this assembly is defined by the following attributes. +// Change them to the values specific to your project. + +[assembly: AssemblyTitle("ln.http")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("")] +[assembly: AssemblyCopyright("${AuthorCopyright}")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// The assembly version has the format "{Major}.{Minor}.{Build}.{Revision}". +// The form "{Major}.{Minor}.*" will automatically update the build and revision, +// and "{Major}.{Minor}.{Build}.*" will update just the revision. + +[assembly: AssemblyVersion("1.0.*")] + +// The following attributes are used to specify the signing key for the assembly, +// if desired. See the Mono documentation for more information about signing. + +//[assembly: AssemblyDelaySign(false)] +//[assembly: AssemblyKeyFile("")] diff --git a/QueryStringParameters.cs b/QueryStringParameters.cs new file mode 100644 index 0000000..18c3631 --- /dev/null +++ b/QueryStringParameters.cs @@ -0,0 +1,65 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Text; +namespace ln.http +{ + public class QueryStringParameters : IDictionary + { + Dictionary parameters = new Dictionary(); + + public QueryStringParameters(String query) + { + if (query.StartsWith("?")) + query = query.Substring(1); + + String[] pairs = query.Split(new char[] { '&' }, StringSplitOptions.RemoveEmptyEntries); + + foreach (String pair in pairs) + { + String[] kv = pair.Split(new char[] { '=' }, 2, StringSplitOptions.RemoveEmptyEntries); + string key = Uri.UnescapeDataString(kv[0].Replace('+',' ')); + string value = kv.Length == 2 ? Uri.UnescapeDataString(kv[1].Replace('+',' ')) : ""; + if (!key.Equals(String.Empty)){ + parameters[key] = value; + } + } + } + + public string this[string key] { + get => parameters[key]; + set => throw new NotImplementedException(); } + + public ICollection Keys => parameters.Keys; + public ICollection Values => parameters.Values; + public int Count => parameters.Count; + public bool IsReadOnly => true; + + public void Add(string key, string value) => throw new NotImplementedException(); + public void Add(KeyValuePair item) => throw new NotImplementedException(); + public void Clear() => throw new NotImplementedException(); + public bool Remove(string key) => throw new NotImplementedException(); + public bool Remove(KeyValuePair item) => throw new NotImplementedException(); + + public bool Contains(KeyValuePair item) => parameters.Contains(item); + public bool ContainsKey(string key) => parameters.ContainsKey(key); + public void CopyTo(KeyValuePair[] array, int arrayIndex) => ((IDictionary)parameters).CopyTo(array, arrayIndex); + public IEnumerator> GetEnumerator() => parameters.GetEnumerator(); + public bool TryGetValue(string key, out string value) => parameters.TryGetValue(key, out value); + IEnumerator IEnumerable.GetEnumerator() => parameters.GetEnumerator(); + + public override string ToString() + { + StringBuilder stringBuilder = new StringBuilder(); + + stringBuilder.Append("[Query"); + foreach (String key in parameters.Keys){ + stringBuilder.AppendFormat(" {0}={1}", key, parameters[key]); + } + stringBuilder.Append("]"); + + return stringBuilder.ToString(); + } + } +} diff --git a/exceptions/HttpException.cs b/exceptions/HttpException.cs new file mode 100644 index 0000000..b57d44c --- /dev/null +++ b/exceptions/HttpException.cs @@ -0,0 +1,29 @@ +using System; + +namespace ln.http.exceptions +{ + public class HttpException : Exception + { + public int StatusCode { get; } = 500; + + public HttpException(String message) + : base(message) + { + } + public HttpException(String message, Exception innerException) + : base(message, innerException) + { + } + public HttpException(int statusCode,String message) + : base(message) + { + StatusCode = statusCode; + } + public HttpException(int statusCode,String message, Exception innerException) + : base(message, innerException) + { + StatusCode = statusCode; + } + + } +} diff --git a/exceptions/IllegalRequestException.cs b/exceptions/IllegalRequestException.cs new file mode 100644 index 0000000..4ee86dc --- /dev/null +++ b/exceptions/IllegalRequestException.cs @@ -0,0 +1,11 @@ +using System; +namespace ln.http.exceptions +{ + public class IllegalRequestException : Exception + { + public IllegalRequestException(String requestLine) + :base(requestLine) + { + } + } +} diff --git a/exceptions/ResourceNotFoundException.cs b/exceptions/ResourceNotFoundException.cs new file mode 100644 index 0000000..81869f0 --- /dev/null +++ b/exceptions/ResourceNotFoundException.cs @@ -0,0 +1,11 @@ +using System; +namespace ln.http.exceptions +{ + public class ResourceNotFoundException : HttpException + { + public ResourceNotFoundException(String resourcePath, String nextResource) + : base(404, String.Format("Could not find resource \"{0}\" within \"{1}\"", nextResource, resourcePath)) + { + } + } +} diff --git a/ln.http.csproj b/ln.http.csproj new file mode 100644 index 0000000..93067a3 --- /dev/null +++ b/ln.http.csproj @@ -0,0 +1,51 @@ + + + + Debug + AnyCPU + {CEEEEB41-3059-46A2-A871-2ADE22C013D9} + Library + ln.http + ln.http + v4.7 + + + true + full + false + bin\Debug + DEBUG; + prompt + 4 + false + + + true + bin\Release + prompt + 4 + false + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/threads/Pool.cs b/threads/Pool.cs new file mode 100644 index 0000000..9c30c28 --- /dev/null +++ b/threads/Pool.cs @@ -0,0 +1,82 @@ +using System; +using System.Threading; +using System.Collections.Generic; +namespace ln.http.threads +{ + public delegate void JobDelegate(); + + public class Pool + { + public int Timeout { get; set; } = 15000; + + ISet threads = new HashSet(); + Queue queuedJobs = new Queue(); + HashSet idleThreads = new HashSet(); + + public Pool() + { + } + + private void AllocateThread() + { + Thread thread = new Thread(pool_thread); + thread.Start(); + } + + public void Enqueue(JobDelegate job) + { + lock (this) + { + queuedJobs.Enqueue(job); + + if (idleThreads.Count == 0) + { + AllocateThread(); + } + else + { + Monitor.Pulse(this); + } + } + } + + + private void pool_thread() + { + Thread me = Thread.CurrentThread; + + try + { + JobDelegate job = null; + do + { + lock (this) + { + if (queuedJobs.Count == 0) + { + idleThreads.Add(me); + bool s = Monitor.Wait(this, Timeout); + idleThreads.Remove(me); + if (!s) + break; + } + job = queuedJobs.Dequeue(); + } + + job(); + + } while (true); + + } + catch (Exception e) + { + Console.WriteLine("Exception in worker thread: {0}", e); + } + + lock (this) + { + threads.Remove(me); + } + } + } +}