Commits

Jeff Laing committed c2508a2

Renamed some initWithDevice to initWithAMDevice to keep it clearer whats happening - some
other cosmetic changes of the same ilk.
Added some stuff about the installation proxy
Change syslog relay to strip leading newline characters
Correct some comments

Comments (0)

Files changed (2)

MobileDeviceAccess.h

  *
  * The application delegate will usually register itself as a listener to the MobileDeviceAccess
  * singleton.  It will then be called back for every iPhone and iPod Touch that connects.  To access
- * files on the AMDevice, create an AFCDirectoryAccess using \p -newAFCDirectoryAccess:
+ * files on the AMDevice, create one of the subclasses AFCDirectoryAccess using the 
+ * corresponding \p -newAFCxxxDirectory: method.
  *
  * \section refs_sec References
  *
 @protected
 	am_service _service;
 	NSString *_lasterror;
+	id _delegate;
 }
 
 /// The last error that occurred on this service
 /// this property will be nil.
 @property (readonly) NSString *lasterror;
 
+/// The delegate for this service.  Whilst AMService does not use it directly,
+/// some of the subclasses (like AMInstallationProxy) do.  The delegate is
+/// *not* retained - it is the callers responsibility to ensure it remains
+/// valid for the life of the service.
+@property (assign) id delegate;
+
 @end
 
 /// This class represents an installed application on the device.  To retrieve
 
 @end
 
+/// This protocol describes the messages that will be sent by AMInstallationProxy
+/// to its delegate.
+@protocol AMInstallationProxyDelegate
+@optional
+
+/// A new current operation is beginning.
+-(void)operationStarted:(NSDictionary*)info;
+
+/// The current operation is continuing.
+-(void)operationContinues:(NSDictionary*)info;
+
+/// The current operation finished (one way or the other)
+-(void)operationCompleted:(NSDictionary*)info;
+
+@end
+
 /// This class communicates with the mobile_installation_proxy.  It can be used
 /// to retrieve information about installed applications on the device (as well as other
 /// installation operations that are not supported by this framework).
 /// <PRE>
 ///	/usr/libexec/mobile_installation_proxy
 /// </PRE>
+/// That binary seems to vet the incoming requests, then pass them to the installd process
+/// for execution.
+///
+/// The protocol also seems to be a one-shot.  That is, you need to establish multiple connections
+/// if you want to perform multiple operations.
+///
 /// See also: http://iphone-docs.org/doku.php?id=docs:protocols:installation_proxy
 @interface AMInstallationProxy : AMService
 
 /// you can't filter on the value of the attribute, just its existance.
 ///
 /// You probably don't want to use lookupType:withAttribute - see browseFiltered: instead.
-- (NSDictionary*)lookupType:(NSString*)type withAttribute:(NSString*)attr;
+- (NSDictionary*)lookupType:(NSString*)type
+			  withAttribute:(NSString*)attr;
+
+/// Ask the installation daemon on the device to create an archive of a specific
+/// application.  Once finished a corresponding
+/// zip file will be present in the Media/ApplicationArchives directory where it could
+/// be retrieved via AFCMediaDirectory.
+/// The contents of the archive depends on the values of the container: and payload:
+/// arguments.
+/// If the uninstall: argument is YES, the application will also be uninstalled.
+- (BOOL)archive:(NSString*)bundleid
+	  container:(BOOL)container
+		payload:(BOOL)payload
+	  uninstall:(BOOL)uninstall;
+
+/// Ask the installation daemon on the device to restore a previously archived application.
+/// The .zip file must be placed in the Media/ApplicationArchives directory.
+- (BOOL)restore:(NSString*)bundleid;
+
+/// Ask the installation daemon on the device what application archives are
+/// available.  Return an array of bundle ids which can be passed to functions
+/// like restore:
+- (NSArray*)archivedAppBundleIds;
+
+/// Ask the installation daemon on the device what application archives are
+/// available.  Return a dictionary keyed by application bundle id.
+///
+/// This seems to be reading the file /var/mobile/Library/MobileInstallation/ArchivedApplications.plist
+/// which is not otherwise accessible (except using AFCRootDirectory on jailbreaked devices)
+- (NSDictionary*)archivedAppInfo;
+
+/// Remove the archive for a given bundle id.  Note that this is more than just removing
+/// the .zip file from the Media/ApplicationArchives directory - if you do that, the
+/// archive remains "known to" the installation daemon and future requests to archive
+/// this bundle will fail.  Sadly, explicit requests to removeArchive: will file if the
+/// .zip file has been removed as well. (The simplest fix for this scenario is to create
+/// a dummy file before calling removeArchive:)
+- (BOOL)removeArchive:(NSString*)bundleid;
+
+/// Ask the installation daemon on the device to install an application.  pathname
+/// must be the name of a directory located in /var/mobile/Media and must contain
+/// a pre-expanded .app
+- (BOOL)install:(NSString*)pathname;
 
 @end
 
 /// <PRE>
 ///	/usr/libexec/afcd --lockdown -d /var/mobile/Media -u mobile
 /// </PRE>
