воскресенье, 21 июня 2020 г.

Управление браузером из внешнего приложения


Общие замечания


Если описывать задачу, которую мы здесь будем решать, в двух словах, то состоит она во взаимодействии настольного приложение с браузером. Мне известно о существовании Selenium WebDriver, однако, насколько я знаю, возможности его ограничиваются имитацией действий пользователя, доступом к DOM, управлением вкладками, окнами и тому подобными вещами. Мне же хотелось бы получить как можно больше полномочий, чтобы выполнять из внешнего приложения задачи, доступные только расширениям браузера. Вот, собственно, исследованием данного вопроса мы здесь и будем заниматься.
Сразу изложу идею. Поскольку изначально было заявлено, что действия, которые браузер должен будет выполнять под управлением внешней программы, то, соответственно помимо самой программы нам понадобится создать расширение для браузера. Далее нам нужно будет реализовать взаимодействие между расширением и приложением. На сегодняшний день эта задача решается предельно просто, а именно посредством веб-сокетов (WebSocket). То есть идея в том, чтобы в настольном приложении запустить сервер, принимающий подключения веб-сокетов, из расширения подключиться к этому серверу и далее через это подключение обмениваться командами и данными. Связь будет двусторонней, поэтому помимо задачи, обозначенной в заголовке, эту же модель взаимодействия можно использовать и для отправки данных приложению, и, если понадобится, то и управления приложением из расширения. Наша же основная задача состоит в том, чтобы отдать из приложения команду расширению и, если команда возвратит ответ, то получить этот ответ.

Сервер


Для создания сервера можно использовать два подхода. Во-первых, учитывая, что нам требуется минимальная функциональность, простейший сервер можно написать самостоятельно. Инструкция имеется здесь Writing a WebSocket server in C#. Во-вторых, существуют готовые решения, со всякими продвинутыми возможностями. Несмотря на то, что продвинутые возможности нам вряд ли понадобятся, лучше все-таки воспользоваться готовым решением, это сильно упростит процесс, ну и кроме того, некоторыми возможностями, может и есть смысл воспользоваться.
Непродолжительный поиск привел меня к следующему решению SuperWebSocket. Среди пакетов NuGet есть несколько от этого автора, которые реализуют веб-сокет-сервер. Я попробовал парочку, оба работают, поэтому, честно говоря, не знаю, какому отдать предпочтение. На самом деле это не так важно, учитывая, что нам не очень-то много от сервера и надо. Пакеты немного устарели, автор создал уже новую версию, но она не распространяется через NuGet, и насколько я понял, ориентирована только на .Net Core. Документация в проекте отсутствует, но автор также является автором проекта SuperSocket, который хорошо задокументирован и он пишет, что документация актуальна и для проекта SuperWebSocket  тоже. Разница там только в том, какой сервер будет создан, а интерфейсы у них общие. Таким образом за документацией мы идем сюда SuperSocket 1.6 Documentation, нас интересует именно эта версия, поскольку последняя, которая 2.0 актуальна для той версии, которой в NuGet нет и мы ее использовать не будем.

Тестовое приложение


Для начала, чтобы уже сразу проверить сервер в работе, мы создадим простое приложение, код которого возьмем из документации с небольшими изменениями и попробуем его сконнектить с простой веб-страничкой.
Создаем консольное приложение. Добавляем пакеты NuGet
SuperSocket.WebSocket и SuperSocket.Engine
В результате помимо этих двух пакетов будет добавлен пакет SuperSocket. У меня все эти три пакета имеют версию 1.6.6.1
После установки SuperSocket.Engine в проекте появится папка Config с файлами log4net.config и log4net.unix.config. Эти файлы нужно выделить, перейти к свойствам и для свойства «Действие при сборке» выбрать «Содержание», а для свойства «Копировать в выходной каталог» выбрать значение «Копировать более позднюю версию»

using System;
using SuperSocket.WebSocket;                        

