Skip to main content

My development PC's homepage lists all the servers I'm running

A lot of what I do on a computer involves web development. For work, I develop several projects which involve running a Django server, which I have to run locally while I'm working on them. And for client-side stuff, I always run a simple HTTP server to serve static files, because browsers apply a lot of security restrictions to pages loaded through file://.

For years, I would type in things like http://localhost:8000 into my browser's address bar, like a chump. Then one day, a lightbulb turned on and I realised that since I already have an HTTP server running on port 80, I could make its homepage be a list of links to the ports I usually run servers on.

A little while later, another, brighter lightbulb turned on and I realised that the homepage could be a script which scans every port to automatically find every server I'm running.

A webpage with two columns. The first lists "moodle" and "wordpress" under the heading "On port 80". The other has the header "Other open ports" and there are two list items. The first reads, "Directory listing for /", with port: 1535, cwd: ~/websites/checkmyworking.com, pid: 63072. The second reads, "Numbas development server", port: 8000, cwd: ~/numbas/editor, pid: 45650.

Over time I've refined it, getting it to fetch a page from each open port in order to scrape the title from the returned HTML, and listing the working directory to help remind me what a server is when the title doesn't tell me much.

This is probably a useful thing for other people who do my kind of work, so I thought I'd share it:

2023/10/index.php (Source)

<?php
    header("Access-Control-Allow-Origin: *");

    $host = gethostname();
?>
<!doctype html>
<html>
        <head>
        <title><?= $host ?>!</title>
        <style>
            body {
                display: grid;
                justify-items: center;
                list-style: none;
                grid-template-columns: auto 1fr;
            }
            ul {
                font-size: 3rem;
            }
            li ~ li {
                margin-top: 1em;
            }
            dl {
                font-size: small;
                display: grid;
            }
            dt {
                grid-row: 1;
            }
        </style>
        </head>
        <body>
        <section>
            <h2>On port 80</h2>
            <ul>
<?php
    foreach(scandir(dirname(__FILE__)) as $p) {
        if(is_dir($p) && !str_starts_with($p, ".")) {
?>
                <li><a href="http://<?= $host ?>/<?= $p ?>"><?= $p ?></a></li>
<?php
        }
    }
?>
            </ul>
        </section>

        <section>
            <h2>Other open ports</h2>
            <ul>
<?php
for($port=1000; $port < 10000; $port++) {
    $connection = @fsockopen($host, $port);

    if (is_resource($connection)) {
        fclose($connection);
        // Find the ID of the process with this port open
        $pids = explode("\n",shell_exec("lsof -i :$port -Fp"));
        foreach($pids as $line) {
            if(preg_match('/^p(\d+)/',$line, $m)) {
                $pid = $m[1];
            }
        }
        $cwd = readlink("/proc/$pid/cwd");

        $HOME = getenv('HOME');

        if(str_starts_with(substr($cwd,0,strlen($HOME)), $HOME)) {
            $cwd = substr($cwd,strlen($HOME));
        } else {
            continue;
        }



        $URL = "http://${host}:${port}";
        $ch = curl_init();
        curl_setopt_array($ch, [
            CURLOPT_FOLLOWLOCATION => true,
            CURLOPT_URL => $URL,
            CURLOPT_RETURNTRANSFER => true
        ]);
        $output = curl_exec($ch);
        curl_close($ch);

        if(!$output) {
            // Not an HTML server
            continue;
        }

        libxml_use_internal_errors(true);
        $doc = new DOMDocument();
        if(!$doc->loadHTML($output)) {
            foreach (libxml_get_errors() as $error) {
                $title = "<div>$error</div>";
            }
        } else {
            $titles = $doc->getElementsByTagName('title');
            foreach($titles as $title_node) {
                $title = $title_node->nodeValue;
            }
        }
        libxml_use_internal_errors(false);
?>
        <li>
            <a href="http://<?= $host ?>:<?= $port ?>"><?= $title ?></a>
            <dl>
                <dt>port</dt>
                <dd><?= $port ?></dd>
                <dt><abbr title="Current working directory of the process">cwd</abbr></dt>
                <dd>~<?= $cwd ?></dd>
                <dt><abbr title="Process ID">pid</abbr></dt>
                <dd><?= $pid ?></dd>
            </dl>
        </li>
<?php
    }
}
?>
            </ul>
        </section>
        </body>
</html>

It's a PHP script, which I've put in my document root, /var/www/html. It first lists directories in the document root, which are usually PHP sites, and then scans every 4-digit port number for HTTP servers.

And another thing

While I'm sharing my fancy setup, I'll tell you about my server command.

To start a local Django development server, you run python manage.py runserver. If you're running several, you'll want them to be on different ports, and in order to access it from other devices you need to give an IP address, so it ends up like python manage.py runserver 0.0.0.0:8102.

After a few years of doing this every morning, you get bored.

For static sites, I like to run Python's simple HTTP server with python -m http.server. Again, this needs to be given an IP address and a unique port number.

I'd like to use consistent port numbers for each project, so that I don't have to reconfigure things when they need to talk to each other.

I'd also usually like to automatically open the server's index page in my browser.

So, I wrote a script which lives in my ~/bin directory to start up a server with less typing and no thinking.

It looks for a file called .serverc in the current directory or any of its parents. If it doesn't find one, it runs python -m http.server on a port corresponding to a hash of the directory's path.

The .serverc can specify a command to run, a port number to use, a working directory, environment variables, and whether to open a page in the browser.

So now when I start work, I just open a terminal for each project I'm working on, run server, and I'm ready to go!

So here's my server script.

And another another thing

There's one last script that I wrote.

I use Makefiles quite a lot because many of my projects have some kind of build step.

It's no fun continually alt-tabbing between my editor, then a terminal to run make, and then the browser to see the result.

So I wrote a script which watches a directory for changes to files, and runs make when files with certain suffixes are changed.

A file called .watchmakerc determines which make targets to run, which directory to look in, and which suffixes trigger a run.

My server script runs this automatically in order to save me a line of typing.

I wrote the script in Python using the watchdog package, because that's what I was most comfortable with, but it's not great for this kind of multithreaded job. A while ago I rewrote it in Elixir as a little project to learn that language, but I'm still using my original version.

So here's watchmake.py.