Add snap support (#60)

* Initial app daemon implementation

* Pass script path to child bridge processes

* Change WebSocket server port

* Fix error sending message whilst WebSocket connection is closing

* Initial ext daemon connection implementation
This commit is contained in:
Matt Hensman
2019-05-15 11:30:30 +01:00
committed by GitHub
parent 8cb097c963
commit 474dbad1aa
20 changed files with 753 additions and 375 deletions

View File

@@ -1,6 +1,7 @@
"use strict";
import semver from "semver";
import nativeMessaging from "./nativeMessaging";
export interface BridgeInfo {
name: string;
@@ -15,7 +16,7 @@ export interface BridgeInfo {
export default async function getBridgeInfo (): Promise<BridgeInfo> {
let applicationVersion: string;
try {
applicationVersion = await browser.runtime.sendNativeMessage(
applicationVersion = await nativeMessaging.sendNativeMessage(
APPLICATION_NAME
, { subject: "bridge:/getInfo"
, data: EXTENSION_VERSION });

View File

@@ -0,0 +1,181 @@
"use strict";
import { Message } from "../types";
const WEBSOCKET_DAEMON_URL = "ws://localhost:9556";
type DisconnectListener = () => void;
type MessageListener = (message: any) => void;
function connectNative (application: string) {
/**
* In order to preserve the synchronous API, messages are
* queued before either the native messaging host or the
* WebSocket connection is ready to send data.
*/
let messageQueue: object[] = [];
/**
* Set once the native messaging host is known to be either
* present/missing. Determines whether messages go to the
* message queue.
*/
let isNativeHostStatusKnown = false;
const port = browser.runtime.connectNative(application);
let socket: WebSocket;
const onDisconnectListeners = new Set<DisconnectListener>();
const onMessageListeners = new Set<MessageListener>();
// Port proxy API
const portObject: browser.runtime.Port = {
error: null as any
, name: ""
, onDisconnect: {
addListener (cb: DisconnectListener) {
onDisconnectListeners.add(cb);
}
, removeListener (cb: DisconnectListener) {
onDisconnectListeners.delete(cb);
}
, hasListener (cb: DisconnectListener) {
return onDisconnectListeners.has(cb);
}
}
, onMessage: {
addListener (cb: MessageListener) {
onMessageListeners.add(cb);
}
, removeListener (cb: MessageListener) {
onMessageListeners.delete(cb);
}
, hasListener (cb: MessageListener) {
return onMessageListeners.has(cb);
}
// Workaround for modified types
, hasListeners () { return false; }
}
, disconnect () {
if (socket) {
socket.close();
} else {
port.disconnect();
}
}
, postMessage (message) {
if (socket) {
switch (socket.readyState) {
case WebSocket.CONNECTING: {
// Queue message until WebSocket is ready
messageQueue.push(message);
break;
}
case WebSocket.OPEN: {
socket.send(JSON.stringify(message));
break;
}
}
} else {
if (!isNativeHostStatusKnown) {
// Queue message until native messaging host is ready
messageQueue.push(message);
}
port.postMessage(message);
}
}
};
port.onDisconnect.addListener(() => {
if (port.error && !isNativeHostStatusKnown) {
isNativeHostStatusKnown = true;
socket = new WebSocket(WEBSOCKET_DAEMON_URL);
socket.addEventListener("open", ev => {
// Send all messages in queue
while (messageQueue.length) {
const message = messageQueue.pop();
socket.send(JSON.stringify(message));
}
});
socket.addEventListener("message", ev => {
for (const listener of onMessageListeners) {
listener(JSON.parse(ev.data));
}
});
socket.addEventListener("close", ev => {
if (ev.code !== 1000) {
this.error = {
// TODO: Set a proper error message
message: ""
};
}
for (const listener of onDisconnectListeners) {
listener();
}
});
}
});
port.onMessage.addListener((message: any) => {
if (!isNativeHostStatusKnown) {
isNativeHostStatusKnown = true;
messageQueue = [];
}
for (const listener of onMessageListeners) {
listener(message);
}
});
return portObject;
}
async function sendNativeMessage (
application: string
, message: any) {
try {
return await browser.runtime.sendNativeMessage(application, message);
} catch (err) {
return await new Promise((resolve, reject) => {
const ws = new WebSocket(WEBSOCKET_DAEMON_URL);
ws.addEventListener("open", () => {
ws.send(JSON.stringify(message));
});
ws.addEventListener("message", ev => {
ws.close();
resolve(JSON.parse(ev.data));
});
ws.addEventListener("error", () => {
console.error("fx_cast (Debug): No bridge application found.");
reject();
});
});
}
}
export default {
connectNative
, sendNativeMessage
};

View File

@@ -1,8 +1,11 @@
"use strict";
import semver from "semver";
import defaultOptions, { Options } from "./defaultOptions";
import getBridgeInfo from "./lib/getBridgeInfo";
import messageRouter from "./lib/messageRouter";
import nativeMessaging from "./lib/nativeMessaging";
import { getChromeUserAgent } from "./lib/userAgents";
import { getWindowCenteredProps } from "./lib/utils";
@@ -18,8 +21,6 @@ import { ReceiverStatusMessage
, ServiceDownMessage
, ServiceUpMessage } from "./messageTypes";
import semver from "semver";
const _ = browser.i18n.getMessage;
@@ -226,7 +227,7 @@ browser.menus.onShown.addListener(info => {
* Split url path into segments and add menu items for each
* partial path as the segments are removed.
*/
{
{
const pathTrimmed = url.pathname.endsWith("/")
? url.pathname.substring(0, url.pathname.length - 1)
: url.pathname;
@@ -536,11 +537,68 @@ interface Shim {
const shimMap = new Map<string, Shim>();
const statusBridge = browser.runtime.connectNative(APPLICATION_NAME);
let statusBridge: browser.runtime.Port;
const statusBridgeReceivers = new Map<string, Receiver>();
statusBridge.onMessage.addListener(async (message: Message) => {
/**
* Create status bridge, set event handlers and initialize.
*/
function initStatusBridge () {
statusBridge = nativeMessaging.connectNative(APPLICATION_NAME);
statusBridge.onDisconnect.addListener(onStatusBridgeDisconnect);
statusBridge.onMessage.addListener(onStatusBridgeMessage);
statusBridge.postMessage({
subject: "bridge:/initialize"
, data: {
mode: "status"
}
});
}
initStatusBridge();
/**
* Runs once the status bridge has disconnected. Sends
* serviceDown messages for all receivers to all shims to
* update receiver availability, then clears the receiver
* list.
*
* Attempts to reinitialize the status bridge after 10
* seconds. If it fails immediately, this handler will be
* triggered again and the timer is reset for another 10
* seconds.
*/
function onStatusBridgeDisconnect () {
// Notify shims for receiver availability
for (const [ , receiver ] of statusBridgeReceivers) {
for (const [, shim ] of shimMap) {
shim.port.postMessage({
subject: "shim:/serviceDown"
, data: { id: receiver.id }
});
}
}
// Cleanup
statusBridgeReceivers.clear();
statusBridge.onDisconnect.removeListener(onStatusBridgeDisconnect);
statusBridge.onMessage.removeListener(onStatusBridgeMessage);
statusBridge = null;
// After 10 seconds, attempt to reinitialize
window.setTimeout(() => {
initStatusBridge();
}, 10000);
}
/**
* Handles incoming status bridge messages.
*/
async function onStatusBridgeMessage (message: Message) {
switch (message.subject) {
case "shim:/serviceUp": {
const receiver = (message as ServiceUpMessage).data;
statusBridgeReceivers.set(receiver.id, receiver);
@@ -591,14 +649,7 @@ statusBridge.onMessage.addListener(async (message: Message) => {
break;
}
}
});
statusBridge.postMessage({
subject: "bridge:/initialize"
, data: {
mode: "status"
}
});
}
async function onConnectShim (port: browser.runtime.Port) {
@@ -664,7 +715,7 @@ async function onConnectShim (port: browser.runtime.Port) {
}
// Spawn bridge app instance
const bridgePort = browser.runtime.connectNative(APPLICATION_NAME);
const bridgePort = nativeMessaging.connectNative(APPLICATION_NAME);
if (bridgePort.error) {
console.error(`Failed connect to ${APPLICATION_NAME}:`

View File

@@ -1,5 +1,7 @@
"use strict";
import nativeMessaging from "../../lib/nativeMessaging";
import ReceiverSelectorManager, {
ReceiverSelectorMediaType } from "../ReceiverSelectorManager";
@@ -27,7 +29,7 @@ export default class NativeMacReceiverSelectorManager
receivers: Receiver[]
, defaultMediaType: ReceiverSelectorMediaType): Promise<void> {
this.bridgePort = browser.runtime.connectNative(APPLICATION_NAME);
this.bridgePort = nativeMessaging.connectNative(APPLICATION_NAME);
this.bridgePort.onMessage.addListener((message: Message) => {
switch (message.subject) {

View File

@@ -122,7 +122,7 @@ class PopupApp extends Component<{}, PopupAppState> {
<ReceiverEntry receiver={ receiver }
onCast={ this.onCast }
isLoading={ this.state.isLoading }
key={ i }/> )}
key={ i }/> ))}
</ul>
</div>
);