Programmatically creating an ad-hoc network in Big Sur - objective-c

Before Mac OS Big Sur, one could create an ad-hoc network by calling the startIBSSModeWithSSID:security:channel:password:error: function of a CWInterface obtained from a CWWifiClient. It seems that after an update to Big Sur, the above function is deprecated and throws a kCWOperationNotPermittedErr (-3930) error every time.
I tried launching the application from root, and it still refused to create an ad-hoc network. Meanwhile, using the "Create Network" option in the WiFi dropdown menu works with an administrator password.
A previous answer on this site I have come across is outdated and the code does not work anymore. There is a post on the Apple Developer forums created 5 months ago but it remains unanswered, with the "solution" being to file a tech support incident.
This is the code I am using:
#import <Foundation/Foundation.h>
#import <CoreWLAN/CoreWLAN.h>
#import <SecurityFoundation/SFAuthorization.h>
#import <objc/message.h>
int main(int argc, const char * argv[]) {
#autoreleasepool {
bool success = 0;
CWWiFiClient* wifiClient = [CWWiFiClient sharedWiFiClient];
CWInterface* interface = [wifiClient interface];
NSString* namestr = #"very_creative_ssid";
NSData* name = [namestr dataUsingEncoding:NSUTF8StringEncoding];
NSString* pass = #"very_cruel_framework"; // not used
NSError* err = nil;
success = [interface startIBSSModeWithSSID:name
security:kCWIBSSModeSecurityNone
channel:11
password:nil
error:&err];
if (!success) {
NSLog(#"%#", err);
return 1;
}
[NSRunLoop.currentRunLoop run];
}
return 0;
}
Is there a way to programmatically create an ad-hoc network in Big Sur without throwing an error?
Edit: Here is the console output (1 line):
2022-01-12 05:25:03.723 cwlantest[15305:448617] Error Domain=com.apple.coreWLAN.error Code=-3930 "(null)"

