Initial Commit

master
Harald Wolff 2019-02-14 09:14:50 +01:00
commit 536852c4cd
14 changed files with 1028 additions and 0 deletions

41
.gitignore vendored 100644
View File

@ -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

145
HTTPServer.cs 100644
View File

@ -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<IPEndPoint, TcpListener> tcpListeners = new Dictionary<IPEndPoint, TcpListener>();
Dictionary<Uri, HttpApplication> applications = new Dictionary<Uri, HttpApplication>();
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();
}
}
}

13
HttpApplication.cs 100644
View File

@ -0,0 +1,13 @@
using System;
using ln.http;
namespace ln.http
{
public abstract class HttpApplication
{
public HttpApplication()
{
}
public abstract HttpResponse GetResponse(HttpRequest httpRequest);
}
}

297
HttpReader.cs 100644
View File

@ -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<string, string> Headers { get; } = new Dictionary<string, string>();
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<string,string> ReadHTTPHeaders()
{
byte[] b = new byte[8192];
int hlen = ReadTo(b, 0, new byte[] { 0x0d, 0x0a, 0x0d, 0x0a });
Dictionary<string, string> headers = new Dictionary<string, string>();
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;
}
*/
}
}

121
HttpRequest.cs 100644
View File

@ -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<String, String> 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<string, string>(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);
// }
//}
}
}

113
HttpResponse.cs 100644
View File

@ -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<string, List<String>> headers = new Dictionary<string, List<string>>();
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<string>();
headers[name].Add(value);
}
public void AddHeader(String name, String value)
{
name = name.ToUpper();
if (!headers.ContainsKey(name))
headers[name] = new List<string>();
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;
}
}
}
}

23
HttpStatusCodes.cs 100644
View File

@ -0,0 +1,23 @@
using System;
using System.Collections.Generic;
using System.Runtime.Remoting.Messaging;
namespace ln.http
{
public class HttpStatusCodes
{
static Dictionary<int, string> statusMessages = new Dictionary<int, string>()
{
{ 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 "";
}
}
}

View File

@ -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("")]

View File

@ -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<String,String>
{
Dictionary<string, string> parameters = new Dictionary<string, string>();
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<string> Keys => parameters.Keys;
public ICollection<string> 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<string, string> item) => throw new NotImplementedException();
public void Clear() => throw new NotImplementedException();
public bool Remove(string key) => throw new NotImplementedException();
public bool Remove(KeyValuePair<string, string> item) => throw new NotImplementedException();
public bool Contains(KeyValuePair<string, string> item) => parameters.Contains(item);
public bool ContainsKey(string key) => parameters.ContainsKey(key);
public void CopyTo(KeyValuePair<string, string>[] array, int arrayIndex) => ((IDictionary<string, string>)parameters).CopyTo(array, arrayIndex);
public IEnumerator<KeyValuePair<string, string>> 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();
}
}
}

View File

@ -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;
}
}
}

View File

@ -0,0 +1,11 @@
using System;
namespace ln.http.exceptions
{
public class IllegalRequestException : Exception
{
public IllegalRequestException(String requestLine)
:base(requestLine)
{
}
}
}

View File

@ -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))
{
}
}
}

51
ln.http.csproj 100644
View File

@ -0,0 +1,51 @@
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProjectGuid>{CEEEEB41-3059-46A2-A871-2ADE22C013D9}</ProjectGuid>
<OutputType>Library</OutputType>
<RootNamespace>ln.http</RootNamespace>
<AssemblyName>ln.http</AssemblyName>
<TargetFrameworkVersion>v4.7</TargetFrameworkVersion>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>bin\Debug</OutputPath>
<DefineConstants>DEBUG;</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
<ConsolePause>false</ConsolePause>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<Optimize>true</Optimize>
<OutputPath>bin\Release</OutputPath>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
<ConsolePause>false</ConsolePause>
</PropertyGroup>
<ItemGroup>
<Reference Include="System" />
</ItemGroup>
<ItemGroup>
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="exceptions\HttpException.cs" />
<Compile Include="exceptions\IllegalRequestException.cs" />
<Compile Include="exceptions\ResourceNotFoundException.cs" />
<Compile Include="threads\Pool.cs" />
<Compile Include="HTTPServer.cs" />
<Compile Include="HttpReader.cs" />
<Compile Include="HttpRequest.cs" />
<Compile Include="HttpResponse.cs" />
<Compile Include="HttpStatusCodes.cs" />
<Compile Include="QueryStringParameters.cs" />
<Compile Include="HttpApplication.cs" />
</ItemGroup>
<ItemGroup>
<Folder Include="exceptions\" />
<Folder Include="threads\" />
</ItemGroup>
<Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" />
</Project>

82
threads/Pool.cs 100644
View File

@ -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<Thread> threads = new HashSet<Thread>();
Queue<JobDelegate> queuedJobs = new Queue<JobDelegate>();
HashSet<Thread> idleThreads = new HashSet<Thread>();
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);
}
}
}
}