namespace SSTest
{
    class Program
    {
        static void Main(string[] args)
        {
            var server = new WebSocketServer();

            Console.WriteLine("Press any key to start the server!");
            server.NewSessionConnected += Server_NewSessionConnected;
            server.NewMessageReceived += Server_NewMessageReceived;

            Console.ReadKey();
            Console.WriteLine();

            //Setup the appServer
            if (!server.Setup(212)) //Setup with listening port
            {
                Console.WriteLine("Failed to setup!");
                Console.ReadKey();
                return;
            }

            Console.WriteLine();

            //Try to start the appServer
            if (!server.Start())
            {
                Console.WriteLine("Failed to start!");
                Console.ReadKey();
                return;
            }

            Console.WriteLine("The server started successfully, press key 'q' to stop it!");
            char c;
            while ((c = Console.ReadKey().KeyChar) != 'q')
            {
                Console.WriteLine();
                continue;
            }

            //Stop the appServer
            server.Stop();

            Console.WriteLine("The server was stopped!");
            Console.ReadKey();
        }

        private static void Server_NewMessageReceived(WebSocketSession sessionstring value)
        {
            Console.WriteLine(value);
            session.Send($"Message received: <b>{value}</b>");
        }

        private static void Server_NewSessionConnected(WebSocketSession session)
        {
            session.Send("Welcome to SuperWebSocket Server");
        }
    }

}


Данный код запускает окно консоли и ждет нажатия любой клавиши, после которого запускает сервер. В примере из документации используется порт 2012, но у меня он не запускался на этом порте, видимо порт используется другой программой, поэтому  я убрал из номера ноль и получился порт 212. Но это может быть любой свободный порт. При подключении к серверу, он отправляет сообщение с приглашением. Когда сервер получает текстовое сообщение он отправляет ответ о том, что получено сообщение такого-то содержания.
Веб-страничка выглядит так
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>

<body>
<input type="text" id="msg"/>
    <button id="btn1" onclick="socket.send(msg.value);">Btn1</button>

<div id="output"></div>

    <script>
        let socket = new WebSocket("ws://127.0.0.1:212");
function print(message) {
    document.getElementById("output").insertAdjacentHTML("beforeend"`<div>${message}</div>`);
}
socket.addEventListener("open"function (evt) {
   print("Connection started"); 
});

socket.addEventListener("message"function (evt) {
    print(evt.data); 
});

socket.addEventListener("close"function (evt) {
    print("Connection closed");
    
});

    </script>
</body>

</html>

Запускать ее можно как с локалхоста, так и из файловой системы, локальные подключение блокироваться не будут. По крайней мере из файрфокса не блокируются. Страничку надо запустить после того, как запущен сервер или обновить после этого. Там появится приветствие с сервера, можно также в текстовое поле что-то написать и нажать кнопку, сообщение будет отправлено и полученный ответ вставлен в страничку. На этом тестирование можно завершить.

О расширении для браузера