I'm going to put this as an answer, if anyone finds anything new or Apple adds this feature in the future, I'll be very happy to be wrong.
TLDR: Not anymore!
Since Apple removed the "Create network..." option from the wifi menubar, the only way to create an ad-hoc network is through Network Sharing. I followed https://www.makeuseof.com/how-to-create-a-secure-ad-hoc-network-in-macos/ under the How to Create a Secure Ad Hoc Network section to make a network:
sudo networksetup -createnetworkservice AdHoc lo0
sudo networksetup -setmanual AdHoc 192.168.1.88 255.255.255.255
And in System Preferences, share your network connection from AdHoc over WiFi.
With that on, I checked the CWInterface.interfaceMode() and it was in HostAP mode. Pure speculation, but I think IBSS was removed completely, it's marked as Deprecated in the developer documentation. -3930 is kCWOperationNotPermittedErr, so I'm not 100% sure that's accurate, but it's possible.
There are private interfaces to set HostAP mode in CoreWLAN:
https://github.com/onmyway133/Runtime-Headers/blob/master/macOS/10.13/CoreWLAN.framework/CWInterface.h https://medium.com/swlh/calling-ios-and-macos-hidden-api-in-style-1a924f244ad1 https://gist.github.com/wolever/4418079
After replacing objc_msgsend with NSInvocation in the last link since objc_msgsend seems to have been removed:
#import <CoreWLAN/CoreWLAN.h>
#import <objc/message.h>
int main(int argc, char* argv[]) {
#autoreleasepool {
int ch;
NSString *ssid = nil, *password = nil;
while((ch = getopt(argc, argv, "s:p:h")) != -1) {
switch(ch) {
case 's':
ssid = [NSString stringWithUTF8String:optarg];
break;
case 'p':
password = [NSString stringWithUTF8String:optarg];
break;
case '?':
case 'h':
default:
printf("USAGE: %s [-s ssid] [-p password] [-h] command\n", argv[0]);
printf("\nOPTIONS:\n");
printf(" -s ssid SSID\n");
printf(" -p password WEP password\n");
printf(" -h Print help\n");
printf("\nCOMMAND:\n");
printf(" status Print interface mode\n");
printf(" start Start Host AP mode\n");
printf(" stop Stop Host AP mode\n");
return 0;
}
}
NSString *command = nil;
if(argv[optind]) {
command = [NSString stringWithUTF8String:argv[optind]];
}
CWInterface *iface = [[CWWiFiClient sharedWiFiClient] interface];
if(!command || [command isEqualToString:#"status"]) {
NSString *mode = nil;
switch(iface.interfaceMode) {
case kCWInterfaceModeStation:
mode = #"Station";
break;
case kCWInterfaceModeIBSS:
mode = #"IBSS";
break;
case kCWInterfaceModeHostAP:
mode = #"HostAP";
break;
case kCWInterfaceModeNone:
default:
mode = #"None";
}
printf("%s\n", [mode UTF8String]);
} else if([command isEqualToString:#"stop"]) {
// Stop Host AP mode
if(getuid() != 0) {
printf("this may need root (trying anyway)...\n");
}
SEL selector = #selector(stopHostAPMode);
NSMethodSignature *signature = [iface methodSignatureForSelector: selector];
NSInvocation *invocation =
[NSInvocation invocationWithMethodSignature:signature];
invocation.target = iface;
invocation.selector = selector;
[invocation invoke];
printf("Done?");
//objc_msgSend(iface, #selector(stopHostAPMode));
} else if([command isEqualToString:#"start"]) {
if(!ssid) {
printf("error: an ssid must be specified\n");
return 1;
}
// known security types:
// 2: no securiry
// 16: wep
// Note: values [-127..127] have been tried, and all but these return errors.
unsigned long long securityType = 2;
if(password) {
if([password length] < 10) {
printf("error: password too short (must be >= 10 characters)\n");
return 1;
}
securityType = 16;
}
NSSet *chans = [iface supportedWLANChannels];
//printf("chan count: %lu\n", [chans count]);
NSEnumerator *enumerator = [chans objectEnumerator];
CWChannel *channel;
while ((channel = [enumerator nextObject])) {
//printf("channel: %lu\n", [channel channelNumber]);
if ([channel channelNumber] == 11)
break;
}
printf("Found Channel: %d\n", channel.channelNumber);
// Start Host AP mode
NSError *error = nil;
NSError **errorptr = &error;
SEL selector = #selector(startHostAPModeWithSSID:securityType:channel:password:error:);
NSMethodSignature *signature = [iface methodSignatureForSelector: selector];
NSInvocation *invocation =
[NSInvocation invocationWithMethodSignature:signature];
invocation.target = iface;
invocation.selector = selector;
NSString * ssidstr = #"Test";
NSString * pass = #"barbarbarr";
NSData * ssidArg = [ssidstr dataUsingEncoding:NSUTF8StringEncoding];
[invocation setArgument: &ssidArg atIndex:2];
[invocation setArgument: &securityType atIndex:3];
[invocation setArgument: &channel atIndex:4];
[invocation setArgument: &pass atIndex:5];
[invocation setArgument: &errorptr atIndex:6];
[invocation invoke];
BOOL success;
[invocation getReturnValue:&success];
if (!success) {
printf("startHostAPModeWithSSID error: %s\n", [(*errorptr).localizedDescription UTF8String]);
return 1;
} else {
printf("Success?\n");
return 0;
}
}
return 0;
}
}
./hostap stop does successfully kick me out of hostap mode started from network sharing, but ./hostap start fails with -3903 kCWNotSupportedErr.
Also, using startHostAPMode: without other settings does succeed, but the wifi menu shows WiFi: Internet Sharing, so I think this is a private api meant specifically for network sharing and will likely need other configuration to get working. You could potentially continue down that road, but it didn't look very promising. The best bet is to just use network sharing or potentially look into scripting System Preferences with AppleScript if you really want a scripted approach.

Related

Need create VPN connect L2TP on osx

Need create VPN connection L2TP on osx without Shared Secret
NSString *server = #"serverIP";
NSString *username = #"user";
NSString *password = #"pass";
const void* passwordData = [[password dataUsingEncoding:NSUTF8StringEncoding] bytes];
[vpnManager loadFromPreferencesWithCompletionHandler:^(NSError *error) {
if (error) {
NSLog(#"Load config failed [%#]", error.localizedDescription);
return;
}
NEVPNProtocol *p = (NEVPNProtocol *)vpnManager.protocolConfiguration;
if (!p) {
p = [[NEVPNProtocol alloc] init];
}
p.username = username;
p.serverAddress = server;
p.passwordReference = (__bridge NSData * _Nullable)(passwordData);
p.disconnectOnSleep = NO;
vpnManager.protocolConfiguration = p;
vpnManager.localizedDescription = #"L2TPOverIPSec";
vpnManager.enabled = YES;
[vpnManager saveToPreferencesWithCompletionHandler:^(NSError *error) {
if (error) {
NSLog(#"Save config failed [%#]", error.localizedDescription);
}
}];
}];
NEVPNConnection *connect = [vpnManager connection];
NSError *error1;
if ([connect startVPNTunnelAndReturnError: &error1]) {
NSLog(#"connect");
} else {
NSLog(#"not connect");
}
after building i get this error Missing protocol or protocol has invalid type, and other 2 protocols use IKE tunnel, what can u advice to me? other option is run terminal from code and add this string networksetup -connectpppoeservice VPNConnect but i dont know if it possible
I know this is the old question, but I'm here to note, that Network Extension framework (where NEVPNProtocol is from) can't go on with OSI level 2 protocols, which is L2TP. (https://forums.developer.apple.com/thread/29909)
It seems now (starting from iOS 8 and os x 10.10) the recommended way on Apple devices is to use built-in protocols, or implement your own but on L3 / L4: https://developer.apple.com/documentation/networkextension
(And so there is no public API for using L2TP)
No, you need use SCNetwork and Helper Tool (to get root access) With this link you will be able to create L2TP Protocol and connect with it.
This works even on the latest version MacOS 11 "Big Sure"

Change system time, time zone and auto checkbox in time and date settings programmatically

I'm trying to figure out how to edit the time and the 'Set Date and Time Automatically' check box programmatically. I've spent a while and cant find the solution.
I've tried looking at the NSUserDefault keys but don't see them.
NSLog(#"%#", [[[NSUserDefaults standardUserDefaults] dictionaryRepresentation] allKeys]);
help appreciated. This is OSX (not iphone).
This is not a complete answer. I just code below will just change the system time. Note: changing the system time requires root permission. Running the code via Xcode IDE as is will fail. Running from Terminal using sudo command works.
//
// main.m
// TimeChange
//
// Created by ... on 4/13/15.
// Copyright (c) 2015 .... All rights reserved.
//
#import <Foundation/Foundation.h>
#import <sys/time.h>
#include <errno.h>
extern int errno;
void NSLogTime(const struct tm *restrict temp, suseconds_t microseconds)
{
char tmbuf[64], buf[64];
strftime(tmbuf, sizeof tmbuf, "%Y-%m-%d %H:%M:%S", temp);
snprintf(buf, sizeof buf, "%s.%06d\n", tmbuf, microseconds);
NSLog(#" %#", [[NSString alloc] initWithUTF8String:buf]);
}
int main(int argc, const char * argv[]) {
#autoreleasepool {
// Built from samples based on the URL listed below
// http://stackoverflow.com/questions/2408976/struct-timeval-to-printable-format
// http://www.linuxquestions.org/questions/programming-9/c-code-to-change-date-time-on-linux-707384/
// Do whatever you need to set the following variable
// In this example I am hard-coding it
int month = 2;
int day = 27;
int year = 2002;
NSLog(#"Getting current date/time...");
struct timeval currentTime;
int success = gettimeofday(&currentTime, 0); // should check for success
struct tm *localTime = localtime(&currentTime.tv_sec);
NSLogTime(localTime, currentTime.tv_usec);
if (localTime)
{
NSLog(#"...create new date/time structure...");
localTime->tm_mon = month - 1;
localTime->tm_mday = day;
localTime->tm_year = year - 1900;
const struct timeval tv = {mktime(localTime), 0};
success = settimeofday(&tv, 0);
// check if we are success
if (success == 0)
{
NSLog(#"...time was changed!");
// get the new time from the system and display it
struct timeval updatedTime;
gettimeofday(&updatedTime, 0); // should check for success
NSLogTime(localtime(&updatedTime.tv_sec), updatedTime.tv_usec);
}
else
{
// display the error message
NSLog(#"Error Setting Date: %s", strerror(errno));
}
}
}
return 0;
}
Below is screen shot of the coding running in the terminal.
Note: the one hour time difference in the output is because Daylight Saving Time (DST) was not in-effect back in Feb 22, 2002.
So I discovered that I could write an applescript that would execute bash script commands. Then I called the script with NSApplescript.
The cool thing is apple script has an elegant password dialog and it only needs to be handled once for everything. This is far nicer than making the terminal appear.
The downside was the process for calling the applescript with NSApplescript.
What should have been a simple process of passing 3 args to the script needed to be handled by about 50 lines of outdated NSAppleEvent code that didn't even work in Apples docs. Luckily, I found a post where someone knew the constants missing from the absent Carbon framework.
The Code:
// Caller responsible for well formed ip address.
+(BOOL)setDateAndTimePreferences:(NSString*)ipAddress setAutoNetworkTime:(BOOL)yNo withTimezone:(NSString*)timezone{
// Load the script from a resource by fetching its URL from within our bundle
// Note: if the script if stored in a nother file location, NSBundle may not be
// necessary. Make sure the path to the script is correct.
NSString* path = [[NSBundle mainBundle] pathForResource:#"date_time_pref" ofType:#"scpt"];
if (path != nil){
NSURL* url = [NSURL fileURLWithPath:path];
if (url != nil)
{
NSDictionary* errors = [NSDictionary dictionary];
NSAppleScript* appleScript =
[[NSAppleScript alloc] initWithContentsOfURL:url error:&errors];
if (appleScript != nil)
{
// Get the value of the setAutoNetwork checkbox.
NSString *chkBox = (yNo == YES)? #"on": #"off";
// Create the arg parameters
NSAppleEventDescriptor* firstParameter =
[NSAppleEventDescriptor descriptorWithString:ipAddress];
NSAppleEventDescriptor* secondParameter =
[NSAppleEventDescriptor descriptorWithString:chkBox];
NSAppleEventDescriptor* thirdParameter =
[NSAppleEventDescriptor descriptorWithString:timezone];
// Create and populate the list of parameters.
NSAppleEventDescriptor* parameters = [NSAppleEventDescriptor listDescriptor];
[parameters insertDescriptor:firstParameter atIndex:1];
[parameters insertDescriptor:secondParameter atIndex:2];
[parameters insertDescriptor:thirdParameter atIndex:3];
// Create the AppleEvent target
ProcessSerialNumber psn = {0, kCurrentProcess};
NSAppleEventDescriptor* target = [NSAppleEventDescriptor
descriptorWithDescriptorType:typeProcessSerialNumber
bytes:&psn length:sizeof(ProcessSerialNumber)];
// We need these constants from the Carbon OpenScripting
// framework, but we don't actually need Carbon.framework.
#define kASAppleScriptSuite 'ascr'
#define kASSubroutineEvent 'psbr'
#define keyASSubroutineName 'snam'
// Create an NSAppleEventDescriptor with the script's method name to call,
// this is used for the script statement: "on set_preferences(arg1,arg2arg3)"
// Note that the routine name must be in lower case.
NSAppleEventDescriptor* handler =
[NSAppleEventDescriptor descriptorWithString:
[#"set_preferences" lowercaseString]];
// Create the event for an AppleScript subroutine,
// set the method name and the list of parameters
NSAppleEventDescriptor* event =
[NSAppleEventDescriptor appleEventWithEventClass:kASAppleScriptSuite
eventID:kASSubroutineEvent
targetDescriptor:target
returnID:kAutoGenerateReturnID
transactionID:kAnyTransactionID];
[event setParamDescriptor:handler forKeyword:keyASSubroutineName];
[event setParamDescriptor:parameters forKeyword:keyDirectObject];
// call the event in AppleScript
if (![appleScript executeAppleEvent:event error:&errors])
{
// report any errors from 'errors'
NSLog(#"Errors %#",[errors description]);
}
[appleScript release];
}
else{
// report any errors from 'errors'
NSLog(#"Error: applescript is nil");
}
}else{
NSLog(#"Could not locate the time_date_preferences script");
return NO;
}
}else{
NSLog(#"Could not locate the time_date_preferences script");
return NO;
}
return YES;
}
The Script:
on set_preferences(ipaddress, chkbox, timezone)
global timezonelist
do shell script "/usr/sbin/systemsetup -setusingnetworktime " & chkbox password "passwordhere" with administrator privileges
do shell script "/usr/sbin/systemsetup -setnetworktimeserver " & ipaddress with administrator privileges
set timezonelist to (do shell script "/usr/sbin/systemsetup -listtimezones" with administrator privileges)
if timezonelist contains timezone then
do shell script "/usr/sbin/systemsetup -settimezone " & timezone with administrator privileges
else
display notification "Please open Date and Time Preferences and set your time zone manually." with title ("Invalid Time Zone")
delay 1
end if
end set_preferences

How to store blocks in Objective C?

I started writing a simple JSON RPC TCP library in Objective C.
I have a method that invokes a RPC Method:
- (void)invokeMethod:(NSString *)method
withParameters:(id)parameters
requestId:(id)requestId
success:(void (^)(id responseObject))success
failure:(void (^)(NSError *error))failure
{
NSAssert(NSClassFromString(#"NSJSONSerialization"), #"NSJSONSerialization not found!");
NSDictionary *requestObject = #{#"jsonrpc": #"2.0",
#"method": method,
#"params": parameters,
#"id": requestId};
NSError *error = nil;
NSData *jsondData = [NSJSONSerialization dataWithJSONObject:requestObject options:0 error:&error];
if (error){
return failure(error);
}
[self->callbacks setObject:#{#"success": success ? [success copy] : [NSNull null],
#"failure": failure ? [failure copy] : [NSNull null]}
forKey:requestId];
NSString *str = [[NSString alloc] initWithData:jsondData encoding:NSUTF8StringEncoding];
NSLog(#"Sending: %#", str);
[self.socket writeData:jsondData withTimeout:-1 tag:1];
}
The class basically represents a TCP connection, when calling the above method, the JSON data is sent with an id over TCP to the server which either returns a success or a failure:
- (void) socket:(GCDAsyncSocket *)sender didReadData:(NSData *)data withTag:(long)tag
{
NSError *error = nil;
[self.socket readDataWithTimeout:-1 tag:2];
// … rpc response parsing code here, removed for simplicity …
// detect if error or success
NSDictionary *cbs = [self->callbacks objectForKey:JSONRPCObjectId];
void(^success)(id resultObject) = [cbs objectForKey:#"success"];
success ? success(JSONRPCObjectResult) : nil;
return;
}
Now, I am unsure how to keep track of the success and failure blocks, currently I am storing them in an NSMutableDict, using the requestId as key. Is it fine to do this or is there a better approach that I should use?
Blocks in objective-c are objects and you can treat the same way as other object, so storing them in NSDictionarys, NSArrays etc is perfectly fine. The only catch is that blocks when initially created exist in the same memory scope as local variable do and so they are no longer valid when the method that the block is defined in returns, just like all other local variables so you have to copy them first, just copy them and put the copy in the collection. There is a block copy function but you can just send them a copy message [myBlock copy];
Quick answer, seeing as you don't have anything workable yet...
This is more than you asked for; so, you'll probably have to pair it down to meet your specific need. Basically, it stores as many blocks as you specify at contiguous memory addresses. Paste this into a header file or somewhere global to the method from which you will call these:
typedef const typeof(id(^)(void)) retained_object;
static id (^retainable_object)(id(^)(void)) = ^ id (id(^object)(void)) {
return ^{
return object();
};
};
typeof (retained_object) *(^(^retain_object)(id (^__strong)(void)))(void) = ^ (id(^retainable_object)(void)) {
typeof(retained_object) * object_address;
object_address = &retainable_object;
typeof(retained_object) * persistent_object = (typeof(retained_object) *)CFBridgingRetain(retainable_object);
return ^ typeof(retained_object) * {
return persistent_object;
};
};
static void (^(^iterator)(const unsigned long))(id(^)(void)) = ^ (const unsigned long object_count) {
id const * retained_objects_ref[object_count];
return ^ (id const * retained_objects_t[]) {
return ^ (id(^object)(void)) {
object();
int index = 0UL;
int * index_t = &index;
for (; (*index_t) < object_count; ((*index_t) = (*index_t) + 1UL)) printf("retained_object: %p\n", (*((id * const)retained_objects_t + (object_count - index)) = retain_object(retainable_object(object()))));
};
}(retained_objects_ref);
};
From some method, add:
iterator(1000)(^ id { return (^{ printf("stored block\n"); }); });
This should store 1,000 blocks at as many unique memory addresses.

EXC_BAD_INSTRUCTION when using NSRunAlertPanel

To show errors on some condition, I am using NSRunAlertPanel on Mac OS X (on the code which is ported from Windows, where I was using MessageBox).
Before actual creation of Windows, some code is being run and calling this code to show some conditional error.
In a thread com.apple.libdispatch-manager, under following call stack
0 _dispatch_mgr_invoke
1 _dispatch_mgr_thread
it is giving EXC_BAD_INSTRUCTION
Is it because Windows is not created before NSRunAlertPanel?
What is the reason of this runtime error? What is exact alternative of MessageBox on Mac OS X?
Long ShowDebugMessageBox (const wchar_t * Message, const wchar_t * Title)
{
NSString * message; ///< Message.
NSString * title; ///< Title.
NSInteger response; ///< response.
message = WideToNSString (Message);
title = WideToNSString (Title);
//response = NSRunAlertPanel(title, message, #"Yes", #"No", #"Cancel");
response = NSRunCriticalAlertPanel (title, message, #"Okay", #"Cancel", nil);
switch(response) {
case NSAlertDefaultReturn:
return IDYES;
case NSAlertAlternateReturn:
return IDNO;
default:
return IDCANCEL;
}
}
NSString * WideToNSString (const wchar_t * Str)
{
if(!Str) {
return nil;
}
NSString * str; ///< String in NSString.
#if CP_SIZEOFWCHAR == 4
str = [[NSString alloc] initWithBytes: (CVPtr) Str
length: sizeof(wchar_t)* wcslen(Str)
encoding: NSUTF32LittleEndianStringEncoding];
//encoding: NSUTF32StringEncoding];
#else
str = [[NSString alloc] initWithBytes: (CVPtr) Str
length: sizeof(wchar_t)* wcslen(Str);
encoding: NSUTF16LittleEndianStringEncoding];
//encoding: NSUTF16StringEncoding];
#endif
return str;
}
class File {
public:
int Open(char * fname, int mode)
{
fd = open(fname, mode);
}
int Close()
{
close(fd);
//fd = 0; //CAUSE of the PROBLEM
}
~File ()
{
//ALERT Display message box about the error.
ALERT(fd != 0);
}
private:
int fd;
};
This is the code to show the message box.
Code to get NSString from wchar_t * string (Wide string) is perfectly fine and was tested. It is used in many places and running fine.
Same code on the other application (which creates Window first) is running fine.
Problem occurs when the destructor of File is called. Since fd is not 0, it shows message box and cause the problem.
When fd is set to 0, no alert box is displayed for constructor. However, other alert are shown but no problem occurred.
Is it due fd?
You haven't provided enough information to say what is causing the exception (please show the code).
I use NSRunCriticalAlertPanel() to display fatal errors in my app, which I am able to call pretty much any time I like:
void criticalAlertPanel(NSString *title, NSString *fmt, ...)
{
va_list va;
va_start(va, fmt);
NSString *message = [[NSString alloc] initWithFormat:fmt arguments:va];
va_end(va);
NSRunCriticalAlertPanel(title, message, #"OK", nil, nil);
}
(this code is ARC enabled).

Threading and Sockets in Objective-C

NOTE: I've edited my question. I've got it to connect and perform the first callback, but subsequent callbacks don't go through at all.
This is my first time writing Objective-C (with GNUstep; it's for a homework assignment). I've got the solution working, but I am trying to add something more to it. The app is a GUI client that connects to a server and gets data from it. Multiple clients can connect to the same server. If any one of the clients changes data that is residing on the server, the server sends a callback to all registered clients. This solution was originally implemented in Java (both client and server) and for the latest assignment, the professor wanted us to write an Objective-C client for it. He said that we don't need to handle callbacks, but I wanted to try anyway.
I am using NSThread and I wrote something that looks like this:
CallbackInterceptorThread.h
#import <Foundation/Foundation.h>
#import "AppDelegate.h"
#interface CallbackInterceptorThread : NSThread {
#private
NSString* clientPort;
AppDelegate* appDelegate;
}
- (id) initWithClientPort: (NSString*) aClientPort
appDelegate: (AppDelegate*) anAppDelegate;
- (void) main;
#end
CallbackInterceptorThread.m
#import <Foundation/Foundation.h>
#import "CallbackInterceptorThread.h"
#define MAXDATASIZE 4096
#implementation CallbackInterceptorThread
- (id) initWithClientPort: (NSString*) aClientPort
appDelegate: (AppDelegate*) anAppDelegate {
if((self = [super init])) {
[clientPort autorelease];
clientPort = [aClientPort retain];
[appDelegate autorelease];
appDelegate = [anAppDelegate retain];
}
return self;
}
- (void) main {
GSRegisterCurrentThread();
NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];
char* buffer = malloc(MAXDATASIZE);
Cst420ServerSocket* socket = [[Cst420ServerSocket alloc] initWithPort: clientPort];
[socket retain];
NSString* returnString;
while(YES) {
printf("Client waiting for callbacks on port %s\n", [clientPort cString]);
if([socket accept]) {
printf("Connection accepted!\n");
while(YES) {
printf("Inner loop\n");
sleep(1);
returnString = [socket receiveBytes: buffer maxBytes: MAXDATASIZE beginAt: 0];
printf("Received from Server |%s|\n", [returnString cString]);
if([returnString length] > 0) {
printf("Got a callback from server\n");
[appDelegate populateGui];
}
printf("Going to sleep now\n");
sleep(1);
}
[socket close];
}
}
}
#end
Cst420ServerSocket has been provided to us by the instructor. It looks like this:
#import "Cst420Socket.h"
#define PORT "4444"
/**
* Cst420Socket.m - objective-c class for manipulating stream sockets.
* Purpose: demonstrate stream sockets in Objective-C.
* These examples are buildable on MacOSX and GNUstep on top of Windows7
*/
// get sockaddr, IPv4 or IPv6:
void *get_in_addr(struct sockaddr *sa){
if (sa->sa_family == AF_INET) {
return &(((struct sockaddr_in*)sa)->sin_addr);
}
return &(((struct sockaddr_in6*)sa)->sin6_addr);
}
#implementation Cst420ServerSocket
- (id) initWithPort: (NSString*) port{
self = [super init];
int ret = 0;
memset(&hints, 0, sizeof hints);
hints.ai_family = AF_INET;
hints.ai_socktype = SOCK_STREAM;
hints.ai_flags = AI_PASSIVE; // use my IP
const char* portStr = [port UTF8String];
if ((rv = getaddrinfo(NULL, portStr, &hints, &servinfo)) != 0) {
fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(rv));
ret = 1;
}else{
for(p = servinfo; p != NULL; p = p->ai_next) {
if ((sockfd = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP))==-1){
perror("server: socket create error");
continue;
}
if (bind(sockfd, p->ai_addr, p->ai_addrlen) == -1) {
#if defined(WINGS)
closesocket(sockfd);
#else
close(sockfd);
#endif
perror("server: bind error");
continue;
}
break;
}
if (p == NULL) {
fprintf(stderr, "server: failed to bind\n");
ret = 2;
}else{
freeaddrinfo(servinfo); // all done with this structure
if (listen(sockfd, BACKLOG) == -1) {
perror("server: listen error");
ret = 3;
}
}
if (ret == 0){
return self;
} else {
return nil;
}
}
}
- (BOOL) accept {
BOOL ret = YES;
#if defined(WINGS)
new_fd = accept(sockfd, NULL, NULL);
#else
new_fd = accept(sockfd, (struct sockaddr *)&their_addr, &sin_size);
#endif
if (new_fd == -1) {
perror("server: accept error");
ret = NO;
}
connected = ret;
return ret;
}
- (int) sendBytes: (char*) byteMsg OfLength: (int) msgLength Index: (int) at{
int ret = send(new_fd, byteMsg, msgLength, 0);
if(ret == -1){
NSLog(#"error sending bytes");
}
return ret;
}
- (NSString* ) receiveBytes: (char*) byteMsg
maxBytes: (int) max
beginAt: (int) at {
int ret = recv(new_fd, byteMsg, max-1, at);
if(ret == -1){
NSLog(#"server error receiving bytes");
}
byteMsg[ret+at] = '\0';
NSString * retStr = [NSString stringWithUTF8String: byteMsg];
return retStr;
}
- (BOOL) close{
#if defined(WINGS)
closesocket(new_fd);
#else
close(new_fd);
#endif
connected = NO;
return YES;
}
- (void) dealloc {
#if defined(WINGS)
closesocket(sockfd);
#else
close(sockfd);
#endif
[super dealloc];
}
#end
Our professor also provided us an example of a simple echo server and client (the server just spits back whatever the client sent it) and I've used the same pattern in the thread.
My initial problem was that my callback interceptor thread didn't accept any (callback) connections from the server. The server said that it could not connect back to the client (ConnectException from Java; it said "Connection refused"). I was able to fix this by changing my instructor's code. In the connect function (not shown), he had set the hints to use AF_UNSPEC instead of AF_INET. So Java was seeing my localhost IP come through as 0:0:0:0:0:0:0:1 (in IPv6 format). When Java tried to connect back to send a callback, it received an exception (not sure why it cannot connect to an IPv6 address).
After fixing this problem, I tried out my app again and this time the callback from the server was received by my client. However, subsequent callbacks fail to work. After receiving the first callback, the busy-loop keeps running (as it should). But when the server sends a second callback, it looks like the client cannot read it in. On the server side I can see that it sent the callback to the client successfully. It's just that the client is having trouble reading in the data. I added some print statements (see above) for debugging and this is what I get:
Client waiting for callbacks on port 2020
Connection accepted!
Inner loop
Received from Server |A callback from server to 127.0.0.1:2020|
Got a callback from server
Going to sleep now
Inner loop
Received from Server ||
Going to sleep now
Inner loop
Received from Server ||
Going to sleep now
Inner loop
... (and it keeps going regardless of the second callback being sent)
Here is how I am starting the thread (from the GUI):
CallbackInterceptorThread* callbackInterceptorThread = [[CallbackInterceptorThread alloc] initWithClientPort: clientPort appDelegate: self];
[callbackInterceptorThread start];
I think I've got it working. So from the Java side (the server), this was what I was doing:
Socket socket = new Socket(clientAddress, clientPort);
BufferedOutputStream out = new BufferedOutputStream(socket.getOutputStream());
out.write(("A callback from server to " + clientAddress + ":" + clientPort).getBytes());
out.flush();
out.close();
I put some debugging print-statements in my professor's code and noticed that in receiveBytes, recv was returning 0. The return value of recv is the length of the message that it received. So it received a zero-length string. But a return value of 0 also means that the peer closed the connection properly (which is exactly what I had done from the Java side with out.close()). So I figured that if I needed to respond to the second callback, I would need to accept the connection again. So I changed my busy loop to this:
printf("Client waiting for callbacks on port %s\n", [clientPort cString]);
while([socket accept]) {
printf("Connection accepted!\n");
returnString = [socket receiveBytes: buffer maxBytes: MAXDATASIZE beginAt: 0];
printf("Received from Server |%s|\n", [returnString cString]);
if([returnString length] > 0) {
printf("Got a callback from server\n");
[appDelegate populateGui];
}
}
[socket close];
and that seemed to do the trick. I am not sure if this is the right way to do it, so I am open to suggestions for improvement!