Backend Development 10 min read

How to Add Real‑Time User Monitoring and Forced Disconnect to Django WebSSH

This article explains how to extend a Django Channels‑based WebSSH tool with real‑time operation monitoring, group‑based messaging, and a forced‑disconnect feature, detailing the necessary layer configuration, consumer modifications, and WebSocket message handling for both operators and observers.

Efficient Ops
Efficient Ops
Efficient Ops
How to Add Real‑Time User Monitoring and Forced Disconnect to Django WebSSH
这个功能我可以不用,但你不能没有。

Previous articles implemented WebSSH operations for physical machines, virtual machines, and Kubernetes Pods, supporting full‑session recording for later review and audit.

This article adds a seemingly flashy but essential feature: real‑time monitoring of user actions and the ability to kick users offline when needed.

Real‑time Operation View

Django Channels uses a concept called a layer to combine multiple channels into a group, allowing messages sent to the group to be received by every channel within it.

For background on Channels, see the earlier articles on implementing a chatroom and a web‑based tail‑f feature.

The original WebSSH used a single connection without a layer. To enable monitoring, we must merge the operator’s and the monitor’s channels into a group so that all operator actions are broadcast to the monitor in real time. The flow change is illustrated below:

Implementation steps (based on the previous article):

Enable the layer in

settings.py

:

<code>CHANNEL_LAYERS = {
    'default': {
        'BACKEND': 'channels_redis.core.RedisChannelLayer',
        'CONFIG': {
            "hosts": [('ops-coffee.cn', 6379)],
        },
    },
}</code>

Modify the existing

SSHConsumer

to support the layer. Key changes include creating a

group_name

during connection, adding the channel to the group, and recording connection details.

<code>class SSHConsumer(WebsocketConsumer):
    def connect(self):
        ssh_connect_args = args(self.scope)
        self.host = Host.objects.get(host=ssh_connect_args.get('host'))
        self.group_name = '%s-%s-%d' % (
            ssh_connect_args.get('host'), ssh_connect_args.get('username'), time.time())
        self.therecord = Record.objects.create(
            host=self.host,
            user=self.scope['user'],
            group=self.group_name,
            channel=self.channel_name,
            cols=ssh_connect_args.get('cols'),
            rows=ssh_connect_args.get('rows'),
            is_connecting=True
        )
        async_to_sync(self.channel_layer.group_add)(
            self.group_name,
            self.channel_name
        )
        self.accept()
        self.ssh = SSHBridge(self.therecord, websocket=self)
        self.ssh.connect(**ssh_connect_args)

    def disconnect(self, close_code):
        self.therecord.is_connecting = False
        self.therecord.save()
        async_to_sync(self.channel_layer.group_discard)(
            self.group_name,
            self.channel_name
        )
        self.ssh.close()

    def receive(self, text_data=None):
        text_data = json.loads(text_data)
        if text_data.get('flag') == 'resize':
            self.ssh.resize_pty(cols=text_data['cols'], rows=text_data['rows'])
        else:
            self.ssh.shell(data=text_data.get('data', ''))

    def ssh_message(self, event):
        self.send(text_data=json.dumps(event['message']))
</code>

During connection, a record stores host, user,

group_name

,

channel_name

, and initial terminal size, marking

is_connecting

as True. The

group_name

follows the same naming rule as the recording file defined in the earlier WebSSH recording article.

When the connection closes,

is_connecting

is set to False, allowing the front‑end to toggle between monitoring/force‑stop buttons and playback/extract‑command buttons.

Next, create a

MonitorConsumer

to handle monitoring connections. It joins the same group as the operator and only receives messages without sending any.

<code>class MonitorConsumer(WebsocketConsumer):
    def connect(self):
        pk = self.scope['url_route']['kwargs'].get('id')
        self.group_name = Record.objects.get(id=pk).group
        async_to_sync(self.channel_layer.group_add)(
            self.group_name,
            self.channel_name
        )
        self.accept()
        self.connecting = Record.objects.get(id=pk).is_connecting
        if not self.connecting:
            self.close()

    def disconnect(self, close_code):
        async_to_sync(self.channel_layer.group_discard)(
            self.group_name,
            self.channel_name
        )
        self.close()

    def receive(self, text_data=None):
        pass

    def ssh_message(self, event):
        self.send(text_data=json.dumps(event['message']))
</code>

The differences from

SSHConsumer

are: the group name is retrieved from the existing record, and the monitor does not send terminal data back to the server.

Finally, adjust

SSHBridge

to broadcast messages to the group instead of a single WebSocket:

<code>async_to_sync(self.websocket.channel_layer.group_send)(
    self.group_name,
    {
        'type': 'ssh.message',
        'message': message
    }
)</code>

With this change, all channels in the group receive the same output, enabling real‑time monitoring.

Kick User Offline

To force‑stop a user, the front‑end sends a request with the record ID to a view, which retrieves the corresponding

group_name

and sends a

disconnect

message to the group. All channels in that group then close.

<code>from asgiref.sync import async_to_sync
from channels.layers import get_channel_layer

async_to_sync(get_channel_layer().group_send)(
    Record.objects.get(id=pk).group,
    {'type': 'disconnect'}
)</code>

Demo and Explanation

All components interlock; reading the entire series will give you a solid grasp of WebSockets and Django Channels, enabling you to build a simple bastion host with powerful monitoring capabilities.

The original goal was to add WebSSH to the Alodi system for quick debugging during development, but it evolved into a feature‑rich series exploring many interesting aspects of real‑time communication.

Backend DevelopmentReal-time MonitoringwebsocketDjangouser-managementChannels
Efficient Ops
Written by

Efficient Ops

This public account is maintained by Xiaotianguo and friends, regularly publishing widely-read original technical articles. We focus on operations transformation and accompany you throughout your operations career, growing together happily.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.