Это основная часть того, что мы будем делать. Расширение мы будем писать для Firefox, весь код я проверял только на Firefox Developer Edition. Выбор этого браузера обусловлен не только тем, что я именно им пользуюсь. Дело в том, что только у него я нашел возможность загрузить расширение из файла. Остальные браузеры позволяют делать это только в режиме разработки, что не очень удобно, хотя, если надо, то это тоже не проблема. Загружать подобное расширение в маркеты, смысла не вижу, поскольку оно будет «открывать все двери» и вряд ли пройдет проверку безопасности. Но даже если и загружать в маркеты, то, насколько я знаю, регистрация в них бесплатна только опять-таки у мозиллы, а платить за то, чтобы поставить на свой браузер свое же расширение – это, на мой взгляд, как-то странно. Собственно, поэтому файрфокс.
Для создания расширения в Visual Studio я создал проект node.js, можно создать любой проект JavaScript. В нем создал папку FirefoxExtension. Для того, чтобы включить поддержку IntelliSense для расширений попробовал подключать разные пакеты, но что-то дело не пошло. В конце концов нашел вот это. Это годится в основном для Chrome, но, поскольку API расширений сейчас разные браузеры поддерживают одни и те же, за исключением некоторых нюансов, то это вполне подойдет, хотя нюансы эти надо учитывать.
Опишу пару примеров отличий. Корневое пространство имен для доступа к API в браузере Chrome называется chrome, в то время как в браузерах Firefox и Edge оно называется browser. Насколько я помню, когда разбирался в этих вопросах впервые, я написал простейшее тестовое расширение для хрома, потом запустил его в файрфоксе и оно там заработало. Так что, по всей видимости в файрфоксе доступ к корневому объекту через chrome тоже поддерживается (сейчас проверять не буду, но насколько я помню – это так). Упомянутый проект chrome.intellisense поддерживает пространство имен chrome, поэтому для того, чтобы он поддерживал и browser, я просто в файле после объявления переменной chrome, объявил переменную browser и присвоил ей chrome. Таким образом эта проблема решилась. Другой важный момент: функции API расширений в основном асинхронные, но в хроме эта модель расширений поддерживается давно, а асинхронные функции появились в языке позже и таким образом асинхронность в них достигается за счет добавления в функции коллбэка, как дополнительного параметра. В файрфоксе же поддержку этой модели расширений реализовали относительно недавно и там функции возвращают реальные промайсы. Таким образом, при использовании chrome.intellisense данное обстоятельство просто придется иметь в виду. Можно использовать что-то другое, что-то типа webextensions-polyfill и т. п., но у меня в Visual Studio задействовать это не получилось, так что ничего по этому поводу сказать не могу.

Реализация расширения


Теперь о самом расширении. Нам нужно, чтобы пользователь имел возможность подключаться к заданному им же порту и отключаться от него, когда потребуется. Таким образом в папку расширения помимо обязательного файла manifest.json добавим файлы default_popup.html и default_popup.js. Это будет всплывающее окошко, которое будет появляться, когда пользователь клацнет по иконке расширения в браузере и скрипт для него. Кроме того, не будем забывать о том, что окошко popup существует только когда оно видимо, таким образом мы не можем разместить вебсокет в нем или его скрипте, поскольку при закрытии окошка будет закрываться и соединение, а нам нужно, чтобы оно работало постоянно. Таким образом нам понадобится еще фалй background.js.
В манифесте помимо чисто описательных пунктов нам нужно будет прописать наше всплывающее окошко, бэкграунд-скрипт, а также набор разрешений. По поводу разрешений тут, по всей видимости следовало бы разрешить все, ну по крайней мере все, к чему мы хотим иметь доступ. А поскольку мы хотим его иметь ко всему то и получается, что разрешить надо все.
Я взял для примера несколько разрешений, здесь не все,  и пробовать из того, что есть, мы тоже будем не все, так что это просто пример и не более.
Это пока неполный код.
manifest.json
{
    "manifest_version"2,
    "name""Desktop interaction",
    "version""1.0.0.0",
    "author""diadiavova",
    "background": {
        "scripts": [
            "background.js"
        ]
    },
    "browser_action": {
        "default_popup""default_popup.html"
    },
    "permissions": [
        "tabs",
        "<all_urls>",
        "activeTab",
        "storage",
        "webRequest",
        "downloads",
        "cookies",
        "notifications",
        "bookmarks"
    ]
}

На странице default_popup.html разместим поле для ввода номера порта, кнопку для сохранения номера порта, чтобы не вводить каждый раз, и кнопку подключения. Последняя – это кнопка-переключатель, код взял отсюда.
default_popup.html

<!DOCTYPE html>

<html lang="en" xmlns="http://www.w3.org/1999/xhtml">

