User Tools

Site Tools


systemd-bpq-telnet-application

This is an old revision of the document!


In Creating a simple telnet application for BPQ , we describe how to write a simple telnet application that is started by systemd as a daemon, accepts telnet connections to a port, and reads/writes data to that port. The tutorial also covers how to add this application to BPQ as a command, and have BPQ initiate the telnet connection to the daemon.

In this tutorial, we will demonstrate an alternative approach to telnet-enabling a simple terminal based application for access via BPQ that does not require adding network code to the application - we get all of our network I/O for free courtesy of the systemd socket trigger.

Our sample application for this tutorial is a WALL command, that allows visitors to the BPQ node to view and leave simple posts on a virtual wall. The application was written by xxxxx and the source code is available here:

This is the code for our WALL application. Examine this code briefly, and note that all the I/O is simple console input/output - there is no code to bind to a network port, handle incoming connections, manage session state, deal with multiplexing, etc. Simple standard I/O is all we need.

Save this python file wherever you want it to reside on your filesystem; make sure that it is accessible by whatever account you want it to launch under. Creating, and securing, a service account for this application is beyond the scope of this tutorial.

#!/usr/bin/python3
# -*- coding: utf-8 -*-

"""
wall.py

Title: NodeWall
Description: 
Author: Daria Juniper -- juniberry@github
Created: March 2023
Version: 1.0


Changelog:
* readability improvements
* beta has been improved and cleaned up, config file added for usability
* datetime sorting bug with sqlite fix
"""

import sys
import os
import sqlite3
import datetime
import configparser

# get the directory path of the running script
script_path = os.path.dirname(os.path.abspath(__file__))

# build paths to required files
config_file_path = os.path.join(script_path, 'wall.ini')
db_file_path = os.path.join(script_path, 'wall.db')

# load config file
config = configparser.ConfigParser()
config.read(config_file_path)

# connect to the database
conn = sqlite3.connect(db_file_path)
c = conn.cursor()

# create a table for the wall messages
c.execute('''CREATE TABLE IF NOT EXISTS messages
             (id INTEGER PRIMARY KEY AUTOINCREMENT,
              callsign TEXT,
              message TEXT,
              timestamp DATETIME DEFAULT CURRENT_TIMESTAMP)''')

# get the user's callsign
callsign = input().strip()
print(config['wall']['banner'])

# show the latest entries on the wall
num_entries = config['posts'].getint('perpage')
max_message_length = config['posts'].getint('maxlen')

start_index = 0

while True:
    c.execute("SELECT COUNT(*) FROM messages")
    num_messages = c.fetchone()[0]

    if start_index <= 0:
        start_index = 0

    c.execute("SELECT * FROM messages ORDER BY timestamp DESC LIMIT ? OFFSET ?", (num_entries, start_index))
    rows = c.fetchall()
    if rows:
        print("\nPosts {0}-{1} of {2}:".format(start_index+1, start_index+len(rows), num_messages))
        for row in rows:
            date_string = datetime.datetime.strptime(row[3], '%Y-%m-%d %H:%M:%S').strftime('(%d-%b %H:%M)')
            print(date_string, "<", row[1], ">", row[2])
    else:
        print("\nNo more posts to show.")

    # present options to the user
    print("\n[P]ost a message ", end="")
    if start_index > 0:
        print("[B]ack ", end="")
    if num_messages > num_entries and start_index + num_entries < num_messages:
        print("[F]orward ", end="")
    print("[D]elete E[x]it")
    choice = input().lower().strip()

    while choice not in ("p", "b", "f", "d", "x"):
        print("Invalid command")
        print("Commands are the letters in square brackets above")
        choice = input().lower().strip()

    if choice == "p":
        # ask the user for a message
        message = input("Enter your post (limit {0} characters): ".format(max_message_length)).strip()[:max_message_length]

        if message:
            # Check if the message starts with "*** Disconnected from"
            if message.startswith("*** Disconnected from"):
                conn.close()
                sys.exit()
            # insert the message into the database
            c.execute("INSERT INTO messages (callsign, message) VALUES (?, ?)", (callsign, message))
            conn.commit()
            print("Posted to the wall.")
        else:
            print("Not posted as it was empty.")
    elif choice == "b":
        # move the start index back by num_entries
        start_index = max(0, start_index - num_entries)
    elif choice == "f":
        # move the start index forward by num_entries
        start_index = min(num_messages - num_entries, start_index + num_entries)
    elif choice == "d":
        # delete the user's latest message
        c.execute("SELECT * FROM messages WHERE callsign=? ORDER BY timestamp DESC LIMIT 1", (callsign,))
        row = c.fetchone()
        if row:
            confirm = input("Delete your latest post? (y/n): ").strip().lower()
            if confirm == "y":
                c.execute("DELETE FROM messages WHERE id=?", (row[0],))
                conn.commit()
                print("Post deleted.")
        else:
            print("You have not posted.")
    elif choice == "x":
        # cleanup and exit
        conn.close()
        print(config['wall']['exitmsg'])
        break
    else:
        print("Invalid choice.")

Create a wall.ini configuration file, and save it to the same directory as your python script:

# wall.ini
# NodeWall - Graffiti Wall for BPQ Nodes

[wall]
# Welcome banner
banner=--VE3QBZ's Wall--
# Message displayed on exit (return to node or disconnect?)
exitmsg=Returning to Node.

[posts]
# Posts per page
perpage=10
# Maximum post length
maxlen=140

Next, we need to define a systemd service template for the WALL script. This file will be named wall@.service, and the @ is important - the @ tells systemd that multiple instances of this service can be launched (ie. that it is a template for multiple service instances, not a singular service). Without the @ in the filename, systemd would only support a single connection at a time, since a second concurrent connection would not result in a second instance of the service being triggered (which might, in itself, be desired, if your particular application can only support a single session at a time for whatever reason).

Be sure to customize the path, user, and group that the service will launch as appropriate for your system.

[Unit]
Description=LinBPQ Wall Server

[Service]
ExecStart=/home/scott/station/packet/linbpq/apps/NodeWall/wall.py
StandardInput=socket
User=scott
Group=scott

Next, we define a trigger for this service template

[Unit]
Description=LinBPQ Wall

[Socket]
ListenStream=8016
Accept=yes

[Install]
WantedBy=sockets.target

These two files are placed in /etc/systemd/system, and we request systemd to reload configuration by issuing the systemctl daemon-reload command

At this point, we have accomplished the following:

  • we have a python script
  • we've defined that python script as a systemd service template, allowing multiple instances of the service to be launched concurrently, and to redirect the service's standard input to a socket
  • we've defined a systemd socket trigger, that listens on port 8016 for connections. On connection, it will launch a copy of the service template, and connect the resulting service's standard input to the socket connection that has been established

You can test that this all works by executing telnet localhost 8016 - you should get connected to the service. NOTE that WALL expects BPQ to pass your callsign in as the first line of input; to continue to test interactively, enter your callsign and hit enter - the WALL application will then continue to execute, displaying the wall contents and waiting for input.

TO BE COMPLETED: document adding the command to BPQ, but for now, you can refer to the simple telnet example linked at the top of this article.

systemd-bpq-telnet-application.1770342990.txt.gz · Last modified: by ve3qbz