Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions x/examples/objc/ProcessInfo.app/Info.plist
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleExecutable</key>
<string>main</string>

<key>CFBundleIdentifier</key>
<string>org.getoutline.test</string>

<key>CFBundleName</key>
<string>ProcessInfo</string>

<key>CFBundlePackageType</key>
<string>APPL</string>

<key>CFBundleShortVersionString</key>
<string>1.0</string>

<key>CFBundleVersion</key>
<string>1</string>

<key>LSRequiresIPhoneOS</key>
<true/>
</dict>
</plist>
85 changes: 85 additions & 0 deletions x/examples/objc/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@



This works as macOS:

```console
$ GOOS=ios GOARCH=arm64 CGO_ENABLED=1 go -C x run ./examples/objc
Attempting to get iOS process info using Cgo...

--- Successfully Retrieved Process Info ---
Process Name: objc
Process ID (PID): 72134
User Name: <redacted>
Full User Name: <redacted>
Globally Unique ID: <redacted>
OS Version: Version 15.6.1 (Build 24G90)
Hostname: <redacted>
Is Mac Catalyst App: false
Is iOS App on Mac: false
Physical Memory (B): 17178869184
System Uptime (s): 198843.16
Processor Count: 10
Active Processor Count: 10
```

This seems to properly build for iOS:

```console
% CC="$(xcrun --sdk iphoneos --find cc) -isysroot \"$(xcrun --sdk iphoneos --show-sdk-path)\"" GOOS=ios GOARCH=arm64 CGO_ENABLED=1 go -C x build  -v ./examples/objc

github.com/Jigsaw-Code/outline-sdk/x/examples/objc
# github.com/Jigsaw-Code/outline-sdk/x/examples/objc
examples/objc/process_info.go:74:47: error: 'userName' is unavailable: not available on iOS
74 | p_info->userName = safe_strdup([[info userName] UTF8String]);
| ^
/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS18.4.sdk/System/Library/Frameworks/Foundation.framework/Headers/NSProcessInfo.h:192:38: note: property 'userName' is declared unavailable here
192 | @property (readonly, copy) NSString *userName API_AVAILABLE(macosx(10.12)) API_UNAVAILABLE(ios, watchos, tvos);
| ^
/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS18.4.sdk/System/Library/Frameworks/Foundation.framework/Headers/NSProcessInfo.h:192:38: note: 'userName' has been explicitly marked unavailable here
examples/objc/process_info.go:75:51: error: 'fullUserName' is unavailable: not available on iOS
75 | p_info->fullUserName = safe_strdup([[info fullUserName] UTF8String]);
| ^
/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS18.4.sdk/System/Library/Frameworks/Foundation.framework/Headers/NSProcessInfo.h:193:38: note: property 'fullUserName' is declared unavailable here
193 | @property (readonly, copy) NSString *fullUserName API_AVAILABLE(macosx(10.12)) API_UNAVAILABLE(ios, watchos, tvos);
| ^
/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS18.4.sdk/System/Library/Frameworks/Foundation.framework/Headers/NSProcessInfo.h:193:38: note: 'fullUserName' has been explicitly marked unavailable here
2 errors generated.
```


For the simulator:

```console
% CC="$(xcrun --sdk iphonesimulator --find cc) -isysroot \"$(xcrun --sdk iphonesimulator --show-sdk-path)\"" GOOS=ios GOARCH=arm64 CGO_ENABLED=1 go -C x build -v ./examples/objc
```

If you build it for the iOS Simulator, you can run on the iOS simulator. It correctly returns the iOS version on the simulator (18.4), though notice that "Is iOS App on Mac" is false, because it's not "on Mac".

```
% CC="$(xcrun --sdk iphonesimulator --find cc) -isysroot \"$(xcrun --sdk iphonesimulator --show-sdk-path)\"" GOOS=ios GOARCH=arm64 CGO_ENABLED=1 go -C x build -v -o examples/objc/ProcessInfo.app ./examples/objc/main.go

% xcrun simctl boot 529EC4D4-FFC6-4249-A829-0D8181639E9D

% xcrun simctl install booted ./x/examples/objc/ProcessInfo.app

% xcrun simctl launch --console booted org.getoutline.test
org.getoutline.test: 43075
Attempting to get iOS process info using Cgo...

--- Successfully Retrieved Process Info ---
Process Name: main
Process ID (PID): 43075
User Name:
Full User Name:
Globally Unique ID: 5A277847-FAB1-491C-930F-AEBFB8BC145C-43075-00000903D8910887
OS Version: Version 18.4 (Build 22E238)
Hostname: fortuna-macbookpro2.roam.internal
Is Mac Catalyst App: false
Is iOS App on Mac: false
Physical Memory (B): 17179869184
System Uptime (s): 413005.42
Processor Count: 10
Active Processor Count: 10
-------------------------------------------
```
250 changes: 250 additions & 0 deletions x/examples/objc/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,250 @@
// Copyright 2025 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
//
// https://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.

