Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -107,4 +107,6 @@ dmypy.json
# Pyre type checker
.pyre/

# End of https://www.gitignore.io/api/python
# End of https://www.gitignore.io/api/python

.venv
2 changes: 1 addition & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,5 @@
"editor.formatOnSave": true,
"editor.defaultFormatter": "ms-python.black-formatter"
},
"flake8.enabled": false,
}

4 changes: 0 additions & 4 deletions MANIFEST.in

This file was deleted.

110 changes: 52 additions & 58 deletions readme.md → README.md
Original file line number Diff line number Diff line change
@@ -1,30 +1,34 @@
## Socket_Singleton.py
# socket_singleton

### Socket-based, single-instance Python applications with a clean interface
Socket-based, single-instance Python applications with a clean interface.

###### _Without lockfiles, mutexes, dependencies, or tomfoolery_
_Without lockfiles, mutexes, dependencies, or tomfoolery._

### Installation & Basic Usage
## Installation & Basic Usage

**Install:**

`pip install Socket_Singleton -U`
```bash
pip install Socket_Singleton -U
```

**Import:**

`from Socket_Singleton import Socket_Singleton`
```python
from socket_singleton import SocketSingleton
```

**Basic Usage:**

```python
# Simple singleton enforcement
Socket_Singleton()
SocketSingleton()
```

or, keep a reference:

```python
app = Socket_Singleton()
app = SocketSingleton()
```

**Basic Example:**
Expand All @@ -34,8 +38,8 @@ We have an application, `app.py` that we want to restrict to a single instance:
```python
#app.py

from Socket_Singleton import Socket_Singleton
Socket_Singleton()
from socket_singleton import SocketSingleton
SocketSingleton()
input() # Blocking call to simulate your_business_logic()
```

Expand All @@ -59,7 +63,7 @@ The interpreter exits immediately and we end up back at the prompt.

**See also:**

