diff --git a/client/electron/go_helpers.ts b/client/electron/go_helpers.ts index fb69cb2ac9..5efd0b43ee 100644 --- a/client/electron/go_helpers.ts +++ b/client/electron/go_helpers.ts @@ -63,19 +63,15 @@ export async function checkUDPConnectivity( * 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 async function fetchResource( - url: string, - debugMode: boolean = false -): Promise { +export async function fetchResource(url: string): Promise { console.debug('[tun2socks] - preparing library calls ...'); const result = await goFetchResource(url); console.debug('[tun2socks] - result: ', result); if (result.Error) { - throw new Error(`Returned error handle: ${result.Error}`); + throw new Error(result.Error.DetailJson ?? result.Error.Code); } return result.Content; } diff --git a/client/electron/go_plugin.ts b/client/electron/go_plugin.ts index 352652d0f6..379d16bd96 100644 --- a/client/electron/go_plugin.ts +++ b/client/electron/go_plugin.ts @@ -12,46 +12,100 @@ // See the License for the specific language governing permissions and // limitations under the License. +/** + * @file This file declares data structures and loads the functions exported by the backend C library + * (e.g., `libbackend.so` on Linux). + * + * The C type definitions can be found in the Go code or the header file generated by the Go compiler + * (e.g., `client/output/build/linux/libbackend.h` for the Linux target). + */ + import {promisify} from 'node:util'; + import koffi from 'koffi'; + import {pathToBackendLibrary} from './app_paths'; -export type GoPlatformErrorHandle = number; +/** Corresponds to the `PlatformError` type defined in the C library. */ +export interface GoPlatformError { + Code: string; + DetailJson: string; +} +/** Corresponds to the `FetchResourceResult` type defined in the C library. */ export interface GoFetchResourceResult { Content: string; - Error: GoPlatformErrorHandle; + Error?: GoPlatformError; } -var goFetchResourceFunc: koffi.KoffiFunction | undefined; +let goFetchResourceFunc: Function | undefined; -export function goFetchResource(url: string): Promise { +/** Corresponds to the `FetchResource` function defined in the C library. */ +export async function goFetchResource( + url: string +): Promise { if (!goFetchResourceFunc) { - const lib = ensureBackendLibraryLoaded(); - const resultStruct = koffi.struct('FetchResourceResult', { - Content: 'CStr', - Error: 'GoPlatformErrorHandle', - }); - goFetchResourceFunc = lib.func('FetchResource', resultStruct, ['str']); + const fetchFunc = ensureBackendLibraryLoaded().func( + 'FetchResource', + koffi.struct('FetchResourceResult', { + Content: cgoString!, + Error: koffi.out(koffi.pointer(cgoPlatformErrorStruct!)), + }), + ['str'] + ); + goFetchResourceFunc = promisify(fetchFunc.async); } - return promisify(goFetchResourceFunc.async)(url); + const result = await goFetchResourceFunc(url); + result.Error = await decodeCGoPlatformErrorPtr(result.Error); + return result; } -var backendLib: koffi.IKoffiLib | undefined; +let backendLib: koffi.IKoffiLib | undefined; +let cgoString: koffi.IKoffiCType | undefined; +let cgoPlatformErrorStruct: koffi.IKoffiCType | undefined; +let freeCGoPlatformErrorFunc: Function | undefined; +/** + * Ensures the backend library is loaded and initializes the common data structures. + * It also sets up the auto-release for the pointer types. + * + * @returns The loaded backend library instance. + */ function ensureBackendLibraryLoaded(): koffi.IKoffiLib { if (!backendLib) { backendLib = koffi.load(pathToBackendLibrary()); - defineCommonFunctions(backendLib); + + // Define C strings and setup auto release + cgoString = koffi.disposable( + 'CGoAutoReleaseString', + 'str', + backendLib.func('FreeCGoString', 'void', ['str']) + ); + + // Define PlatformError pointers and release function + cgoPlatformErrorStruct = koffi.struct('PlatformError', { + Code: cgoString, + DetailJson: cgoString, + }); + freeCGoPlatformErrorFunc = promisify( + backendLib.func('FreeCGoPlatformError', 'void', [ + koffi.pointer(cgoPlatformErrorStruct), + ]).async + ); } return backendLib; } -var goStr: koffi.IKoffiCType | undefined; -var goFreeString: koffi.KoffiFunction | undefined; - -function defineCommonFunctions(lib: koffi.IKoffiLib) { - goFreeString = lib.func('FreeString', koffi.types.void, [koffi.types.str]); - goStr = koffi.disposable('CStr', koffi.types.str, goFreeString); - koffi.alias('GoPlatformErrorHandle', koffi.types.uintptr_t); +/** Decode a `PlatformError*` to a TypeScript structure, and release the pointer. */ +async function decodeCGoPlatformErrorPtr( + ptr: unknown +): Promise { + if (!ptr) { + return null; + } + try { + return koffi.decode(ptr, cgoPlatformErrorStruct!) as GoPlatformError; + } finally { + await freeCGoPlatformErrorFunc!(ptr); + } } diff --git a/client/electron/index.ts b/client/electron/index.ts index bf93f1d71b..59916f89dc 100644 --- a/client/electron/index.ts +++ b/client/electron/index.ts @@ -504,7 +504,7 @@ function main() { // Fetches a resource (usually the dynamic key config) from a remote URL. ipcMain.handle( 'outline-ipc-fetch-resource', - async (_, url: string): Promise => fetchResource(url, debugMode) + (_, url: string): Promise => fetchResource(url) ); // Connects to a proxy server specified by a config. diff --git a/client/go/outline/electron/cstring.go b/client/go/outline/electron/cstring.go index 67af47c632..2096752f04 100644 --- a/client/go/outline/electron/cstring.go +++ b/client/go/outline/electron/cstring.go @@ -14,13 +14,26 @@ package main -/* -#include -*/ +// #include import "C" -import "unsafe" +import ( + "log/slog" + "unsafe" +) -//export FreeString -func FreeString(str *C.char) { - C.free(unsafe.Pointer(str)) +// NewCGoString allocates memory for a C string based on the given Go string. +// It should be paired with [FreeCGoString] to avoid memory leaks. +func NewCGoString(s string) *C.char { + res := C.CString(s) + slog.Debug("malloc CGoString", "addr", res) + return res +} + +// FreeCGoString releases the memory allocated by NewCGoString. +// It also accepts null. +// +//export FreeCGoString +func FreeCGoString(s *C.char) { + slog.Debug("free CGoString", "addr", s) + C.free(unsafe.Pointer(s)) } diff --git a/client/go/outline/electron/fetch.go b/client/go/outline/electron/fetch.go index f2dece2a81..8be793a50b 100644 --- a/client/go/outline/electron/fetch.go +++ b/client/go/outline/electron/fetch.go @@ -17,20 +17,34 @@ package main /* #include "platerr.h" -struct FetchResourceResult { +// FetchResourceResult represents the result of fetching a resource located at a URL. +typedef struct t_FetchResourceResult { + // The content of the fetched resource. + // Caller is responsible for freeing this pointer using FreeCGoString. const char *Content; - PlatformErrorHandle Error; -}; + + // If this is not null, it represents the error encountered during fetching. + // Caller is responsible for freeing this pointer using FreeCGoPlatformError. + const PlatformError *Error; +} FetchResourceResult; */ import "C" import "github.com/Jigsaw-Code/outline-apps/client/go/outline" +// FetchResource fetches a resource located at the given URL. +// +// The function returns a C FetchResourceResult containing the Content of the resource +// and any Error encountered during fetching. +// +// You don't need to free the memory of FetchResourceResult struct itself, as it's not a pointer. +// However, you are responsible for freeing the memory of its Content and Error fields. +// //export FetchResource -func FetchResource(cstr *C.char) C.struct_FetchResourceResult { +func FetchResource(cstr *C.char) C.FetchResourceResult { url := C.GoString(cstr) result := outline.FetchResource(url) - return C.struct_FetchResourceResult{ - Content: C.CString(result.Content), - Error: ToCPlatformErrorHandle(result.Error), + return C.FetchResourceResult{ + Content: NewCGoString(result.Content), + Error: ToCGoPlatformError(result.Error), } } diff --git a/client/go/outline/electron/handle.go b/client/go/outline/electron/init.go similarity index 56% rename from client/go/outline/electron/handle.go rename to client/go/outline/electron/init.go index 94315df130..63268e67bc 100644 --- a/client/go/outline/electron/handle.go +++ b/client/go/outline/electron/init.go @@ -14,20 +14,26 @@ package main -/* -#include -*/ -import "C" import ( - "runtime/cgo" + "log/slog" + "os" ) -const NilHandle = 0 +// init initializes the backend module. +// It sets up a default logger based on the OUTLINE_DEBUG environment variable. +func init() { + opts := slog.HandlerOptions{Level: slog.LevelInfo} -//export FreeHandle -func FreeHandle(ptr C.uintptr_t) { - if ptr != NilHandle { - h := cgo.Handle(ptr) - h.Delete() + dbg := os.Getenv("OUTLINE_DEBUG") + if dbg != "" && dbg != "false" { + dbg = "true" + opts.Level = slog.LevelDebug + } else { + dbg = "false" } + + logger := slog.New(slog.NewTextHandler(os.Stderr, &opts)) + slog.SetDefault(logger) + + slog.Info("Backend module initialized", "debug", dbg) } diff --git a/client/go/outline/electron/platerr.go b/client/go/outline/electron/platerr.go index 2f09d90db4..b3242c344a 100644 --- a/client/go/outline/electron/platerr.go +++ b/client/go/outline/electron/platerr.go @@ -15,26 +15,42 @@ package main /* +#include #include "platerr.h" */ import "C" + import ( - "runtime/cgo" + "fmt" + "log/slog" + "unsafe" "github.com/Jigsaw-Code/outline-apps/client/go/outline/platerrors" ) -func ToCPlatformErrorHandle(err *platerrors.PlatformError) C.PlatformErrorHandle { - if err == nil { - return NilHandle +// ToCGoPlatformError allocates memory for a C PlatformError if the given Go PlatformError is not nil. +// It should be paired with [FreeCGoPlatformError] to avoid memory leaks. +func ToCGoPlatformError(e *platerrors.PlatformError) *C.PlatformError { + if e == nil { + return nil } - return C.PlatformErrorHandle(cgo.NewHandle(err)) + json, err := platerrors.MarshalJSONString(e) + if err != nil { + json = fmt.Sprintf("%s, failed to retrieve details due to: %s", e.Code, err.Error()) + } + + res := (*C.PlatformError)(C.malloc(C.sizeof_PlatformError)) + slog.Debug("malloc CGoPlatformError", "addr", unsafe.Pointer(res)) + res.Code = NewCGoString(e.Code) + res.DetailJson = NewCGoString(json) + return res } -func FromCPlatformErrorHandle(err C.PlatformErrorHandle) *platerrors.PlatformError { - if err == NilHandle { - return nil - } - h := cgo.Handle(err) - return h.Value().(*platerrors.PlatformError) +// FreeCGoPlatformError releases the memory allocated by ToCGoPlatformError. +// It also accepts null. +// +//export FreeCGoPlatformError +func FreeCGoPlatformError(e *C.PlatformError) { + slog.Debug("free CGoPlatformError", "addr", unsafe.Pointer(e)) + C.free(unsafe.Pointer(e)) } diff --git a/client/go/outline/electron/platerr.h b/client/go/outline/electron/platerr.h index 16750cc4f6..01e8175e2a 100644 --- a/client/go/outline/electron/platerr.h +++ b/client/go/outline/electron/platerr.h @@ -12,6 +12,18 @@ // See the License for the specific language governing permissions and // limitations under the License. -#include +#pragma once -typedef uintptr_t PlatformErrorHandle; +// PlatformError represents an error that originate from the native network code. +typedef struct t_PlatformError +{ + + // A code can be used to identify the specific type of the error. + // Caller is responsible for freeing this pointer using FreeCGoString. + const char *Code; + + // A JSON string of the error details that can be parsed by TypeScript. + // Caller is responsible for freeing this pointer using FreeCGoString. + const char *DetailJson; + +} PlatformError;