diff --git a/chrome.go b/extension.go similarity index 64% rename from chrome.go rename to extension.go index cbfd58b..1ce80fb 100644 --- a/chrome.go +++ b/extension.go @@ -91,23 +91,23 @@ func handleRun(w http.ResponseWriter, r *http.Request) { } } -type chromeExtInstall struct{} +type extensionSetup struct{} -func (c *chromeExtInstall) Spec() cli.CommandSpec { +func (e *extensionSetup) Spec() cli.CommandSpec { return cli.CommandSpec{ - Name: "install-for-chrome-ext", - Desc: `Installs the chrome native message host manifest. -This allows the sail chrome extension to manage sail.`, + Name: "setup-extension", + Desc: `Installs the extension API native message host manifest. +This allows the sail extension to manage sail.`, } } -func (c *chromeExtInstall) Run(fl *flag.FlagSet) { - nativeHostDirs, err := nativeMessageHostManifestDirectories() +func (e *extensionSetup) Run(fl *flag.FlagSet) { + nativeHostDirsChrome, err := nativeMessageHostManifestDirectoriesChrome() if err != nil { flog.Fatal("failed to get native message host manifest directory: %v", err) } - for _, dir := range nativeHostDirs { + for _, dir := range nativeHostDirsChrome { if dir == "" { continue } @@ -116,14 +116,34 @@ func (c *chromeExtInstall) Run(fl *flag.FlagSet) { if err != nil { flog.Fatal("failed to ensure manifest directory exists: %v", err) } - err = writeNativeHostManifest(dir) + err = writeNativeHostManifestChrome(dir) + if err != nil { + flog.Fatal("failed to write native messaging host manifest: %v", err) + } + } + + nativeHostDirsFirefox, err := nativeMessageHostManifestDirectoriesFirefox() + if err != nil { + flog.Fatal("failed to get native message host manifest directory: %v", err) + } + + for _, dir := range nativeHostDirsFirefox { + if dir == "" { + continue + } + + err = os.MkdirAll(dir, 0755) + if err != nil { + flog.Fatal("failed to ensure manifest directory exists: %v", err) + } + err = writeNativeHostManifestFirefox(dir) if err != nil { flog.Fatal("failed to write native messaging host manifest: %v", err) } } } -func writeNativeHostManifest(dir string) error { +func writeNativeHostManifestChrome(dir string) error { binPath, err := os.Executable() if err != nil { return err @@ -143,7 +163,27 @@ func writeNativeHostManifest(dir string) error { return ioutil.WriteFile(dst, []byte(manifest), 0644) } -func nativeMessageHostManifestDirectories() ([]string, error) { +func writeNativeHostManifestFirefox(dir string) error { + binPath, err := os.Executable() + if err != nil { + return err + } + + manifest := fmt.Sprintf(`{ + "name": "com.coder.sail", + "description": "sail message host", + "path": "%v", + "type": "stdio", + "allowed_extensions": [ + "sail@coder.com" + ] + }`, binPath) + + dst := path.Join(dir, "com.coder.sail.json") + return ioutil.WriteFile(dst, []byte(manifest), 0644) +} + +func nativeMessageHostManifestDirectoriesChrome() ([]string, error) { homeDir, err := os.UserHomeDir() if err != nil { return nil, xerrors.Errorf("failed to get user home dir: %w", err) @@ -171,9 +211,31 @@ func nativeMessageHostManifestDirectories() ([]string, error) { return []string{ chromeDir, - chromiumDir, chromeBetaDir, chromeDevDir, chromeCanaryDir, + chromiumDir, + }, nil +} + +func nativeMessageHostManifestDirectoriesFirefox() ([]string, error) { + homeDir, err := os.UserHomeDir() + if err != nil { + return nil, xerrors.Errorf("failed to get user home dir: %w", err) + } + + var firefoxDir string + + switch runtime.GOOS { + case "linux": + firefoxDir = path.Join(homeDir, ".mozilla", "native-messaging-hosts") + case "darwin": + firefoxDir = path.Join(homeDir, "Library", "Application Support", "Mozilla", "NativeMessagingHosts") + default: + return nil, xerrors.Errorf("unsupported os %q", runtime.GOOS) + } + + return []string{ + firefoxDir, }, nil } diff --git a/extension/logo128.png b/extension/logo128.png new file mode 100644 index 0000000..308d886 Binary files /dev/null and b/extension/logo128.png differ diff --git a/extension/manifest.json b/extension/manifest.json index fbc3d59..0f77799 100644 --- a/extension/manifest.json +++ b/extension/manifest.json @@ -1,9 +1,8 @@ { "manifest_version": 2, - "name": "Sail", - "version": "1.0.5", - "author": "Coder", + "version": "1.0.8", + "author": "Coder", "description": "Work in immutable, pre-configured development environments.", "background": { @@ -28,5 +27,14 @@ "browser_action": { "default_title": "Sail", "default_popup": "out/popup.html" - } + }, + "icons": { + "128": "logo128.png" + }, + "browser_specific_settings": { + "gecko": { + "id": "sail@coder.com", + "strict_min_version": "48.0" + } + } } \ No newline at end of file diff --git a/extension/src/background.ts b/extension/src/background.ts index a11cf67..2ceb126 100644 --- a/extension/src/background.ts +++ b/extension/src/background.ts @@ -1,4 +1,4 @@ -import { ExtensionMessage } from "./common"; +import { ExtensionMessage, SocketMessage } from "./common"; export class SailConnector { private port: chrome.runtime.Port; @@ -60,3 +60,28 @@ chrome.runtime.onMessage.addListener((data: ExtensionMessage, sender, sendRespon return true; } }); + +chrome.runtime.onConnect.addListener((port) => { + let socket: WebSocket | null; + + port.onMessage.addListener((message: SocketMessage) => { + switch (message.type) { + case "init": + socket = new WebSocket(message.data); + + socket.addEventListener("open", () => port.postMessage({ type: "open" } as SocketMessage)) + socket.addEventListener("close", e => port.disconnect()) + socket.addEventListener("message", e => port.postMessage({ type: "message", data: e.data } as SocketMessage)) + break; + case "message": + if (socket) { + socket.send(message.data) + } + break; + default: + throw new Error('unknown message type: ' + message.type); + } + }) + + port.postMessage({ type: "init" } as SocketMessage) +}) \ No newline at end of file diff --git a/extension/src/common.ts b/extension/src/common.ts index a2ce071..6845426 100644 --- a/extension/src/common.ts +++ b/extension/src/common.ts @@ -4,6 +4,11 @@ export interface ExtensionMessage { readonly url?: string; } +export interface SocketMessage { + readonly type: "init" | "open" | "message"; + readonly data?: string; +} + export const requestSail = (): Promise => { return new Promise((resolve, reject) => { chrome.runtime.sendMessage({ diff --git a/extension/src/content.ts b/extension/src/content.ts index 80f1163..74545be 100644 --- a/extension/src/content.ts +++ b/extension/src/content.ts @@ -1,40 +1,43 @@ -import { requestSail } from "./common"; +import { requestSail, SocketMessage } from "./common"; const doConnection = (socketUrl: string, projectUrl: string, onMessage: (data: { readonly type: "data" | "error"; readonly v: string; -}) => void): Promise => { - return new Promise((resolve, reject) => { - const socket = new WebSocket(socketUrl); - socket.addEventListener("open", () => { - socket.send(JSON.stringify({ - project: projectUrl, - })); - - resolve(socket); - }); - socket.addEventListener("close", (event) => { - reject(`socket closed: ${event.code}`); - }); - - socket.addEventListener("message", (event) => { - const data = JSON.parse(event.data); - if (!data) { - return; - } - const type = data.type; - const content = atob(data.v); - - switch (type) { - case "data": - case "error": - onMessage({ type, v: content }); - break; - default: - throw new Error("unknown message type: " + type); - } - }); - }); +}) => void): Promise => { + return new Promise((resolve, reject) => { + const port = chrome.runtime.connect(); + + port.onMessage.addListener((message: SocketMessage) => { + switch (message.type) { + case 'init': + port.postMessage({ type: 'init', data: socketUrl } as SocketMessage); + break; + case 'open': + port.postMessage({ type: 'message', data: JSON.stringify({ project: projectUrl }) } as SocketMessage); + resolve(port); + break; + case 'message': + const data = JSON.parse(message.data); + if (!data) { + return; + } + const type = data.type; + const content = atob(data.v); + + switch (type) { + case 'data': + case 'error': + onMessage({ type, v: content }); + break; + default: + throw new Error('unknown message type: ' + type); + } + break; + default: + throw new Error('unknown message type: ' + message.type); + } + }); + }); }; const ensureButton = (): void | HTMLElement => { @@ -123,8 +126,8 @@ const ensureButton = (): void | HTMLElement => { term.scrollTop = term.scrollHeight; } }); - }).then((socket) => { - socket.addEventListener("close", () => { + }).then((port) => { + port.onDisconnect.addListener(() => { btn.innerText = "Open in Sail"; btn.classList.remove("disabled"); term.remove(); diff --git a/extension/src/popup.html b/extension/src/popup.html index 79a83ae..95eb16d 100644 --- a/extension/src/popup.html +++ b/extension/src/popup.html @@ -1,7 +1,17 @@ - -
    - - + + + + + + + After installing sail, run `sail setup-extension`. + diff --git a/extension/src/popup.ts b/extension/src/popup.ts index 24a53d2..3fff916 100644 --- a/extension/src/popup.ts +++ b/extension/src/popup.ts @@ -1,16 +1,12 @@ import { requestSail } from "./common"; -const root = document.getElementById("root") as HTMLElement; -// const projects = document.getElementById("projects") as HTMLUListElement; -document.body.style.width = "150px"; - requestSail().then((url) => { document.body.innerText = "Sail is setup and working properly!"; }).catch((ex) => { const has = (str: string) => ex.toString().indexOf(str) !== -1; if (has("not found")) { - document.body.innerText = "After installing sail, run `sail install-for-chrome-ext`."; + document.body.innerText = "After installing sail, run `sail setup-extension`."; } else { document.body.innerText = ex.toString(); } diff --git a/main.go b/main.go index 0423173..5846776 100644 --- a/main.go +++ b/main.go @@ -65,7 +65,7 @@ func (r rootCmd) Subcommands() []cli.Command { &lscmd{}, &rmcmd{gf: &r.globalFlags}, &proxycmd{}, - &chromeExtInstall{}, + &extensionSetup{}, &versioncmd{}, } } @@ -74,7 +74,8 @@ func main() { root := &rootCmd{} if (len(os.Args) >= 2 && strings.HasPrefix(os.Args[1], "chrome-extension://")) || - (len(os.Args) >= 3 && strings.HasPrefix(os.Args[2], "chrome-extension://")) { + (len(os.Args) >= 3 && strings.HasPrefix(os.Args[2], "chrome-extension://")) || + (len(os.Args) >= 2 && strings.HasSuffix(os.Args[1], "com.coder.sail.json")) { runNativeMsgHost() return }