Note
The above shows how I connect to the server using Putty (you can also use netcat, or telnet). The server part is invisible.
Introduction
This is a very simple program to remotely eject CD and pop-up a message box, written in C#. To use it, run it on the target PC. It will listen on port 4444. Then connect to it using Putty, Netcat or Telnet. When the connection is established, type the following commands:
- 0 for help
- 1 for displaying a Hello World message in a Message Box to the target PC user
- 2 for ejecting the PC's CD tray
- 3 for closing the PC's CD tray
- 9 to shutdown the Server Process and also close port 4444
It runs on Windows Vista and Windows XP.
Background
The goal of this article is to educate myself on how to remotely eject a CD and pop up a message box with as minimal coding as possible. Hence, I have only implemented 5 commands as mentioned above in the Introduction section. This project consists of only the server part, you can use Putty, Netcat or Telnet to connect. All commands are sent from the command line. I have left out the client part in order to make this project as small and as simple as possible.
Using the Code
I compiled it in Visual C# 2005 Express and tested it on Vista and WinXP. Below is the latest version of the source code:
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Text;
using System.Windows.Forms;
using System.Net.Sockets;
using System.IO;
using System.Threading;
using System.Runtime.InteropServices;
using System.Net;
namespace RatServer
{
public partial class Form1 : Form
{
#region Variable Declarations
const string HELP = "0",
MESSAGE = "1",
EJECTCD = "2",
CLOSECD = "3",
SHUTDOWN = "9";
const string strHelp = "Command Menu:\r\n" +
"0 This Help\r\n" +
"1 Message\r\n" +
"2 Eject CD Tray\r\n" +
"3 Close CD Tray\r\n" +
"9 Shutdown the Server Process and Port\r\n";
TcpListener tcpListener;
Thread th_message,
th_ejectcd,
th_closecd;
Socket socketForClient;
NetworkStream networkStream;
StreamReader streamReader;
StreamWriter streamWriter;
#endregion
public Form1()
{
InitializeComponent();
}
private void Form1_Shown(object sender, EventArgs e)
{
this.Hide();
tcpListener = new TcpListener(System.Net.IPAddress.Any, 4444);
tcpListener.Start();
for(;;) RunServer();
}
private void RunServer()
{
socketForClient = tcpListener.AcceptSocket();
networkStream = new NetworkStream(socketForClient);
streamReader = new StreamReader(networkStream);
streamWriter = new StreamWriter(networkStream);
try
{
string line;
while (true)
{
line = "";
line = streamReader.ReadLine();
if (line.LastIndexOf(HELP) >= 0)
{
streamWriter.Write(strHelp);
streamWriter.Flush();
}
if (line.LastIndexOf(MESSAGE) >= 0)
{
th_message = new Thread(new ThreadStart(MessageCommand));
th_message.Start();
}
if(line.LastIndexOf(EJECTCD) >= 0)
{
th_ejectcd = new Thread(new ThreadStart(EjectCD));
th_ejectcd.Start();
}
if(line.LastIndexOf(CLOSECD) >= 0)
{
th_closecd = new Thread(new ThreadStart(CloseCD));
th_closecd.Start();
}
if(line.LastIndexOf(SHUTDOWN) >= 0)
{
streamWriter.Flush();
CleanUp();
System.Environment.Exit(System.Environment.ExitCode);
}
}
}
catch (Exception err)
{
CleanUp();
}
}
private void CleanUp()
{
streamReader.Close();
networkStream.Close();
socketForClient.Close();
}
private void MessageCommand()
{
MessageBox.Show("Hello World","Greetings",MessageBoxButtons.OK);
}
[DllImport("winmm.dll", EntryPoint = "mciSendStringA")]
public static extern void mciSendStringA(string lpstrCommand,
string lpstrReturnString, Int32 uReturnLength, Int32 hwndCallback);
string rt = "";
private void EjectCD()
{
mciSendStringA("set CDAudio door open", rt, 127, 0);
}
private void CloseCD()
{
mciSendStringA("set CDAudio door closed", rt, 127, 0);
}
}
}
Points of Interest
The form opacity property is set to 0
and in the Form1_Shown
event handler, a line...
this.Hide()
... is used to make the form invisible. Without first setting the opacity to 0
, the form will flash briefly when the program executes. Also, inserting this.Hide()
in the Form_Load
event does not work to hide the form. That is why I chose to put this.Hide()
in the Form_Shown
event instead.
Thereafter, the following lines of code listen for connections on TCP port 4444:
tcpListener = new TcpListener(System.Net.IPAddress.Any, 4444);
tcpListener.Start();
It will bind on all IP addresses, whether it is loopback (127.0.0.1), intranet (e.g. 192.168.0.1) or even public IP (e.g. 219.95.12.23). The next line of code is a perpetual loop:
for(;;) RunServer();
I designed a loop so that if a client were to suddenly disconnect, the loop will call RunServer
again. As such, a user can connect and disconnect as many times as she/he likes and each time, the for
loop will keep calling RunServer()
.
We now look at the RunServer()
method. This code creates a socket each time a client connects:
socketForClient = tcpListener.AcceptSocket();
The try...catch
block is used so that if a client suddenly disconnects, it will throw an exception which is caught by the catch{}
block. In the catch{}
block, the CleanUp()
method closes all sockets so that they can be re-used. I have read elsewhere that an alternative would be to check for 0 bytes read in socket.read()
. If amount of bytes read is 0, it means that the client has closed the connection. However, since I am already using the streamReader.readline()
method, I did not want to rewrite the code to put in socket.read().
If a client disconnects, control returns to the Form1_Shown
method where the for(;;)
loop will call RunServer()
again. To shutdown the server, the client sends the SHUTDOWN
command coded as "9
". To summarize. If you connect to the server and issue the command "9
", it will shut down the server and close port 4444 cleanly. However, if you suddenly close the connection, the server will still be listening on port 4444 and you can re-connect again and again.
On the other hand, if the client did not disconnect, the program will enter the while
loop which is another perpetual loop:
while (true)
{
line = "";
line = streamReader.ReadLine();
if (line.LastIndexOf(HELP) >= 0)
{
streamWriter.Write(strHelp);
streamWriter.Flush();
}
if (line.LastIndexOf(MESSAGE) >= 0)
{
th_message = new Thread(new ThreadStart(MessageCommand));
th_message.Start();
}
if(line.LastIndexOf(EJECTCD) >= 0)
{
th_ejectcd = new Thread(new ThreadStart(EjectCD));
th_ejectcd.Start();
}
if(line.LastIndexOf(CLOSECD) >= 0)
{
th_closecd = new Thread(new ThreadStart(CloseCD));
th_closecd.Start();
}
if(line.LastIndexOf(SHUTDOWN) >= 0)
{
streamWriter.Flush();
CleanUp();
System.Environment.Exit(System.Environment.ExitCode);
}
}
This is the heart of the server. Here is where all commands get processed. As long as the client does not send the SHUTDOWN
command coded as "9
", the server will keep looping in the while loop to process commands that the client keeps sending. Each separate command is handled by a separate if
-statement. Here is where you can add as many functionalities for the server as you desire. For this simple example, I have only put in 5 functionalities. If you wish to put in a new functionality, just insert another if
-statement. You will note that the LastIndexOf
method will look for command strings within the stream sent by the client. The client sends either "0
", "1
", "2
", "3
", or "9
" . In order to make the program more legible, I have earlier coded these strings into meaningful words:
const string HELP = "0",
MESSAGE = "1",
EJECTCD = "2",
CLOSECD = "3",
SHUTDOWN = "9";
To illustrate, take this portion of the code:
if (line.LastIndexOf(MESSAGE) >= 0)
{
th_message = new Thread(new ThreadStart(MessageCommand));
th_message.Start();
}
Once a MESSAGE
command is received, it will spawn a new thread to call the MessageCommand()
method. This will prevent the code from blocking - i.e. halting and waiting for the MessageCommand()
to complete. Using threads to handle each command ensures that multiple commands can run concurrently.
To eject and close the CD tray, we need to call the Win32
API functions. These are low level functions "hidden" within DLL files which collectively comprise the heart of the Operating System. For our purpose, we need to use the mciSendStringA()
function which is exported by the winmm.dll library. And in C#, the Win32
API can be called by including the header...
using System.Runtime.InteropServices;
... and followed by importing the winmm.dll:
[DllImport("winmm.dll", EntryPoint = "mciSendStringA")]
public static extern void mciSendStringA(string lpstrCommand,
string lpstrReturnString, Int32 uReturnLength, Int32 hwndCallback);
If client sends the SHUTDOWN
command, the program will close all sockets by calling the CleanUp()
method and then terminate with the System.Environment.Exit()
method:
if(line.LastIndexOf(SHUTDOWN) >= 0)
{
streamWriter.Flush();
CleanUp();
System.Environment.Exit(System.Environment.ExitCode);
}
History
Updated on August 8, 2007
All source code in this article is the updated code as of August 8th, 2007. The source code download is also the August 8th version. In this article, I have removed all references to the previous version, and only show the most current code. This is to keep the article short and easier to follow. This update fixes the problem of the server refusing to accept new connections whenever a client suddenly disconnects. The server is now able to detect the dropped connection, close the socket (for re-use) and go back to listening for new connections.