mirror of
https://github.com/hensm/fx_cast.git
synced 2026-06-11 10:09:59 +00:00
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:
@@ -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 });
|
||||
|
||||
181
ext/src/lib/nativeMessaging.ts
Normal file
181
ext/src/lib/nativeMessaging.ts
Normal 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
|
||||
};
|
||||
@@ -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}:`
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user