<head>
    <meta charset="utf-8" />
    <title></title>
    <style>
        body {
            width270px;
            height100px;
        }

        /* The switch - the box around the slider */
        .switch {
            positionrelative;
            displayinline-block;
            width60px;
            height34px;
            left200px;
            top40px;
        }

        /* Hide default HTML checkbox */
        .switch input {
            displaynone;
        }

        /* The slider */
        .slider {
            positionabsolute;
            cursorpointer;
            top0;
            left0;
            right0;
            bottom0;
            background-color#ccc;
            -webkit-transition.4s;
            transition.4s;
        }

        .slider:before {
            positionabsolute;
            content"";
            height26px;
            width26px;
            left4px;
            bottom4px;
            background-colorwhite;
            -webkit-transition.4s;
            transition.4s;
        }

        input:checked+.slider {
            background-color#2196F3;
        }

        input:focus+.slider {
            box-shadow0 0 1px #2196F3;
        }

        input:checked+.slider:before {
            -webkit-transformtranslateX(26px);
            -ms-transformtranslateX(26px);
            transformtranslateX(26px);
        }

        /* Rounded sliders */
        .slider.round {
            border-radius34px;
        }

        .slider.round:before {
            border-radius50%;
        }

        .portcontainer {
            margin20px;
        }
    </style>
</head>
<body>
    <div id="portcontainer">
        <span>Port: </span><input type="number" min="1" max="65535" id="numPort" /><button
            id="btnSave">Сохранить</button>
    </div>
    <label class="switch">
        <input type="checkbox" id="connectionSwitch">
        <span class="slider round"></span>
    </label>
    <script src="default_popup.js"></script>
</body>

</html>