//go:build darwin

package main

/*
// These Cgo directives are essential for compiling on an Apple platform.
#cgo CFLAGS: -x objective-c
#cgo LDFLAGS: -framework Foundation

#import <Foundation/Foundation.h>
#include <stdlib.h> // For malloc, free, strdup

// A C struct to hold all the process information we want to retrieve.
// This allows us to get all the data in a single Cgo call.
typedef struct {
char* processName;
int processIdentifier;
char* globallyUniqueString;
char* operatingSystemVersionString;
char* hostName;
unsigned long long physicalMemory;
double systemUptime;
int processorCount;
int activeProcessorCount;
// New fields from your request
int isMacCatalystApp;
int isiOSAppOnMac;
int isIOS;
char* userName;
char* fullUserName;
} ProcessInfo_t;

// A helper function to safely duplicate a C string that might be NULL.
// strdup(NULL) is undefined behavior, so this prevents crashes.
static char* safe_strdup(const char* s) {
if (s == NULL) {
// Return a dynamically allocated empty string.
return strdup("");
}
return strdup(s);
}


// This C function (using Objective-C) populates and returns a struct
// containing a wide range of process information.
static ProcessInfo_t* get_all_process_info() {
// Autorelease pool is good practice for managing memory in Objective-C code
// called from other languages.
@autoreleasepool {
NSProcessInfo *info = [NSProcessInfo processInfo];

// Allocate memory for our C struct.
ProcessInfo_t *p_info = (ProcessInfo_t*)malloc(sizeof(ProcessInfo_t));
if (p_info == NULL) {
return NULL; // Failed to allocate memory
}

// Use safe_strdup to create heap-allocated C string copies that Go can safely manage and free.
p_info->processName = safe_strdup([[info processName] UTF8String]);
p_info->globallyUniqueString = safe_strdup([[info globallyUniqueString] UTF8String]);
p_info->operatingSystemVersionString = safe_strdup([[info operatingSystemVersionString] UTF8String]);
p_info->hostName = safe_strdup([[info hostName] UTF8String]);

#if TARGET_OS_OSX
// NSUserName and NSFullUserName are only available on macOS.
// We use the modern, non-deprecated functions here.
p_info->userName = safe_strdup([NSUserName() UTF8String]);
p_info->fullUserName = safe_strdup([NSFullUserName() UTF8String]);
#else
// On other platforms (like iOS), provide empty strings to avoid crashes.
p_info->userName = safe_strdup(NULL);
p_info->fullUserName = safe_strdup(NULL);
#endif

// Populate numeric fields directly.
p_info->processIdentifier = [info processIdentifier];
p_info->physicalMemory = [info physicalMemory];
p_info->systemUptime = [info systemUptime];
p_info->processorCount = (int)[info processorCount];
p_info->activeProcessorCount = (int)[info activeProcessorCount];

// Populate boolean flags (as integers), checking for API availability.
if (@available(macOS 10.15, iOS 13.0, *)) {
p_info->isMacCatalystApp = [info isMacCatalystApp] ? 1 : 0;
} else {
p_info->isMacCatalystApp = 0; // Default to false on older systems.
}

#if TARGET_OS_IOS
p_info->isIOS = 1;
#else
p_info->isIOS = 0;
#endif

if (@available(macOS 11.0, iOS 14.0, *)) {
p_info->isiOSAppOnMac = [info isiOSAppOnMac] ? 1 : 0;
} else {
p_info->isiOSAppOnMac = 0; // Default to false on older systems.
}

return p_info;
}
}
*/
import "C"
import (
"fmt"
"log"
"log/slog"
"os"
"unsafe"

"golang.org/x/sys/unix"
)

// A Go struct that mirrors the C struct, providing an idiomatic way
// to work with the process information in Go.
type ProcessInfo struct {
ProcessName string
ProcessIdentifier int
GloballyUniqueString string
OperatingSystemVersionString string
HostName string
PhysicalMemoryBytes uint64
SystemUptimeSeconds float64
ProcessorCount int
ActiveProcessorCount int
IsMacCatalystApp bool
IsIOSAppOnMac bool
IsIOS bool
UserName string
FullUserName string
}