+///
+/// Common subdirectories to access within the Media directory are:
+/// - DCIM
+/// - ApplicationArchives
+/// - PublicStaging
 @interface AFCMediaDirectory : AFCDirectoryAccess {
 }
 @end
 - (AMSpringboardServices*)newAMSpringboardServices;
 
 /// Create an installation proxy relay.
-- (AMInstallationProxy*)newAMInstallationProxy;
+/// If an object is passed for the delegate, it will be notified during some of the
+/// operations performed by the proxy.
+- (AMInstallationProxy*)newAMInstallationProxyWithDelegate:(id<AMInstallationProxyDelegate>)delegate;
 
 /// Create a fileset relay.  This can be used to en-masse extract certain groups
 /// of information from the device. For more information, see AMFileRelay.

MobileDeviceAccess.m

 typedef void (*NOTIFY_CALLBACK)(CFStringRef notification, void* data);
 mach_error_t AMDListenForNotifications(am_service socket, NOTIFY_CALLBACK cb, void* data);
 
-#if 0
-Yeah, I’ve just found out how to handle this and terminate sync when user slides cancel switch.
-
-We need following functions (meta-language):
-
-ERROR AMDObserveNotification(HANDLE proxy, CFSTR notification);
-
-ERROR AMDListenForNotifications(HANDLE proxy, NOTIFY_CALLBACK cb, USERDATA data);
-
-and callback delegate:
-
-typedef void (*NOTIFY_CALLBACK)(CFSTR notification, USERDATA data);
-
-First, we need AMDObserveNotification to subscribe notifications about “com.apple.itunes-client.syncCancelRequest”. Then we should start listening for notifications (second function) until we get “AMDNotificationFaceplant”.
-That’s it. When notification got, you should unlock and close lock file handle (don’t sure if you need to post “syncDidFinish” to proxy, seems it doesn’t matter) and terminate sync gracefully.
-
-P.S. The same notification is also got when you unplug your device, so you should always be ready for errors.
-#endif
 @interface AMDevice(Private)
 - (am_service)_startService:(NSString*)name;
 @end
 @implementation AMService
 
 @synthesize lasterror = _lasterror;
+@synthesize delegate = _delegate;
 
 - (void)clearLastError
 {
 	_lasterror = [msg retain];
 }
 