Теперь о скриптах. При переключении выключателя popup-окне у нас должно выполняться подключение или отключение в бэкграунд-скрипте. Таким образом скрипт этого окошка должен обмениваться сообщениями с background.js. Я обычно организовываю обмен сообщениями следующим образом:
Сообщение представляет из себя объект, в котором обязательно присутствует поле cmd, содержащее имя команды. На принимающей стороне объявляю переменную commands, представляющую из себя объект, у которого имена ключей совпадают с именами команд, передаваемых в сообщениях, а значениями этих ключей будут функции, принимающие и обрабатывающие сообщения.
Итак, со стороны окошка popup нам потребуется отправлять сообщения о включении и выключении переключателя, а также запрашивать состояние подключения, последнее нам потребуется делать при открытии окошка, чтобы установить переключатель во включенное состояние, если сокет соединен с сервером. Принимать же он должен только сообщение о закрытии соединения, чтобы переключатель установить в выключенное состояние, если соединение по каким-то причинам разорвалось пока окошко было активно (например приложение закрылось или остановило сервер.
default_popup.js

document.getElementById("connectionSwitch").addEventListener("change"async function (evt)
{
    browser.runtime.sendMessage(
        evt.target.checked
            ? { "cmd": "connect""port": +document.getElementById("numPort").value }
            : { "cmd": "disconnect" });
});

document.getElementById("btnSave").addEventListener("click"function (evt)
{
    let port = document.getElementById("numPort").value;
    browser.storage.local.set({ "lastPort": port });
});

let commands = {
    "turnOff": function (options)
    {
        let chb = document.getElementById("connectionSwitch");
        if (chb.checkedchb.checked = false;
    }
}
browser.runtime.onMessage.addListener(async function (messagesendersendResponse)
{
    if (message.cmd in commands)
    {
        return await commands[message.cmd](message);
    }
});

browser.runtime.sendMessage({ "cmd": "connectionInfo" }).then(async function (data)
{
    let lastPort = (await browser.storage.local.get("lastPort")).lastPort;
    let numPort = document.getElementById("numPort");
    if (data.readyState == WebSocket.OPEN)
    {
        document.getElementById("connectionSwitch").checked = true;
        numPort.value = data.port;
    }
    else if (lastPort)
    {
        numPort.value = lastPort;
    }
});

Основная логика у нас будет реализована в background.js, поэтому о нем немного подробнее. Естественно нам нужно реализовать прием сообщений.
browser.runtime.onMessage.addListener(async function (messagesendersendResponse)
{
    if (message.cmd in commands)
    {
        let result = await commands[message.cmd](message);
        return result;
    }
});

Команды, которые мы будем принимать это connect, disconnect и connectionInfo. Создаем переменную socket.
let socket;

Кроме того, нам нужно будет добавить две функции, для подписки на события сокета и отписки от них. Когда соединение будет разрываться, мы будем отписываться от всех событий и затирать ссылку на сокет, а при подключении будем создавать новый экземпляр и подписываться на его события.
function addListeners()
{

    socket.addEventListener("open"socket_open);
    socket.addEventListener("close"socket_close);
    socket.addEventListener("error"socket_error);
    socket.addEventListener("message"socket_message);
}
function removeListeners()
{
    socket.removeEventListener("open"socket_open);
    socket.removeEventListener("close"socket_close);
    socket.removeEventListener("error"socket_error);
    socket.removeEventListener("message"socket_message);
}

function socket_open(evt)
{
    console.log("Connection started");
}
function socket_close(evt)
{
    console.log("Connection closed");
    browser.runtime.sendMessage({ "cmd": "turnOff" });
}
function socket_error(evt)
{
    console.log(evt);

}
function socket_message(evt)
{
    console.log(evt.data);
    let msg = JSON.parse(evt.data);
    if (msg.cmd in appCommands)
    {
        appCommands[msg.cmd](msg);
    }
}

В обработчиках мы просто отправляем сообщение на консоль расширения. При разрыве соединения отправляем сообщение нашему окошку popup, а вот при получении сообщения от приложения как раз и выполняется основная работа. Здесь все реализовано так же, как и при обмене сообщениями, за исключением того, что от приложения мы не будем принимать JavaScript объект, а вместо этого мы получим строку. Таким образом целесообразно организовать обмен так, чтобы этой строкой был код JSON, который легко превратить в JavaScript объект. Получив этот код, мы его парсим и только после этого выполняем ту же операцию, что и при приеме сообщений.
Теперь осталось только реализовать команды. Для команд сообщений внутри расширения все довольно просто
let commands = {
    "connect": function (options)
    {
        this.disconnect();
        try
        {
            socket = new WebSocket(`ws://127.0.0.1:${options.port}`);
            addListeners();
            return { "success": true };
        }
        catch (e)
        {
            return { "success": false"reason": e };
        }
    },
    "disconnect": function (options)
    {
        if (socket)
        {
            if (socket.readyState != WebSocket.CLOSED && socket.readyState != WebSocket.CLOSING)
            {
                removeListeners();
                socket.close();
            }
            socket = null;
        }
    },
    "connectionInfo": function (options)
    {
        if (socket)
        {
            return { "readyState": socket.readyState"port": new URL(socket.url).port }
        }
        else return { "readyState": 0 }
    }

};

А вот что делать с командами, поступающими от приложения? Здесь проблема в том, какие именно команды нам нужны. Например можно создать команду, которая будет выполнять определенный JavaScript код на активной вкладке браузера.
let appCommands = {
    "xscript": async function (msg)
    {
        socket.send(JSON.stringify(await browser.tabs.executeScript({ "code": msg.code })));
    }
}

Таким образом, если мы отправим расширению сообщение типа
{"cmd":"xscript""code""document.title"}

То оно должно будет вернуть приложению заголовок активной страницы. Но мы не можем предусмотреть таким образом все. В этой связи много команд лучше не реализовывать, поскольку это сильно усложнит использование расширения и вряд ли охватит все возможности. Лучше сделать наиболее часто используемые команды и главную команду eval, которая будет принимать код, который, в свою очередь, будет передан функции eval, затем исполнен и результат возвращен приложению.
Таким образом полный код бэкграунд-скрипта у нас будет следующим
Background.js
/**
 * @type WebSocket
 * */
let socket;

function socket_open(evt)
{
    console.log("Connection started");
}
function socket_close(evt)
{
    console.log("Connection closed");
    browser.runtime.sendMessage({ "cmd": "turnOff" });
}
function socket_error(evt)
{
    console.log(evt);

}
function socket_message(evt)
{
    console.log(evt.data);
    let msg = JSON.parse(evt.data);
    if (msg.cmd in appCommands)
    {
        appCommands[msg.cmd](msg);
    }
}

let appCommands = {
    "eval": function (msg)
    {
        let result = eval(msg.code);
        if (result.constructor == Promise)
        {
            result.then(function (data)
            {
                socket.send(JSON.stringify(data));
            },
                function (err)
                {
                    socket.send(JSON.stringify({ "error": err }));
                });
        }
        else
        {
            socket.send(JSON.stringify(result));
        }
    },
    "xscript": async function (msg)
    {
        socket.send(JSON.stringify(await browser.tabs.executeScript({ "code": msg.code })));
    }
}

<![if !supportLineBreakNewLine]>
<![endif]>
function addListeners()
{

    socket.addEventListener("open"socket_open);
    socket.addEventListener("close"socket_close);
    socket.addEventListener("error"socket_error);
    socket.addEventListener("message"socket_message);
}
function removeListeners()
{
    socket.removeEventListener("open"socket_open);
    socket.removeEventListener("close"socket_close);
    socket.removeEventListener("error"socket_error);
    socket.removeEventListener("message"socket_message);
}

function clearSocket()
{
    socket.close();
    removeListeners();
    socket = null;
}



<![if !supportLineBreakNewLine]>
<![endif]>
let commands = {
    "connect": function (options)
    {
        this.disconnect();
        try
        {
            socket = new WebSocket(`ws://127.0.0.1:${options.port}`);
            addListeners();
            return { "success": true };
        }
        catch (e)
        {
            return { "success": false"reason": e };
        }
    },
    "disconnect": function (options)
    {
        if (socket)
        {
            if (socket.readyState != WebSocket.CLOSED && socket.readyState != WebSocket.CLOSING)
            {
                removeListeners();
                socket.close();
            }
            socket = null;
        }
    },
    "connectionInfo": function (options)
    {
        if (socket)
        {
            return { "readyState": socket.readyState"port": new URL(socket.url).port }
        }
        else return { "readyState": 0 }
    }

};

browser.runtime.onMessage.addListener(async function (messagesendersendResponse)
{
    if (message.cmd in commands)
    {
        let result = await commands[message.cmd](message);
        return result;
    }
});


Если мы оставим все как есть, то при попытке выполнить команду eval мы получим сообщение системы безопасности, о том, что это запрещено. Чтобы разрешить выполнение этой функции в манифест нам нужно добавить ключ content_security_policy.
"content_security_policy""default-src 'self';script-src 'unsafe-eval' 'self'; style-src 'self' 'unsafe-inline'; connect-src ws://127.0.0.1:*"

Здесь помимо скриптов мы еще установили правила для стилей и соединений. Для стилей это нужно из-за того, что обычно по умолчанию в расширениях разрешены встроенные в страницу стили, а поскольку у нас для default-src установлено значение ‘self’, то для неуказанных параметров применяться будет именно оно, стало быть его нужно добавить. Ну и для connect-src мы указали, что будем соединяться только с локалхостом через вебсокет. Само собой можно добавить, скажем wss://127.0.0.1:*  или если мы собираемся использовать соединения с разными адресами, протоколами и т. д., то все тоже надо прописать. Но поскольку в данном случае все это вроде как не нужно, я думаю, того что есть будет достаточно.
С расширением закончили, перейдем к приложению.

Приложение


Приложение для тестов я написал на языке VB.Net, но его код настолько прост, что написание его не составить труда на любом языке. Главное мы уже сделали.
Создаем приложене WinForms, добавляем в него все те же пакеты NuGet, что и в консольном приложении, которое мы создали раенне. Так же поступаем с содержимым папки Config. После чего добавляем на форму Panel вверху, на которой мы разместим все для управления. А под ней SplitContainer с вертикальным расположением панелей. Панели контейнера зальем многострочными TextBox. Сверху tbMessageToSeng, снизу tbMessageReceived. В верхний будем вводить команду, в нижнем будут появляться ответы. На панели разместим NumericUpDowun nudPort для ввода имени порта, CheckBox с видом кнопки chbRunServer, для запуска сервера. ComboBox для выбора имени команды (у нас там будут имена eval и xscript) cbCommand. И Button btnSend для отправки сообщения.
Код формы
Imports Newtonsoft.Json
Imports SuperSocket.SocketBase
Imports SuperWebSocket

Public Class Form1

    Dim WithEvents server As New WebSocketServer()

    Private Sub chbRunServer_CheckedChanged(sender As Object, e As EventArgs) Handles chbRunServer.CheckedChanged
        Dim chb As CheckBox = sender
        If chb.Checked Then
            chb.Text = "Остановить"
            If server.Setup(nudPort.ValueThen
                server.Start()
            Else
                chb.Checked = False
            End If
        Else
            chb.Text = "Запустить"
            server.Stop()
        End If
    End Sub

    Private Sub server_NewMessageReceived(session As WebSocketSession, value As String) Handles server.NewMessageReceived
        tbMessageReceived.Invoke(Sub() tbMessageReceived.AppendText(vbCrLf & value))

    End Sub

    Private Sub btnSend_Click(sender As Object, e As EventArgs) Handles btnSend.Click
        SendMsg()
    End Sub

    Private Sub SendMsg()
        Dim msg As New Dictionary(Of String, String)()
        msg.Add("cmd", cbCommand.Text)
        msg.Add("code", tbMessageToSend.Text)

        For Each ssn In server.GetAllSessions
            ssn.Send(JsonConvert.SerializeObject(msg))
        Next
    End Sub
End Class

Логика приложения довольно проста. При изменении состояния чекбокса выполняется запуск или остановка сервера, в соответствии с этим меняется текст чекбокса (запустить или остановить). У сервера обрабатывается событие получения нового сообщения, которое отправляется в tbMessageReceived, поскольку обработчик этого события выполняется в отдельном потоке, запись в текстовое поле производится через Invoke.
Для отправки сообщения мы сначала создаем словарик с полями cmd и code, который потом будет сериализован в JSON и отправлен расширению. А вот дальше идет цикл, в котором перебираются сессии сервера и всем отправляется одно и то же сообщение. Тут дело в том, что сервер, который мы использовали, рассчитан на многопользовательские подключения. Каждое подключение создает новую сессию. Мы не использовали несколько подключений, поэтому можем оправлять одно сообщение всем сессиям в количестве одна штука, но если нужно использовать несколько подключений, то само собой, придется как-то организовать работу с сессиями.

Запускаем – проверяем


Далее в FireFox  открываем страничку about:debugging#/runtime/this-firefox, жмем «Загрузить временное дополнение», и выбираем наш файл manifest.json. Запускаем приложение. Вводим номер порта, например тот же 212, и жмем кнопку (она же чекбокс) «Запустить». После этого в браузере, где после загрузки расширения появился его значек на панели справа вверху(поскольку мы не добавляли значков, то будет отображаться стандартный файрфоксовский). Там появится наше окошко popup, в котором надо будет ввести тот же номер порта, что и в приложении, и подключиться с помощью кнопки подключения. Теперь все готово и можно приступать к тестированию функционала.
Для начала откроем в браузере какую-нибудь веб-страничку (страничка отладчика не подойдет, как и любая специальная вкладка). В приложении выберем в комбобоксе команду xscript и введем в верхнее текстовое поле
document.title
Нажмем отправить и получим в нижем поле заголовок страницы активной вкладки браузера в виде JSON массива с единственной строкой.
Можно таким же образом изменить содержимое, скажем, главного заголовка страницы (если он там есть).
document.querySelector("h1").textContent = "Новый заголовок";
Выполнив эту команду, мы увидим, что заголовок страницы изменился.
Мы можем сделать то же самое и с помощью команды eval, но тогда придется для запроса title ввести в текстовое поле команду
browser.tabs.executeScript({"code","document.title"})
Но с помощью команды eval мы также можем сделать что-нибудь не связанное со страницей. Например, если мы захотим добавить на панель закладок букмарклет, который запускает окошко alert с заголовком страницы, то можно выполнить следующий код
browser.bookmarks.create({ "title": "Show title""parentId": "toolbar_____""url": "javascript:(function(){alert(document.title)})();" })

После этого можно перейти к браузеру, и клацнуть по этому букмарклету и прочитать заголовок в алерте. А в поле ответа расширения мы увидим информацию о новой вкладке.