// getProcessInfo is a Go wrapper function that calls the underlying C function
// and converts the C struct into a Go struct.
func getProcessInfo() (*ProcessInfo, error) {
// Call the C function to get the populated struct.
cInfo := C.get_all_process_info()

// Check if the C function returned NULL, which indicates an error.
if cInfo == nil {
return nil, fmt.Errorf("failed to get process info from NSProcessInfo")
}

// The memory for the C struct and its string members was allocated in C.
// We must free all of it to prevent memory leaks. The defer statements
// ensure C.free is called for each allocated piece of memory right
// before the function returns.
defer C.free(unsafe.Pointer(cInfo.processName))
defer C.free(unsafe.Pointer(cInfo.globallyUniqueString))
defer C.free(unsafe.Pointer(cInfo.operatingSystemVersionString))
defer C.free(unsafe.Pointer(cInfo.hostName))
defer C.free(unsafe.Pointer(cInfo.userName))
defer C.free(unsafe.Pointer(cInfo.fullUserName))
defer C.free(unsafe.Pointer(cInfo))

// Create a Go struct and copy the data from the C struct, converting types as needed.
goInfo := &ProcessInfo{
ProcessName: C.GoString(cInfo.processName),
ProcessIdentifier: int(cInfo.processIdentifier),
GloballyUniqueString: C.GoString(cInfo.globallyUniqueString),
OperatingSystemVersionString: C.GoString(cInfo.operatingSystemVersionString),
HostName: C.GoString(cInfo.hostName),
PhysicalMemoryBytes: uint64(cInfo.physicalMemory),
SystemUptimeSeconds: float64(cInfo.systemUptime),
ProcessorCount: int(cInfo.processorCount),
ActiveProcessorCount: int(cInfo.activeProcessorCount),
IsMacCatalystApp: cInfo.isMacCatalystApp != 0,
IsIOSAppOnMac: cInfo.isiOSAppOnMac != 0,
IsIOS: cInfo.isIOS != 0,
UserName: C.GoString(cInfo.userName),
FullUserName: C.GoString(cInfo.fullUserName),
}

return goInfo, nil
}

// CstrToString converts a null-terminated []int8 byte slice to a string.
func CstrToString(arr []byte) string {
buf := make([]byte, 0, len(arr))
for _, v := range arr {
if v == 0x00 {
break
}
buf = append(buf, byte(v))
}
return string(buf)
}

func main() {
fmt.Println("Attempting to get iOS process info using Cgo...")

// Call our Go wrapper function.
info, err := getProcessInfo()
if err != nil {
log.Fatalf("Error: %v", err)
}

osHostname, err := os.Hostname()
if err != nil {
osHostname = err.Error()
}

uts := new(unix.Utsname)
err = unix.Uname(uts)
if err != nil {
slog.Error("uname failed", "error", err)
}
// Print all the retrieved information in a formatted way.
fmt.Printf("\n--- Successfully Retrieved Process Info ---\n")
fmt.Printf("Process Name: %s\n", info.ProcessName)
fmt.Printf("Process ID (PID): %d\n", info.ProcessIdentifier)
fmt.Printf("User Name: %s\n", info.UserName)
fmt.Printf("Full User Name: %s\n", info.FullUserName)
fmt.Printf("Globally Unique ID: %s\n", info.GloballyUniqueString)
fmt.Printf("OS Version: %s\n", info.OperatingSystemVersionString)
fmt.Printf("Hostname: %s\n", info.HostName)
fmt.Printf("Is Mac Catalyst App: %t\n", info.IsMacCatalystApp)
fmt.Printf("Is iOS App on Mac: %t\n", info.IsIOSAppOnMac)
fmt.Printf("Is iOS: %t\n", info.IsIOS)
fmt.Printf("Physical Memory (B): %d\n", info.PhysicalMemoryBytes)
fmt.Printf("System Uptime (s): %.2f\n", info.SystemUptimeSeconds)
fmt.Printf("Processor Count: %d\n", info.ProcessorCount)
fmt.Printf("Active Processor Count: %d\n", info.ActiveProcessorCount)
fmt.Printf("os.Args: %s\n", os.Args)
fmt.Printf("os.Getpid(): %d\n", os.Getpid())
fmt.Printf("os.Hostname(): %s\n", osHostname)
fmt.Printf("unix.Uname:\n")
fmt.Printf(" Sysname: %s\n", CstrToString(uts.Sysname[:]))
fmt.Printf(" Nodename: %s\n", CstrToString(uts.Nodename[:]))
fmt.Printf(" Release: %s\n", CstrToString(uts.Release[:]))
fmt.Printf(" Version: %s\n", CstrToString(uts.Version[:]))
fmt.Printf(" Machine: %s\n", CstrToString(uts.Machine[:]))
fmt.Println("-------------------------------------------")
}
Loading