Waker.cs
class Waker
{
Timer timer;
public Waker()
{
timer = null;
}
public void WakeUpApplicationPool(object obj)
{
string url = "http://www.example.com";
try
{
HttpWebRequest request = (HttpWebRequest)WebRequest.Create(url);
HttpWebResponse response = (HttpWebResponse)request.GetResponse();
Program.LogToFile("WakeUpApplicationPool: " + response.StatusDescription);
response.Close();
}
catch (Exception ex)
{
Program.LogToFile("WakeUpApplicationPool_Error: " + ex.ToString());
}
}
public void Start()
{
TimerCallback callback = new TimerCallback(WakeUpApplicationPool);
int DUE_TIME = 0; //The amount of time to delay before the callback parameter invokes its methods.
int PERIOD = int.Parse(ConfigurationManager.AppSettings["WakerIntervalPeriod"]); //The time interval (miliseconds) between invocations of the methods referenced by callback
timer = new Timer(callback, null, DUE_TIME, PERIOD);
}
public void Stop()
{
timer.Dispose();
}
}
Program.cs:
static void Main(string[] args)
{
try
{
Waker waker = new Waker();
waker.Start();
}
catch(Exception ex)
{
LogToFile(ex.ToString());
}
}
Log file:
15 Apr 2015 18:29:39 - WakeUpApplicationPool: OK
15 Apr 2015 18:31:39 - WakeUpApplicationPool: OK
15 Apr 2015 18:33:59 - WakeUpApplicationPool_Error: System.Net.WebException: Unable to connect to the remote server ---> System.Net.Sockets.SocketException: A connection attempt failed because the connected party did not properly respond after a period of time, or established connection failed because connected host has failed to respond 205.144.171.35:80
at System.Net.Sockets.Socket.DoConnect(EndPoint endPointSnapshot, SocketAddress socketAddress)
at System.Net.ServicePoint.ConnectSocketInternal(Boolean connectFailure, Socket s4, Socket s6, Socket& socket, IPAddress& address, ConnectSocketState state, IAsyncResult asyncResult, Exception& exception)
--- End of inner exception stack trace ---
at System.Net.HttpWebRequest.GetResponse()
at ConsoleReporting.Waker.WakeUpApplicationPool(Object obj)
15 Apr 2015 18:35:39 - WakeUpApplicationPool: OK
15 Apr 2015 18:37:39 - WakeUpApplicationPool: OK
15 Apr 2015 18:41:18 - WakeUpApplicationPool_Error: System.Net.WebException: The operation has timed out
at System.Net.HttpWebRequest.GetResponse()
at ConsoleReporting.Waker.WakeUpApplicationPool(Object obj)
15 Apr 2015 18:43:18 - WakeUpApplicationPool_Error: System.Net.WebException: The operation has timed out
at System.Net.HttpWebRequest.GetResponse()
at ConsoleReporting.Waker.WakeUpApplicationPool(Object obj)
15 Apr 2015 18:45:18 - WakeUpApplicationPool_Error: System.Net.WebException: The operation has timed out
at System.Net.HttpWebRequest.GetResponse()
at ConsoleReporting.Waker.WakeUpApplicationPool(Object obj)
15 Apr 2015 18:47:18 - WakeUpApplicationPool_Error: System.Net.WebException: The operation has timed out
at System.Net.HttpWebRequest.GetResponse()
at ConsoleReporting.Waker.WakeUpApplicationPool(Object obj)
The problem is:
My code is not working after it hit the Timed Out error. But after I restart the Program.exe, it is working again but it hit the Timed Out error after 10 minutes.
I want to use this Program.exe to wake up my application pool which hosted at hosting provider.
So could anyone tell the reason and solution is? I referred this,but it is not working for my code either
The problem is solved after I set the WakerIntervalPeriod to 10 minutes instead of 5 minuets.
Related
I am getting "HTTP/2 client preface string missing or corrupt."
My thoughts are that it has to do with the headers not being set correctly. It is likely the implementation of WifiClient/WifiSecureClient. I've been thinking about this for over several weeks and I'm stuck. Any advice?
[Updated: Answer below]
The client was generated using the nanopb protocol buffer compiler:
protoc --plugin=protoc-gen-nanopb=~/grpc/nanopb/generator/protoc-gen-nanopb --nanopb_out=. helloworld.proto
Arduino client:
DHT dht(DHTPIN, DHTTYPE);
WiFiClient client;
//WiFiClientSecure client;
void setup() {
Serial.setDebugOutput(true);
Serial.begin(115200);
delay(10);
WiFi.begin("<SSID>", "<My Password>");
delay(3000);
while (WiFi.status() != WL_CONNECTED) {
Serial.println("WIFI connection failed, reconnecting...");
delay(2000);
}
Serial.print("WiFi connected, ");
Serial.print("IP address: ");
Serial.println(WiFi.localIP());
Serial.println("Starting DHT11 sensor...");
dht.begin();
}
void loop() {
Serial.print("connecting to ");
Serial.println(addr);
// client.setInsecure();
if (!client.connect(addr, port)) {
Serial.println(addr);
Serial.println(port);
Serial.println("connection failed");
Serial.println("wait 5 sec to reconnect...");
delay(5000);
return;
}
Serial.println("reading humidity/temp...");
float hum = dht.readHumidity();
float tmp = dht.readTemperature(true);
Serial.println(hum);
Serial.println(tmp);
if (isnan(hum) || isnan(tmp)) {
Serial.println("failed to read sensor data");
delay(2000);
return;
}
float hiCel = dht.computeHeatIndex(tmp, hum, true);
helloworld_TempEvent temp = helloworld_TempEvent_init_zero;
temp.deviceId = 1;
temp.eventId = 0;
temp.humidity = hum;
temp.tempCel = tmp;
temp.heatIdxCel = hiCel;
sendTemp(temp);
delay(1000);
}
void sendTemp(helloworld_TempEvent e) {
uint8_t buffer[128];
pb_ostream_t stream = pb_ostream_from_buffer(buffer, sizeof(buffer));
if (!pb_encode(&stream, helloworld_TempEvent_fields, &e)) {
Serial.println("failed to encode temp proto");
Serial.println(PB_GET_ERROR(&stream));
return;
}
Serial.print("sending temp... ");
Serial.println(e.tempCel);
client.write(buffer, stream.bytes_written);
}
The server was generated using the standard java protocol buffer compiler. The only thing I changed was adding a TempEvent (below).
... (helloworld template stuff) ...
// The request message containing temperatures
message TempEvent {
int32 deviceId = 1;
int32 eventId = 2;
float humidity = 3;
float tempCel = 4;
float heatIdxCel = 5;
}
The sample java client works without any issues. Where my problem lies is the simple client using nanopb on an ESP8266-01 wifi module which is sending the data using gRPC.
public class Server {
// Doesn't work
public static void main(String[] args) throws IOException, InterruptedException {
io.grpc.Server server = ServerBuilder
.forPort(8080)
.addService(new HelloServiceImpl()).build();
server.start();
server.awaitTermination();
}
// Works just fine
public static void main(String[] args) throws IOException, InterruptedException {
try (ServerSocket server = new ServerSocket(8080)) {
System.out.println("Server accepting connections on port " + server.getLocalPort());
TemperatureClient tempClient = new TemperatureClient();
while(true) {
Socket client = server.accept();
System.out.println("Client connected using remote port " + client.getPort());
final Thread t = new Thread(() -> {
try {
TempEvent p = TempEvent.parseFrom(client.getInputStream());
float i = p.getTempCel();
System.out.println("TEMP " + i);
} catch (IOException ioe) {
ioe.printStackTrace();
}
});
t.start();
}
}
}
The client is able to hit the server:
Nov 29, 2021 5:49:30 PM io.grpc.netty.shaded.io.grpc.netty.NettyServerTransport notifyTerminated
INFO: Transport failed
io.grpc.netty.shaded.io.netty.handler.codec.http2.Http2Exception: HTTP/2 client preface string missing or corrupt. Hex dump for received bytes: 080c10641d0000d84125e17aa0422de4459e42
at io.grpc.netty.shaded.io.netty.handler.codec.http2.Http2Exception.connectionError(Http2Exception.java:108)
at io.grpc.netty.shaded.io.netty.handler.codec.http2.Http2ConnectionHandler$PrefaceDecoder.readClientPrefaceString(Http2ConnectionHandler.java:306)
at io.grpc.netty.shaded.io.netty.handler.codec.http2.Http2ConnectionHandler$PrefaceDecoder.decode(Http2ConnectionHandler.java:239)
at io.grpc.netty.shaded.io.netty.handler.codec.http2.Http2ConnectionHandler.decode(Http2ConnectionHandler.java:438)
at io.grpc.netty.shaded.io.netty.handler.codec.ByteToMessageDecoder.decodeRemovalReentryProtection(ByteToMessageDecoder.java:508)
at io.grpc.netty.shaded.io.netty.handler.codec.ByteToMessageDecoder.callDecode(ByteToMessageDecoder.java:447)
at io.grpc.netty.shaded.io.netty.handler.codec.ByteToMessageDecoder.channelRead(ByteToMessageDecoder.java:276)
at io.grpc.netty.shaded.io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:379)
at io.grpc.netty.shaded.io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:365)
at io.grpc.netty.shaded.io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:357)
at io.grpc.netty.shaded.io.netty.channel.DefaultChannelPipeline$HeadContext.channelRead(DefaultChannelPipeline.java:1410)
at io.grpc.netty.shaded.io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:379)
at io.grpc.netty.shaded.io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:365)
at io.grpc.netty.shaded.io.netty.channel.DefaultChannelPipeline.fireChannelRead(DefaultChannelPipeline.java:919)
at io.grpc.netty.shaded.io.netty.channel.nio.AbstractNioByteChannel$NioByteUnsafe.read(AbstractNioByteChannel.java:166)
at io.grpc.netty.shaded.io.netty.channel.nio.NioEventLoop.processSelectedKey(NioEventLoop.java:719)
at io.grpc.netty.shaded.io.netty.channel.nio.NioEventLoop.processSelectedKeysOptimized(NioEventLoop.java:655)
at io.grpc.netty.shaded.io.netty.channel.nio.NioEventLoop.processSelectedKeys(NioEventLoop.java:581)
at io.grpc.netty.shaded.io.netty.channel.nio.NioEventLoop.run(NioEventLoop.java:493)
at io.grpc.netty.shaded.io.netty.util.concurrent.SingleThreadEventExecutor$4.run(SingleThreadEventExecutor.java:989)
at io.grpc.netty.shaded.io.netty.util.internal.ThreadExecutorMap$2.run(ThreadExecutorMap.java:74)
at io.grpc.netty.shaded.io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30)
at java.base/java.lang.Thread.run(Thread.java:831)
To debug this, I wanted to first see if I could use grpcurl, but I get this:
localhost#pro ~ % grpcurl -plaintext localhost:50051 list
Failed to list services: server does not support the reflection API
localhost#pro ~ % grpcurl -insecure localhost:50051 list
Failed to dial target host "localhost:50051": tls: first record does not look like a TLS handshake
I started looking into the implementation for WifiClient.h used in my implementation code, but does anyone have any ideas on a simple way to test this without digging into everything? I was thinking this should be super simple... but it is turning out to be much more entailed to generate a simple client than I thought. I feel like I am missing something here.
From other forums on here: "The client and server aren't agreeing. Typically this is because one is plaintext and the other using TLS. But it can also be due to HTTP/1 vs HTTP/2 in certain environments."
After looking at the Go Lang implementation, I just tried using WiFiClientSecure client.setInsecure(); // didn't work and the hex dump is below.
17:36:33.030 [grpc-nio-worker-ELG-3-1] DEBUG io.grpc.netty.shaded.io.grpc.netty.NettyServerHandler - [id: 0x41b96938, L:/192.168.0.23:8080 - R:/192.168.0.24:61587] OUTBOUND SETTINGS: ack=false settings={MAX_CONCURRENT_STREAMS=2147483647, INITIAL_WINDOW_SIZE=1048576, MAX_HEADER_LIST_SIZE=8192}
17:36:33.031 [grpc-nio-worker-ELG-3-1] DEBUG io.grpc.netty.shaded.io.grpc.netty.NettyServerHandler - [id: 0x41b96938, L:/192.168.0.23:8080 - R:/192.168.0.24:61587] OUTBOUND WINDOW_UPDATE: streamId=0 windowSizeIncrement=983041
17:36:33.063 [grpc-nio-worker-ELG-3-1] DEBUG io.grpc.netty.shaded.io.grpc.netty.NettyServerHandler - [id: 0x41b96938, L:/192.168.0.23:8080 - R:/192.168.0.24:61587] OUTBOUND GO_AWAY: lastStreamId=2147483647 errorCode=1 length=126 bytes=485454502f3220636c69656e74207072656661636520737472696e67206d697373696e67206f7220636f72727570742e204865782064756d7020666f72207265...
17:36:33.064 [grpc-nio-worker-ELG-3-1] DEBUG io.grpc.netty.shaded.io.netty.handler.codec.http2.Http2ConnectionHandler - [id: 0x41b96938, L:/192.168.0.23:8080 - R:/192.168.0.24:61587] Sent GOAWAY: lastStreamId '2147483647', errorCode '1', debugData 'HTTP/2 client preface string missing or corrupt. Hex dump for received bytes: 16030100d4010000d00303000000005c2f03aae7147c5f36'. Forcing shutdown of the connection.
Dec 10, 2021 5:36:33 PM io.grpc.netty.shaded.io.grpc.netty.NettyServerTransport notifyTerminated
INFO: Transport failed
io.grpc.netty.shaded.io.netty.handler.codec.http2.Http2Exception: HTTP/2 client preface string missing or corrupt. Hex dump for received bytes: 16030100d4010000d00303000000005c2f03aae7147c5f36
at io.grpc.netty.shaded.io.netty.handler.codec.http2.Http2Exception.connectionError(Http2Exception.java:108)
at io.grpc.netty.shaded.io.netty.handler.codec.http2.Http2ConnectionHandler$PrefaceDecoder.readClientPrefaceString(Http2ConnectionHandler.java:306)
at io.grpc.netty.shaded.io.netty.handler.codec.http2.Http2ConnectionHandler$PrefaceDecoder.decode(Http2ConnectionHandler.java:239)
at io.grpc.netty.shaded.io.netty.handler.codec.http2.Http2ConnectionHandler.decode(Http2ConnectionHandler.java:438)
at io.grpc.netty.shaded.io.netty.handler.codec.ByteToMessageDecoder.decodeRemovalReentryProtection(ByteToMessageDecoder.java:508)
at io.grpc.netty.shaded.io.netty.handler.codec.ByteToMessageDecoder.callDecode(ByteToMessageDecoder.java:447)
at io.grpc.netty.shaded.io.netty.handler.codec.ByteToMessageDecoder.channelRead(ByteToMessageDecoder.java:276)
at io.grpc.netty.shaded.io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:379)
at io.grpc.netty.shaded.io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:365)
at io.grpc.netty.shaded.io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:357)
at io.grpc.netty.shaded.io.netty.channel.DefaultChannelPipeline$HeadContext.channelRead(DefaultChannelPipeline.java:1410)
at io.grpc.netty.shaded.io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:379)
at io.grpc.netty.shaded.io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:365)
at io.grpc.netty.shaded.io.netty.channel.DefaultChannelPipeline.fireChannelRead(DefaultChannelPipeline.java:919)
at io.grpc.netty.shaded.io.netty.channel.nio.AbstractNioByteChannel$NioByteUnsafe.read(AbstractNioByteChannel.java:166)
at io.grpc.netty.shaded.io.netty.channel.nio.NioEventLoop.processSelectedKey(NioEventLoop.java:719)
at io.grpc.netty.shaded.io.netty.channel.nio.NioEventLoop.processSelectedKeysOptimized(NioEventLoop.java:655)
at io.grpc.netty.shaded.io.netty.channel.nio.NioEventLoop.processSelectedKeys(NioEventLoop.java:581)
at io.grpc.netty.shaded.io.netty.channel.nio.NioEventLoop.run(NioEventLoop.java:493)
at io.grpc.netty.shaded.io.netty.util.concurrent.SingleThreadEventExecutor$4.run(SingleThreadEventExecutor.java:989)
at io.grpc.netty.shaded.io.netty.util.internal.ThreadExecutorMap$2.run(ThreadExecutorMap.java:74)
at io.grpc.netty.shaded.io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30)
at java.base/java.lang.Thread.run(Thread.java:831)
WiFiClient client;
if (!client.connect(addr, port)) {
This forms a basic TCP connection. However, gRPC is a complex protocol based on HTTP/2. Currently you are just writing raw protobuf messages to a TCP socket, which can work for communication but is certainly not what a gRPC server is expecting.
Nanopb does not have gRPC support in itself. There is a third-party project adding it, but it is currently unmaintained.
I am unable to use cancellation tokens to stop a TCP Listener. The first code extract is an example where I can successfully stop a test while loop in a method from another class. So I don't understand why I cant apply this similar logic to the TCP Listener Class. Spent many days reading convoluted answers on this topic and cannot find a suitable solution.
My software application requires that the TCP Listener must give the user the ability to stop it from the server end, not the client. If a user wants to re-configure the port number for this listener then they would currently have to shutdown the software in order for Windows to close the underlying socket, this is no good as would affect the other services running in my app.
This first extract of code is just an example where I am able to stop a while loop from running, this works OK but is not that relevant other than the faat I would expect this to work for my TCP Listener:
public void Cancel(CancellationToken cancelToken) // EXAMPLE WHICH IS WORKING
{
Task.Run(async () =>
{
while (!cancelToken.IsCancellationRequested)
{
await Task.Delay(500);
log.Info("Test Message!");
}
}, cancelToken);
}
Now below is the actual TCP Listener code I am struggling with
public void TcpServerIN(string inboundEncodingType, string inboundIpAddress, string inboundLocalPortNumber, CancellationToken cancelToken)
{
TcpListener listener = null;
Task.Run(() =>
{
while (!cancelToken.IsCancellationRequested)
{
try
{
IPAddress localAddr = IPAddress.Parse(inboundIpAddress);
int port = int.Parse(inboundLocalPortNumber);
listener = new TcpListener(localAddr, port);
// Start listening for client requests.
listener.Start();
log.Info("TcpListenerIN listener started");
// Buffer for reading data
Byte[] bytes = new Byte[1024];
String data = null;
// Enter the listening loop.
while (true)
{
// Perform a blocking call to accept client requests.
TcpClient client = listener.AcceptTcpClient();
// Once each client has connected, start a new task with included parameters.
var task = Task.Run(() =>
{
// Get a stream object for reading and writing
NetworkStream stream = client.GetStream();
data = null;
int i;
// Loop to receive all the data sent by the client.
while ((i = stream.Read(bytes, 0, bytes.Length)) != 0)
{
// Select Encoding format set by string inboundEncodingType parameter.
if (inboundEncodingType == "UTF8") { data = Encoding.UTF8.GetString(bytes, 0, i); }
if (inboundEncodingType == "ASCII") { data = Encoding.ASCII.GetString(bytes, 0, i); }
// Use this if you want to echo each message directly back to TCP Client
//stream.Write(msg, 0, msg.Length);
// If any TCP Clients are connected then pass the appended string through
// the rules engine for processing, if not don't send.
if ((listConnectedClients != null) && (listConnectedClients.Any()))
{
// Pass the appended message string through the SSSCRulesEngine
SendMessageToAllClients(data);
}
}
// When the remote client disconnetcs, close/release the socket on the TCP Server.
client.Close();
});
}
}
catch (SocketException ex)
{
log.Error(ex);
}
finally
{
// If statement is required to prevent an en exception thrown caused by the user
// entering an invalid IP Address or Port number.
if (listener != null)
{
// Stop listening for new clients.
listener.Stop();
}
}
}
MessageBox.Show("CancellationRequested");
log.Info("TCP Server IN CancellationRequested");
}, cancelToken);
}
Interesting to see that no one had come back with any solutions, admittedly it took me a long while to figure out a solution. The key to stopping the TCP Listener when using a synchronous blocking mode like the example below is to register the Cancellation Token with the TCP Listener itself, as well the TCP Client that may have already been connected at the time the Cancellation Token was fired. (see comments that are marked as IMPORTANT)
The example code may differ slightly in your own environment and I have extracted some code bloat that is unique to my project, but you'll get the idea in what we're doing here. In my project this TCP Server is started as a background service using NET Core 5.0 IHosted Services. My code below was adapted from the notes on MS Docs: https://learn.microsoft.com/en-us/dotnet/api/system.net.sockets.tcplistener?view=net-5.0
The main difference between the MS Docs and my example below is I wanted to allow multiple TCP Clients to connect hence the reason why I start up a new inner Task each time a new TCP Client connects.
/// <summary>
/// </summary>
/// <param name="server"></param>
/// <param name="port"></param>
/// <param name="logger"></param>
/// <param name="cancelToken"></param>
public void TcpServerRun(
int pluginId,
string pluginName,
string encoding,
int bufferForReadingData,
string ipAddress,
int port,
bool logEvents,
IServiceScopeFactory _scopeFactory,
CancellationToken cancelToken)
{
IPAddress localAddrIN = IPAddress.Parse(ipAddress);
TcpListener listener = new TcpListener(localAddrIN, port);
Task.Run(() =>
{
// Dispose the DbContext instance when the task has completed. 'using' = dispose when finished...
using var scope = _scopeFactory.CreateScope();
var logger = scope.ServiceProvider.GetRequiredService<ILogger<TcpServer>>();
try
{
listener.Start();
cancelToken.Register(listener.Stop); // THIS IS IMPORTANT!
string logData = "TCP Server with name [" + pluginName + "] started Succesfully";
// Custom Logger - you would use your own logging method here...
WriteLogEvent("Information", "TCP Servers", "Started", pluginName, logData, null, _scopeFactory);
while (!cancelToken.IsCancellationRequested)
{
TcpClient client = listener.AcceptTcpClient();
logData = "A TCP Client with IP Address [" + client.Client.RemoteEndPoint.ToString() + "] connected to the TCP Server with name: [" + pluginName + "]";
// Custom Logger - you would use your own logging method here...
WriteLogEvent("Information", "TCP Servers", "Connected", pluginName, logData, null, _scopeFactory);
// Once each client has connected, start a new task with included parameters.
var task = Task.Run(async () =>
{
// Get a stream object for reading and writing
NetworkStream stream = client.GetStream();
// Buffer for reading data
Byte[] bytes = new Byte[bufferForReadingData]; // Bytes variable
String data = null;
int i;
cancelToken.Register(client.Close); // THIS IS IMPORTANT!
// Checks CanRead to verify that the NetworkStream is readable.
if (stream.CanRead)
{
// Loop to receive all the data sent by the client.
while ((i = stream.Read(bytes, 0, bytes.Length)) != 0 & !cancelToken.IsCancellationRequested)
{
data = Encoding.ASCII.GetString(bytes, 0, i);
logData = "TCP Server with name [" + pluginName + "] received data [" + data + "] from a TCP Client with IP Address [" + client.Client.RemoteEndPoint.ToString() + "]";
// Custom Logger - you would use your own logging method here...
WriteLogEvent("Information", "TCP Servers", "Receive", pluginName, logData, null, _scopeFactory);
}
// Shutdown and end connection
client.Close();
logData = "A TCP Client disconnected from the TCP Server with name: [" + pluginName + "]";
// Custom Logger - you would use your own logging method here...
WriteLogEvent("Information", "TCP Servers", "Disconnected", pluginName, logData, null, _scopeFactory);
}
}, cancelToken);
}
}
catch (SocketException ex)
{
// When the cancellation token is called, we will always encounter
// a socket exception for the listener.AcceptTcpClient(); blocking
// call in the while loop thread. We want to catch this particular exception
// and mark the exception as an accepted event without logging it as an error.
// A cancellation token is passed usually when the running thread is manually stopped
// by the user from the UI, or will occur when the IHosted service Stop Method
// is called during a system shutdown.
// For all other unexpected socket exceptions we provide en error log underneath
// in the else statement block.
if (ex.SocketErrorCode == SocketError.Interrupted)
{
string logData = "TCP Server with name [" + pluginName + "] was stopped due to a CancellationTokenSource cancellation. This event is triggered when the SMTP Server is manually stopped from the UI by the user or during a system shutdown.";
WriteLogEvent("Information", "TCP Servers", "Stopped", pluginName, logData, null, _scopeFactory);
}
else
{
string logData = "TCP Server with name [" + pluginName + "] encountered a socket exception error and exited the running thread.";
WriteLogEvent("Error", "TCP Servers", "Socket Exception", pluginName, logData, ex, _scopeFactory);
}
}
finally
{
// Call the Stop method to close the TcpListener.
// Closing the listener does not close any exisiting connections,
// simply stops listening for new connections, you are responsible
// closing the existing connections which we achieve by registering
// the cancel token with the listener.
listener.Stop();
}
});
}
I have got a solution, it's actually a demo on how Win App Driver should work but I can't for the life of me get it to work. Using Win App Driver with selenium and appium web drivers (as mentioned at 5 minutes into this video). I have the solution as shown below and when I run my AddAlarm test I get the error ... "the target machine actively refused it 127.0.0.1:4723".
The full error message is at the bottom of this post.
My question is, what do I need to do to make the application we're testing "Alarm & Clock" actually launch on the url 127.0.0.1:4723 is there anything I have to do to make it available on that url / port? Also, how do I verify is "app" and "Microsoft.WindowsAlarms_8wekyb3d8bbwe!App" are correct in the setup?
//Class with my test "AddAlarm"
using Microsoft.VisualStudio.TestTools.UnitTesting;
using OpenQA.Selenium.Appium.Windows;
using System.Threading;
using System;
namespace AlarmClockTest
{
[TestClass]
public class ScenarioAlarm : AutoTest_SynTQ.UnitTestSession
{
private const string NewAlarmName = "Sample Test Alarm";
[TestMethod]
public void AlarmAdd()
{
// Navigate to New Alarm page
session.FindElementByAccessibilityId("AddAlarmButton").Click();
// Set alarm name
session.FindElementByAccessibilityId("AlarmNameTextBox").Clear();
session.FindElementByAccessibilityId("AlarmNameTextBox").SendKeys(NewAlarmName);
// Set alarm hour
WindowsElement hourSelector = session.FindElementByAccessibilityId("HourLoopingSelector");
hourSelector.FindElementByName("3").Click();
Assert.AreEqual("3", hourSelector.Text);
// Set alarm minute
WindowsElement minuteSelector = session.FindElementByAccessibilityId("MinuteLoopingSelector");
minuteSelector.FindElementByName("55").Click();
Assert.AreEqual("55", minuteSelector.Text);
// Save the newly configured alarm
session.FindElementByAccessibilityId("AlarmSaveButton").Click();
Thread.Sleep(TimeSpan.FromSeconds(3));
// Verify that a new alarm entry is created with the given hour, minute, and name
WindowsElement alarmEntry = session.FindElementByXPath($"//ListItem[starts-with(#Name, \"{NewAlarmName}\")]");
Assert.IsNotNull(alarmEntry);
Assert.IsTrue(alarmEntry.Text.Contains("3"));
Assert.IsTrue(alarmEntry.Text.Contains("55"));
Assert.IsTrue(alarmEntry.Text.Contains(NewAlarmName));
// Verify that the alarm is active and deactivate it
WindowsElement alarmEntryToggleSwitch = alarmEntry.FindElementByAccessibilityId("AlarmToggleSwitch") as WindowsElement;
Assert.IsTrue(alarmEntryToggleSwitch.Selected);
alarmEntryToggleSwitch.Click();
Assert.IsFalse(alarmEntryToggleSwitch.Selected);
}
[ClassInitialize]
public static void ClassInitialize(TestContext context)
{
Setup(context);
}
[ClassCleanup]
public static void ClassCleanup()
{
// Try to delete any alarm entry that may have been created
while (true)
{
try
{
var alarmEntry = session.FindElementByXPath($"//ListItem[starts-with(#Name, \"{NewAlarmName}\")]");
session.Mouse.ContextClick(alarmEntry.Coordinates);
session.FindElementByName("Delete").Click();
}
catch
{
break;
}
}
TearDown();
}
[TestInitialize]
public override void TestInit()
{
// Invoke base class test initialization to ensure that the app is in the main page
base.TestInit();
// Navigate to Alarm tab
session.FindElementByAccessibilityId("AlarmPivotItem").Click();
}
}
}
//Inherited class below
using Microsoft.VisualStudio.TestTools.UnitTesting;
using OpenQA.Selenium.Appium.Windows;
using OpenQA.Selenium.Remote;
using System;
using System.Threading;
namespace AutoTest_SynTQ
{
[TestClass]
public class UnitTestSession
{
private const string WindowsApplicationDriverUrl = "http://127.0.0.1:4723";
private const string AlarmClockAppId = "Microsoft.WindowsAlarms_8wekyb3d8bbwe!App";
protected static WindowsDriver<WindowsElement> session;
protected static RemoteTouchScreen touchScreen;
public static void Setup(TestContext context)
{
// Launch Alarms & Clock application if it is not yet launched
if (session == null || touchScreen == null)
{
TearDown();
// Create a new session to bring up the Alarms & Clock application
DesiredCapabilities appCapabilities = new DesiredCapabilities();
appCapabilities.SetCapability("app", AlarmClockAppId);
session = new WindowsDriver<WindowsElement>(new Uri(WindowsApplicationDriverUrl), appCapabilities);
Assert.IsNotNull(session);
Assert.IsNotNull(session.SessionId);
// Set implicit timeout to 1.5 seconds to make element search to retry every 500 ms for at most three times
session.Manage().Timeouts().ImplicitlyWait(TimeSpan.FromSeconds(1.5));
// Initialize touch screen object
touchScreen = new RemoteTouchScreen(session);
Assert.IsNotNull(touchScreen);
}
}
public static void TearDown()
{
// Cleanup RemoteTouchScreen object if initialized
touchScreen = null;
// Close the application and delete the session
if (session != null)
{
session.Quit();
session = null;
}
}
[TestInitialize]
public virtual void TestInit()
{
WindowsElement alarmTabElement = null;
// Attempt to go back to the main page in case Alarms & Clock app is started in EditAlarm view
try
{
alarmTabElement = session.FindElementByAccessibilityId("AlarmPivotItem");
}
catch
{
// Click back button if application is in a nested page such as New Alarm or New Timer
session.FindElementByAccessibilityId("Back").Click();
Thread.Sleep(TimeSpan.FromSeconds(1));
alarmTabElement = session.FindElementByAccessibilityId("AlarmPivotItem");
}
// Verify that the app is in the main view showing alarmTabElement
Assert.IsNotNull(alarmTabElement);
Assert.IsTrue(alarmTabElement.Displayed);
}
}
}
Test Name: AlarmAdd
Test FullName: AlarmClockTest.ScenarioAlarm.AlarmAdd
Test Source: C:\Users\ECombe.OPTIDOORS\Documents\SynTQCodedUITesting\AutoTest_SynTQ\SCN_Alarm.cs : line 30
Test Outcome: Failed
Test Duration: 0:00:00
Result StackTrace:
at OpenQA.Selenium.Remote.RemoteWebDriver.UnpackAndThrowOnError(Response errorResponse)
at OpenQA.Selenium.Remote.RemoteWebDriver.Execute(String driverCommandToExecute, Dictionary2 parameters)
at OpenQA.Selenium.Remote.RemoteWebDriver.StartSession(ICapabilities desiredCapabilities)
at OpenQA.Selenium.Remote.RemoteWebDriver..ctor(ICommandExecutor commandExecutor, ICapabilities desiredCapabilities)
at OpenQA.Selenium.Appium.AppiumDriver1..ctor(Uri remoteAddress, ICapabilities desiredCapabilities)
at OpenQA.Selenium.Appium.Windows.WindowsDriver1..ctor(Uri remoteAddress, DesiredCapabilities desiredCapabilities)
at AutoTest_SynTQ.UnitTestSession.Setup(TestContext context) in C:\Users\ECombe.OPTIDOORS\Documents\SynTQCodedUITesting\AutoTest_SynTQ\UnitTestSession.cs:line 28
at AlarmClockTest.ScenarioAlarm.ClassInitialize(TestContext context) in C:\Users\ECombe.OPTIDOORS\Documents\SynTQCodedUITesting\AutoTest_SynTQ\SCN_Alarm.cs:line 71
Result Message:
Class Initialization method AlarmClockTest.ScenarioAlarm.ClassInitialize threw exception. OpenQA.Selenium.WebDriverException: OpenQA.Selenium.WebDriverException: Unexpected error. System.Net.WebException: Unable to connect to the remote server ---> System.Net.Sockets.SocketException: No connection could be made because the target machine actively refused it 127.0.0.1:4723
at System.Net.Sockets.Socket.DoConnect(EndPoint endPointSnapshot, SocketAddress socketAddress)
at System.Net.ServicePoint.ConnectSocketInternal(Boolean connectFailure, Socket s4, Socket s6, Socket& socket, IPAddress& address, ConnectSocketState state, IAsyncResult asyncResult, Exception& exception)
--- End of inner exception stack trace ---
at OpenQA.Selenium.Appium.Service.AppiumCommandExecutor.Execute(Command commandToExecute)
at OpenQA.Selenium.Remote.RemoteWebDriver.Execute(String driverCommandToExecute, Dictionary2 parameters).
The answer in my case was to use Developer mode. The actual problem was winappdriver.exe closing immediately. More details here.
I'm making a BLE cross-platform app in Xamarin, but fail to discover services of a connected peripheral.
Here's the stripped down code from the shared-project, just to be able to reproduce this issue;
internal class BleTransport
{
//Private stuff
private CBCentralManager Central = null;
private CBPeripheral CurrentPeripheral = null;
private bool IsScanning = false;
internal BleTransport ()
{
//Constructor -- initialize everything that needs to be initialized!
Central = new CBCentralManager (DispatchQueue.MainQueue);
//Setup delegates to the central's events
Central.UpdatedState += (object sender, EventArgs e) => CentralStateChanged.Set ();
Central.DiscoveredPeripheral += (object sender, CBDiscoveredPeripheralEventArgs e) => DiscoveredPeripheral (e.Peripheral, e.RSSI, e.AdvertisementData);
Central.ConnectedPeripheral += (object sender, CBPeripheralEventArgs e) => ConnectedPeripheral (e.Peripheral);
Central.FailedToConnectPeripheral += (object sender, CBPeripheralErrorEventArgs e) => FailedToConnectPeripheral (e.Error, e.Peripheral);
Central.DisconnectedPeripheral += (object sender, CBPeripheralErrorEventArgs e) => DisconnectedPeripheral (e.Error, e.Peripheral);
}
readonly AutoResetEvent CentralStateChanged = new AutoResetEvent (false);
private async Task<bool> WaitForCentralState (CBCentralManagerState state, TimeSpan timespan)
{
Console.WriteLine("Waiting for state : " + state);
//TODO; Keep track of time and decrease time-span accordingly
bool timedout = false;
while (!timedout && Central.State != state)
{
timedout = await Task.Run (delegate {
//WaitOne returns false if we timeout
return !CentralStateChanged.WaitOne (timespan);
});
}
//Return true if we made it, amd false if we timedout
return !timedout;
}
private void StartScanning()
{
Console.WriteLine("Start scan requestd!");
if (!IsScanning)
{
//Start!
IsScanning = true;
Console.WriteLine("Kicking scan for peripherals!");
CBUUID[] uuids = null;
Central.ScanForPeripherals (uuids);
}
}
private void StopScanning()
{
Console.WriteLine("Stop scan requested!");
if (IsScanning)
{
//Stop!
IsScanning = false;
Central.StopScan();
}
}
public async Task<bool> SetupTransport ()
{
Console.WriteLine("Setting up transport!");
//Wait for state update...
if (!await WaitForCentralState (CBCentralManagerState.PoweredOn, TimeSpan.FromSeconds (2))) {
//Failed detecting a powered-on BLE interface
Console.WriteLine("Failed detecting usable BLE interface!");
return false;
}
//Kick scanning...
StartScanning();
//Wait for discovery and connection to a valid peripheral, or timeout trying...
await Task.Delay(10000);
return true;
}
#region Internal BLE utility methods
private void DiscoveredPeripheral (CBPeripheral peripheral, NSNumber rssi, NSDictionary advertisementData)
{
Console.WriteLine("DiscoveredPeripheral: " +peripheral.Name);
//If the discovered device's name IS a valid serial number, AND match the serial number that we're intended to establish connection to...
if (peripheral.Name == "MyPeripheralName") {
Console.WriteLine("It's the peripheral we're looking for!");
//Stop scanning...
StopScanning();
//Save reference and connect to it!
CurrentPeripheral = peripheral;
Central.ConnectPeripheral(CurrentPeripheral, new PeripheralConnectionOptions());
}
}
private void ConnectedPeripheral (CBPeripheral peripheral)
{
Console.WriteLine("ConnectedPeripheral: " + peripheral.Name);
//Great -- explore services and charateristics and don't stop until we know that this device is legit!
CurrentPeripheral.DiscoveredService += (object sender, NSErrorEventArgs e) => DiscoveredServices (e.Error);
CurrentPeripheral.DiscoveredCharacteristic += (object sender, CBServiceEventArgs e) => DiscoveredCharacteristics (e.Error, e.Service);
//Kick service discovery!
CurrentPeripheral.DiscoverServices();
}
private void FailedToConnectPeripheral (NSError error, CBPeripheral peripheral)
{
Console.WriteLine("FailedToConnectPeripheral: " + peripheral.Name);
}
private void DisconnectedPeripheral (NSError error, CBPeripheral peripheral)
{
Console.WriteLine("DisconnectedPeripheral: " + peripheral.Name + "(Error = " + error + ")");
}
private void DiscoveredServices (NSError error)
{
Console.WriteLine("DiscoveredService: " + error + " -- " + NSThread.Current.IsMainThread);
if (error != null) {
//No error -- great!
foreach (CBService service in CurrentPeripheral.Services) {
Console.WriteLine ("Discovered service: " + service);
}
}
else
{
//Opps -- failed!
//Disconnect and indicate connection attempt finished
}
}
private void DiscoveredCharacteristics (NSError error, CBService service)
{
Console.WriteLine("DiscoveredCharacteristics for service: " + service);
if (error != null) {
//No error -- great!
foreach (CBCharacteristic characteristic in service.Characteristics) {
Console.WriteLine ("Discovered charateristic: " + characteristic);
}
}
else
{
//Opps -- failed!
//Disconnect and indicate connection attempt finished
}
}
#endregion
}
The problem is that AFTER discovery is done, scanning is stopped, and the app connect to the peripheral and we arrive in the ConnectedPeripheral (CBPeripheral peripheral) function, I get a SIGSEGV crash with the following stack trace;
2015-04-21 23:33:37.205 BLE-test[1487:322866] critical: Stacktrace:
2015-04-21 23:33:37.206 BLE-test[1487:322866] critical: at <unknown> <0xffffffff>
2015-04-21 23:33:37.206 BLE-test[1487:322866] critical: at (wrapper managed-to-native) UIKit.UIApplication.UIApplicationMain (int,string[],intptr,intptr) <0xffffffff>
2015-04-21 23:33:37.207 BLE-test[1487:322866] critical: at UIKit.UIApplication.Main (string[],intptr,intptr) [0x00005] in /Developer/MonoTouch/Source/monotouch/src/UIKit/UIApplication.cs:62
2015-04-21 23:33:37.207 BLE-test[1487:322866] critical: at UIKit.UIApplication.Main (string[],string,string) [0x0001c] in /Developer/MonoTouch/Source/monotouch/src/UIKit/UIApplication.cs:45
2015-04-21 23:33:37.208 BLE-test[1487:322866] critical: at BLEtest.Application.Main (string[]) [0x00008] in /Users/markus/Xamarin/BLE-test/BLE-test/Main.cs:17
2015-04-21 23:33:37.208 BLE-test[1487:322866] critical: at (wrapper runtime-invoke) object.runtime_invoke_dynamic (intptr,intptr,intptr,intptr) <0xffffffff>
Followed by a native stack-trace that I've symbolicated into;
Incident Identifier: DCFCEE70-3D12-4F69-8303-777B2959563D
CrashReporter Key: 25c23a5bf636eb183168b18867096174f0d59b79
Hardware Model: iPhone6,2
Process: BLE-test [1241]
Path: /private/var/mobile/Containers/Bundle/Application/C091012A-B1CB-464B-BDA5-B193C9C26BFC/BLE-test.app/BLE-test
Identifier: com.your-company.BLEtest
Version: 1.0 (1.0)
Code Type: ARM-64 (Native)
Parent Process: launchd [1]
Date/Time: 2015-04-21 22:22:57.387 +0200
Launch Time: 2015-04-21 22:22:56.120 +0200
OS Version: iOS 8.3 (12F70)
Report Version: 105
Exception Type: EXC_BAD_ACCESS (SIGABRT)
Exception Subtype: KERN_INVALID_ADDRESS at 0x000000000004b2c0
Triggered by Thread: 0
Thread 0 name: Dispatch queue: com.apple.main-thread
Thread 0 Crashed:
0 libsystem_kernel.dylib 0x0000000195233270 __pthread_kill + 8
1 libsystem_pthread.dylib 0x00000001952d116c pthread_kill + 108
2 libsystem_c.dylib 0x00000001951aab14 abort + 108
3 BLE-test 0x00000001001f6878 mono_handle_native_sigsegv (mini-exceptions.c:2360)
4 BLE-test 0x0000000100200554 mono_sigsegv_signal_handler (mini.c:6879)
5 libsystem_platform.dylib 0x00000001952c8958 _sigtramp + 64
6 libobjc.A.dylib 0x0000000194aa5ea8 _class_getVariable + 160
7 libobjc.A.dylib 0x0000000194aa5ea8 _class_getVariable + 160
8 libobjc.A.dylib 0x0000000194a9a934 object_getInstanceVariable + 72
9 BLE-test 0x00000001002b10b0 get_raw_gchandle (runtime.m:304)
10 BLE-test 0x00000001002b1044 get_gchandle (runtime.m:311)
11 BLE-test 0x00000001002b0c6c xamarin_get_gchandle (runtime.m:317)
12 BLE-test 0x00000001002b0bc0 xamarin_get_nsobject_with_type_for_ptr_created (runtime.m:201)
13 BLE-test 0x00000001002b9508 xamarin_trampoline (.monotouch-trampoline-setup-callstack.inc:181)
14 CoreBluetooth 0x0000000182f37198 -[CBPeripheral handleServicesDiscovered:] + 740
15 CoreBluetooth 0x0000000182f34b38 -[CBPeripheral handleMsg:args:] + 280
16 CoreBluetooth 0x0000000182f309d0 -[CBCentralManager xpcConnection:didReceiveMsg:args:] + 160
17 libdispatch.dylib 0x00000001950ed990 _dispatch_call_block_and_release + 20
18 libdispatch.dylib 0x00000001950ed950 _dispatch_client_callout + 12
19 libdispatch.dylib 0x00000001950f80a0 _dispatch_queue_drain + 1444
20 libdispatch.dylib 0x00000001950f0a58 _dispatch_queue_invoke + 128
21 libdispatch.dylib 0x00000001950f1db8 _dispatch_main_queue_callback_4CF + 500
22 CoreFoundation 0x000000018327f7f4 __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__ + 8
23 CoreFoundation 0x000000018327d89c __CFRunLoopRun + 1488
24 CoreFoundation 0x00000001831a92d0 CFRunLoopRunSpecific + 392
25 GraphicsServices 0x000000018c9c76f8 GSEventRunModal + 164
26 UIKit 0x0000000187d6efa8 UIApplicationMain + 1484
27 BLE-test 0x00000001000a0b94 wrapper_managed_to_native_UIKit_UIApplication_UIApplicationMain_int_string___intptr_intptr + 340
28 BLE-test 0x000000010007e60c UIKit_UIApplication_Main_string___intptr_intptr + 44
29 BLE-test 0x000000010007e5c8 UIKit_UIApplication_Main_string___string_string + 184
30 BLE-test 0x0000000100050110 BLEtest_Application_Main_string__ + 160
31 BLE-test 0x000000010017c06c wrapper_runtime_invoke_object_runtime_invoke_dynamic_intptr_intptr_intptr_intptr + 156
32 BLE-test 0x0000000100202614 mono_jit_runtime_invoke (mini.c:6737)
33 BLE-test 0x000000010024d31c mono_runtime_invoke (object.c:2842)
34 BLE-test 0x00000001002513f8 mono_runtime_exec_main (object.c:4099)
35 BLE-test 0x00000001002afa34 xamarin_main (monotouch-main.m:400)
36 BLE-test 0x00000001001ca924 main + 92
37 libdyld.dylib 0x000000019511aa04 start + 0
Observations and thoughts;
If I remove the delegates to the peripheral after it has been connected, but before I try to discover its services (which means that I'll never see the result of the discovery, but just for test), the DiscoverService routine does NOT result in a crash;
// CurrentPeripheral.DiscoveredService += (object sender, NSErrorEventArgs e) => ...
// CurrentPeripheral.DiscoveredCharacteristic += (object sender, ...
Likewise, I've also tried to keep the delegates above, but remove the actual call to DiscoverServices and that also work without crashing (which is again a stupid test perhaps, but I just wanted to rule-out that something happened when I assigned the delegates in the first place).
I've also read about other peoples experiences with these kind of issues/errors and someone said that it's a GC-issue where an object might be GC'd even though it's still referenced?
Someone also mentioned that things worked in Release-mode but not in debug mode -- so I've tested to run my app in release which ALSO seems to work -- but I don't have any UI so I can't guarantee that is actually works -- but I do know that it does not crash with SIGSEGV...
I'm confused. Please help me understand why Xamarin does not like me. I'm happy to adjust and play by the Xamarin-rules, as long as I understand the constraints and what I can/can't do.
UPPDATE;
I just tried to see if the error was the callback/event-handlers inside the
CBPeripheral, or the actual DiscoverServices method call. So I tried to call
the function and wait 500ms and then see if the services had been discovered in
my peripheral object;
Console.WriteLine ("List of services (before discovery attempt)");
if (CurrentPeripheral.Services != null)
foreach (CBService service in CurrentPeripheral.Services) {
Console.WriteLine ("Service: " + service);
}
CurrentPeripheral.DiscoverServices();
await Task.Delay(500);
Console.WriteLine ("List of services (after discovery attempt)");
if (CurrentPeripheral.Services != null)
foreach (CBService service in CurrentPeripheral.Services) {
Console.WriteLine ("Service: " + service);
}
... and it works! Hence, this means that the call to DiscoverServices works
just fine, but the callbacks does not!
Ok, problem solved. A newbie thing, of course...
When I launched Xamarin Studio and started this project, I played around and looked at the different options, settings, features, menus etc, just to get a grip of the concept as such. One of the things I found was the iOS-build options where one could change the Linker Options. I figured that "Link all assemblies" was a good choice, just to be on the safe side, rather than "Don't link" which was preselected.
Didn't think more about it and continued with the coding.
But when I found the issue raised and explain in this thread, you desperately start to search for anything anywhere, and I started to hunt for GC issues which was what thought was the source of the error. So, reading through the best-practices for Memory management at this link (http://developer.xamarin.com/guides/cross-platform/deployment,_testing,_and_metrics/memory_perf_best_practices/), I found that;
The Link All Assemblies should be used with caution as it may break the application in unexpected ways.
Hence, I changed back to Don't link and now everything works like a charm...
Stuped newbie error? Yes, for sure, but I hope that someone else making the same misstake will find this post and find it useful.
Thanks,
/Markus
I am using PushStreamContent to keep a persistent connection to each client. Pushing short heartbeat messages to each client stream every 20 seconds works great with 100 clients, but at about 200 clients, the client first starts receiving it a few seconds delayed, then it doesn't show up at all.
My controller code is
// Based loosely on https://aspnetwebstack.codeplex.com/discussions/359056
// and http://blogs.msdn.com/b/henrikn/archive/2012/04/23/using-cookies-with-asp-net-web-api.aspx
public class LiveController : ApiController
{
public HttpResponseMessage Get(HttpRequestMessage request)
{
if (_timer == null)
{
// 20 second timer
_timer = new Timer(TimerCallback, this, 20000, 20000);
}
// Get '?clientid=xxx'
HttpResponseMessage response = request.CreateResponse();
var kvp = request.GetQueryNameValuePairs().Where(q => q.Key.ToLower() == "clientid").FirstOrDefault();
string clientId = kvp.Value;
HttpContext.Current.Response.ClientDisconnectedToken.Register(
delegate(object obj)
{
// Client has cleanly disconnected
var disconnectedClientId = (string)obj;
CloseStreamFor(disconnectedClientId);
}
, clientId);
response.Content = new PushStreamContent(
delegate(Stream stream, HttpContent content, TransportContext context)
{
SaveStreamFor(clientId, stream);
}
, "text/event-stream");
return response;
}
private static void CloseStreamFor(string clientId)
{
Stream oldStream;
_streams.TryRemove(clientId, out oldStream);
if (oldStream != null)
oldStream.Close();
}
private static void SaveStreamFor(string clientId, Stream stream)
{
_streams.TryAdd(clientId, stream);
}
private static void TimerCallback(object obj)
{
DateTime start = DateTime.Now;
// Disable timer
_timer.Change(Timeout.Infinite, Timeout.Infinite);
// Every 20 seconds, send a heartbeat to each client
var recipients = _streams.ToArray();
foreach (var kvp in recipients)
{
string clientId = kvp.Key;
var stream = kvp.Value;
try
{
// ***
// Adding this Trace statement and running in debugger caused
// heartbeats to be reliably flushed!
// ***
Trace.WriteLine(string.Format("** {0}: Timercallback: {1}", DateTime.Now.ToString("G"), clientId));
WriteHeartBeat(stream);
}
catch (Exception ex)
{
CloseStreamFor(clientId);
}
}
// Trace... (this trace statement had no effect)
_timer.Change(20000, 20000); // re-enable timer
}
private static void WriteHeartBeat(Stream stream)
{
WriteStream(stream, "event:heartbeat\ndata:-\n\n");
}
private static void WriteStream(Stream stream, string data)
{
byte[] arr = Encoding.ASCII.GetBytes(data);
stream.Write(arr, 0, arr.Length);
stream.Flush();
}
private static readonly ConcurrentDictionary<string, Stream> _streams = new ConcurrentDictionary<string, Stream>();
private static Timer _timer;
}
Could there be some ASP.NET or IIS setting that affects this? I am running on Windows Server 2008 R2.
UPDATE:
Heartbeats are reliably sent if 1) the Trace.WriteLine statement is added, 2) Visual Studio 2013 debugger is attached and debugging and capturing the Trace.WriteLines).
Both of these are necessary; if the Trace.WriteLine is removed, running under the debugger has no effect. And if the Trace.WriteLine is there but the program is not running under the debugger (instead SysInternals' DbgView is showing the trace messages), the heartbeats are unreliable.
UPDATE 2:
Two support incidents with Microsoft later, here are the conclusions:
1) The delays with 200 clients were resolved by using a business class Internet connection instead of a Home connection
2) whether the debugger is attached or not really doesn't make any difference;
3) The following two additions to web.config are required to ensure heartbeats are sent timely, and failed heartbeats due to client disconnecting "uncleanly" (e.g. by unplugging computer rather than normal closing of program which cleanly issues TCP RST) trigger a timely ClientDisconnected callback as well:
<httpRuntime executionTimeout="5" />
<serverRuntime appConcurrentRequestLimit="50000" uploadReadAheadSize="1" frequentHitThreshold="2147483647" />