How Shadowsocks-Windows Implements Multiple Instance Enforcement Using Mutex and IPC
Shadowsocks-Windows guarantees a single running UI process by combining a named Mutex for instance detection with Named Pipe IPC to forward commands from secondary launches to the primary instance.
The shadowsocks/shadowsocks-windows client ensures only one graphical interface runs per installation directory through a robust multiple instance enforcement strategy. By leveraging Windows synchronization primitives and inter-process communication, the application blocks redundant UI processes while delegating user actions—such as adding servers via URLs—to the already-active instance. This design hashes the executable path to create unique resource identifiers, allowing independent copies of Shadowsocks-Windows to coexist without interference.
The Dual-Mechanism Architecture
The enforcement system operates through two coordinated components that work together to manage process lifecycle and command delegation.
Named Mutex for Instance Detection
At startup, the application attempts to acquire a global Mutex whose name incorporates a hash of the executable's full path. In shadowsocks-csharp/Program.cs, the implementation creates this synchronization object using a deterministic string format:
private static readonly Mutex mutex = new Mutex(true, $"Shadowsocks_{ExecutablePath.GetHashCode()}");
The program calls mutex.WaitOne(TimeSpan.Zero, true) to test ownership. If the method returns false, the current process recognizes that another instance already holds the lock, setting hasAnotherInstance = true and triggering the IPC forwarding path rather than initializing a full UI.
Named Pipe IPC for Command Forwarding
When a secondary instance detects an existing primary process, it communicates via the IPCService class implemented in shadowsocks-csharp/Controller/Service/IPCService.cs. The service establishes a Named Pipe server using a path derived from the same executable hash:
private static readonly string PIPE_PATH = $"Shadowsocks\\{Program.ExecutablePath.GetHashCode()}";
This ensures that only instances from the same installation directory communicate with each other, maintaining isolation between independent copies.
Step-by-Step Execution Flow
The complete lifecycle follows a six-step process that distinguishes between primary and secondary instances:
-
Primary Instance Startup: The first process successfully acquires the Mutex and initializes the full application stack, including the
IPCServiceserver. -
IPC Server Initialization: The primary instance executes
new IPCService().RunServer(), which enters an infinite loop listening for connections on the named pipe. -
Secondary Launch Detection: A subsequent process start creates the same Mutex name, but
WaitOnereturnsfalse, settinghasAnotherInstance = true. -
Command Forwarding: If the secondary launch includes arguments such as
--openurl <url>parsed byCommandLineOption.cs, the process callsIPCService.RequestOpenUrl(url)instead of showing a UI. -
Server-Side Processing: The primary instance's server reads the opcode (
OP_OPEN_URL = 1) and payload from the pipe, then raises theOpenUrlRequestedevent subscribed to byMainController.AskAddServerBySSURLinShadowsocksController.cs. -
User Notification: Secondary launches without special commands display a toast notification informing the user that Shadowsocks is already running, suggesting they copy the folder to run an independent instance.
Implementation Details by File
Mutex Creation and Detection in Program.cs
The entry point in shadowsocks-csharp/Program.cs handles the initial synchronization check. The code first computes the hash of the current process executable path, then attempts to claim the Mutex:
public static readonly string ExecutablePath = Process.GetCurrentProcess().MainModule?.FileName;
private static readonly Mutex mutex = new Mutex(true, $"Shadowsocks_{ExecutablePath.GetHashCode()}");
// Detection logic
bool hasAnotherInstance = !mutex.WaitOne(TimeSpan.Zero, true);
When hasAnotherInstance evaluates to true, the program skips UI initialization and proceeds to the IPC client logic.
IPC Server Implementation in IPCService.cs
The RunServer method in IPCService.cs implements the primary instance's listening loop using NamedPipeServerStream. The server asynchronously waits for connections and processes opcodes:
private const int OP_OPEN_URL = 1;
private static readonly string PIPE_PATH = $"Shadowsocks\\{Program.ExecutablePath.GetHashCode()}";
public async void RunServer()
{
byte[] buf = new byte[4096];
while (true)
{
using (NamedPipeServerStream stream = new NamedPipeServerStream(PIPE_PATH))
{
await stream.WaitForConnectionAsync();
await stream.ReadAsync(buf, 0, INT32_LEN);
int opcode = IPAddress.NetworkToHostOrder(BitConverter.ToInt32(buf, 0));
if (opcode == OP_OPEN_URL)
{
await stream.ReadAsync(buf, 0, INT32_LEN);
int strlen = IPAddress.NetworkToHostOrder(BitConverter.ToInt32(buf, 0));
await stream.ReadAsync(buf, 0, strlen);
string url = Encoding.UTF8.GetString(buf, 0, strlen);
OpenUrlRequested?.Invoke(this, new RequestAddUrlEventArgs(url));
}
stream.Close();
}
}
}
The primary instance subscribes to the OpenUrlRequested event to handle incoming requests:
IPCService ipcService = new IPCService();
Task.Run(() => ipcService.RunServer());
ipcService.OpenUrlRequested += (_, e) => MainController.AskAddServerBySSURL(e.Url);
Client-Side Request Forwarding
Secondary instances act as clients using the static RequestOpenUrl method. This implementation attempts to connect to the existing pipe and transmit the operation code and URL payload:
public static void RequestOpenUrl(string url)
{
(NamedPipeClientStream pipe, bool exist) = TryConnect();
if (!exist) return; // No primary instance – nothing to forward
byte[] opAddUrl = BitConverter.GetBytes(IPAddress.HostToNetworkOrder(OP_OPEN_URL));
pipe.Write(opAddUrl, 0, INT32_LEN); // opcode
byte[] b = Encoding.UTF8.GetBytes(url);
byte[] blen = BitConverter.GetBytes(IPAddress.HostToNetworkOrder(b.Length));
pipe.Write(blen, 0, INT32_LEN); // length
pipe.Write(b, 0, b.Length); // payload
pipe.Close();
}
Per-Directory Isolation Using Executable Path Hashing
Both the Mutex name and Named Pipe path incorporate Program.ExecutablePath.GetHashCode(), creating a unique namespace for each installation directory. This design decision allows users to run multiple independent copies of Shadowsocks-Windows simultaneously, provided each resides in a distinct folder. Each copy maintains its own Mutex and IPC channel, preventing cross-talk between separate installations while still enforcing single-instance constraints within each directory.
Summary
- Mutex-based detection in
Program.csuses a path-derived hash to identify existing instances and prevent duplicate UI initialization. - Named Pipe IPC in
IPCService.csenables secondary processes to forward commands—such asOP_OPEN_URL—to the primary instance. - The
OpenUrlRequestedevent bridges the IPC service with theShadowsocksControllerto handle delegated actions. - Path-hash isolation allows separate Shadowsocks-Windows installations to operate independently without Mutex collisions.
- Secondary launches either forward specific commands via
RequestOpenUrlor notify users that the application is already active.
Frequently Asked Questions
How does Shadowsocks-Windows detect if another instance is already running?
The application creates a named Mutex at startup using a string formatted as Shadowsocks_{ExecutablePath.GetHashCode()}. It then calls mutex.WaitOne(TimeSpan.Zero, true); if this returns false, another process already owns the Mutex, indicating an active instance in the same directory.
What happens when I try to open a URL with Shadowsocks already running?
The secondary process detects the existing instance and calls IPCService.RequestOpenUrl(url). This static method connects to the named pipe server running in the primary instance, transmits the OP_OPEN_URL opcode and URL payload, then terminates. The primary instance receives this via IPCService.RunServer() and processes it through the OpenUrlRequested event handler.
Can I run multiple copies of Shadowsocks-Windows simultaneously?
Yes. Because both the Mutex and Named Pipe names are derived from the executable's full path hash, copies located in different directories generate unique resource identifiers. Each installation operates as an isolated instance with its own Mutex and IPC channel.
Where is the IPC logic implemented in the source code?
The inter-process communication system is implemented in shadowsocks-csharp/Controller/Service/IPCService.cs, containing the RunServer method for the primary instance and RequestOpenUrl for secondary clients. The integration point with the application controller resides in shadowsocks-csharp/Program.cs, which initializes the service and handles the OpenUrlRequested event.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →