Skip to content

Commit

Permalink
feat(client/windows): introduce go plugin to call fetchResource as …
Browse files Browse the repository at this point in the history
…a library (#2263)

This PR introduces a new mechanism for Electron's renderer process to directly call Go functions.
  • Loading branch information
jyyi1 authored Nov 19, 2024
1 parent ba3cfa0 commit 769fc25
Show file tree
Hide file tree
Showing 13 changed files with 312 additions and 59 deletions.
5 changes: 5 additions & 0 deletions .github/workflows/build_and_test_debug_client.yml
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,11 @@ jobs:
with:
go-version-file: '${{ github.workspace }}/go.mod'

- name: Install zig
uses: mlugg/setup-zig@v1
with:
version: 0.13.0

- name: Build Windows Client
run: npm run action client/electron/build windows

Expand Down
21 changes: 16 additions & 5 deletions client/electron/app_paths.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import * as path from 'path';

import {app} from 'electron';

const isWindows = os.platform() === 'win32';
const IS_WINDOWS = os.platform() === 'win32';

/**
* Get the unpacked asar folder path.
Expand All @@ -39,7 +39,7 @@ function unpackedAppPath() {
*/
export function getAppPath() {
const electronAppPath = app.getAppPath();
if (isWindows && electronAppPath.includes('app.asar')) {
if (IS_WINDOWS && electronAppPath.includes('app.asar')) {
return path.dirname(app.getPath('exe'));
}
return electronAppPath;
Expand All @@ -51,8 +51,19 @@ export function pathToEmbeddedTun2socksBinary() {
'client',
'output',
'build',
isWindows ? 'windows' : 'linux',
'tun2socks' + (isWindows ? '.exe' : '')
IS_WINDOWS ? 'windows' : 'linux',
'tun2socks' + (IS_WINDOWS ? '.exe' : '')
);
}

export function pathToBackendLibrary() {
return path.join(
unpackedAppPath(),
'client',
'output',
'build',
IS_WINDOWS ? 'windows' : 'linux',
IS_WINDOWS ? 'backend.dll' : 'libbackend.so'
);
}

Expand All @@ -63,7 +74,7 @@ export function pathToEmbeddedTun2socksBinary() {
* @returns A string representing the path of the directory that contains service binaries.
*/
export function pathToEmbeddedOutlineService() {
if (isWindows) {
if (IS_WINDOWS) {
return getAppPath();
}
return path.join(
Expand Down
6 changes: 4 additions & 2 deletions client/electron/electron-builder.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@
"files": [
"client/electron/linux_proxy_controller/dist",
"client/electron/icons/png",
"client/output/build/linux"
"client/output/build/linux",
"!client/output/build/linux/*.h"
],
"icon": "client/electron/icons/png",
"maintainer": "Jigsaw LLC",
Expand All @@ -51,7 +52,8 @@
},
"win": {
"files": [
"client/output/build/windows"
"client/output/build/windows",
"!client/output/build/windows/*.h"
],
"icon": "client/electron/icons/win/icon.ico",
"sign": "client/electron/windows/electron_builder_signing_plugin.cjs",
Expand Down
19 changes: 0 additions & 19 deletions client/electron/go_helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,22 +57,3 @@ export async function checkUDPConnectivity(
}
return true;
}

/**
* Fetches a resource from the given URL.
*
* @param url The URL of the resource to fetch.
* @param debugMode Optional. Whether to forward logs to stdout. Defaults to false.
* @returns A Promise that resolves to the fetched content as a string.
* @throws ProcessTerminatedExitCodeError if tun2socks failed to run.
*/
export function fetchResource(
url: string,
debugMode: boolean = false
): Promise<string> {
const tun2socks = new ChildProcessHelper(pathToEmbeddedTun2socksBinary());
tun2socks.isDebugModeEnabled = debugMode;

console.debug('[tun2socks] - fetching resource ...');
return tun2socks.launch(['-fetchUrl', url]);
}
68 changes: 68 additions & 0 deletions client/electron/go_plugin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
// Copyright 2024 The Outline Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

import {promisify} from 'node:util';

import koffi from 'koffi';

import {pathToBackendLibrary} from './app_paths';

let invokeGoAPIFunc: Function | undefined;

export type GoApiName = 'FetchResource';

/**
* Calls a Go function by invoking the `InvokeGoAPI` function in the native backend library.
*
* @param api The name of the Go API to invoke.
* @param input The input string to pass to the API.
* @returns A Promise that resolves to the output string returned by the API.
* @throws An Error containing PlatformError details if the API call fails.
*
* @remarks
* Ensure that the function signature and data structures are consistent with the C definitions
* in `./client/go/outline/electron/go_plugin.go`.
*/
export async function invokeGoApi(
api: GoApiName,
input: string
): Promise<string> {
if (!invokeGoAPIFunc) {
const backendLib = koffi.load(pathToBackendLibrary());

// Define C strings and setup auto release
const cgoString = koffi.disposable(
'CGoAutoReleaseString',
'str',
backendLib.func('FreeCGoString', 'void', ['str'])
);

// Define InvokeGoAPI data structures and function
const invokeGoApiResult = koffi.struct('InvokeGoAPIResult', {
Output: cgoString,
ErrorJson: cgoString,
});
invokeGoAPIFunc = promisify(
backendLib.func('InvokeGoAPI', invokeGoApiResult, ['str', 'str']).async
);
}

console.debug('[Backend] - calling InvokeGoAPI ...');
const result = await invokeGoAPIFunc(api, input);
console.debug('[Backend] - InvokeGoAPI returned', result);
if (result.ErrorJson) {
throw Error(result.ErrorJson);
}
return result.Output;
}
17 changes: 13 additions & 4 deletions client/electron/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ import {
import {autoUpdater} from 'electron-updater';

import {lookupIp} from './connectivity';
import {fetchResource} from './go_helpers';
import {GoApiName, invokeGoApi} from './go_plugin';
import {GoVpnTunnel} from './go_vpn_tunnel';
import {installRoutingServices, RoutingDaemon} from './routing_service';
import {TunnelStore} from './tunnel_store';
Expand Down Expand Up @@ -499,10 +499,19 @@ function main() {
mainWindow?.webContents.send('outline-ipc-push-clipboard');
});

// Fetches a resource (usually the dynamic key config) from a remote URL.
// This IPC handler allows the renderer process to call Go API functions exposed by the backend.
// It takes two arguments:
// - api: The name of the Go API function to call.
// - input: A string representing the input data to the Go function.
//
// The handler returns the output string from the Go function if successful.
// Both the input string and output string need to be interpreted by the renderer process according
// to the specific API being called.
// If Go function encounters an error, it throws an Error that can be parsed by the `PlatformError`.
ipcMain.handle(
'outline-ipc-fetch-resource',
async (_, url: string): Promise<string> => fetchResource(url, debugMode)
'outline-ipc-invoke-go-api',
(_, api: GoApiName, input: string): Promise<string> =>
invokeGoApi(api, input)
);

// Connects to a proxy server specified by a config.
Expand Down
5 changes: 4 additions & 1 deletion client/electron/webpack_electron_main.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ import {fileURLToPath} from 'url';

import webpack from 'webpack';


const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

Expand All @@ -37,6 +36,10 @@ export default ({sentryDsn, appVersion}) => [
use: 'ts-loader',
exclude: /node_modules/,
},
{
test: /\.node$/,
use: 'node-loader',
},
],
},
resolve: {
Expand Down
35 changes: 24 additions & 11 deletions client/go/Taskfile.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,32 +25,45 @@ vars:

tasks:
electron:
desc: "Build the tun2socks binary for Electron platforms"
desc: "Build the tun2socks binary and library for Electron platforms"
internal: true
requires: {vars: [TARGET_OS]}
requires: {vars: [TARGET_OS, TARGET_ARCH]}
vars:
# TODO(fortuna): remove this, it's not really needed since we don't release it separately anymore.
TUN2SOCKS_VERSION: "v1.16.11"
OUTPUT: '{{.OUT_DIR}}/{{.TARGET_OS}}/tun2socks{{if eq .TARGET_OS "windows"}}.exe{{end}}'
# Linux=libbackend.so; Windows=backend.dll
OUTPUT_LIB: '{{.OUT_DIR}}/{{.TARGET_OS}}/{{if eq .TARGET_OS "linux"}}libbackend.so{{else}}backend.dll{{end}}'
cmds:
- rm -rf "{{dir .OUTPUT}}" && mkdir -p "{{dir .OUTPUT}}"
# C cross-compile (zig) targets:
# Linux (x64) = x86_64-linux-gnu (`x86_64-linux` defaults to `x86_64-linux-musl`, prefer `gnu` over `musl`)
# Windows (x86) = x86-windows
# Linker flags: https://pkg.go.dev/cmd/link
# -s Omit the symbol table and debug information.
# -w Omit the DWARF symbol table.
# -X Set the value of the string variable.
# -s Omit the symbol table and debug information.
# -w Omit the DWARF symbol table.
# -X Set the value of the string variable.
- |
{{if ne OS .TARGET_OS -}}
GOOS={{.TARGET_OS}} GOARCH=amd64 CGO_ENABLED=1 CC='zig cc -target x86_64-{{.TARGET_OS}}'
{{if or (ne OS .TARGET_OS) (ne ARCH .TARGET_ARCH) -}}
GOOS={{.TARGET_OS}} GOARCH={{.TARGET_ARCH}} CGO_ENABLED=1 \
CC='zig cc -target {{if eq .TARGET_ARCH "386"}}x86{{else}}x86_64{{end}}-{{.TARGET_OS}}{{if eq .TARGET_OS "linux"}}-gnu{{end}}'
{{- end}} \
go build -trimpath -ldflags="-s -w -X=main.version={{.TUN2SOCKS_VERSION}}" -o '{{.OUTPUT}}' '{{.TASKFILE_DIR}}/outline/electron'
- |
{{if or (ne OS .TARGET_OS) (ne ARCH .TARGET_ARCH) -}}
GOOS={{.TARGET_OS}} GOARCH={{.TARGET_ARCH}} CGO_ENABLED=1 \
CC='zig cc -target {{if eq .TARGET_ARCH "386"}}x86{{else}}x86_64{{end}}-{{.TARGET_OS}}{{if eq .TARGET_OS "linux"}}-gnu{{end}}'
{{- end}} \
go build -trimpath -buildmode=c-shared -ldflags="-s -w -X=main.version={{.TUN2SOCKS_VERSION}}" -o '{{.OUTPUT_LIB}}' '{{.TASKFILE_DIR}}/outline/electron'
windows:
desc: "Build the tun2socks binary for Windows"
cmds: [{task: electron, vars: {TARGET_OS: "windows"}}]
desc: "Build the tun2socks binary and library for Windows"
# 32bit Windows 10 still exists until October 14, 2025
cmds: [{task: electron, vars: {TARGET_OS: "windows", TARGET_ARCH: "386"}}]

linux:
desc: "Build the tun2socks binary for Linux"
cmds: [{task: electron, vars: {TARGET_OS: "linux"}}]
desc: "Build the tun2socks binary and library for Linux"
cmds: [{task: electron, vars: {TARGET_OS: "linux", TARGET_ARCH: "amd64"}}]

android:
desc: "Build the tun2socks.aar library for Android"
Expand Down
Loading

0 comments on commit 769fc25

Please sign in to comment.