[Common TCP/UDP Port Numbers](https://en.wikipedia.org/wiki/List_of_TCP_and_UDP_port_numbers)
[Common TCP/UDP Port Numbers](https://en.wikipedia.org/wiki/List_of_TCP_and_UDP_port_numbers)
[Windows Socket Error Code 10048](https://learn.microsoft.com/en-us/windows/win32/winsock/windows-sockets-error-codes-2#WSAEADDRINUSE)

It is recommended to specify a port in the constructor\*
Expand All @@ -68,7 +72,7 @@ It is recommended to specify a port in the constructor\*

**Constructor:**

`Socket_Singleton(address="127.0.0.1", port=1337, timeout=0, client=True, strict=True, release_threshold=0, max_clients=0, verbose=False, secret=None)`
`SocketSingleton(address="127.0.0.1", port=1337, timeout=0, client=True, strict=True, release_threshold=0, max_clients=0, verbose=False, secret=None)`

### `address`

Expand All @@ -78,9 +82,9 @@ The IP address to bind the socket to. Defaults to `"127.0.0.1"` (localhost). Thi

```python
# Binds to localhost - machine-wide singleton
app = Socket_Singleton()
app = SocketSingleton()
# or explicitly:
app = Socket_Singleton(address="127.0.0.1")
app = SocketSingleton(address="127.0.0.1")
```

**Multi-homed systems example:**
Expand All @@ -93,10 +97,10 @@ On a system with multiple network interfaces, you can create separate singleton
# Interface 2: 10.0.0.50 (VPN network)

# Internal network singleton
internal_app = Socket_Singleton(address="192.168.1.100", port=1337)
internal_app = SocketSingleton(address="192.168.1.100", port=1337)

# VPN network singleton (separate instance)
vpn_app = Socket_Singleton(address="10.0.0.50", port=1337)
vpn_app = SocketSingleton(address="10.0.0.50", port=1337)

# These can coexist because they're on different interfaces!
```
Expand All @@ -107,13 +111,13 @@ In containerized environments, you might want per-container singletons:

```python
# Container A
app_a = Socket_Singleton(address="172.17.0.2", port=1337)
app_a = SocketSingleton(address="172.17.0.2", port=1337)

# Container B
app_b = Socket_Singleton(address="172.17.0.3", port=1337)
app_b = SocketSingleton(address="172.17.0.3", port=1337)

# Host machine
host_app = Socket_Singleton(address="127.0.0.1", port=1337)
host_app = SocketSingleton(address="127.0.0.1", port=1337)

# All three can run simultaneously on different addresses
```
Expand All @@ -124,7 +128,7 @@ You can bind to all available interfaces using `"0.0.0.0"`:

```python
# Binds to all network interfaces
app = Socket_Singleton(address="0.0.0.0", port=1337)
app = SocketSingleton(address="0.0.0.0", port=1337)
```

Note: For most applications, the default `127.0.0.1` (localhost) is what you want - a machine-wide singleton instance. The `address` parameter provides flexibility for specialized network configurations.
Expand All @@ -146,11 +150,11 @@ If `False`, client processes won't send arguments to the host. Defaults to `True
If `False`, raises `MultipleSingletonsError` instead of `SystemExit` when a second instance tries to run. Defaults to `True`.

```python
from Socket_Singleton import Socket_Singleton, MultipleSingletonsError
from socket_singleton import SocketSingleton, MultipleSingletonsError

def main():
try:
app = Socket_Singleton(strict=False)
app = SocketSingleton(strict=False)
except MultipleSingletonsError as err:
print("We are not the singleton.")
print(err)
Expand All @@ -169,7 +173,7 @@ Release the port after this many client connections. Defaults to `0` (never rele

```python
# Stop accepting connections after 10 clients
app = Socket_Singleton(release_threshold=10)
app = SocketSingleton(release_threshold=10)
```

### `max_clients`
Expand All @@ -178,7 +182,7 @@ Stop processing arguments after this many client connections. Defaults to `0` (p

```python
# Rate limit: ignore arguments after 5 clients, but keep accepting connections
app = Socket_Singleton(max_clients=5)
app = SocketSingleton(max_clients=5)
```

**Combined usage:**
Expand All @@ -187,7 +191,7 @@ You can use both parameters together for more complex scenarios:

```python
# Throttle arguments at 5 clients, stop accepting at 10 clients
app = Socket_Singleton(max_clients=5, release_threshold=10)
app = SocketSingleton(max_clients=5, release_threshold=10)
```

**Important:** When using both parameters together:
Expand All @@ -202,10 +206,10 @@ Enable verbose output for debugging. When `True`, prints warnings for connection

```python
# Silent operation (default)
app = Socket_Singleton()
app = SocketSingleton()

# Verbose mode - prints warnings for errors
app = Socket_Singleton(verbose=True)
app = SocketSingleton(verbose=True)
```

**When verbose mode is enabled, you'll see warnings for:**
Expand All @@ -221,39 +225,38 @@ Optional secret string for client verification. If provided, clients must send t

**Security Note:**

By default, `Socket_Singleton` accepts connections from any process that can connect to the port. This is fine for localhost-only singleton enforcement, but if you're concerned about unauthorized applications connecting and injecting arguments, you can use the `secret` parameter.
By default, `SocketSingleton` accepts connections from any process that can connect to the port. This is fine for localhost-only singleton enforcement, but if you're concerned about unauthorized applications connecting and injecting arguments, you can use the `secret` parameter.

**Basic usage:**

```python
# Host process
app = Socket_Singleton(secret="my-secret-key")
app = SocketSingleton(secret="my-secret-key")
app.trace(callback)

# Client processes (must use same secret)
Socket_Singleton(secret="my-secret-key") # Will send secret + args from the process
SocketSingleton(secret="my-secret-key") # Will send secret + args from the process
```

**Using environment variables:**

```python
import os
from Socket_Singleton import Socket_Singleton
from socket_singleton import SocketSingleton

# Read secret from environment variable
secret = os.getenv("SOCKET_SINGLETON_SECRET")
app = Socket_Singleton(secret=secret)
app = SocketSingleton(secret=secret)
```

**How it works interally:**
**How it works internally:**

- If `secret` is `None` (default): No verification - any connection is accepted by the host
- If `secret` is provided to the host: Clients must send the secret as the first part of their message over the socket (before a null byte `\x00`), followed by arguments from their process
- Invalid secrets are silently ignored (or logged if `verbose=True`)

**Important:** Both host and client processes must use the same `secret` value. If they don't match, the client's arguments will be ignored.


## Methods

### `trace(observer, *args, **kwargs)`
Expand All @@ -267,6 +270,7 @@ When you register an observer with `trace()`, you can optionally provide additio
**Observer signature:**

Your observer callback receives arguments in this order:

1. **First parameter**: A tuple containing all arguments from a single client process
2. **Followed by**: Any `*args` you provided to `trace()` (unpacked)
3. **Followed by**: Any `**kwargs` you provided to `trace()` (unpacked)
Expand All @@ -276,7 +280,7 @@ Your observer callback receives arguments in this order:
```python
#app.py

from Socket_Singleton import Socket_Singleton
from socket_singleton import SocketSingleton

def callback(client_args, prefix="Received: "):
# client_args is a tuple like ("foo", "bar", "baz")
Expand All @@ -285,7 +289,7 @@ def callback(client_args, prefix="Received: "):
# do_a_thing(client_args)

def main():
app = Socket_Singleton()
app = SocketSingleton()
app.trace(callback, prefix=">>> ") # Store "prefix" to be passed later
input() # Blocking call to simulate your_business_logic()

Expand Down Expand Up @@ -352,10 +356,10 @@ Release the port, allowing other instances to bind. Stops the server thread, can
```python
#app.py

from Socket_Singleton import Socket_Singleton
from socket_singleton import SocketSingleton

def main():
app = Socket_Singleton()
app = SocketSingleton()
# Do some work...
app.release() # Release the port, allowing other instances to run
print("Port released - other instances can now bind!")
Expand Down Expand Up @@ -387,7 +391,6 @@ And in a new shell (after `release()` was called):
- **Context manager alternative**: For most use cases, the context manager protocol (see below) is cleaner and automatically handles cleanup.
- **Timer cancellation**: If a timeout was set, calling `release()` will cancel it prematurely.


## Properties

### `arguments`
Expand All @@ -407,63 +410,54 @@ An integer property describing how many client processes have connected since in
print(f"Connected clients: {app.clients}")
```


## Context Manager

The context manager protocol is implemented for automatic resource cleanup:

```python
with Socket_Singleton():
with SocketSingleton():
input() # Blocking call to simulate your_business_logic()
```

`Socket_Singleton.__enter__()` returns `self` so you can have access to the object if needed:
`SocketSingleton.__enter__()` returns `self` so you can have access to the object if needed:

```python
with Socket_Singleton() as ss:
with SocketSingleton() as ss:
ss.trace(callback)
input() # Blocking call to simulate your_business_logic()
```

The port is automatically released when exiting the `with` block.


## Testing

The project includes a comprehensive test suite using Python's built-in `unittest` framework.
The project includes a comprehensive test suite using `pytest`.

**Run all tests:**

```bash
python -m unittest tests
```

**Run tests with verbose output:**

```bash
python -m unittest -v tests
pytest
```

**Run a specific test class:**

```bash
python -m unittest tests.TestInProcess
python -m unittest tests.TestArgumentPassing
python -m unittest tests.TestConcurrency
pytest tests/test_socket_singleton.py::TestInProcess
```

**Run a specific test method:**

```bash
python -m unittest tests.TestArgumentPassing.test_multiple_observers
pytest tests/test_socket_singleton.py::TestInProcess::test_arguments_with_newlines
```

**Test structure:**

- `tests.py` - Main test suite with organized test classes
- `test_app.py` - Helper script for subprocess-based tests
- `test_socket_singleton.py` - Main test suite with organized test classes
- `helper_app.py` - Helper script for subprocess-based tests

Tests are organized by concern:

- **TestInProcess**: Fast in-process tests (properties, trace/untrace, context manager)
- **TestSingletonEnforcement**: Singleton behavior requiring separate processes
- **TestArgumentPassing**: Argument passing between processes
Expand Down
29 changes: 0 additions & 29 deletions publish.bat

This file was deleted.

Loading