+- (void)performDelegateSelector:(SEL)sel
+			  		 withObject:(id)info
+{
+	if (_delegate) {
+		if ([_delegate respondsToSelector:sel]) {
+			[_delegate performSelector:sel withObject:info];
+		}
+	}
+}
+
 - (void)dealloc
 {
 	[_lasterror release];
 - (id)initWithName:(NSString*)name onDevice:(AMDevice*)device
 {
 	if (self = [super init]) {
+		_delegate = nil;
 		_service = [device _startService:name];
 		if (_service == nil) {
 			[self release];
 
 @implementation AFCMediaDirectory
 
-- (id)initWithDevice:(AMDevice*)device
+- (id)initWithAMDevice:(AMDevice*)device
 {
 	if (self = [super initWithName:@"com.apple.afc" onDevice:device]) {
 		int ret = AFCConnectionOpen(_service, 0/*timeout*/, &_afc);
 
 @implementation AFCCrashLogDirectory
 
-- (id)initWithDevice:(AMDevice*)device
+- (id)initWithAMDevice:(AMDevice*)device
 {
 	if (self = [super initWithName:@"com.apple.crashreportcopymobile" onDevice:device]) {
 		int ret = AFCConnectionOpen(_service, 0/*timeout*/, &_afc);
 
 @implementation AFCRootDirectory
 
-- (id)initWithDevice:(AMDevice*)device
+- (id)initWithAMDevice:(AMDevice*)device
 {
 	if (self = [super initWithName:@"com.apple.afc2" onDevice:device]) {
 		int ret = AFCConnectionOpen(_service, 0/*timeout*/, &_afc);
 
 @implementation AFCApplicationDirectory
 
-- (id)initWithDevice:(AMDevice*)device
+- (id)initWithAMDevice:(AMDevice*)device
 			 andName:(NSString*)identifier
 {
-	MyLogMethodEntry();
-
 	if (self = [super initWithName:@"com.apple.mobile.house_arrest" onDevice:device]) {
 		NSDictionary *message;
 		message = [NSDictionary dictionaryWithObjectsAndKeys:
 
 @implementation AMInstallationProxy
 
+#if 0
+Yeah, I’ve just found out how to handle this and terminate sync when user slides cancel switch.
+
+We need following functions (meta-language):
+
+ERROR AMDObserveNotification(HANDLE proxy, CFSTR notification);
+
+ERROR AMDListenForNotifications(HANDLE proxy, NOTIFY_CALLBACK cb, USERDATA data);
+
+and callback delegate:
+
+typedef void (*NOTIFY_CALLBACK)(CFSTR notification, USERDATA data);
+
+First, we need AMDObserveNotification to subscribe notifications about “com.apple.itunes-client.syncCancelRequest”. Then we should start listening for notifications (second function) until we get “AMDNotificationFaceplant”.
+That’s it. When notification got, you should unlock and close lock file handle (don’t sure if you need to post “syncDidFinish” to proxy, seems it doesn’t matter) and terminate sync gracefully.
+
+P.S. The same notification is also got when you unplug your device, so you should always be ready for errors.
+#endif
+
 - (NSArray *)browse:(NSString*)type
 {
 	return [self browseFiltered:nil];
 	return result;
 }
 
+- (BOOL)archive:(NSString*)bundleid
+		container:(BOOL)container
+		payload:(BOOL)payload
+		uninstall:(BOOL)uninstall
+{
+	BOOL result = NO;
+	NSDictionary *message;
+	NSString *mode;
+
+	if (container) {
+		if (payload) {
+			mode = @"All";
+		} else {
+			mode = @"DocumentsOnly";
+		}
+	} else {
+		if (payload) {
+			mode = @"ApplicationOnly";
+		} else {
+			return NO;
+		}
+	}
+
+	// { "Command" => "Archive";	"ApplicationIdentifier" => ...; ClientOptions => ... }
+	// it also takes {"ArchiveType" => oneof "All", "DocumentsOnly", "ApplicationOnly" }
+	// it also takes {"SkipUninstall" => True / False}
+	message = [NSDictionary dictionaryWithObjectsAndKeys:
+					// value																key
+					@"Archive",																@"Command",
+					bundleid,																@"ApplicationIdentifier",
+					[NSDictionary dictionaryWithObjectsAndKeys:
+						// value										key
+						mode,											@"ArchiveType",
+						uninstall ? kCFBooleanFalse : kCFBooleanTrue,	@"SkipUninstall",
+						nil],																@"ClientOptions",
+					nil];
+
+	[self performDelegateSelector:@selector(operationStarted:) withObject:message];
+	if ([self sendXMLRequest:message]) {
+		for (;;) {
+			// read next slab of information
+			// The reply will contain an entry for Status, a string from the following:
+			//	"TakingInstallLock"
+			//	"EmptyingApplication"
+			//	"ArchivingApplication"
+			//	"RemovingApplication"			/__ only if the application was uninstalled
+			//	"GeneratingApplicationMap"		\
+			//	"Complete"
+			// All except "Complete" also include PercentageComplete, an integer between 0 and 100 (but it goes up and down)
+			//
+			// If its already been archived, we might get:
+			//    Error = AlreadyArchived;
+			// If we keep listening we'll then get
+			//    Error = APIInternalError;
+			NSDictionary *reply = [self readXMLReply];
+			if (!reply) break;
+			[self performDelegateSelector:@selector(operationContinues:) withObject:reply];
+
+			NSString *s = [reply objectForKey:@"Status"];
+			if ([s isEqual:@"Complete"]) break;
+		}
+		result = YES;
+	}
+	[self performDelegateSelector:@selector(operationCompleted:) withObject:message];
+	return result;
+}
+
+- (BOOL)restore:(NSString*)bundleid
+{
+	BOOL result = NO;
+	NSDictionary *message;
+	message = [NSDictionary dictionaryWithObjectsAndKeys:
+					// value																key
+					@"Restore",																@"Command",
+					bundleid,																@"ApplicationIdentifier",
+					nil];
+	[self performDelegateSelector:@selector(operationStarted:) withObject:message];
+	if ([self sendXMLRequest:message]) {
+		for (;;) {
+			// read next slab of information
+			// The reply will contain an entry for Status, a string from the following:
+			//	"TakingInstallLock"
+			//	"RestoringApplication"
+			//	"SandboxingApplication"
+			//	"GeneratingApplicationMap"
+			//	"Complete"
+			// All except "Complete" also include PercentageComplete, an integer between 0 and 100 (but it goes up and down)
+			//
+			// Looks like instead of Status, we can get:
+			// Error = APIInternalError
+			// (that happened if I passed in ClientOptions.  Bad...
+			// Error = APIEpicFail;
+			// (that happened if I passed in a missing bundle-id
+			NSDictionary *reply = [self readXMLReply];
+			if (!reply) break;
+			[self performDelegateSelector:@selector(operationContinues:) withObject:reply];
+			NSString *s = [reply objectForKey:@"Status"];
+			if ([s isEqual:@"Complete"]) break;
+		}
+		result = YES;
+	}
+	[self performDelegateSelector:@selector(operationCompleted:) withObject:message];
+	return result;
+}
+
+- (NSArray*)archivedAppBundleIds
+{
+	NSDictionary *appInfo = [self archivedAppInfo];
+	return [appInfo allKeys];
+}
+
+- (NSDictionary*)archivedAppInfo
+{
+	NSDictionary *message;
+	message = [NSDictionary dictionaryWithObjectsAndKeys:
+					// value					key
+					@"LookupArchives",			@"Command",
+					nil];
+	if ([self sendXMLRequest:message]) {
+		NSDictionary *reply = [self readXMLReply];
+		if (reply) {
+			return [[[reply objectForKey:@"LookupResult"] retain] autorelease];
+		}
+	}
+	return nil;
+}
+
+/// Remove the archive for a given bundle id.
+- (BOOL)removeArchive:(NSString*)bundleid
+{
+	NSDictionary *message;
+	message = [NSDictionary dictionaryWithObjectsAndKeys:
+					// value					key
+					@"RemoveArchive",			@"Command",
+					bundleid,					@"ApplicationIdentifier",
+					nil];
+
+	[self performDelegateSelector:@selector(operationStarted:) withObject:message];
+	if ([self sendXMLRequest:message]) {
+		for (;;) {
+			// read next slab of information
+			// The reply will contain an entry for Status, a string from the following:
+			//	"RemovingArchive"
+			//	"Complete"
+			// All except "Complete" also include PercentageComplete, an integer between 0 and 100 (but it goes up and down)
+			//
+			// Looks like instead of Status, we can get:
+			// Error = APIEpicFail;
+			// (that happened if I passed in a missing bundle-id
+			// Error = APIInternalError
+			// (that happened if I kept reading?
+			NSDictionary *reply = [self readXMLReply];
+			if (!reply) break;
+			[self performDelegateSelector:@selector(operationContinues:) withObject:reply];
+			NSString *s = [reply objectForKey:@"Status"];
+			if ([s isEqual:@"Complete"]) break;
+		}
+	}
+	[self performDelegateSelector:@selector(operationCompleted:) withObject:message];
+	return NO;
+}
+
 //
 // lookup is similiar to browse - it looks like it has clever filtering
 // capability but its not that useful.  You can look up all attributes that
 						attr, @"Attribute",
 						nil],				@"ClientOptions",
 					nil];
+	[self performDelegateSelector:@selector(operationStarted:) withObject:message];
 	if ([self sendXMLRequest:message]) {
 		NSDictionary *reply = [self readXMLReply];
 		if (reply) {
+			[self performDelegateSelector:@selector(operationContinues:) withObject:reply];
 			NSMutableDictionary *result = [NSMutableDictionary new];
 			NSDictionary *lookup_result = [reply objectForKey:@"LookupResult"];
 			for (NSString *key in lookup_result) {
 			return [NSDictionary dictionaryWithDictionary:result];
 		}
 	}
+	[self performDelegateSelector:@selector(operationCompleted:) withObject:message];
 	return nil;
 }
 
-- (id)initWithDevice:(AMDevice*)device
+- (id)initWithAMDevice:(AMDevice*)device
 {
 	if (self = [super initWithName:@"com.apple.mobile.installation_proxy" onDevice:device]) {
 	}
 #if 0
 http://libiphone.lighthouseapp.com/projects/27916/tickets/104/a/365185/0001-new-installation_proxy-interface.patch
 
-// { "Command" => "Browse";	ClientOptions => { "ApplicationType" => "User"||"System"||"Any"||"Internal" }  }
-// { "Command" => "Lookup";	ClientOptions => { "ApplicationType" => "User"||"System"||"Any"||"Internal" "Attribute"="attrname" } }
-//		<- { Status => Complete; LookupResult => ... }
-//		<- { Error => LookupFailed; }
-// { "Command" => "Install"; "PackagePath" => "..."; "ClientOptions" = "..." }
+// { "Command" => "Install";
+//			"PackagePath" => "...";	// Will be prefixed with /var/mobile/Media/
+//									// if PackageType="Developer", it should be a pointer to an expanded .app
+//									// containing code signature stuff, etc.
+//			"ClientOptions" = { "PackageType" = "Developer"; "ApplicationSINF" = ... "; "iTunesMetadata" = "...",  }
+//							  { "PackageType" = "Customer";
+//							  { "PackageType" = "CarrierBundle"; ...
+//		<- { Status => Complete; }
+//		<- { Status => "..."; PercentComplete = ... }
 // { "Command" => "Upgrade"; "PackagePath" => "..." }
+//
+
+2010-06-08 20:01:34.628 iPodBackup[40130:a0f] operationContinues::{
+    PercentComplete = 0;
+    Status = TakingInstallLock;
+}
+2010-06-08 20:01:34.640 iPodBackup[40130:a0f] operationContinues::{
+    PercentComplete = 5;
+    Status = CreatingStagingDirectory;
+}
+2010-06-08 20:01:34.656 iPodBackup[40130:a0f] operationContinues::{
+    PercentComplete = 10;
+    Status = StagingPackage;
+}
+2010-06-08 20:01:34.694 iPodBackup[40130:a0f] operationContinues::{
+    PercentComplete = 15;
+    Status = ExtractingPackage;
+}
+2010-06-08 20:01:34.697 iPodBackup[40130:a0f] operationContinues::{
+    PercentComplete = 20;
+    Status = InspectingPackage;
+}
+2010-06-08 20:01:34.701 iPodBackup[40130:a0f] operationContinues::{
+    PercentComplete = 30;
+    Status = PreflightingApplication;
+}
+2010-06-08 20:01:34.704 iPodBackup[40130:a0f] operationContinues::{
+    PercentComplete = 30;
+    Status = InstallingEmbeddedProfile;
+}
+2010-06-08 20:01:34.708 iPodBackup[40130:a0f] operationContinues::{
+    PercentComplete = 40;
+    Status = VerifyingApplication;
+}
+2010-06-08 20:01:34.722 iPodBackup[40130:a0f] operationContinues::{
+    Error = BundleVerificationFailed;
+}
+2010-06-08 20:01:34.728 iPodBackup[40130:a0f] operationContinues::{
+    Error = APIInternalError;
+}
+2010-06-08 20:01:34.730 iPodBackup[40130:a0f] operationCompleted::{
+    ClientOptions =     {
+        PackageType = Developer;
+    };
+    Command = Install;
+    PackagePath = "PublicStaging/AncientFrogHD.app";
+}
+
 // { "Command" => "Uninstall";	"ApplicationIdentifier" => ...; ClientOptions => ... }
-// { "Command" => "Archive";	"ApplicationIdentifier" => ...; ClientOptions => ... }
-// { "Command" => "Restore";	"ApplicationIdentifier" => ...; ClientOptions => ... }
+
 // { "Command" => "RemoveArchive";	"ApplicationIdentifier" => ...; ClientOptions => ... }
-// { "Command" => "LookupArchives"; ClientOptions => ... }
-//		<- { Status => Complete; LookupResult => {...} }
 // { "Command" => "CheckCapabilitiesMatch"; Capabilities => ...; ClientOptions => ... }
 //		<- { Status => Complete; LookupResult => ... }
 //		<- { Error = APIInternalError; }
+//
+
+2010-06-08 20:16:59.431 iPodBackup[40417:a0f] operationStarted::{
+    ClientOptions =     {
+        PackageType = Developer;
+    };
+    Command = Install;
+    PackagePath = "PublicStaging/Rooms.app";
+}
+2010-06-08 20:16:59.462 iPodBackup[40417:a0f] operationContinues::{
+    PercentComplete = 0;
+    Status = TakingInstallLock;
+}
+2010-06-08 20:16:59.479 iPodBackup[40417:a0f] operationContinues::{
+    PercentComplete = 5;
+    Status = CreatingStagingDirectory;
+}
+2010-06-08 20:16:59.488 iPodBackup[40417:a0f] operationContinues::{
+    PercentComplete = 10;
+    Status = StagingPackage;
+}
+2010-06-08 20:16:59.504 iPodBackup[40417:a0f] operationContinues::{
+    PercentComplete = 15;
+    Status = ExtractingPackage;
+}
+2010-06-08 20:16:59.511 iPodBackup[40417:a0f] operationContinues::{
+    PercentComplete = 20;
+    Status = InspectingPackage;
+}
+2010-06-08 20:16:59.518 iPodBackup[40417:a0f] operationContinues::{
+    PercentComplete = 30;
+    Status = PreflightingApplication;
+}
+2010-06-08 20:16:59.526 iPodBackup[40417:a0f] operationContinues::{
+    PercentComplete = 30;
+    Status = InstallingEmbeddedProfile;
+}
+2010-06-08 20:16:59.533 iPodBackup[40417:a0f] operationContinues::{
+    PercentComplete = 40;
+    Status = VerifyingApplication;
+}
+2010-06-08 20:17:02.915 iPodBackup[40417:a0f] operationContinues::{
+    PercentComplete = 50;
+    Status = CreatingContainer;
+}
+2010-06-08 20:17:02.926 iPodBackup[40417:a0f] operationContinues::{
+    PercentComplete = 60;
+    Status = InstallingApplication;
+}
+2010-06-08 20:17:02.935 iPodBackup[40417:a0f] operationContinues::{
+    PercentComplete = 70;
+    Status = PostflightingApplication;
+}
+2010-06-08 20:17:02.939 iPodBackup[40417:a0f] operationContinues::{
+    PercentComplete = 80;
+    Status = SandboxingApplication;
+}
+2010-06-08 20:17:02.977 iPodBackup[40417:a0f] operationContinues::{
+    PercentComplete = 90;
+    Status = GeneratingApplicationMap;
+}
+2010-06-08 20:17:04.943 iPodBackup[40417:a0f] operationContinues::{
+    Status = Complete;
+}
 #endif
 
+- (BOOL)install:(NSString*)pathname
+{
+	NSDictionary *message;
+	message = [NSDictionary dictionaryWithObjectsAndKeys:
+					// value					key
+					@"Install",				@"Command",
+					pathname,				@"PackagePath",
+					[NSDictionary dictionaryWithObjectsAndKeys:
+						@"Developer", @"PackageType",
+						nil],				@"ClientOptions",
+					nil];
+	[self performDelegateSelector:@selector(operationStarted:) withObject:message];
+	if ([self sendXMLRequest:message]) {
+		for (;;) {
+			// read next slab of information
+			// The reply will contain an entry for Status, a string from the following:
+			//	"RemovingArchive"
+			//	"Complete"
+			// All except "Complete" also include PercentageComplete, an integer between 0 and 100 (but it goes up and down)
+			//
+			// Looks like instead of Status, we can get:
+			// Error = APIEpicFail;
+			// (that happened if I passed in a missing bundle-id
+			// Error = APIInternalError
+			// (that happened if I kept reading?
+			NSDictionary *reply = [self readXMLReply];
+			if (!reply) break;
+			[self performDelegateSelector:@selector(operationContinues:) withObject:reply];
+			NSString *s = [reply objectForKey:@"Status"];
+			if ([s isEqual:@"Complete"]) break;
+		}
+	}
+	[self performDelegateSelector:@selector(operationCompleted:) withObject:message];
+	return NO;
+}
+
 @end
 
 @implementation AMSyslogRelay
 			// which are \0 terminated - they may contain \n characters within
 			// a record, and they never seem to send us an unterminated message
 			// (again, the server code seems to preclude it)
+			//
+			// Control characters seem to be escaped with \ - ie, tab comes through as \ followed by t
 			UInt8 buffer[0x4000];
 			const CFIndex len = CFReadStreamRead(stream,buffer,sizeof(buffer));
 			if (len) {
 				p = buffer;
 				buffer[sizeof(buffer)-1] = '\000';
 				while (left>0) {
+					// remove leading newlines
+					while (*p == '\n') {
+						if (--left) p++;
+						else break;
+					}
 					q = p;
 					while (*q && left>0) {q++;left--;}
 					if (left) {
 	[super dealloc];
 }
 
-- (id)initWithDevice:(AMDevice*)device listener:(id)listener message:(SEL)message
+- (id)initWithAMDevice:(AMDevice*)device listener:(id)listener message:(SEL)message
 {
 	if (self = [super initWithName:@"com.apple.syslog_relay" onDevice:device]) {
 		_listener = listener;
 	return(result);
 }
 
-- (id)initWithDevice:(AMDevice*)device
+- (id)initWithAMDevice:(AMDevice*)device
 {
 	if (self = [super initWithName:@"com.apple.mobile.file_relay" onDevice:device]) {
 		_used = NO;
 	if (status != ERR_SUCCESS) NSLog(@"AMDListenForNotifications returned %lx",status);
 }
 
-- (id)initWithDevice:(AMDevice*)device
+- (id)initWithAMDevice:(AMDevice*)device
 {
 	if (self = [super initWithName:@"com.apple.mobile.notification_proxy" onDevice:device]) {
 		_messages = [NSMutableDictionary new];
 	return nil;
 }
 
-- (id)initWithDevice:(AMDevice*)device
+- (id)initWithAMDevice:(AMDevice*)device
 {
 	if (self = [super initWithName:@"com.apple.springboardservices" onDevice:device]) {
 		// nothing special
 
 @implementation AMMobileSync
 
-- (id)initWithDevice:(AMDevice*)device
+- (id)initWithAMDevice:(AMDevice*)device
 {
 	if (self = [super initWithName:@"com.apple.mobilesync" onDevice:device]) {
 		// wait for the version exchange
 
 http://iphone-docs.org/doku.php?id=docs:protocols:screenshot
 
+http://libimobiledevice.org/docs/mobilesync.html
+
 Like other DeviceLink protocols, it starts with a simple handshake (binary plists represented as ruby objects):
 
 < ["DLMessageVersionExchange", 100, 0]
 	AFCMediaDirectory *result = nil;
 	if ([self deviceConnect]) {
 		if ([self startSession]) {
-			result = [[AFCMediaDirectory alloc] initWithDevice:self];
+			result = [[AFCMediaDirectory alloc] initWithAMDevice:self];
 			[self stopSession];
 		}
 		[self deviceDisconnect];
 	AFCCrashLogDirectory *result = nil;
 	if ([self deviceConnect]) {
 		if ([self startSession]) {
-			result = [[AFCCrashLogDirectory alloc] initWithDevice:self];
+			result = [[AFCCrashLogDirectory alloc] initWithAMDevice:self];
 			[self stopSession];
 		}
 		[self deviceDisconnect];
 	AFCRootDirectory *result = nil;
 	if ([self deviceConnect]) {
 		if ([self startSession]) {
-			result = [[AFCRootDirectory alloc] initWithDevice:self];
+			result = [[AFCRootDirectory alloc] initWithAMDevice:self];
 			[self stopSession];
 		}
 		[self deviceDisconnect];
 	AFCApplicationDirectory *result = nil;
 	if ([self deviceConnect]) {
 		if ([self startSession]) {
-			result = [[AFCApplicationDirectory alloc] initWithDevice:self andName:name];
+			result = [[AFCApplicationDirectory alloc] initWithAMDevice:self andName:name];
 			[self stopSession];
 		}
 		[self deviceDisconnect];
 	return result;
 }
 
-- (AMInstallationProxy*)newAMInstallationProxy
+- (AMInstallationProxy*)newAMInstallationProxyWithDelegate:(id<AMInstallationProxyDelegate>)delegate
 {
 	AMInstallationProxy *result = nil;
 	if ([self deviceConnect]) {
 		if ([self startSession]) {
-			result = [[AMInstallationProxy alloc] initWithDevice:self];
+			result = [[AMInstallationProxy alloc] initWithAMDevice:self];
+			result.delegate = delegate;
 			[self stopSession];
 		}
 		[self deviceDisconnect];
 	AMNotificationProxy *result = nil;
 	if ([self deviceConnect]) {
 		if ([self startSession]) {
-			result = [[AMNotificationProxy alloc] initWithDevice:self];
+			result = [[AMNotificationProxy alloc] initWithAMDevice:self];
 			[self stopSession];
 		}
 		[self deviceDisconnect];
 	AMSpringboardServices *result = nil;
 	if ([self deviceConnect]) {
 		if ([self startSession]) {
-			result = [[AMSpringboardServices alloc] initWithDevice:self];
+			result = [[AMSpringboardServices alloc] initWithAMDevice:self];
 			[self stopSession];
 		}
 		[self deviceDisconnect];
 	AMSyslogRelay *result = nil;
 	if ([self deviceConnect]) {
 		if ([self startSession]) {
-			result = [[AMSyslogRelay alloc] initWithDevice:self listener:listener message:message];
+			result = [[AMSyslogRelay alloc] initWithAMDevice:self listener:listener message:message];
 			[self stopSession];
 		}
 		[self deviceDisconnect];
 	AMFileRelay *result = nil;
 	if ([self deviceConnect]) {
 		if ([self startSession]) {
-			result = [[AMFileRelay alloc] initWithDevice:self];
+			result = [[AMFileRelay alloc] initWithAMDevice:self];
 			[self stopSession];
 		}
 		[self deviceDisconnect];
 	AMMobileSync *result = nil;
 	if ([self deviceConnect]) {
 		if ([self startSession]) {
-			result = [[AMMobileSync alloc] initWithDevice:self];
+			result = [[AMMobileSync alloc] initWithAMDevice:self];
 			[self stopSession];
 		}
 		[self deviceDisconnect];
 		if ([self startSession]) {
 			CFDictionaryRef dict = nil;
 			if (
-				// "User", "System", "Internal" ??
 				[self checkStatus:AMDeviceLookupApplications(_device, nil, &dict)
 							 from:"AMDeviceLookupApplications"]
 			) {
 				result = [[NSMutableArray new] autorelease];
 				for (NSString *key in (NSDictionary*)dict) {
 					NSDictionary *info = [(NSDictionary*)dict objectForKey:key];
+					// "User", "System", "Internal" ??
 					if ([[info objectForKey:@"ApplicationType"] isEqual:@"User"]) {
 						AMApplication *newapp = [[AMApplication alloc] initWithDictionary:info];
 						[result addObject:newapp];
 + (MobileDeviceAccess*)singleton
 {
 	static MobileDeviceAccess *_singleton = nil;
-
-	if (_singleton == nil) {
-		_singleton = [[MobileDeviceAccess alloc] init];
-		
+	@synchronized(self) {
+		if (_singleton == nil) {
+			_singleton = [[MobileDeviceAccess alloc] init];
+			
+		}
 	}
 	return _singleton;
 }