diff --git a/osx/Firefly Helper/English.lproj/InfoPlist.strings b/osx/Firefly Helper/English.lproj/InfoPlist.strings
new file mode 100644
index 00000000..1427aaaf
Binary files /dev/null and b/osx/Firefly Helper/English.lproj/InfoPlist.strings differ
diff --git a/osx/Firefly Helper/English.lproj/MainMenu.nib/classes.nib b/osx/Firefly Helper/English.lproj/MainMenu.nib/classes.nib
new file mode 100644
index 00000000..e547306d
--- /dev/null
+++ b/osx/Firefly Helper/English.lproj/MainMenu.nib/classes.nib
@@ -0,0 +1,18 @@
+{
+ IBClasses = (
+ {
+ ACTIONS = {prefsMenuChosen = id; startStopMenuChosen = id; };
+ CLASS = FireflyHelper;
+ LANGUAGE = ObjC;
+ OUTLETS = {statusMenu = NSMenu; };
+ SUPERCLASS = NSObject;
+ },
+ {CLASS = FirstResponder; LANGUAGE = ObjC; SUPERCLASS = NSObject; },
+ {
+ CLASS = MyASKNibObjectInfoManager;
+ LANGUAGE = ObjC;
+ SUPERCLASS = ASKNibObjectInfoManager;
+ }
+ );
+ IBVersion = 1;
+}
\ No newline at end of file
diff --git a/osx/Firefly Helper/English.lproj/MainMenu.nib/info.nib b/osx/Firefly Helper/English.lproj/MainMenu.nib/info.nib
new file mode 100644
index 00000000..1459ec90
--- /dev/null
+++ b/osx/Firefly Helper/English.lproj/MainMenu.nib/info.nib
@@ -0,0 +1,23 @@
+
+
+
+
+ IBDocumentLocation
+ 155 322 356 240 0 0 1440 878
+ IBEditorPositions
+
+ 209
+ 283 632 160 106 0 0 1440 878
+ 29
+ 521 639 125 44 0 0 1440 878
+
+ IBFramework Version
+ 446.1
+ IBOpenObjects
+
+ 209
+
+ IBSystem Version
+ 8J135
+
+
diff --git a/osx/Firefly Helper/English.lproj/MainMenu.nib/keyedobjects.nib b/osx/Firefly Helper/English.lproj/MainMenu.nib/keyedobjects.nib
new file mode 100644
index 00000000..7f747c51
Binary files /dev/null and b/osx/Firefly Helper/English.lproj/MainMenu.nib/keyedobjects.nib differ
diff --git a/osx/Firefly Helper/English.lproj/MainMenu.nib/objects.nib b/osx/Firefly Helper/English.lproj/MainMenu.nib/objects.nib
new file mode 100644
index 00000000..9f5a751b
Binary files /dev/null and b/osx/Firefly Helper/English.lproj/MainMenu.nib/objects.nib differ
diff --git a/osx/Firefly Helper/English.lproj/MainMenu~.nib/classes.nib b/osx/Firefly Helper/English.lproj/MainMenu~.nib/classes.nib
new file mode 100644
index 00000000..e547306d
--- /dev/null
+++ b/osx/Firefly Helper/English.lproj/MainMenu~.nib/classes.nib
@@ -0,0 +1,18 @@
+{
+ IBClasses = (
+ {
+ ACTIONS = {prefsMenuChosen = id; startStopMenuChosen = id; };
+ CLASS = FireflyHelper;
+ LANGUAGE = ObjC;
+ OUTLETS = {statusMenu = NSMenu; };
+ SUPERCLASS = NSObject;
+ },
+ {CLASS = FirstResponder; LANGUAGE = ObjC; SUPERCLASS = NSObject; },
+ {
+ CLASS = MyASKNibObjectInfoManager;
+ LANGUAGE = ObjC;
+ SUPERCLASS = ASKNibObjectInfoManager;
+ }
+ );
+ IBVersion = 1;
+}
\ No newline at end of file
diff --git a/osx/Firefly Helper/English.lproj/MainMenu~.nib/info.nib b/osx/Firefly Helper/English.lproj/MainMenu~.nib/info.nib
new file mode 100644
index 00000000..1459ec90
--- /dev/null
+++ b/osx/Firefly Helper/English.lproj/MainMenu~.nib/info.nib
@@ -0,0 +1,23 @@
+
+
+
+
+ IBDocumentLocation
+ 155 322 356 240 0 0 1440 878
+ IBEditorPositions
+
+ 209
+ 283 632 160 106 0 0 1440 878
+ 29
+ 521 639 125 44 0 0 1440 878
+
+ IBFramework Version
+ 446.1
+ IBOpenObjects
+
+ 209
+
+ IBSystem Version
+ 8J135
+
+
diff --git a/osx/Firefly Helper/English.lproj/MainMenu~.nib/keyedobjects.nib b/osx/Firefly Helper/English.lproj/MainMenu~.nib/keyedobjects.nib
new file mode 100644
index 00000000..7f747c51
Binary files /dev/null and b/osx/Firefly Helper/English.lproj/MainMenu~.nib/keyedobjects.nib differ
diff --git a/osx/Firefly Helper/English.lproj/MainMenu~.nib/objects.nib b/osx/Firefly Helper/English.lproj/MainMenu~.nib/objects.nib
new file mode 100644
index 00000000..9f5a751b
Binary files /dev/null and b/osx/Firefly Helper/English.lproj/MainMenu~.nib/objects.nib differ
diff --git a/osx/Firefly Helper/Firefly Helper.xcodeproj/project.pbxproj b/osx/Firefly Helper/Firefly Helper.xcodeproj/project.pbxproj
new file mode 100644
index 00000000..914cd9fa
--- /dev/null
+++ b/osx/Firefly Helper/Firefly Helper.xcodeproj/project.pbxproj
@@ -0,0 +1,348 @@
+// !$*UTF8*$!
+{
+ archiveVersion = 1;
+ classes = {
+ };
+ objectVersion = 42;
+ objects = {
+
+/* Begin PBXBuildFile section */
+ 10AE11000A39121A00525B7B /* FireflyHelper.m in Sources */ = {isa = PBXBuildFile; fileRef = 10AE10FE0A39121A00525B7B /* FireflyHelper.m */; };
+ 10AE11DD0A3929D700525B7B /* FireflyHelper.icns in Resources */ = {isa = PBXBuildFile; fileRef = 10AE11DC0A3929D700525B7B /* FireflyHelper.icns */; };
+ 10C790F80A65BCD500732D76 /* ff_logo_status_menu.tif in Resources */ = {isa = PBXBuildFile; fileRef = 10C790F70A65BCD400732D76 /* ff_logo_status_menu.tif */; };
+ 10C791120A65C54000732D76 /* FireflyServer.m in Sources */ = {isa = PBXBuildFile; fileRef = 10C791110A65C54000732D76 /* FireflyServer.m */; };
+ 10C792820A65EF1800732D76 /* Server in Resources */ = {isa = PBXBuildFile; fileRef = 10C792440A65EF1800732D76 /* Server */; };
+ 8D11072A0486CEB800E47090 /* MainMenu.nib in Resources */ = {isa = PBXBuildFile; fileRef = 29B97318FDCFA39411CA2CEA /* MainMenu.nib */; };
+ 8D11072B0486CEB800E47090 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 089C165CFE840E0CC02AAC07 /* InfoPlist.strings */; };
+ 8D11072D0486CEB800E47090 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 29B97316FDCFA39411CA2CEA /* main.m */; settings = {ATTRIBUTES = (); }; };
+ 8D11072F0486CEB800E47090 /* Cocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1058C7A1FEA54F0111CA2CBB /* Cocoa.framework */; };
+/* End PBXBuildFile section */
+
+/* Begin PBXBuildStyle section */
+ E423408A0A771CDF00669C87 /* Development */ = {
+ isa = PBXBuildStyle;
+ buildSettings = {
+ COPY_PHASE_STRIP = NO;
+ };
+ name = Development;
+ };
+ E423408B0A771CDF00669C87 /* Deployment */ = {
+ isa = PBXBuildStyle;
+ buildSettings = {
+ COPY_PHASE_STRIP = YES;
+ };
+ name = Deployment;
+ };
+/* End PBXBuildStyle section */
+
+/* Begin PBXFileReference section */
+ 089C165DFE840E0CC02AAC07 /* English */ = {isa = PBXFileReference; fileEncoding = 10; lastKnownFileType = text.plist.strings; name = English; path = English.lproj/InfoPlist.strings; sourceTree = ""; };
+ 1058C7A1FEA54F0111CA2CBB /* Cocoa.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Cocoa.framework; path = /System/Library/Frameworks/Cocoa.framework; sourceTree = ""; };
+ 10AE10FE0A39121A00525B7B /* FireflyHelper.m */ = {isa = PBXFileReference; fileEncoding = 30; lastKnownFileType = sourcecode.c.objc; path = FireflyHelper.m; sourceTree = ""; };
+ 10AE10FF0A39121A00525B7B /* FireflyHelper.h */ = {isa = PBXFileReference; fileEncoding = 30; lastKnownFileType = sourcecode.c.h; path = FireflyHelper.h; sourceTree = ""; };
+ 10AE11DC0A3929D700525B7B /* FireflyHelper.icns */ = {isa = PBXFileReference; lastKnownFileType = image.icns; path = FireflyHelper.icns; sourceTree = ""; };
+ 10C78FB60A637C5300732D76 /* FireflyPrefsProtocol.h */ = {isa = PBXFileReference; fileEncoding = 30; lastKnownFileType = sourcecode.c.h; name = FireflyPrefsProtocol.h; path = ../FireflyPrefsProtocol.h; sourceTree = ""; };
+ 10C790F70A65BCD400732D76 /* ff_logo_status_menu.tif */ = {isa = PBXFileReference; lastKnownFileType = image.tiff; path = ff_logo_status_menu.tif; sourceTree = ""; };
+ 10C791100A65C54000732D76 /* FireflyServer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FireflyServer.h; sourceTree = ""; };
+ 10C791110A65C54000732D76 /* FireflyServer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FireflyServer.m; sourceTree = ""; };
+ 10C792440A65EF1800732D76 /* Server */ = {isa = PBXFileReference; lastKnownFileType = folder; path = Server; sourceTree = ""; };
+ 10C7931F0A661A2F00732D76 /* FireflyCommon.h */ = {isa = PBXFileReference; fileEncoding = 30; lastKnownFileType = sourcecode.c.h; name = FireflyCommon.h; path = ../FireflyCommon.h; sourceTree = ""; };
+ 13E42FB307B3F0F600E4EEF1 /* CoreData.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreData.framework; path = /System/Library/Frameworks/CoreData.framework; sourceTree = ""; };
+ 29B97316FDCFA39411CA2CEA /* main.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; };
+ 29B97319FDCFA39411CA2CEA /* English */ = {isa = PBXFileReference; lastKnownFileType = wrapper.nib; name = English; path = English.lproj/MainMenu.nib; sourceTree = ""; };
+ 29B97324FDCFA39411CA2CEA /* AppKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AppKit.framework; path = /System/Library/Frameworks/AppKit.framework; sourceTree = ""; };
+ 29B97325FDCFA39411CA2CEA /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = /System/Library/Frameworks/Foundation.framework; sourceTree = ""; };
+ 32CA4F630368D1EE00C91783 /* Firefly Minder_Prefix.pch */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "Firefly Minder_Prefix.pch"; sourceTree = ""; };
+ 8D1107310486CEB800E47090 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; };
+ 8D1107320486CEB800E47090 /* Firefly Helper.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Firefly Helper.app"; sourceTree = BUILT_PRODUCTS_DIR; };
+/* End PBXFileReference section */
+
+/* Begin PBXFrameworksBuildPhase section */
+ 8D11072E0486CEB800E47090 /* Frameworks */ = {
+ isa = PBXFrameworksBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 8D11072F0486CEB800E47090 /* Cocoa.framework in Frameworks */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXFrameworksBuildPhase section */
+
+/* Begin PBXGroup section */
+ 080E96DDFE201D6D7F000001 /* Classes */ = {
+ isa = PBXGroup;
+ children = (
+ 10C78FB60A637C5300732D76 /* FireflyPrefsProtocol.h */,
+ 10AE10FE0A39121A00525B7B /* FireflyHelper.m */,
+ 10C791100A65C54000732D76 /* FireflyServer.h */,
+ 10C791110A65C54000732D76 /* FireflyServer.m */,
+ 10AE10FF0A39121A00525B7B /* FireflyHelper.h */,
+ );
+ name = Classes;
+ sourceTree = "";
+ };
+ 1058C7A0FEA54F0111CA2CBB /* Linked Frameworks */ = {
+ isa = PBXGroup;
+ children = (
+ 1058C7A1FEA54F0111CA2CBB /* Cocoa.framework */,
+ );
+ name = "Linked Frameworks";
+ sourceTree = "";
+ };
+ 1058C7A2FEA54F0111CA2CBB /* Other Frameworks */ = {
+ isa = PBXGroup;
+ children = (
+ 29B97324FDCFA39411CA2CEA /* AppKit.framework */,
+ 13E42FB307B3F0F600E4EEF1 /* CoreData.framework */,
+ 29B97325FDCFA39411CA2CEA /* Foundation.framework */,
+ );
+ name = "Other Frameworks";
+ sourceTree = "";
+ };
+ 19C28FACFE9D520D11CA2CBB /* Products */ = {
+ isa = PBXGroup;
+ children = (
+ 8D1107320486CEB800E47090 /* Firefly Helper.app */,
+ );
+ name = Products;
+ sourceTree = "";
+ };
+ 29B97314FDCFA39411CA2CEA /* Firefly Minder */ = {
+ isa = PBXGroup;
+ children = (
+ 080E96DDFE201D6D7F000001 /* Classes */,
+ 10C7931F0A661A2F00732D76 /* FireflyCommon.h */,
+ 29B97315FDCFA39411CA2CEA /* Other Sources */,
+ 29B97317FDCFA39411CA2CEA /* Resources */,
+ 29B97323FDCFA39411CA2CEA /* Frameworks */,
+ 19C28FACFE9D520D11CA2CBB /* Products */,
+ );
+ name = "Firefly Minder";
+ sourceTree = "";
+ };
+ 29B97315FDCFA39411CA2CEA /* Other Sources */ = {
+ isa = PBXGroup;
+ children = (
+ 32CA4F630368D1EE00C91783 /* Firefly Minder_Prefix.pch */,
+ 29B97316FDCFA39411CA2CEA /* main.m */,
+ );
+ name = "Other Sources";
+ sourceTree = "";
+ };
+ 29B97317FDCFA39411CA2CEA /* Resources */ = {
+ isa = PBXGroup;
+ children = (
+ 10C792440A65EF1800732D76 /* Server */,
+ 8D1107310486CEB800E47090 /* Info.plist */,
+ 089C165CFE840E0CC02AAC07 /* InfoPlist.strings */,
+ 29B97318FDCFA39411CA2CEA /* MainMenu.nib */,
+ 10AE11DC0A3929D700525B7B /* FireflyHelper.icns */,
+ 10C790F70A65BCD400732D76 /* ff_logo_status_menu.tif */,
+ );
+ name = Resources;
+ sourceTree = "";
+ };
+ 29B97323FDCFA39411CA2CEA /* Frameworks */ = {
+ isa = PBXGroup;
+ children = (
+ 1058C7A0FEA54F0111CA2CBB /* Linked Frameworks */,
+ 1058C7A2FEA54F0111CA2CBB /* Other Frameworks */,
+ );
+ name = Frameworks;
+ sourceTree = "";
+ };
+/* End PBXGroup section */
+
+/* Begin PBXNativeTarget section */
+ 8D1107260486CEB800E47090 /* Firefly Helper */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = C01FCF4A08A954540054247B /* Build configuration list for PBXNativeTarget "Firefly Helper" */;
+ buildPhases = (
+ 8D1107290486CEB800E47090 /* Resources */,
+ 8D11072C0486CEB800E47090 /* Sources */,
+ 8D11072E0486CEB800E47090 /* Frameworks */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ );
+ name = "Firefly Helper";
+ productInstallPath = "$(HOME)/Applications";
+ productName = "Firefly Minder";
+ productReference = 8D1107320486CEB800E47090 /* Firefly Helper.app */;
+ productType = "com.apple.product-type.application";
+ };
+/* End PBXNativeTarget section */
+
+/* Begin PBXProject section */
+ 29B97313FDCFA39411CA2CEA /* Project object */ = {
+ isa = PBXProject;
+ buildConfigurationList = C01FCF4E08A954540054247B /* Build configuration list for PBXProject "Firefly Helper" */;
+ buildSettings = {
+ };
+ buildStyles = (
+ E423408A0A771CDF00669C87 /* Development */,
+ E423408B0A771CDF00669C87 /* Deployment */,
+ );
+ hasScannedForEncodings = 1;
+ mainGroup = 29B97314FDCFA39411CA2CEA /* Firefly Minder */;
+ projectDirPath = "";
+ targets = (
+ 8D1107260486CEB800E47090 /* Firefly Helper */,
+ );
+ };
+/* End PBXProject section */
+
+/* Begin PBXResourcesBuildPhase section */
+ 8D1107290486CEB800E47090 /* Resources */ = {
+ isa = PBXResourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 8D11072A0486CEB800E47090 /* MainMenu.nib in Resources */,
+ 8D11072B0486CEB800E47090 /* InfoPlist.strings in Resources */,
+ 10AE11DD0A3929D700525B7B /* FireflyHelper.icns in Resources */,
+ 10C790F80A65BCD500732D76 /* ff_logo_status_menu.tif in Resources */,
+ 10C792820A65EF1800732D76 /* Server in Resources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXResourcesBuildPhase section */
+
+/* Begin PBXSourcesBuildPhase section */
+ 8D11072C0486CEB800E47090 /* Sources */ = {
+ isa = PBXSourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 8D11072D0486CEB800E47090 /* main.m in Sources */,
+ 10AE11000A39121A00525B7B /* FireflyHelper.m in Sources */,
+ 10C791120A65C54000732D76 /* FireflyServer.m in Sources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXSourcesBuildPhase section */
+
+/* Begin PBXVariantGroup section */
+ 089C165CFE840E0CC02AAC07 /* InfoPlist.strings */ = {
+ isa = PBXVariantGroup;
+ children = (
+ 089C165DFE840E0CC02AAC07 /* English */,
+ );
+ name = InfoPlist.strings;
+ sourceTree = "";
+ };
+ 29B97318FDCFA39411CA2CEA /* MainMenu.nib */ = {
+ isa = PBXVariantGroup;
+ children = (
+ 29B97319FDCFA39411CA2CEA /* English */,
+ );
+ name = MainMenu.nib;
+ sourceTree = "";
+ };
+/* End PBXVariantGroup section */
+
+/* Begin XCBuildConfiguration section */
+ C01FCF4B08A954540054247B /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ COPY_PHASE_STRIP = NO;
+ GCC_DYNAMIC_NO_PIC = NO;
+ GCC_ENABLE_FIX_AND_CONTINUE = YES;
+ GCC_MODEL_TUNING = G5;
+ GCC_OPTIMIZATION_LEVEL = 0;
+ INFOPLIST_FILE = Info.plist;
+ INSTALL_PATH = "$(HOME)/Applications";
+ LIBRARY_SEARCH_PATHS = (
+ "$(inherited)",
+ "$(LIBRARY_SEARCH_PATHS_QUOTED_1)",
+ );
+ LIBRARY_SEARCH_PATHS_QUOTED_1 = "\"$(SRCROOT)/Server/plugins\"";
+ PRODUCT_NAME = "Firefly Helper";
+ WRAPPER_EXTENSION = app;
+ ZERO_LINK = NO;
+ };
+ name = Debug;
+ };
+ C01FCF4C08A954540054247B /* Release */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ARCHS = (
+ ppc,
+ i386,
+ );
+ GCC_GENERATE_DEBUGGING_SYMBOLS = NO;
+ GCC_MODEL_TUNING = G5;
+ INFOPLIST_FILE = Info.plist;
+ INSTALL_PATH = "$(HOME)/Applications";
+ LIBRARY_SEARCH_PATHS = (
+ "$(inherited)",
+ "$(LIBRARY_SEARCH_PATHS_QUOTED_1)",
+ );
+ LIBRARY_SEARCH_PATHS_QUOTED_1 = "\"$(SRCROOT)/Server/plugins\"";
+ PRODUCT_NAME = "Firefly Helper";
+ WRAPPER_EXTENSION = app;
+ };
+ name = Release;
+ };
+ C01FCF4F08A954540054247B /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ GCC_ENABLE_OBJC_EXCEPTIONS = YES;
+ GCC_VERSION_i386 = 4.0;
+ GCC_VERSION_ppc = 3.3;
+ GCC_WARN_ABOUT_RETURN_TYPE = YES;
+ GCC_WARN_UNUSED_VARIABLE = YES;
+ MACOSX_DEPLOYMENT_TARGET = "";
+ MACOSX_DEPLOYMENT_TARGET_i386 = 10.4;
+ MACOSX_DEPLOYMENT_TARGET_ppc = 10.3;
+ PREBINDING = NO;
+ SDKROOT = /Developer/SDKs/MacOSX10.4u.sdk;
+ SDKROOT_i386 = /Developer/SDKs/MacOSX10.4u.sdk;
+ SDKROOT_ppc = /Developer/SDKs/MacOSX10.3.9.sdk;
+ ZERO_LINK = NO;
+ };
+ name = Debug;
+ };
+ C01FCF5008A954540054247B /* Release */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ GCC_ENABLE_OBJC_EXCEPTIONS = YES;
+ GCC_VERSION_i386 = 4.0;
+ GCC_VERSION_ppc = 3.3;
+ GCC_WARN_ABOUT_RETURN_TYPE = YES;
+ GCC_WARN_UNUSED_VARIABLE = YES;
+ MACOSX_DEPLOYMENT_TARGET = "";
+ MACOSX_DEPLOYMENT_TARGET_i386 = 10.4;
+ MACOSX_DEPLOYMENT_TARGET_ppc = 10.3;
+ PREBINDING = NO;
+ SDKROOT = /Developer/SDKs/MacOSX10.4u.sdk;
+ SDKROOT_i386 = /Developer/SDKs/MacOSX10.4u.sdk;
+ SDKROOT_ppc = /Developer/SDKs/MacOSX10.3.9.sdk;
+ ZERO_LINK = NO;
+ };
+ name = Release;
+ };
+/* End XCBuildConfiguration section */
+
+/* Begin XCConfigurationList section */
+ C01FCF4A08A954540054247B /* Build configuration list for PBXNativeTarget "Firefly Helper" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ C01FCF4B08A954540054247B /* Debug */,
+ C01FCF4C08A954540054247B /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+ C01FCF4E08A954540054247B /* Build configuration list for PBXProject "Firefly Helper" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ C01FCF4F08A954540054247B /* Debug */,
+ C01FCF5008A954540054247B /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+/* End XCConfigurationList section */
+ };
+ rootObject = 29B97313FDCFA39411CA2CEA /* Project object */;
+}
diff --git a/osx/Firefly Helper/Firefly Minder_Prefix.pch b/osx/Firefly Helper/Firefly Minder_Prefix.pch
new file mode 100644
index 00000000..d608b080
--- /dev/null
+++ b/osx/Firefly Helper/Firefly Minder_Prefix.pch
@@ -0,0 +1,7 @@
+//
+// Prefix header for all source files of the 'Firefly Minder' target in the 'Firefly Minder' project
+//
+
+#ifdef __OBJC__
+ #import
+#endif
diff --git a/osx/Firefly Helper/FireflyHelper.h b/osx/Firefly Helper/FireflyHelper.h
new file mode 100644
index 00000000..f16b103a
--- /dev/null
+++ b/osx/Firefly Helper/FireflyHelper.h
@@ -0,0 +1,57 @@
+//
+// FireflyHelper.h
+//
+// The "controller" part of our Model-View-Controller trio, plus what
+// little "view" there is for this mostly-faceless program.
+//
+// The Firefly Helper manages setup and startup of the server, as well
+// as communication withe the Prefs pane via Distributed Objects. It
+// also optionally handles the Status Item (the menu by the clock in
+// the menu bar).
+//
+// Created by Mike Kobb on 7/12/06.
+// Copyright 2006 Roku, LLC. All rights reserved.
+//
+
+#import
+#import "../FireflyPrefsProtocol.h"
+#import "FireflyServer.h"
+
+@interface FireflyHelper : NSObject < FireflyPrefsServerProtocol >
+{
+ IBOutlet NSMenu *statusMenu;
+ NSStatusItem *statusItem;
+
+ NSProtocolChecker *protocolChecker;
+ NSConnection *prefsConnection;
+
+ FireflyServer *fireflyServer;
+
+ // Our client
+ id client;
+ int clientIdent;
+}
+
+- (IBAction)startStopMenuChosen:(id)sender;
+- (IBAction)prefsMenuChosen:(id)sender;
+
+// NSApplication delegate methods
+- (void)applicationDidFinishLaunching:(NSNotification *)aNotification;
+- (void)applicationWillTerminate:(NSNotification *)aNotification;
+
+// private utilities
+- (void)serverNotification:(NSNotification *)theNotification;
+- (BOOL)checkClient;
+
+// wrappers to client calls; trap exceptions
+- (void)tellClientStatusChanged:(FireflyServerStatus)newStatus;
+- (void)tellClientVersionChanged:(NSString *)newVersion;
+- (void)tellClientURLChanged:(NSString *)newURL;
+
+// Methods for status item
+- (void)displayStatusItem;
+- (void)hideStatusItem;
+- (int)numberOfItemsInMenu:(NSMenu *)menu;
+- (void)menuNeedsUpdate:(NSMenu *)menu;
+
+@end
diff --git a/osx/Firefly Helper/FireflyHelper.icns b/osx/Firefly Helper/FireflyHelper.icns
new file mode 100644
index 00000000..f473fc10
Binary files /dev/null and b/osx/Firefly Helper/FireflyHelper.icns differ
diff --git a/osx/Firefly Helper/FireflyHelper.m b/osx/Firefly Helper/FireflyHelper.m
new file mode 100644
index 00000000..bb23c770
--- /dev/null
+++ b/osx/Firefly Helper/FireflyHelper.m
@@ -0,0 +1,622 @@
+//
+// FireflyHelper.m
+//
+// The "controller" part of our Model-View-Controller trio, plus what
+// little "view" there is for this mostly-faceless program.
+//
+// The Firefly Helper manages setup and startup of the server, as well
+// as communication withe the Prefs pane via Distributed Objects. It
+// also optionally handles the Status Item (the menu by the clock in
+// the menu bar).
+//
+// Created by Mike Kobb on 7/12/06.
+// Copyright 2006 Roku, LLC. All rights reserved.
+//
+
+#import "FireflyHelper.h"
+#import "FireflyServer.h"
+#include "../FireflyCommon.h"
+#include
+#include
+#include
+#include
+
+@implementation FireflyHelper
+
+// ---------------------------------------------------------------------------
+// init
+//
+// Note that we create this path in this way primarily because we cribbed
+// the code from the prefs pane, and we eventually will probably
+// change both, so for now it's best to be consistent.
+// ---------------------------------------------------------------------------
+- (id)init
+{
+ protocolChecker = nil;
+ prefsConnection = nil;
+ fireflyServer = nil;
+ statusItem = nil;
+ client = nil;
+ clientIdent = 0;
+
+ return self;
+}
+
+// ---------------------------------------------------------------------------
+// dealloc
+// ---------------------------------------------------------------------------
+- (void)dealloc
+{
+ [protocolChecker release];
+ [prefsConnection release];
+ [statusItem release];
+ [FireflyServer release];
+
+ [super dealloc];
+}
+
+// ---------------------------------------------------------------------------
+// applicationDidFinishLaunching
+//
+// We implement this delegate method so that we will be called when the
+// Firefly Helper app is launched. We check to see if the prefs say
+// we should launch the server when we launch, and if they do, we launch.
+// ---------------------------------------------------------------------------
+- (void)applicationDidFinishLaunching:(NSNotification *)aNotification
+{
+ // Perform any checking of the installation here. Probably just look for
+ // the config file and put up a dialog if not found.
+
+ bool bSuccess = false;
+ NSString *errorString = @"";
+ do // while(false)
+ {
+ // Create and initialize our fireflyServer object
+ NSString *serverPath =
+ [[NSBundle bundleForClass:[self class]] pathForResource:@"firefly"
+ ofType:nil
+ inDirectory:@"Server"];
+ fireflyServer =
+ [[[FireflyServer alloc] initWithServerPath:serverPath] retain];
+ if( nil == fireflyServer )
+ {
+ errorString =
+ NSLocalizedString( @"Failed to initialize Firefly server",
+ @"explanatory text for failure to launch Firefly helper" );
+ break;
+ }
+
+ // Register for notifications from our server. Go ahead and do this before we
+ // start it.
+ [[NSNotificationCenter defaultCenter] addObserver:self
+ selector:@selector(serverNotification:)
+ name:@STATUS_CHANGE
+ object:fireflyServer ];
+ [[NSNotificationCenter defaultCenter] addObserver:self
+ selector:@selector(serverNotification:)
+ name:@VERSION_CHANGE
+ object:fireflyServer ];
+ [[NSNotificationCenter defaultCenter] addObserver:self
+ selector:@selector(serverNotification:)
+ name:@URL_CHANGE
+ object:fireflyServer ];
+
+ // Must call this or Bonjour stuff won't work
+ [fireflyServer setup];
+
+ CFBooleanRef shouldLaunch = CFPreferencesCopyAppValue( CFSTR(FF_PREFS_LAUNCH_AT_LOGIN),
+ CFSTR(FF_PREFS_DOMAIN) );
+ if( NULL != shouldLaunch )
+ {
+ if( CFBooleanGetValue( shouldLaunch ) )
+ [fireflyServer start];
+ CFRelease( shouldLaunch );
+ }
+
+ // Okay, we're open for business. Let's vend our interface for
+ // the prefs pane to use
+ CFStringRef userName = CSCopyUserName( false );
+ NSString *serviceName = [@"FireflyHelper" stringByAppendingString:(NSString*)userName];
+ CFRelease(userName);
+ protocolChecker = [NSProtocolChecker
+ protocolCheckerWithTarget:self
+ protocol:@protocol(FireflyPrefsServerProtocol)];
+ prefsConnection = [NSConnection defaultConnection];
+ [prefsConnection setRootObject:protocolChecker];
+ if( ![prefsConnection registerName:serviceName] )
+ {
+ errorString =
+ NSLocalizedString( @"Unable to open communication channel for Preference pane",
+ @"explanatory text for failure to luanch Firefly helper" );
+ break;
+ }
+
+ // Made it through!
+ bSuccess = true;
+
+ // If we're supposed to put up a Menu Extra (NSStatusItem), do so
+ CFBooleanRef showMenu = CFPreferencesCopyAppValue( CFSTR(FF_PREFS_SHOW_MENU_EXTRA),
+ CFSTR(FF_PREFS_DOMAIN) );
+ if( NULL != showMenu )
+ {
+ if( CFBooleanGetValue( showMenu ) )
+ [self displayStatusItem];
+ CFRelease( showMenu );
+ }
+
+ } while( false );
+
+ // If we encountered a critical failure, we need to display an alert and
+ // then quit.
+ if( !bSuccess )
+ {
+ NSString *alertString = NSLocalizedString( @"Firefly cannot start",
+ @"Alert message when firefly helper can't start" );
+ NSString *quitString = NSLocalizedString( @"Quit",
+ @"Label for quit button in failure alert" );
+
+ NSAlert *alert = [NSAlert alertWithMessageText:alertString
+ defaultButton:quitString
+ alternateButton:nil
+ otherButton:nil
+ informativeTextWithFormat:errorString];
+
+ [alert setAlertStyle:NSCriticalAlertStyle];
+ [NSApp activateIgnoringOtherApps:YES];
+ [alert runModal];
+ [NSApp terminate:self];
+ }
+}
+
+// ---------------------------------------------------------------------------
+// applicationWillTerminate
+//
+// We implement this delegate method so that we will be called when the
+// Firefly Helper app is quitting. When the user logs out, we quit their
+// Firefly server process.
+// ---------------------------------------------------------------------------
+- (void)applicationWillTerminate:(NSNotification *)aNotification
+{
+ [[NSNotificationCenter defaultCenter] removeObserver:self];
+ // Notify the prefs pane we're bailing?
+ [fireflyServer shutdown]; // also stops the server
+ [prefsConnection invalidate];
+}
+
+// ---------------------------------------------------------------------------
+// serverNotification
+//
+// Our handler for notifications from the server
+// ---------------------------------------------------------------------------
+- (void)serverNotification:(NSNotification *)theNotification
+{
+ if( [[theNotification name] isEqualToString:@STATUS_CHANGE] )
+ {
+ [self tellClientStatusChanged:[fireflyServer status]];
+ }
+ else if( [[theNotification name] isEqualToString:@VERSION_CHANGE] )
+ {
+ [self tellClientVersionChanged:[fireflyServer version]];
+ }
+ else if( [[theNotification name] isEqualToString:@URL_CHANGE] )
+ {
+ [self tellClientURLChanged:[fireflyServer configURL]];
+ }
+}
+
+// ---------------------------------------------------------------------------
+// checkClient
+//
+// Call this function to see if a client is really connected. If our client
+// ivar is nil, returns NO, but if it's not nil, we try to "ping" the client.
+// If that ping fails, the client ivar is released and set to nil, and we
+// also return NO.
+// ---------------------------------------------------------------------------
+- (BOOL)checkClient
+{
+ BOOL bRetVal = NO;
+ @try
+ {
+ bRetVal = [client stillThere];
+ }
+ @catch( NSException *exception )
+ {
+ [client autorelease];
+ client = nil;
+ NSLog( @"Client disappeared; setting to nil" );
+ }
+
+ return bRetVal;
+}
+
+// ---------------------------------------------------------------------------
+// tellClientStatusChanged
+//
+// A wrapper for the call to statusChanged, that tests client for nil and
+// traps any IPC-related exception. If an exception fires, this function
+// will call our checkClient function to see if the client is still valid.
+// ---------------------------------------------------------------------------
+- (void)tellClientStatusChanged:(FireflyServerStatus)newStatus
+{
+ if( nil != client )
+ {
+ @try
+ {
+ [client statusChanged:newStatus];
+ }
+ @catch( NSException *exception )
+ {
+ [self checkClient];
+ }
+ }
+}
+
+// ---------------------------------------------------------------------------
+// tellClientVersionChanged
+//
+// A wrapper for the call to versionChanged. See tellClientStatusChanged
+// for details.
+// ---------------------------------------------------------------------------
+- (void)tellClientVersionChanged:(NSString *)newVersion
+{
+ if( nil != client )
+ {
+ @try
+ {
+ [client versionChanged:newVersion];
+ }
+ @catch( NSException *exception )
+ {
+ [self checkClient];
+ }
+ }
+}
+
+// ---------------------------------------------------------------------------
+// tellClientURLChanged
+//
+// A wrapper for the call to configUrlChanged. See tellClientStatusChanged
+// for details.
+// ---------------------------------------------------------------------------
+- (void)tellClientURLChanged:(NSString *)newURL
+{
+ if( nil != client )
+ {
+ @try
+ {
+ [client configUrlChanged:newURL];
+ }
+ @catch( NSException * )
+ {
+ [self checkClient];
+ }
+ }
+}
+
+// ===========================================================================
+// Items pertaining to the Status Item (our little menu bar item)
+// ===========================================================================
+
+// ---------------------------------------------------------------------------
+// displayStatusItem
+//
+// Adds our Firefly menu to the menu bar
+// ---------------------------------------------------------------------------
+- (void)displayStatusItem
+{
+ if( nil == statusItem )
+ {
+ statusItem = [[[NSStatusBar systemStatusBar] statusItemWithLength:
+ NSSquareStatusItemLength] retain];
+ if( nil != statusItem )
+ {
+ [statusMenu setDelegate:self];
+ [statusItem setMenu:statusMenu];
+ NSImage *statusImage =
+ [[NSImage alloc] initWithContentsOfFile:
+ [[NSBundle mainBundle] pathForResource:@"ff_logo_status_menu"
+ ofType:@"tif"]];
+ [statusItem setImage:statusImage];
+ [statusItem setHighlightMode:YES];
+ }
+ }
+}
+
+// ---------------------------------------------------------------------------
+// hideStatusItem
+//
+// Takes our Firefly menu out of the menu bar
+// ---------------------------------------------------------------------------
+- (void)hideStatusItem
+{
+ if( nil != statusItem )
+ {
+ [[statusItem statusBar] removeStatusItem:statusItem];
+ [statusItem autorelease];
+ statusItem = nil;
+ }
+}
+
+// ---------------------------------------------------------------------------
+// startStopMenuChosen
+//
+// Somebody chose "Start Firefly" or "Stop Firefly" from the menu
+// ---------------------------------------------------------------------------
+- (IBAction)startStopMenuChosen:(id)sender
+{
+ if( nil != fireflyServer )
+ {
+ if( [fireflyServer isRunning] )
+ [fireflyServer stop];
+ else
+ [fireflyServer start];
+ }
+}
+
+// ---------------------------------------------------------------------------
+// prefsMenuChosen
+//
+// Somebody chose "Firefly PreferencesÉ"
+// ---------------------------------------------------------------------------
+- (IBAction)prefsMenuChosen:(id)sender
+{
+ NSDictionary *errorDict = nil;
+ NSString *scriptSource = @"tell application \"System Preferences\"\n"
+ "activate\n"
+ "set current pane to pane \"org.fireflymediaserver.prefpanel\"\n"
+ "end tell\n";
+ NSAppleScript *myScript = [[NSAppleScript alloc] initWithSource:scriptSource];
+
+ if( nil != myScript )
+ {
+ [myScript executeAndReturnError:&errorDict];
+ [myScript release];
+ }
+}
+
+// ---------------------------------------------------------------------------
+// numberOfItemsInMenu
+//
+// NSMenu delegate method. We always return -1 because we don't change the
+// number of items in the menu.
+// ---------------------------------------------------------------------------
+- (int)numberOfItemsInMenu:(NSMenu *)menu
+{
+ return -1;
+}
+
+// ---------------------------------------------------------------------------
+// menuNeedsUpdate
+//
+// NSMenu delegate method. If our status has changed, we update the menu
+// item text.
+// ---------------------------------------------------------------------------
+- (void)menuNeedsUpdate:(NSMenu *)menu
+{
+ if( menu == statusMenu && 3 == [menu numberOfItems] )
+ {
+ // Just need to update the status of the server and the start/stop
+ // menu
+ id item = [menu itemAtIndex:0];
+ [item setTitle:StringForFireflyStatus( [self fireflyStatus] )];
+
+ item = [menu itemAtIndex:1];
+ if( [self fireflyIsRunning] )
+ {
+ [item setTitle:NSLocalizedString( @"Stop Firefly",
+ @"Text for status menu" )];
+ }
+ else
+ {
+ [item setTitle:NSLocalizedString( @"Start Firefly",
+ @"Text for status menu" )];
+ }
+ }
+}
+
+// ===========================================================================
+// Implementation of FireflyPrefsProtocol
+// ===========================================================================
+
+// ---------------------------------------------------------------------------
+// registerClient
+//
+// When the Prefs pane starts up and connects to us, it will register itself
+// with us so that we may notify it of changes in the server status while
+// it's open. This registration process also gives us the ability to detect
+// when a
+// ---------------------------------------------------------------------------
+- (BOOL)registerClient:(id) newClient withIdentifier:(int) ident
+{
+ BOOL bRetVal = NO;
+ if( nil != client )
+ {
+ // Hm. We already have a client connected. Let's see if it's really
+ // still there. This will set client to nil if the client has died.
+ if( [self checkClient] )
+ NSLog(@"registerClient called, but valid client already connected!\n");
+ }
+
+ if( nil == client )
+ {
+ client = [newClient retain];
+ clientIdent = ident;
+ bRetVal = YES;
+ }
+
+ return bRetVal;
+}
+
+// ---------------------------------------------------------------------------
+// unregisterClientId
+//
+// When the Prefs pane is closing, it unregisters, so we know not to try to
+// send it more udpates
+// ---------------------------------------------------------------------------
+- (void)unregisterClientId:(int) ident
+{
+ if( ident == clientIdent )
+ {
+ clientIdent = 0;
+ [client autorelease];
+ client = nil;
+ }
+}
+
+// ---------------------------------------------------------------------------
+// startFirefly
+//
+// Starts the server. Return value indicates that the server process was
+// started. If a client has registered for status updates, it will see the
+// status kFireflyStatusStarting. The server could possibly quit before
+// coming online, so clients really should register for status updates.
+// ---------------------------------------------------------------------------
+- (FireflyStartResult)startFirefly
+{
+ FireflyStartResult retVal = kFireflyStartFail;
+ if( nil != fireflyServer && [fireflyServer start] )
+ retVal = kFireflyStartSuccess;
+ return retVal;
+}
+
+// ---------------------------------------------------------------------------
+// stopFirefly
+//
+// Signals the server to stop. A successful return value indicates that the
+// server was not running, or has been successfully signaled to stop.
+// Clients should register for status updates and look for the actual change
+// to kFireflyStatusStopped to confirm shutdown.
+// ---------------------------------------------------------------------------
+- (FireflyStopResult)stopFirefly
+{
+ FireflyStopResult retVal = kFireflyStopFail;
+ if( nil != fireflyServer && [fireflyServer stop] )
+ retVal = kFireflyStopSuccess;
+ return retVal;
+}
+
+// ---------------------------------------------------------------------------
+// rescanLibrary
+//
+// Tells the server to re-scan the library. Returns a failure result if
+// the server is not running.
+// ---------------------------------------------------------------------------
+- (FireflyRescanResult)rescanLibrary
+{
+ FireflyRescanResult retVal = kFireflyRescanInvalid;
+ if( nil != fireflyServer )
+ retVal = [fireflyServer status];
+ return retVal;
+}
+
+// ---------------------------------------------------------------------------
+// fireflyStatus
+//
+// Replies with the state of the server
+// ---------------------------------------------------------------------------
+- (FireflyServerStatus)fireflyStatus
+{
+ FireflyServerStatus retVal = kFireflyStatusInvalid;
+ if( nil != fireflyServer )
+ retVal = [fireflyServer status];
+ return retVal;
+}
+
+// ---------------------------------------------------------------------------
+// stopFirefly
+//
+// Signals the server to restart. A successful return value indicates that
+// the server has been successfully signaled to restart.
+// Clients should register for status updates and look for the actual changes
+// in server status to verify that the signal is handled.
+// ---------------------------------------------------------------------------
+- (FireflyRestartResult)restartFirefly
+{
+ FireflyRestartResult retVal = kFireflyRestartFail;
+ if( nil != fireflyServer && [fireflyServer restart] )
+ retVal = kFireflyRestartSuccess;
+ return retVal;
+}
+
+// ---------------------------------------------------------------------------
+// fireflyVersion
+//
+// Replies with the version of the server. Returns nil if the
+// server is not running or we are unable to ascertain the version. Note
+// that a method invocation on nil returns nil, so we don't need to check
+// fireflyServer for nil.
+// ---------------------------------------------------------------------------
+- (NSString *)fireflyVersion
+{
+ return [fireflyServer version];
+}
+
+// ---------------------------------------------------------------------------
+// fireflyConfigURL
+//
+// Replies with the URL to the advanced configuration page for the server.
+// Returns nil if the server is not running or we are unable to
+// ascertain the URL. See note about nil above.
+// ---------------------------------------------------------------------------
+- (NSString *)fireflyConfigURL
+{
+ return [fireflyServer configURL];
+}
+
+// ---------------------------------------------------------------------------
+// showHelperMenu
+//
+// Allows the prefs pane to specify whether to show the item. This setting
+// is persistent
+// ---------------------------------------------------------------------------
+- (void)showHelperMenu:(BOOL)bShowMenu
+{
+ if( bShowMenu )
+ [self displayStatusItem];
+ else
+ [self hideStatusItem];
+}
+
+// ---------------------------------------------------------------------------
+// fireflyVersion
+//
+// Returns YES if the server is running, NO if not
+// ---------------------------------------------------------------------------
+- (BOOL)fireflyIsRunning
+{
+ BOOL retVal = NO;
+ if( nil != fireflyServer )
+ retVal = [fireflyServer isRunning];
+
+ return retVal;
+}
+
+// ===========================================================================
+// Implementation of NSMenuValidation
+// ===========================================================================
+
+// ---------------------------------------------------------------------------
+// validateMenuItem
+//
+// Our first item is always disabled. Our last is always enabled. The
+// one in the middle depends upon our status
+// ---------------------------------------------------------------------------
+- (BOOL)validateMenuItem:(id )menuItem
+{
+ BOOL bRetVal = NO;
+ if( nil != statusMenu && 3 == [statusMenu numberOfItems] )
+ {
+ if( menuItem == [statusMenu itemAtIndex:2] )
+ bRetVal = YES;
+ else if( menuItem == [statusMenu itemAtIndex:1] )
+ {
+ FireflyServerStatus status = [self fireflyStatus];
+ if( status != kFireflyStatusStopping && status != kFireflyStatusInvalid )
+ bRetVal = YES;
+ }
+ }
+ return bRetVal;
+}
+
+
+@end
diff --git a/osx/Firefly Helper/FireflyServer.h b/osx/Firefly Helper/FireflyServer.h
new file mode 100644
index 00000000..b764a0d8
--- /dev/null
+++ b/osx/Firefly Helper/FireflyServer.h
@@ -0,0 +1,91 @@
+//
+// FireflyServer.h
+//
+// The "model" part of our Model-View-Controller trio. Represents the
+// firefly server itself, and encapsulates launching, quitting, status,
+// etc.
+//
+// Created by Mike Kobb on 7/12/06.
+// Copyright 2006 Roku, LLC. All rights reserved.
+//
+
+#import
+#include "../FireflyCommon.h"
+
+#define STATUS_CHANGE "org.fireflymediaserver.status-change"
+#define VERSION_CHANGE "org.fireflymediaserver.version-change"
+#define URL_CHANGE "org.fireflymediaserver.url-change"
+
+@interface FireflyServer : NSObject
+{
+ NSString *fireflyServerPath;
+ NSTask *serverTask;
+
+ NSString *serverVersion;
+ NSString *serverURL;
+ unsigned short serverPort;
+
+ id delegate;
+ FireflyServerStatus status;
+
+ // We use Bonjour to find our server port and help notice if the server
+ // stops
+ NSNetServiceBrowser *netBrowser;
+ BOOL bScanIsActive;
+ NSMutableArray *pendingNetServices;
+ NSNetService *fireflyService;
+ char ffid[9];
+
+}
+
+// public methods for managing the lifecycle of the server object
+- (void)setup;
+- (void)shutdown;
+
+// public methods for controlling the server process
+- (id)initWithServerPath:(NSString *) serverPath;
+- (BOOL)start;
+- (BOOL)stop;
+- (BOOL)restart;
+- (void)setDelegate:(id) delegate;
+
+// public methods for querying server status & properties
+- (BOOL)isRunning;
+- (FireflyServerStatus)status;
+- (NSString *)version;
+- (NSString *)configURL;
+
+// private utilities
+- (void)setStatus:(FireflyServerStatus)newStatus;
+- (void)setURL:(NSString *)newURL;
+- (void)setVersion:(NSString *)newVersion;
+- (void)taskEnded:(NSNotification *)notification;
+- (NSString*)fireflyConfigFilePath;
+- (BOOL)startAndUpdateStatus:(BOOL)bUpdate;
+- (void)killRunningFireflies;
+
+// Bonjour delegate methods (NSNetServiceBrowser)
+- (void)netServiceBrowserWillSearch:(NSNetServiceBrowser *)netServiceBrowser;
+- (void)netServiceBrowserDidStopSearch:(NSNetServiceBrowser *)netServiceBrowser;
+- (void)netServiceBrowser:(NSNetServiceBrowser *)netServiceBrowser
+ didRemoveService:(NSNetService *)netService
+ moreComing:(BOOL)moreServicesComing;
+- (void)netServiceBrowser:(NSNetServiceBrowser *)netServiceBrowser
+ didRemoveDomain:(NSString *)domainName
+ moreComing:(BOOL)moreDomainsComing;
+- (void)netServiceBrowser:(NSNetServiceBrowser *)netServiceBrowser
+ didNotSearch:(NSDictionary *)errorInfo;
+- (void)netServiceBrowser:(NSNetServiceBrowser *)netServiceBrowser
+ didFindService:(NSNetService *)netService
+ moreComing:(BOOL)moreServicesComing;
+- (void)netServiceBrowser:(NSNetServiceBrowser *)netServiceBrowser
+ didFindDomain:(NSString *)domainName
+ moreComing:(BOOL)moreDomainsComing;
+
+// Bonjour delegate methods (NSNetService)
+- (void)netServiceDidResolveAddress:(NSNetService *)service;
+- (void)netService:(NSNetService *)service didNotResolve:(NSDictionary *)errorInfo;
+- (void)netServiceWillResolve:(NSNetService *)service;
+
+
+@end
diff --git a/osx/Firefly Helper/FireflyServer.m b/osx/Firefly Helper/FireflyServer.m
new file mode 100644
index 00000000..8c9bfe37
--- /dev/null
+++ b/osx/Firefly Helper/FireflyServer.m
@@ -0,0 +1,659 @@
+//
+// FireflyServer.m
+//
+// The "model" part of our Model-View-Controller trio. Represents the
+// firefly server itself, and encapsulates launching, quitting, status,
+// etc.
+//
+// Created by Mike Kobb on 7/12/06.
+// Copyright 2006 Roku, LLC. All rights reserved.
+//
+#import
+#import // for sockaddr_in
+#include // for inet_ntoa and so forth
+#include // AF_INET6
+#import "FireflyServer.h"
+
+@implementation FireflyServer
+
+// ---------------------------------------------------------------------------
+// initWithServerPath
+//
+// Initialize the server object to manage the server at the given path.
+// ---------------------------------------------------------------------------
+- (id)initWithServerPath:(NSString *) path
+{
+ if( ( self = [super init] ) != nil )
+ {
+ fireflyServerPath = [[NSString stringWithString:path] retain];
+ serverTask = nil;
+ delegate = nil;
+ serverVersion = nil;
+ serverURL = nil;
+ status = kFireflyStatusStopped;
+
+ // Bonjour stuff below
+ netBrowser = [[NSNetServiceBrowser alloc] init];
+ pendingNetServices = [[NSMutableArray arrayWithCapacity:5] retain];
+ fireflyService = nil;
+
+ // Pick a random ffid that we'll be able to use to identify our
+ // server easily
+ srand((unsigned int)time(NULL));
+ sprintf( ffid, "%08x", rand() );
+
+ // Register for task ending notifications, so we can find out
+ // if our server process quits.
+ [[NSNotificationCenter defaultCenter] addObserver:self
+ selector:@selector(taskEnded:)
+ name:NSTaskDidTerminateNotification
+ object:nil];
+ }
+
+ return self;
+}
+
+// ---------------------------------------------------------------------------
+// dealloc
+//
+//
+// ---------------------------------------------------------------------------
+- (void)dealloc
+{
+ // First, kill the server!
+
+
+ [fireflyServerPath release];
+ [serverTask release];
+ [serverVersion release];
+ [serverURL release];
+ [netBrowser release];
+ [pendingNetServices release];
+ [fireflyService release];
+ [[NSNotificationCenter defaultCenter] removeObserver:self];
+ [super dealloc];
+}
+
+// ---------------------------------------------------------------------------
+// setup
+//
+// Not to be confused with 'start', this function is to be called before
+// starting to use the object. It handles starting our Bonjour stuff and
+// so on.
+// ---------------------------------------------------------------------------
+- (void)setup
+{
+ // It's time to start our scan, limiting to the local host.
+ [netBrowser setDelegate:self];
+ [netBrowser searchForServicesOfType:@"_http._tcp." inDomain:@"local."];
+}
+
+// ---------------------------------------------------------------------------
+// shutdown
+//
+// Not to be confused with 'stop', this function is to be called when
+// preparing to dispose the object. It handles shutting down our Bonjour
+// stuff and so on. But, it does also stop the server if it's running.
+// ---------------------------------------------------------------------------
+- (void)shutdown
+{
+ // shut down the firefly server
+ [self setStatus:kFireflyStatusStopping];
+ [self stop];
+
+ // Now shut down the Bonjour scan.
+ [netBrowser stop];
+
+ // FIXME: Is it safe to do this right after calling stop, or should we
+ // put these in didStop?
+ NSEnumerator *enumerator = [pendingNetServices objectEnumerator];
+ NSNetService *service;
+ while( service = [enumerator nextObject] )
+ [service stop];
+ [pendingNetServices removeAllObjects];
+}
+
+// ---------------------------------------------------------------------------
+// setDelegate
+//
+// We will message our delegate when important things happen, like the server
+// starting or stopping, etc.
+// ---------------------------------------------------------------------------
+- (void)setDelegate:(id) delegateToSet
+{
+ delegate = delegateToSet;
+}
+
+// ---------------------------------------------------------------------------
+// isRunning
+//
+// Is the server running?
+// ---------------------------------------------------------------------------
+- (BOOL)isRunning
+{
+ BOOL retVal = NO;
+ if( nil != serverTask )
+ retVal = [serverTask isRunning];
+ return retVal;
+}
+
+// ---------------------------------------------------------------------------
+// start
+//
+// Starts the server. Note that this function may fail if the server is
+// already running.
+// ---------------------------------------------------------------------------
+- (BOOL)start
+{
+ BOOL retVal = NO;
+
+ FireflyServerStatus curStatus = [self status];
+ if( curStatus == kFireflyStatusStopped ||
+ curStatus == kFireflyStatusStartFailed ||
+ curStatus == kFireflyStatusCrashed )
+ {
+ retVal = [self startAndUpdateStatus:YES];
+ }
+
+ return retVal;
+}
+
+// ---------------------------------------------------------------------------
+// stop
+//
+// Signals the server to stop. Returns YES if the signal was sent successfully
+// ---------------------------------------------------------------------------
+- (BOOL)stop
+{
+ BOOL retVal = NO;
+ if( nil != serverTask )
+ {
+ [self setStatus:kFireflyStatusStopping];
+ [serverTask terminate];
+ retVal = YES;
+ }
+ return retVal;
+}
+
+// ---------------------------------------------------------------------------
+// restart
+//
+// restarts the server. Tells the server to shut down after setting our
+// status to "restarting". When the server shuts down, the taskEnded
+// method will see that status, and restart the server.
+// ---------------------------------------------------------------------------
+- (BOOL)restart
+{
+ BOOL retVal = NO;
+ if( nil != serverTask )
+ {
+ [self setStatus:kFireflyStatusRestarting];
+ [serverTask terminate];
+ retVal = YES;
+ }
+ return retVal;
+}
+
+// ---------------------------------------------------------------------------
+// status
+//
+// Returns the current server status
+// ---------------------------------------------------------------------------
+- (FireflyServerStatus)status
+{
+ return status;
+}
+
+// ---------------------------------------------------------------------------
+// version
+//
+// Returns the current server version, or nil if it's not yet known
+// ---------------------------------------------------------------------------
+- (NSString *)version
+{
+ return serverVersion;
+}
+
+// ---------------------------------------------------------------------------
+// configURL
+//
+// Returns the server's advanced user configuration URL, or nil if it's not
+// yet known
+// ---------------------------------------------------------------------------
+- (NSString *)configURL
+{
+ return serverURL;
+}
+
+// ===========================================================================
+// Private utilities.
+// ===========================================================================
+
+// ---------------------------------------------------------------------------
+// setStatus
+//
+// Sets the status and notifies interested parties of the change.
+// ---------------------------------------------------------------------------
+- (void)setStatus:(FireflyServerStatus) newStatus
+{
+ status = newStatus;
+
+ [[NSNotificationCenter defaultCenter] postNotificationName:@STATUS_CHANGE
+ object:self];
+}
+
+// ---------------------------------------------------------------------------
+// setURL
+//
+// Sets the server config URL and notifies interested parties of the change.
+// ---------------------------------------------------------------------------
+- (void)setURL:(NSString *) newUrl
+{
+ [serverURL autorelease];
+ serverURL = [[NSString stringWithString:newUrl] retain];
+ [[NSNotificationCenter defaultCenter] postNotificationName:@URL_CHANGE
+ object:self];
+}
+
+// ---------------------------------------------------------------------------
+// setVersion
+//
+// Sets the server version and notifies interested parties of the change.
+// ---------------------------------------------------------------------------
+- (void)setVersion:(NSString *)newVersion
+{
+ [serverVersion autorelease];
+ serverVersion = [[NSString stringWithString:newVersion] retain];
+ [[NSNotificationCenter defaultCenter] postNotificationName:@VERSION_CHANGE
+ object:self];
+}
+
+// ---------------------------------------------------------------------------
+// taskEnded
+//
+// We register this function to be called when tasks end. If the task is
+// our server task, then we dispose the (now useless) object and notify
+// interested parties of the change. We check for normal versus abnormal
+// termination and set status accordingly.
+// ---------------------------------------------------------------------------
+- (void)taskEnded:(NSNotification *)notification
+{
+ if( serverTask == [notification object] )
+ {
+ int termStatus = [[notification object] terminationStatus];
+ [serverTask autorelease];
+ serverTask = nil;
+ if( kFireflyStatusRestarting == status )
+ {
+ // Don't post the message saying that the server stopped;
+ // just start up and let the success or failure of that startup
+ // handle the status update.
+ [self startAndUpdateStatus:NO];
+ }
+ else
+ {
+ if( 0 == termStatus )
+ [self setStatus:kFireflyStatusStopped];
+ else if( kFireflyStatusStarting == status )
+ [self setStatus:kFireflyStatusStartFailed];
+ else
+ [self setStatus:kFireflyStatusCrashed];
+ NSLog(@"Server Task ended with status %d\n", termStatus);
+ }
+ }
+}
+
+// ---------------------------------------------------------------------------
+// fireflyConfigFilePath
+//
+// Build the path to the config file, test that it's valid and return it. If
+// we can't find a file at the expected location, we return nil
+// ---------------------------------------------------------------------------
+- (NSString*)fireflyConfigFilePath
+{
+ NSString *retVal = nil;
+ NSArray * appSupportDirArray =
+ NSSearchPathForDirectoriesInDomains( NSLibraryDirectory,
+ NSUserDomainMask,
+ YES );
+ if( [appSupportDirArray count] > 0 )
+ {
+ BOOL bIsDir = NO;
+ NSFileManager *mgr = [NSFileManager defaultManager];
+ NSString *configFilePath =
+ [[appSupportDirArray objectAtIndex:0]
+ stringByAppendingPathComponent:@"Application Support/Firefly/"
+ FIREFLY_CONF_NAME];
+ if( [mgr fileExistsAtPath:configFilePath isDirectory:&bIsDir] && !bIsDir )
+ retVal = configFilePath;
+ }
+
+ return retVal;
+}
+
+// ---------------------------------------------------------------------------
+// startAndUpdateStatus
+//
+// Private utility that actually starts the server. If the bUpdate flag is
+// NO, then this utility will leave the current status in place, even though
+// the server is starting. This is intended for use by the restart function,
+// so that the status will remain in "restarting" until the server actually
+// comes online (or fails)
+// ---------------------------------------------------------------------------
+- (BOOL)startAndUpdateStatus:(BOOL)bUpdate
+{
+ BOOL retVal = NO;
+
+ [self killRunningFireflies];
+
+ NSString *configFilePath = [self fireflyConfigFilePath];
+ if( nil != configFilePath )
+ {
+ NSArray *array = [NSArray arrayWithObjects:
+ @"-y", @"-f", @"-c",
+ configFilePath,
+ @"-b",
+ [NSString stringWithUTF8String:ffid], // best 10.4<->10.3 compromise method...
+ nil];
+ @try
+ {
+ serverTask = [[[NSTask alloc] init] retain];
+ [serverTask setLaunchPath:fireflyServerPath];
+ [serverTask setCurrentDirectoryPath:[fireflyServerPath stringByDeletingLastPathComponent]];
+ [serverTask setArguments:array];
+
+ if( bUpdate )
+ [self setStatus:kFireflyStatusStarting];
+ [serverTask launch];
+ retVal = YES;
+ }
+ @catch( NSException *exception )
+ {
+ if( [[exception name] isEqual:NSInvalidArgumentException] )
+ ;
+ NSLog(@"FireflyServer: Caught %@: %@", [exception name], [exception reason]);
+ [self setStatus:kFireflyStatusStartFailed];
+ }
+ }
+ else
+ {
+ NSLog(@"couldn't find config file at %@\n", configFilePath);
+ }
+ return retVal;
+}
+
+// ---------------------------------------------------------------------------
+// killRunningFireflies
+//
+// This may seem like paranoia, but things really go badly if there is more
+// than one copy of Firefly running (e.g. started from the command line, or
+// failing to quit when signaled). So, we enforce some preconditions here.
+// ---------------------------------------------------------------------------
+- (void)killRunningFireflies
+{
+ kinfo_proc *result;
+ size_t length;
+ GetProcesses( &result, &length );
+
+ // Okay, now we have our list of processes. Let's find OUR copy of
+ // firefly. Note that Firefly runs as multiple processes, so we look
+ // through all processes, not stopping when we find one. We *are*
+ // careful to only kill processes owned by the current user!
+ if( NULL != result )
+ {
+ int procCount = length / sizeof(kinfo_proc);
+ int i = 0;
+ uid_t ourUID = getuid();
+ for( ; i < procCount; i++ )
+ {
+ if( ourUID == result[i].kp_eproc.e_pcred.p_ruid &&
+ 0 == strcasecmp( result[i].kp_proc.p_comm, FIREFLY_SERVER_NAME ) )
+ {
+ NSLog(@"Killing rogue firefly, pid %d\n", result[i].kp_proc.p_pid);
+ kill( result[i].kp_proc.p_pid, SIGKILL );
+ }
+ }
+ free( result );
+ }
+}
+
+// ===========================================================================
+// Below are the delegate methods for Bonjour discovery of the server.
+// ===========================================================================
+
+// ---------------------------------------------------------------------------
+// netServiceBrowserWillSearch:
+//
+// Lets us know that the Bonjour search has started
+// ---------------------------------------------------------------------------
+- (void)netServiceBrowserWillSearch:(NSNetServiceBrowser *)netServiceBrowser
+{
+#ifdef FIREFLY_DEBUG
+ NSLog(@"NSNetServiceBrowser started\n");
+#endif
+ bScanIsActive = YES;
+}
+
+// ---------------------------------------------------------------------------
+// netServiceBrowserDidStopSearch:
+//
+// Should only stop if we ask it to, as when we're quitting the app
+// ---------------------------------------------------------------------------
+- (void)netServiceBrowserDidStopSearch:(NSNetServiceBrowser *)netServiceBrowser
+{
+#ifdef FIREFLY_DEBUG
+ NSLog(@"NSNetServiceBrowser stopped\n");
+#endif
+ bScanIsActive = NO;
+}
+
+// ---------------------------------------------------------------------------
+// netServiceBrowser:didRemoveService:moreComing:
+//
+// Called when a Bonjour service goes away.
+// ---------------------------------------------------------------------------
+- (void)netServiceBrowser:(NSNetServiceBrowser *)netServiceBrowser
+ didRemoveService:(NSNetService *)netService
+ moreComing:(BOOL)moreServicesComing
+{
+ // Is it our service? If so, we need to switch the status text and change
+ // the start/stop button, and also update the web page button and text.
+ if( [netService isEqual:fireflyService] )
+ {
+ [fireflyService autorelease];
+ fireflyService = nil;
+ // FIXME: AND? Theoretically, we should be notified that our NSTask
+ // went away, so this notification isn't needed to detect that the
+ // server stopped. But, what if due to some error, the Bonjour
+ // service croaked but left the server itself running?
+ }
+}
+
+// ---------------------------------------------------------------------------
+// netServiceBrowser:didRemoveDomain:moreComing:
+//
+// unless our local host goes away, we really don't care.
+// ---------------------------------------------------------------------------
+- (void)netServiceBrowser:(NSNetServiceBrowser *)netServiceBrowser
+ didRemoveDomain:(NSString *)domainName
+ moreComing:(BOOL)moreDomainsComing
+{
+}
+
+// ---------------------------------------------------------------------------
+// netServiceBrowser:didNotSearch:
+//
+// Called if the search failed to start. We need to alert the user.
+// ---------------------------------------------------------------------------
+- (void)netServiceBrowser:(NSNetServiceBrowser *)netServiceBrowser
+ didNotSearch:(NSDictionary *)errorInfo
+{
+ // FIXME: display error info? Try again? Quit?
+}
+
+// ---------------------------------------------------------------------------
+// netServiceBrowser:didFindService:moreComing:
+//
+// A Bonjour service has been discovered. It might be our server. We need
+// to ask the service to resolve, so we can see if it is.
+// ---------------------------------------------------------------------------
+- (void)netServiceBrowser:(NSNetServiceBrowser *)netServiceBrowser
+ didFindService:(NSNetService *)netService
+ moreComing:(BOOL)moreServicesComing
+{
+ // We need to ask this object to resolve itself so we can figure out if it's the server
+ // we want. In case the user closes the panel, we need to have a list of all the ones
+ // pending so we can stop them.
+ [pendingNetServices addObject:netService];
+ [netService setDelegate:self];
+ [netService resolve];
+
+ if( !moreServicesComing )
+ bScanIsActive = NO;
+}
+
+// ---------------------------------------------------------------------------
+// netServiceBrowser:didFindDomain:moreComing:
+//
+// Don't think we care about this one, but I'm pretty sure we're supposed to
+// implement it (why?)
+// ---------------------------------------------------------------------------
+- (void)netServiceBrowser:(NSNetServiceBrowser *)netServiceBrowser
+ didFindDomain:(NSString *)domainName
+ moreComing:(BOOL)moreDomainsComing
+{
+}
+
+// ---------------------------------------------------------------------------
+// netServiceDidResolveAddress:
+//
+// We asked a service to resolve, and it has. Time to check to see if it's
+// our server.
+// ---------------------------------------------------------------------------
+- (void)netServiceDidResolveAddress:(NSNetService *)service
+{
+ // Is it a firefly service, and if so, is it ours?
+
+ // NOTE: EXTREMELY IMPORTANT!
+ // protocolSpecificInformation is a deprecated API, and the Tiger-on approved way is TXTRecordData.
+ // protocolSpecificInformation, though, is available back to 10.2. These return the data in
+ // DIFFERENT FORMATS. TXTRecordData returns the data in the mDNS-standard format of a packed
+ // array of Pascal strings, while protocolSpecificInformation returns the strings separated by 0x01
+ // characters. The TXTRecordContainsKey function and related utilities assume (at least on Tiger)
+ // the packed-Pascal format, so we have to use strstr instead. Happily, all we really care
+ // about here is that the ffid tag exists, so we don't need to do much parsing.
+ const char *version = NULL;
+ const char* txtRecordBytes = NULL;
+ NSData *data = nil;
+
+ NSString *txtRecord = [service protocolSpecificInformation];
+ if( nil != txtRecord )
+ data = [txtRecord dataUsingEncoding:NSUTF8StringEncoding];
+ txtRecordBytes = (const char*)[data bytes];
+ if( NULL != txtRecordBytes &&
+ NULL != ( version = strnstr( txtRecordBytes, "\001mtd-version=", [data length] ) ) )
+ {
+ // Okay, this is a firefly server. Let's see if it's *our* server
+ int i = 0;
+ char buf[256]; // max allowed size, but we'll still be careful not to overrun.
+ strncpy( buf, txtRecordBytes, 255 );
+ buf[255] = '\0';
+#ifdef FIREFLY_DEBUG
+ NSLog( @"Text record is: %s\n", buf );
+#endif
+ const char *ffidptr = strnstr( txtRecordBytes, "\001ffid=", [data length] );
+ if( NULL != ffidptr )
+ {
+ // This is a bit of a pain due to the stuff described in the big
+ // comment above.
+
+ // advance over the key
+ ffidptr += 6;
+ while( '\0' != ffidptr[i] &&
+ '\001' != ffidptr[i] &&
+ ((ffidptr-txtRecordBytes)+i) < [data length] )
+ {
+#ifdef FIREFLY_DEBUG
+ NSLog(@"Adding %c (%d)to ffidptr\n", ffidptr[i], ffidptr[i]);
+#endif
+ buf[i] = ffidptr[i++];
+ }
+ buf[i] = '\0';
+ NSLog(@"Comparing buf %s against our ffid %s\n", buf, ffid);
+ if( 0 == strcmp( buf, ffid ) )
+ {
+ // WOOT! This is us. Get the version and port
+ i = 0;
+ version += 13;
+ while( '\0' != version[i] &&
+ '\001' != version[i] &&
+ ((version-txtRecordBytes)+i) < [data length] )
+ buf[i] = version[i++];
+ buf[i] = '\0';
+ [self setVersion:[NSString stringWithUTF8String:buf]];
+
+ // Time to get the port.
+ NSArray *svcAddresses = [service addresses];
+ if( 0 != [svcAddresses count] )
+ {
+ NSData *addrData = [svcAddresses objectAtIndex:0];
+ struct sockaddr_in *addrPtr = (struct sockaddr_in*)[addrData bytes];
+ if( NULL != addrPtr )
+ {
+ serverPort = ntohs(addrPtr->sin_port);
+ [self setURL:[NSString stringWithFormat:@"http://localhost:%u", serverPort]];
+ }
+ }
+
+ // Okay, it's the one we want, so let's remember it and update
+ // our status
+ [fireflyService autorelease];
+ fireflyService = [service retain];
+ [self setStatus:kFireflyStatusActive];
+#ifdef FIREFLY_DEBUG
+ NSBeep();
+ sleep(1);
+ NSBeep();
+ sleep(1);
+ NSBeep();
+#endif
+ }
+ }
+ }
+
+ // It's no longer pending, so remove from array. If it was ours, we've
+ // retained it.
+ [pendingNetServices removeObject:service];
+
+ // If we're no longer scanning and we've exhausted all the
+ // services that we found without identifying the correct server,
+ // we need to ...?
+ if( !bScanIsActive && 0 == [pendingNetServices count] && nil == fireflyService )
+ ; //FIXME
+}
+
+// ---------------------------------------------------------------------------
+// netService:didNotResolve:
+//
+// We tried to resolve a service, and failed. It's probably not really
+// running. We could always try again, but it doesn't seem to be necessary
+// ---------------------------------------------------------------------------
+- (void)netService:(NSNetService *)service didNotResolve:(NSDictionary *)errorInfo
+{
+ [pendingNetServices removeObject:service];
+#ifdef FIREFLY_DEBUG
+ if( nil == fireflyService )
+ NSLog(@"Failed to resolve service: %@\n", [errorInfo valueForKey:NSNetServicesErrorCode] );
+#endif
+}
+
+// ---------------------------------------------------------------------------
+// netServiceWillResolve:
+//
+// Just lets us know resolution has started.
+// ---------------------------------------------------------------------------
+- (void)netServiceWillResolve:(NSNetService *)service
+{
+}
+
+@end
diff --git a/osx/Firefly Helper/Info.plist b/osx/Firefly Helper/Info.plist
new file mode 100644
index 00000000..20bd6da0
--- /dev/null
+++ b/osx/Firefly Helper/Info.plist
@@ -0,0 +1,34 @@
+
+
+
+
+ CFBundleDevelopmentRegion
+ English
+ CFBundleExecutable
+ ${EXECUTABLE_NAME}
+ CFBundleIconFile
+ FireflyHelper
+ CFBundleIdentifier
+ org.fireflymediaserver.fireflyhelper
+ CFBundleInfoDictionaryVersion
+ 6.0
+ CFBundleName
+ ${PRODUCT_NAME}
+ CFBundlePackageType
+ APPL
+ CFBundleSignature
+ ????
+ CFBundleVersion
+ 102
+ CFBundleShortVersionString
+ 1.0b2
+ NSHumanReadableCopyright
+ Š 2006 Roku LLC
+ LSUIElement
+ 1
+ NSMainNibFile
+ MainMenu
+ NSPrincipalClass
+ NSApplication
+
+
diff --git a/osx/Firefly Helper/Japanese.lproj/InfoPlist.strings b/osx/Firefly Helper/Japanese.lproj/InfoPlist.strings
new file mode 100644
index 00000000..8a78ff4d
Binary files /dev/null and b/osx/Firefly Helper/Japanese.lproj/InfoPlist.strings differ
diff --git a/osx/Firefly Helper/Japanese.lproj/MainMenu.nib/classes.nib b/osx/Firefly Helper/Japanese.lproj/MainMenu.nib/classes.nib
new file mode 100644
index 00000000..b9b4b09f
--- /dev/null
+++ b/osx/Firefly Helper/Japanese.lproj/MainMenu.nib/classes.nib
@@ -0,0 +1,4 @@
+{
+ IBClasses = ({CLASS = FirstResponder; LANGUAGE = ObjC; SUPERCLASS = NSObject; });
+ IBVersion = 1;
+}
\ No newline at end of file
diff --git a/osx/Firefly Helper/Japanese.lproj/MainMenu.nib/info.nib b/osx/Firefly Helper/Japanese.lproj/MainMenu.nib/info.nib
new file mode 100644
index 00000000..4c74210b
--- /dev/null
+++ b/osx/Firefly Helper/Japanese.lproj/MainMenu.nib/info.nib
@@ -0,0 +1,17 @@
+
+
+
+
+ IBDocumentLocation
+ 94 103 356 240 0 0 1280 1002
+ IBEditorPositions
+
+ 29
+ 93 343 318 44 0 0 1280 1002
+
+ IBFramework Version
+ 403.0
+ IBSystem Version
+ 8A278
+
+
diff --git a/osx/Firefly Helper/Japanese.lproj/MainMenu.nib/keyedobjects.nib b/osx/Firefly Helper/Japanese.lproj/MainMenu.nib/keyedobjects.nib
new file mode 100644
index 00000000..ee1187e8
Binary files /dev/null and b/osx/Firefly Helper/Japanese.lproj/MainMenu.nib/keyedobjects.nib differ
diff --git a/osx/Firefly Helper/ff_logo_status_menu.tif b/osx/Firefly Helper/ff_logo_status_menu.tif
new file mode 100644
index 00000000..7dde9746
Binary files /dev/null and b/osx/Firefly Helper/ff_logo_status_menu.tif differ
diff --git a/osx/Firefly Helper/main.m b/osx/Firefly Helper/main.m
new file mode 100644
index 00000000..0751d443
--- /dev/null
+++ b/osx/Firefly Helper/main.m
@@ -0,0 +1,14 @@
+//
+// main.m
+// Firefly Minder
+//
+// Created by Mike Kobb on 6/8/06.
+// Copyright __MyCompanyName__ 2006. All rights reserved.
+//
+
+#import
+
+int main(int argc, char *argv[])
+{
+ return NSApplicationMain(argc, (const char **) argv);
+}
diff --git a/osx/Firefly Helper/version.plist b/osx/Firefly Helper/version.plist
new file mode 100644
index 00000000..6f3c68ca
--- /dev/null
+++ b/osx/Firefly Helper/version.plist
@@ -0,0 +1,16 @@
+
+
+
+
+ BuildVersion
+ 1
+ CFBundleShortVersionString
+ 0.1
+ CFBundleVersion
+ 0.1
+ ProjectName
+ NibPBTemplates
+ SourceVersion
+ 1160200
+
+
diff --git a/osx/FireflyCommon.h b/osx/FireflyCommon.h
new file mode 100644
index 00000000..817ae0db
--- /dev/null
+++ b/osx/FireflyCommon.h
@@ -0,0 +1,229 @@
+/*
+ * FireflyCommonConstants.h
+ *
+ * Created by Mike Kobb on 7/12/06.
+ * Copyright 2006 Roku LLC. All rights reserved.
+ *
+ * This file contains common constants and types needed by both the
+ * prefs pane and helper apps.
+ */
+
+#ifndef __ORG_FIREFLYMEDIASERVER_FIREFLY_COMMON_H
+#define __ORG_FIREFLYMEDIASERVER_FIREFLY_COMMON_H
+
+#include // used by GetProcesses
+
+#define FIREFLY_SERVER_NAME "firefly"
+#define FIREFLY_DIR_NAME "Firefly"
+#define FIREFLY_CONF_NAME "firefly.conf"
+
+#define FF_PREFS_DOMAIN "org.fireflymediaserver.firefly"
+#define FF_PREFS_LAUNCH_AT_LOGIN "org.fireflymediaserver.launchAtLogin"
+#define FF_PREFS_SHOW_MENU_EXTRA "org.fireflymediaserver.showMenuExtra"
+
+// Define this to enable certain debug output
+//#define FIREFLY_DEBUG
+
+
+typedef enum
+{
+ kFireflyStartInvalid = 0,
+ kFireflyStartSuccess = 1,
+ kFireflyStartFail = 2
+} FireflyStartResult;
+
+typedef enum
+{
+ kFireflyStopInvalid = 0,
+ kFireflyStopSuccess = 1,
+ kFireflyStopFail = 2
+} FireflyStopResult;
+
+typedef enum
+{
+ kFireflyRestartInvalid = 0,
+ kFireflyRestartSuccess = 1,
+ kFireflyRestartFail = 2
+} FireflyRestartResult;
+
+typedef enum
+{
+ kFireflyRescanInvalid = 0,
+ kFireflyRescanSuccess = 1,
+ kFireflyRescanFail = 2
+} FireflyRescanResult;
+
+
+typedef enum
+{
+ kFireflyStatusInvalid,
+ kFireflyStatusStopped,
+ kFireflyStatusStarting,
+ kFireflyStatusActive,
+ kFireflyStatusScanning,
+ kFireflyStatusStopping,
+ kFireflyStatusRestarting,
+ kFireflyStatusStartFailed,
+ kFireflyStatusCrashed
+} FireflyServerStatus;
+
+static NSString*
+StringForFireflyStatus( FireflyServerStatus inStatus )
+{
+ NSString *retVal = nil;
+ switch( inStatus )
+ {
+ case kFireflyStatusStopped:
+ retVal = NSLocalizedString( @"Firefly is not running",
+ @"Status message for Firefly" );
+ break;
+
+ case kFireflyStatusStarting:
+ retVal = NSLocalizedString( @"Firefly is starting",
+ @"Status message for Firefly" );
+ break;
+
+ case kFireflyStatusActive:
+ retVal = NSLocalizedString( @"Firefly is running",
+ @"Status message for Firefly" );
+ break;
+
+ case kFireflyStatusScanning:
+ retVal = NSLocalizedString( @"Firefly is scanning the library",
+ @"Status message for Firefly" );
+ break;
+
+ case kFireflyStatusStopping:
+ retVal = NSLocalizedString( @"Firefly is stopping",
+ @"Status message for Firefly" );
+ break;
+
+ case kFireflyStatusRestarting:
+ retVal = NSLocalizedString( @"Firefly is restarting",
+ @"Status message for Firefly" );
+ break;
+
+ case kFireflyStatusStartFailed:
+ retVal = NSLocalizedString( @"Firefly failed to start",
+ @"Status message for Firefly" );
+ break;
+
+ case kFireflyStatusCrashed:
+ retVal = NSLocalizedString( @"Firefly stopped unexpectedly",
+ @"Status message for Firefly" );
+ break;
+
+ case kFireflyStatusInvalid:
+ default:
+ retVal = NSLocalizedString( @"Firefly status is unknown",
+ @"Status message for Firefly" );
+ break;
+ }
+
+ return retVal;
+}
+
+// ===========================================================================
+// Process management the Unix way -- Finding if the server is already
+// running, or finding a specific process
+// ===========================================================================
+
+// This just makes syntax more convenient (don't have to say 'struct' everyplace)
+typedef struct kinfo_proc kinfo_proc;
+
+// ------------------------------------------------------------------------
+// GetProcesses
+//
+// Static utility function allocates and returns an array of kinfo_proc
+// structures representing the currently-running processes on the machine.
+// The calling function is responsible for disposing the returned pointer
+// with free()
+//
+// Because Firefly runs as a BSD daemon, the Process Manager is not useful
+// in finding it. Instead, we have to talk to the BSD layer. This code
+// was provided by Apple in a tech note.
+// ------------------------------------------------------------------------
+static void
+GetProcesses( kinfo_proc **outResult, size_t *outLength)
+{
+ int err;
+ kinfo_proc * result;
+ BOOL done;
+ static const int name[] = { CTL_KERN, KERN_PROC, KERN_PROC_ALL, 0 };
+ // Declaring name as const requires us to cast it when passing it to
+ // sysctl because the prototype doesn't include the const modifier.
+ // (That's the Apple comment, but they don't say *why* they made it const...)
+ size_t length;
+
+ // We call sysctl with result == NULL and length == 0.
+ // That will succeed, and set length to the appropriate length.
+ // We then allocate a buffer of that size and call sysctl again
+ // with that buffer. If that succeeds, we're done. If that fails
+ // with ENOMEM, we have to throw away our buffer and loop. Note
+ // that the loop causes use to call sysctl with NULL again; this
+ // is necessary because the ENOMEM failure case sets length to
+ // the amount of data returned, not the amount of data that
+ // could have been returned.
+
+ result = NULL;
+ done = NO;
+ do
+ {
+ // Call sysctl with a NULL buffer.
+ length = 0;
+ err = sysctl( (int *) name, (sizeof(name) / sizeof(*name)) - 1,
+ NULL, &length,
+ NULL, 0);
+ if (err == -1)
+ err = errno;
+
+ // Allocate an appropriately sized buffer based on the results
+ // from the previous call.
+ if (err == 0)
+ {
+ result = malloc(length);
+ if (result == NULL)
+ err = ENOMEM;
+ }
+
+ // Call sysctl again with the new buffer. If we get an ENOMEM
+ // error, toss away our buffer and start again.
+ if (err == 0)
+ {
+ err = sysctl( (int *) name, (sizeof(name) / sizeof(*name)) - 1,
+ result, &length,
+ NULL, 0);
+ if (err == -1)
+ err = errno;
+ if (err == 0)
+ done = YES;
+ else if (err == ENOMEM)
+ {
+ free(result);
+ result = NULL;
+ err = 0;
+ }
+ }
+ } while (err == 0 && !done);
+
+ // Clean up and establish post conditions.
+ if( err != 0 )
+ {
+ if( result != NULL)
+ free(result);
+ *outResult = NULL;
+ *outLength = 0;
+ }
+
+ if( err == 0 )
+ {
+ *outResult = result;
+ *outLength = length;
+ }
+}
+
+
+
+// __ORG_FIREFLYMEDIASERVER_FIREFLY_COMMON_H
+#endif
+
diff --git a/osx/FireflyPrefs/English.lproj/FireflyPrefsPref.nib/classes.nib b/osx/FireflyPrefs/English.lproj/FireflyPrefsPref.nib/classes.nib
new file mode 100644
index 00000000..c65366f9
--- /dev/null
+++ b/osx/FireflyPrefs/English.lproj/FireflyPrefsPref.nib/classes.nib
@@ -0,0 +1,61 @@
+{
+ IBClasses = (
+ {CLASS = FirstResponder; LANGUAGE = ObjC; SUPERCLASS = NSObject; },
+ {
+ CLASS = NSPreferencePane;
+ LANGUAGE = ObjC;
+ OUTLETS = {
+ "_firstKeyView" = id;
+ "_initialKeyView" = id;
+ "_lastKeyView" = id;
+ "_window" = id;
+ };
+ SUPERCLASS = NSObject;
+ },
+ {
+ ACTIONS = {
+ applyNowButtonClicked = id;
+ browseButtonClicked = id;
+ helperMenuCheckboxClicked = id;
+ logoButtonClicked = id;
+ myAction = id;
+ passwordChanged = id;
+ portChanged = id;
+ portPopupChanged = id;
+ pwCheckBoxChanged = id;
+ serverStartOptionChanged = id;
+ shareNameChanged = id;
+ startStopButtonClicked = id;
+ webPageButtonClicked = id;
+ };
+ CLASS = OrgFireflyMediaServerPrefs;
+ LANGUAGE = ObjC;
+ OUTLETS = {
+ applyNowButton = NSButton;
+ browseButton = NSButton;
+ helperMenuCheckbox = NSButton;
+ libraryField = NSTextField;
+ libraryIcon = NSImageView;
+ logTextView = NSTextView;
+ mainTabView = NSTabView;
+ myOutlet = id;
+ nameField = NSTextField;
+ panelVersionText = NSTextField;
+ passwordCheckbox = NSButton;
+ passwordField = NSTextField;
+ portField = NSTextField;
+ portPopup = NSPopUpButton;
+ progressSpinner = NSProgressIndicator;
+ serverStartOptions = NSPopUpButton;
+ serverVersionText = NSTextField;
+ startStopButton = NSButton;
+ statusText = NSTextField;
+ webPageButton = NSButton;
+ webPageInfoText = NSTextField;
+ };
+ SUPERCLASS = NSPreferencePane;
+ },
+ {CLASS = TextFormatter; LANGUAGE = ObjC; SUPERCLASS = NSFormatter; }
+ );
+ IBVersion = 1;
+}
\ No newline at end of file
diff --git a/osx/FireflyPrefs/English.lproj/FireflyPrefsPref.nib/info.nib b/osx/FireflyPrefs/English.lproj/FireflyPrefsPref.nib/info.nib
new file mode 100644
index 00000000..c40710df
--- /dev/null
+++ b/osx/FireflyPrefs/English.lproj/FireflyPrefsPref.nib/info.nib
@@ -0,0 +1,20 @@
+
+
+
+
+ IBDocumentLocation
+ 41 85 481 349 0 0 1440 878
+ IBFramework Version
+ 446.1
+ IBLockedTabItems
+
+ 99
+
+ IBOpenObjects
+
+ 12
+
+ IBSystem Version
+ 8J135
+
+
diff --git a/osx/FireflyPrefs/English.lproj/FireflyPrefsPref.nib/keyedobjects.nib b/osx/FireflyPrefs/English.lproj/FireflyPrefsPref.nib/keyedobjects.nib
new file mode 100644
index 00000000..8b2f3e89
Binary files /dev/null and b/osx/FireflyPrefs/English.lproj/FireflyPrefsPref.nib/keyedobjects.nib differ
diff --git a/osx/FireflyPrefs/English.lproj/FireflyPrefsPref~.nib/classes.nib b/osx/FireflyPrefs/English.lproj/FireflyPrefsPref~.nib/classes.nib
new file mode 100644
index 00000000..c65366f9
--- /dev/null
+++ b/osx/FireflyPrefs/English.lproj/FireflyPrefsPref~.nib/classes.nib
@@ -0,0 +1,61 @@
+{
+ IBClasses = (
+ {CLASS = FirstResponder; LANGUAGE = ObjC; SUPERCLASS = NSObject; },
+ {
+ CLASS = NSPreferencePane;
+ LANGUAGE = ObjC;
+ OUTLETS = {
+ "_firstKeyView" = id;
+ "_initialKeyView" = id;
+ "_lastKeyView" = id;
+ "_window" = id;
+ };
+ SUPERCLASS = NSObject;
+ },
+ {
+ ACTIONS = {
+ applyNowButtonClicked = id;
+ browseButtonClicked = id;
+ helperMenuCheckboxClicked = id;
+ logoButtonClicked = id;
+ myAction = id;
+ passwordChanged = id;
+ portChanged = id;
+ portPopupChanged = id;
+ pwCheckBoxChanged = id;
+ serverStartOptionChanged = id;
+ shareNameChanged = id;
+ startStopButtonClicked = id;
+ webPageButtonClicked = id;
+ };
+ CLASS = OrgFireflyMediaServerPrefs;
+ LANGUAGE = ObjC;
+ OUTLETS = {
+ applyNowButton = NSButton;
+ browseButton = NSButton;
+ helperMenuCheckbox = NSButton;
+ libraryField = NSTextField;
+ libraryIcon = NSImageView;
+ logTextView = NSTextView;
+ mainTabView = NSTabView;
+ myOutlet = id;
+ nameField = NSTextField;
+ panelVersionText = NSTextField;
+ passwordCheckbox = NSButton;
+ passwordField = NSTextField;
+ portField = NSTextField;
+ portPopup = NSPopUpButton;
+ progressSpinner = NSProgressIndicator;
+ serverStartOptions = NSPopUpButton;
+ serverVersionText = NSTextField;
+ startStopButton = NSButton;
+ statusText = NSTextField;
+ webPageButton = NSButton;
+ webPageInfoText = NSTextField;
+ };
+ SUPERCLASS = NSPreferencePane;
+ },
+ {CLASS = TextFormatter; LANGUAGE = ObjC; SUPERCLASS = NSFormatter; }
+ );
+ IBVersion = 1;
+}
\ No newline at end of file
diff --git a/osx/FireflyPrefs/English.lproj/FireflyPrefsPref~.nib/info.nib b/osx/FireflyPrefs/English.lproj/FireflyPrefsPref~.nib/info.nib
new file mode 100644
index 00000000..c40710df
--- /dev/null
+++ b/osx/FireflyPrefs/English.lproj/FireflyPrefsPref~.nib/info.nib
@@ -0,0 +1,20 @@
+
+
+
+
+ IBDocumentLocation
+ 41 85 481 349 0 0 1440 878
+ IBFramework Version
+ 446.1
+ IBLockedTabItems
+
+ 99
+
+ IBOpenObjects
+
+ 12
+
+ IBSystem Version
+ 8J135
+
+
diff --git a/osx/FireflyPrefs/English.lproj/FireflyPrefsPref~.nib/keyedobjects.nib b/osx/FireflyPrefs/English.lproj/FireflyPrefsPref~.nib/keyedobjects.nib
new file mode 100644
index 00000000..ce074d1f
Binary files /dev/null and b/osx/FireflyPrefs/English.lproj/FireflyPrefsPref~.nib/keyedobjects.nib differ
diff --git a/osx/FireflyPrefs/English.lproj/InfoPlist.strings b/osx/FireflyPrefs/English.lproj/InfoPlist.strings
new file mode 100644
index 00000000..e48cbef5
Binary files /dev/null and b/osx/FireflyPrefs/English.lproj/InfoPlist.strings differ
diff --git a/osx/FireflyPrefs/FireflyCommon.h b/osx/FireflyPrefs/FireflyCommon.h
new file mode 100644
index 00000000..817ae0db
--- /dev/null
+++ b/osx/FireflyPrefs/FireflyCommon.h
@@ -0,0 +1,229 @@
+/*
+ * FireflyCommonConstants.h
+ *
+ * Created by Mike Kobb on 7/12/06.
+ * Copyright 2006 Roku LLC. All rights reserved.
+ *
+ * This file contains common constants and types needed by both the
+ * prefs pane and helper apps.
+ */
+
+#ifndef __ORG_FIREFLYMEDIASERVER_FIREFLY_COMMON_H
+#define __ORG_FIREFLYMEDIASERVER_FIREFLY_COMMON_H
+
+#include // used by GetProcesses
+
+#define FIREFLY_SERVER_NAME "firefly"
+#define FIREFLY_DIR_NAME "Firefly"
+#define FIREFLY_CONF_NAME "firefly.conf"
+
+#define FF_PREFS_DOMAIN "org.fireflymediaserver.firefly"
+#define FF_PREFS_LAUNCH_AT_LOGIN "org.fireflymediaserver.launchAtLogin"
+#define FF_PREFS_SHOW_MENU_EXTRA "org.fireflymediaserver.showMenuExtra"
+
+// Define this to enable certain debug output
+//#define FIREFLY_DEBUG
+
+
+typedef enum
+{
+ kFireflyStartInvalid = 0,
+ kFireflyStartSuccess = 1,
+ kFireflyStartFail = 2
+} FireflyStartResult;
+
+typedef enum
+{
+ kFireflyStopInvalid = 0,
+ kFireflyStopSuccess = 1,
+ kFireflyStopFail = 2
+} FireflyStopResult;
+
+typedef enum
+{
+ kFireflyRestartInvalid = 0,
+ kFireflyRestartSuccess = 1,
+ kFireflyRestartFail = 2
+} FireflyRestartResult;
+
+typedef enum
+{
+ kFireflyRescanInvalid = 0,
+ kFireflyRescanSuccess = 1,
+ kFireflyRescanFail = 2
+} FireflyRescanResult;
+
+
+typedef enum
+{
+ kFireflyStatusInvalid,
+ kFireflyStatusStopped,
+ kFireflyStatusStarting,
+ kFireflyStatusActive,
+ kFireflyStatusScanning,
+ kFireflyStatusStopping,
+ kFireflyStatusRestarting,
+ kFireflyStatusStartFailed,
+ kFireflyStatusCrashed
+} FireflyServerStatus;
+
+static NSString*
+StringForFireflyStatus( FireflyServerStatus inStatus )
+{
+ NSString *retVal = nil;
+ switch( inStatus )
+ {
+ case kFireflyStatusStopped:
+ retVal = NSLocalizedString( @"Firefly is not running",
+ @"Status message for Firefly" );
+ break;
+
+ case kFireflyStatusStarting:
+ retVal = NSLocalizedString( @"Firefly is starting",
+ @"Status message for Firefly" );
+ break;
+
+ case kFireflyStatusActive:
+ retVal = NSLocalizedString( @"Firefly is running",
+ @"Status message for Firefly" );
+ break;
+
+ case kFireflyStatusScanning:
+ retVal = NSLocalizedString( @"Firefly is scanning the library",
+ @"Status message for Firefly" );
+ break;
+
+ case kFireflyStatusStopping:
+ retVal = NSLocalizedString( @"Firefly is stopping",
+ @"Status message for Firefly" );
+ break;
+
+ case kFireflyStatusRestarting:
+ retVal = NSLocalizedString( @"Firefly is restarting",
+ @"Status message for Firefly" );
+ break;
+
+ case kFireflyStatusStartFailed:
+ retVal = NSLocalizedString( @"Firefly failed to start",
+ @"Status message for Firefly" );
+ break;
+
+ case kFireflyStatusCrashed:
+ retVal = NSLocalizedString( @"Firefly stopped unexpectedly",
+ @"Status message for Firefly" );
+ break;
+
+ case kFireflyStatusInvalid:
+ default:
+ retVal = NSLocalizedString( @"Firefly status is unknown",
+ @"Status message for Firefly" );
+ break;
+ }
+
+ return retVal;
+}
+
+// ===========================================================================
+// Process management the Unix way -- Finding if the server is already
+// running, or finding a specific process
+// ===========================================================================
+
+// This just makes syntax more convenient (don't have to say 'struct' everyplace)
+typedef struct kinfo_proc kinfo_proc;
+
+// ------------------------------------------------------------------------
+// GetProcesses
+//
+// Static utility function allocates and returns an array of kinfo_proc
+// structures representing the currently-running processes on the machine.
+// The calling function is responsible for disposing the returned pointer
+// with free()
+//
+// Because Firefly runs as a BSD daemon, the Process Manager is not useful
+// in finding it. Instead, we have to talk to the BSD layer. This code
+// was provided by Apple in a tech note.
+// ------------------------------------------------------------------------
+static void
+GetProcesses( kinfo_proc **outResult, size_t *outLength)
+{
+ int err;
+ kinfo_proc * result;
+ BOOL done;
+ static const int name[] = { CTL_KERN, KERN_PROC, KERN_PROC_ALL, 0 };
+ // Declaring name as const requires us to cast it when passing it to
+ // sysctl because the prototype doesn't include the const modifier.
+ // (That's the Apple comment, but they don't say *why* they made it const...)
+ size_t length;
+
+ // We call sysctl with result == NULL and length == 0.
+ // That will succeed, and set length to the appropriate length.
+ // We then allocate a buffer of that size and call sysctl again
+ // with that buffer. If that succeeds, we're done. If that fails
+ // with ENOMEM, we have to throw away our buffer and loop. Note
+ // that the loop causes use to call sysctl with NULL again; this
+ // is necessary because the ENOMEM failure case sets length to
+ // the amount of data returned, not the amount of data that
+ // could have been returned.
+
+ result = NULL;
+ done = NO;
+ do
+ {
+ // Call sysctl with a NULL buffer.
+ length = 0;
+ err = sysctl( (int *) name, (sizeof(name) / sizeof(*name)) - 1,
+ NULL, &length,
+ NULL, 0);
+ if (err == -1)
+ err = errno;
+
+ // Allocate an appropriately sized buffer based on the results
+ // from the previous call.
+ if (err == 0)
+ {
+ result = malloc(length);
+ if (result == NULL)
+ err = ENOMEM;
+ }
+
+ // Call sysctl again with the new buffer. If we get an ENOMEM
+ // error, toss away our buffer and start again.
+ if (err == 0)
+ {
+ err = sysctl( (int *) name, (sizeof(name) / sizeof(*name)) - 1,
+ result, &length,
+ NULL, 0);
+ if (err == -1)
+ err = errno;
+ if (err == 0)
+ done = YES;
+ else if (err == ENOMEM)
+ {
+ free(result);
+ result = NULL;
+ err = 0;
+ }
+ }
+ } while (err == 0 && !done);
+
+ // Clean up and establish post conditions.
+ if( err != 0 )
+ {
+ if( result != NULL)
+ free(result);
+ *outResult = NULL;
+ *outLength = 0;
+ }
+
+ if( err == 0 )
+ {
+ *outResult = result;
+ *outLength = length;
+ }
+}
+
+
+
+// __ORG_FIREFLYMEDIASERVER_FIREFLY_COMMON_H
+#endif
+
diff --git a/osx/FireflyPrefs/FireflyLogo.png b/osx/FireflyPrefs/FireflyLogo.png
new file mode 100644
index 00000000..fcc645cb
Binary files /dev/null and b/osx/FireflyPrefs/FireflyLogo.png differ
diff --git a/osx/FireflyPrefs/FireflyPrefs.xcodeproj/project.pbxproj b/osx/FireflyPrefs/FireflyPrefs.xcodeproj/project.pbxproj
new file mode 100644
index 00000000..57dbe3bb
--- /dev/null
+++ b/osx/FireflyPrefs/FireflyPrefs.xcodeproj/project.pbxproj
@@ -0,0 +1,346 @@
+// !$*UTF8*$!
+{
+ archiveVersion = 1;
+ classes = {
+ };
+ objectVersion = 42;
+ objects = {
+
+/* Begin PBXBuildFile section */
+ 1061CA7B0A23A648002E88E0 /* OrgFireflyMediaServerPrefs.h in Headers */ = {isa = PBXBuildFile; fileRef = 1061CA790A23A648002E88E0 /* OrgFireflyMediaServerPrefs.h */; };
+ 1061CB530A266BAE002E88E0 /* FireflyLogo.png in Resources */ = {isa = PBXBuildFile; fileRef = 1061CB520A266BAE002E88E0 /* FireflyLogo.png */; };
+ 1062C6FB0A7554840003FC27 /* FireflyCommon.h in Headers */ = {isa = PBXBuildFile; fileRef = 1062C6FA0A7554840003FC27 /* FireflyCommon.h */; };
+ 1098819D0A704B3200F53ED9 /* firefly.conf in Resources */ = {isa = PBXBuildFile; fileRef = 1098819C0A704B3200F53ED9 /* firefly.conf */; };
+ 10B4B4030A34E3A9008238B7 /* OrgFireflyMediaServerPrefs.m in Sources */ = {isa = PBXBuildFile; fileRef = 10B4B4020A34E3A9008238B7 /* OrgFireflyMediaServerPrefs.m */; };
+ 10C78FE00A637CA900732D76 /* FireflyPrefsProtocol.h in Headers */ = {isa = PBXBuildFile; fileRef = 10C78FDF0A637CA900732D76 /* FireflyPrefsProtocol.h */; };
+ 10C7930B0A660CCA00732D76 /* Firefly Helper.app in Resources */ = {isa = PBXBuildFile; fileRef = 10C792B50A660CC900732D76 /* Firefly Helper.app */; };
+ 8D202CEA0486D31800D8A456 /* FireflyPrefs_Prefix.pch in Headers */ = {isa = PBXBuildFile; fileRef = 32DBCFA20370C41700C91783 /* FireflyPrefs_Prefix.pch */; };
+ 8D202CED0486D31800D8A456 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 089C167DFE841241C02AAC07 /* InfoPlist.strings */; };
+ 8D202CEE0486D31800D8A456 /* FireflyPrefsPref.tiff in Resources */ = {isa = PBXBuildFile; fileRef = F506C040013D9D8001CA16C8 /* FireflyPrefsPref.tiff */; };
+ 8D202CEF0486D31800D8A456 /* FireflyPrefsPref.nib in Resources */ = {isa = PBXBuildFile; fileRef = F506C042013D9D8C01CA16C8 /* FireflyPrefsPref.nib */; };
+ 8D202CF30486D31800D8A456 /* Cocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1058C7ADFEA557BF11CA2CBB /* Cocoa.framework */; };
+ 8D202CF40486D31800D8A456 /* PreferencePanes.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F506C035013D953901CA16C8 /* PreferencePanes.framework */; };
+/* End PBXBuildFile section */
+
+/* Begin PBXFileReference section */
+ 089C1672FE841209C02AAC07 /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = /System/Library/Frameworks/Foundation.framework; sourceTree = ""; };
+ 089C167EFE841241C02AAC07 /* English */ = {isa = PBXFileReference; fileEncoding = 10; lastKnownFileType = text.plist.strings; name = English; path = English.lproj/InfoPlist.strings; sourceTree = ""; };
+ 089C167FFE841241C02AAC07 /* AppKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AppKit.framework; path = /System/Library/Frameworks/AppKit.framework; sourceTree = ""; };
+ 1058C7ADFEA557BF11CA2CBB /* Cocoa.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Cocoa.framework; path = /System/Library/Frameworks/Cocoa.framework; sourceTree = ""; };
+ 1061CA790A23A648002E88E0 /* OrgFireflyMediaServerPrefs.h */ = {isa = PBXFileReference; fileEncoding = 30; lastKnownFileType = sourcecode.c.h; path = OrgFireflyMediaServerPrefs.h; sourceTree = ""; };
+ 1061CB520A266BAE002E88E0 /* FireflyLogo.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = FireflyLogo.png; sourceTree = ""; };
+ 1062C6FA0A7554840003FC27 /* FireflyCommon.h */ = {isa = PBXFileReference; fileEncoding = 30; lastKnownFileType = sourcecode.c.h; path = FireflyCommon.h; sourceTree = ""; };
+ 1098819C0A704B3200F53ED9 /* firefly.conf */ = {isa = PBXFileReference; fileEncoding = 30; lastKnownFileType = text; path = firefly.conf; sourceTree = ""; };
+ 10B4B4020A34E3A9008238B7 /* OrgFireflyMediaServerPrefs.m */ = {isa = PBXFileReference; fileEncoding = 30; lastKnownFileType = sourcecode.c.objc; path = OrgFireflyMediaServerPrefs.m; sourceTree = ""; };
+ 10C78FDF0A637CA900732D76 /* FireflyPrefsProtocol.h */ = {isa = PBXFileReference; fileEncoding = 30; lastKnownFileType = sourcecode.c.h; name = FireflyPrefsProtocol.h; path = ../FireflyPrefsProtocol.h; sourceTree = ""; };
+ 10C792B50A660CC900732D76 /* Firefly Helper.app */ = {isa = PBXFileReference; lastKnownFileType = wrapper.application; path = "Firefly Helper.app"; sourceTree = ""; };
+ 32DBCFA20370C41700C91783 /* FireflyPrefs_Prefix.pch */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FireflyPrefs_Prefix.pch; sourceTree = ""; };
+ 8D202CF70486D31800D8A456 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; };
+ 8D202CF80486D31800D8A456 /* Firefly.prefPane */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = Firefly.prefPane; sourceTree = BUILT_PRODUCTS_DIR; };
+ F506C035013D953901CA16C8 /* PreferencePanes.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = PreferencePanes.framework; path = /System/Library/Frameworks/PreferencePanes.framework; sourceTree = ""; };
+ F506C040013D9D8001CA16C8 /* FireflyPrefsPref.tiff */ = {isa = PBXFileReference; lastKnownFileType = image.tiff; path = FireflyPrefsPref.tiff; sourceTree = ""; };
+ F506C043013D9D8C01CA16C8 /* English */ = {isa = PBXFileReference; lastKnownFileType = wrapper.nib; name = English; path = English.lproj/FireflyPrefsPref.nib; sourceTree = ""; };
+/* End PBXFileReference section */
+
+/* Begin PBXFrameworksBuildPhase section */
+ 8D202CF20486D31800D8A456 /* Frameworks */ = {
+ isa = PBXFrameworksBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 8D202CF30486D31800D8A456 /* Cocoa.framework in Frameworks */,
+ 8D202CF40486D31800D8A456 /* PreferencePanes.framework in Frameworks */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXFrameworksBuildPhase section */
+
+/* Begin PBXGroup section */
+ 089C166AFE841209C02AAC07 /* FireflyPrefs */ = {
+ isa = PBXGroup;
+ children = (
+ 08FB77AFFE84173DC02AAC07 /* Classes */,
+ 32DBCFA10370C40200C91783 /* Other Sources */,
+ 089C167CFE841241C02AAC07 /* Resources */,
+ 089C1671FE841209C02AAC07 /* Frameworks and Libraries */,
+ 19C28FB8FE9D52D311CA2CBB /* Products */,
+ );
+ name = FireflyPrefs;
+ sourceTree = "";
+ };
+ 089C1671FE841209C02AAC07 /* Frameworks and Libraries */ = {
+ isa = PBXGroup;
+ children = (
+ 1058C7ACFEA557BF11CA2CBB /* Linked Frameworks */,
+ 1058C7AEFEA557BF11CA2CBB /* Other Frameworks */,
+ );
+ name = "Frameworks and Libraries";
+ sourceTree = "";
+ };
+ 089C167CFE841241C02AAC07 /* Resources */ = {
+ isa = PBXGroup;
+ children = (
+ 10C792B50A660CC900732D76 /* Firefly Helper.app */,
+ 1098819C0A704B3200F53ED9 /* firefly.conf */,
+ 8D202CF70486D31800D8A456 /* Info.plist */,
+ 089C167DFE841241C02AAC07 /* InfoPlist.strings */,
+ F506C040013D9D8001CA16C8 /* FireflyPrefsPref.tiff */,
+ F506C042013D9D8C01CA16C8 /* FireflyPrefsPref.nib */,
+ 1061CB520A266BAE002E88E0 /* FireflyLogo.png */,
+ );
+ name = Resources;
+ sourceTree = "";
+ };
+ 08FB77AFFE84173DC02AAC07 /* Classes */ = {
+ isa = PBXGroup;
+ children = (
+ 1062C6FA0A7554840003FC27 /* FireflyCommon.h */,
+ 10C78FDF0A637CA900732D76 /* FireflyPrefsProtocol.h */,
+ 10B4B4020A34E3A9008238B7 /* OrgFireflyMediaServerPrefs.m */,
+ 1061CA790A23A648002E88E0 /* OrgFireflyMediaServerPrefs.h */,
+ );
+ name = Classes;
+ sourceTree = "";
+ };
+ 1058C7ACFEA557BF11CA2CBB /* Linked Frameworks */ = {
+ isa = PBXGroup;
+ children = (
+ 1058C7ADFEA557BF11CA2CBB /* Cocoa.framework */,
+ F506C035013D953901CA16C8 /* PreferencePanes.framework */,
+ );
+ name = "Linked Frameworks";
+ sourceTree = "";
+ };
+ 1058C7AEFEA557BF11CA2CBB /* Other Frameworks */ = {
+ isa = PBXGroup;
+ children = (
+ 089C1672FE841209C02AAC07 /* Foundation.framework */,
+ 089C167FFE841241C02AAC07 /* AppKit.framework */,
+ );
+ name = "Other Frameworks";
+ sourceTree = "";
+ };
+ 19C28FB8FE9D52D311CA2CBB /* Products */ = {
+ isa = PBXGroup;
+ children = (
+ 8D202CF80486D31800D8A456 /* Firefly.prefPane */,
+ );
+ name = Products;
+ sourceTree = "";
+ };
+ 32DBCFA10370C40200C91783 /* Other Sources */ = {
+ isa = PBXGroup;
+ children = (
+ 32DBCFA20370C41700C91783 /* FireflyPrefs_Prefix.pch */,
+ );
+ name = "Other Sources";
+ sourceTree = "";
+ };
+/* End PBXGroup section */
+
+/* Begin PBXHeadersBuildPhase section */
+ 8D202CE90486D31800D8A456 /* Headers */ = {
+ isa = PBXHeadersBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 8D202CEA0486D31800D8A456 /* FireflyPrefs_Prefix.pch in Headers */,
+ 1061CA7B0A23A648002E88E0 /* OrgFireflyMediaServerPrefs.h in Headers */,
+ 10C78FE00A637CA900732D76 /* FireflyPrefsProtocol.h in Headers */,
+ 1062C6FB0A7554840003FC27 /* FireflyCommon.h in Headers */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXHeadersBuildPhase section */
+
+/* Begin PBXNativeTarget section */
+ 8D202CE80486D31800D8A456 /* Firefly */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = 1DBD214808BA80EA00186707 /* Build configuration list for PBXNativeTarget "Firefly" */;
+ buildPhases = (
+ 8D202CE90486D31800D8A456 /* Headers */,
+ 8D202CEC0486D31800D8A456 /* Resources */,
+ 8D202CF00486D31800D8A456 /* Sources */,
+ 8D202CF20486D31800D8A456 /* Frameworks */,
+ 8D202CF50486D31800D8A456 /* Rez */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ );
+ name = Firefly;
+ productInstallPath = "$(HOME)/Library/PreferencePanes";
+ productName = FireflyPrefs;
+ productReference = 8D202CF80486D31800D8A456 /* Firefly.prefPane */;
+ productType = "com.apple.product-type.bundle";
+ };
+/* End PBXNativeTarget section */
+
+/* Begin PBXProject section */
+ 089C1669FE841209C02AAC07 /* Project object */ = {
+ isa = PBXProject;
+ buildConfigurationList = 1DBD214C08BA80EA00186707 /* Build configuration list for PBXProject "FireflyPrefs" */;
+ hasScannedForEncodings = 1;
+ mainGroup = 089C166AFE841209C02AAC07 /* FireflyPrefs */;
+ projectDirPath = "";
+ targets = (
+ 8D202CE80486D31800D8A456 /* Firefly */,
+ );
+ };
+/* End PBXProject section */
+
+/* Begin PBXResourcesBuildPhase section */
+ 8D202CEC0486D31800D8A456 /* Resources */ = {
+ isa = PBXResourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 8D202CED0486D31800D8A456 /* InfoPlist.strings in Resources */,
+ 8D202CEE0486D31800D8A456 /* FireflyPrefsPref.tiff in Resources */,
+ 8D202CEF0486D31800D8A456 /* FireflyPrefsPref.nib in Resources */,
+ 1061CB530A266BAE002E88E0 /* FireflyLogo.png in Resources */,
+ 10C7930B0A660CCA00732D76 /* Firefly Helper.app in Resources */,
+ 1098819D0A704B3200F53ED9 /* firefly.conf in Resources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXResourcesBuildPhase section */
+
+/* Begin PBXRezBuildPhase section */
+ 8D202CF50486D31800D8A456 /* Rez */ = {
+ isa = PBXRezBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXRezBuildPhase section */
+
+/* Begin PBXSourcesBuildPhase section */
+ 8D202CF00486D31800D8A456 /* Sources */ = {
+ isa = PBXSourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 10B4B4030A34E3A9008238B7 /* OrgFireflyMediaServerPrefs.m in Sources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXSourcesBuildPhase section */
+
+/* Begin PBXVariantGroup section */
+ 089C167DFE841241C02AAC07 /* InfoPlist.strings */ = {
+ isa = PBXVariantGroup;
+ children = (
+ 089C167EFE841241C02AAC07 /* English */,
+ );
+ name = InfoPlist.strings;
+ sourceTree = "";
+ };
+ F506C042013D9D8C01CA16C8 /* FireflyPrefsPref.nib */ = {
+ isa = PBXVariantGroup;
+ children = (
+ F506C043013D9D8C01CA16C8 /* English */,
+ );
+ name = FireflyPrefsPref.nib;
+ sourceTree = "";
+ };
+/* End PBXVariantGroup section */
+
+/* Begin XCBuildConfiguration section */
+ 1DBD214908BA80EA00186707 /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ COPY_PHASE_STRIP = NO;
+ GCC_DYNAMIC_NO_PIC = NO;
+ GCC_ENABLE_FIX_AND_CONTINUE = YES;
+ GCC_MODEL_TUNING = G5;
+ GCC_OPTIMIZATION_LEVEL = 0;
+ GCC_PRECOMPILE_PREFIX_HEADER = YES;
+ GCC_PREFIX_HEADER = FireflyPrefs_Prefix.pch;
+ INFOPLIST_FILE = Info.plist;
+ INSTALL_PATH = "$(HOME)/Library/PreferencePanes";
+ MACOSX_DEPLOYMENT_TARGET_ppc = 10.3;
+ PRODUCT_NAME = Firefly;
+ WRAPPER_EXTENSION = prefPane;
+ ZERO_LINK = YES;
+ };
+ name = Debug;
+ };
+ 1DBD214A08BA80EA00186707 /* Release */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ARCHS = (
+ ppc,
+ i386,
+ );
+ GCC_GENERATE_DEBUGGING_SYMBOLS = NO;
+ GCC_MODEL_TUNING = G5;
+ GCC_PRECOMPILE_PREFIX_HEADER = YES;
+ GCC_PREFIX_HEADER = FireflyPrefs_Prefix.pch;
+ INFOPLIST_FILE = Info.plist;
+ INSTALL_PATH = "$(HOME)/Library/PreferencePanes";
+ MACOSX_DEPLOYMENT_TARGET_ppc = 10.3;
+ PRODUCT_NAME = Firefly;
+ SDKROOT_ppc = /Developer/SDKs/MacOSX10.3.9.sdk;
+ WRAPPER_EXTENSION = prefPane;
+ };
+ name = Release;
+ };
+ 1DBD214D08BA80EA00186707 /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ GCC_ENABLE_OBJC_EXCEPTIONS = YES;
+ GCC_VERSION_i386 = 4.0;
+ GCC_VERSION_ppc = 3.3;
+ GCC_WARN_ABOUT_RETURN_TYPE = YES;
+ GCC_WARN_UNUSED_VARIABLE = YES;
+ GENERATE_PKGINFO_FILE = YES;
+ MACOSX_DEPLOYMENT_TARGET_i386 = 10.4;
+ MACOSX_DEPLOYMENT_TARGET_ppc = 10.2;
+ PREBINDING = NO;
+ SDKROOT = /Developer/SDKs/MacOSX10.4u.sdk;
+ SDKROOT_i386 = /Developer/SDKs/MacOSX10.4u.sdk;
+ SDKROOT_ppc = /Developer/SDKs/MacOSX10.3.9.sdk;
+ };
+ name = Debug;
+ };
+ 1DBD214E08BA80EA00186707 /* Release */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ GCC_ENABLE_OBJC_EXCEPTIONS = YES;
+ GCC_VERSION_i386 = 4.0;
+ GCC_VERSION_ppc = 3.3;
+ GCC_WARN_ABOUT_RETURN_TYPE = YES;
+ GCC_WARN_UNUSED_VARIABLE = YES;
+ GENERATE_PKGINFO_FILE = YES;
+ MACOSX_DEPLOYMENT_TARGET_i386 = 10.4;
+ MACOSX_DEPLOYMENT_TARGET_ppc = 10.2;
+ PREBINDING = NO;
+ SDKROOT = /Developer/SDKs/MacOSX10.4u.sdk;
+ SDKROOT_i386 = /Developer/SDKs/MacOSX10.4u.sdk;
+ SDKROOT_ppc = /Developer/SDKs/MacOSX10.2.8.sdk;
+ };
+ name = Release;
+ };
+/* End XCBuildConfiguration section */
+
+/* Begin XCConfigurationList section */
+ 1DBD214808BA80EA00186707 /* Build configuration list for PBXNativeTarget "Firefly" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ 1DBD214908BA80EA00186707 /* Debug */,
+ 1DBD214A08BA80EA00186707 /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+ 1DBD214C08BA80EA00186707 /* Build configuration list for PBXProject "FireflyPrefs" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ 1DBD214D08BA80EA00186707 /* Debug */,
+ 1DBD214E08BA80EA00186707 /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+/* End XCConfigurationList section */
+ };
+ rootObject = 089C1669FE841209C02AAC07 /* Project object */;
+}
diff --git a/osx/FireflyPrefs/FireflyPrefsPref.tiff b/osx/FireflyPrefs/FireflyPrefsPref.tiff
new file mode 100644
index 00000000..f596c55e
Binary files /dev/null and b/osx/FireflyPrefs/FireflyPrefsPref.tiff differ
diff --git a/osx/FireflyPrefs/FireflyPrefs_Prefix.pch b/osx/FireflyPrefs/FireflyPrefs_Prefix.pch
new file mode 100644
index 00000000..647c7e48
--- /dev/null
+++ b/osx/FireflyPrefs/FireflyPrefs_Prefix.pch
@@ -0,0 +1,8 @@
+//
+// Prefix header for all source files of the 'FireflyPrefs' target in the 'FireflyPrefs' project.
+//
+
+#ifdef __OBJC__
+ #import
+ #import
+#endif
diff --git a/osx/FireflyPrefs/Info.plist b/osx/FireflyPrefs/Info.plist
new file mode 100644
index 00000000..be466185
--- /dev/null
+++ b/osx/FireflyPrefs/Info.plist
@@ -0,0 +1,36 @@
+
+
+
+
+ CFBundleDevelopmentRegion
+ English
+ CFBundleExecutable
+ ${EXECUTABLE_NAME}
+ CFBundleIconFile
+
+ CFBundleIdentifier
+ org.fireflymediaserver.prefpanel
+ CFBundleInfoDictionaryVersion
+ 6.0
+ CFBundleName
+ ${PRODUCT_NAME}
+ CFBundlePackageType
+ BNDL
+ CFBundleSignature
+ ????
+ CFBundleVersion
+ 102
+ CFBundleShortVersionString
+ 1.0b3
+ NSHumanReadableCopyright
+ Š 2006 Roku LLC
+ NSMainNibFile
+ FireflyPrefsPref
+ NSPrefPaneIconFile
+ FireflyPrefsPref.tiff
+ NSPrefPaneIconLabel
+ Firefly
+ NSPrincipalClass
+ OrgFireflyMediaServerPrefs
+
+
diff --git a/osx/FireflyPrefs/OrgFireflyMediaServerPrefs.h b/osx/FireflyPrefs/OrgFireflyMediaServerPrefs.h
new file mode 100644
index 00000000..b660e472
--- /dev/null
+++ b/osx/FireflyPrefs/OrgFireflyMediaServerPrefs.h
@@ -0,0 +1,158 @@
+/* OrgFireflyMediaServerPrefs */
+
+#import
+#import
+#import
+#import "../FireflyPrefsProtocol.h"
+
+@interface OrgFireflyMediaServerPrefs : NSPreferencePane < FireflyPrefsClientProtocol >
+{
+ IBOutlet NSButton *browseButton;
+ IBOutlet NSTextField *libraryField;
+ IBOutlet NSImageView *libraryIcon;
+ IBOutlet NSTextField *nameField;
+ IBOutlet NSButton *passwordCheckbox;
+ IBOutlet NSButton *helperMenuCheckbox;
+ IBOutlet NSTextField *passwordField;
+ IBOutlet NSTextField *portField;
+ IBOutlet NSPopUpButton *portPopup;
+ IBOutlet NSPopUpButton *serverStartOptions;
+ IBOutlet NSTextField *panelVersionText;
+ IBOutlet NSTextField *serverVersionText;
+ IBOutlet NSButton *startStopButton;
+ IBOutlet NSTextField *statusText;
+ IBOutlet NSButton *webPageButton;
+ IBOutlet NSTextField *webPageInfoText;
+ IBOutlet NSTabView *mainTabView;
+ IBOutlet NSButton *applyNowButton;
+ IBOutlet NSProgressIndicator *progressSpinner;
+ IBOutlet NSTextView *logTextView;
+ IBOutlet NSScrollView *logTextScroller;
+
+ CFStringRef appID;
+ NSMutableString *ourHostName;
+ NSMutableString *fireflyFolderPath;
+ NSMutableString *fireflyHelperPath;
+ NSMutableString *serverURL;
+ NSMutableString *logFilePath;
+ NSMutableString *playlistPath;
+ NSString *userName;
+
+ // Handling of the config file
+ NSMutableString *configFilePath;
+ BOOL configAppearsValid;
+ NSMutableString *configError;
+ NSMutableArray *configFileStrings;
+ unsigned long idxOfServerName;
+ unsigned long idxOfPassword;
+ unsigned long idxOfPort;
+ unsigned long idxOfLibraryPath;
+ unsigned long idxOfNextSection;
+ unsigned long idxOfDbPath;
+ unsigned long idxOfLogPath;
+ unsigned long idxOfPlaylistPath;
+
+ // Track whether we need to save
+ BOOL bConfigNeedsSaving;
+
+ // The actual preferences we manage with this GUI
+ NSMutableString *serverName;
+ NSMutableString *serverPassword;
+ NSMutableString *libraryPath;
+ unsigned short serverPort; // 0 means automatic
+ BOOL bStartServerOnLogin;
+ BOOL bShowHelperMenu;
+
+ // Timer mechanism for setting up IPC
+ int ipcTries;
+ NSTimer *ipcTimer;
+
+ // Interprocess communication with Firefly Helper
+ id serverProxy;
+ NSProtocolChecker *protocolChecker;
+ int clientIdent;
+
+ // Log view updating
+ NSTimer *logTimer;
+ NSDate *logDate;
+}
+
+- (IBAction)browseButtonClicked:(id)sender;
+- (IBAction)passwordChanged:(id)sender;
+- (IBAction)shareNameChanged:(id)sender;
+- (IBAction)portPopupChanged:(id)sender;
+- (IBAction)portChanged:(id)sender;
+- (IBAction)pwCheckBoxChanged:(id)sender;
+- (IBAction)serverStartOptionChanged:(id)sender;
+- (IBAction)startStopButtonClicked:(id)sender;
+- (IBAction)webPageButtonClicked:(id)sender;
+- (IBAction)applyNowButtonClicked:(id)sender;
+- (IBAction)helperMenuCheckboxClicked:(id)sender;
+
+// Overrides of NSPreferencePane methods
+- (void)willSelect;
+- (void)didSelect;
+- (NSPreferencePaneUnselectReply)shouldUnselect;
+- (void)willUnselect;
+
+// Checking the validity of the Firefly installation.
+- (BOOL)validateInstall;
+
+// Tracking the need to save the config
+- (void)setConfigNeedsSaving:(BOOL)needsSaving;
+
+// UI utility functions
+- (void)disableAllControls;
+- (void)updateServerStatus:(FireflyServerStatus) status;
+- (void)setIconForPath;
+
+// Functions for loading and saving our configuration, as well as
+// reading and writing the config file.
+- (BOOL)loadSettings;
+- (BOOL)saveSettings;
+- (BOOL)updateLoginItem;
+- (void)readSettingsForHelper:(BOOL*)outHelper andServer:(BOOL*)outServer;
+- (BOOL)readConfigFromPath:(NSString*)path;
+- (BOOL)writeConfigToPath:(NSString*)path;
+- (BOOL)createDefaultConfigFile;
+- (NSString *)readValueFromBuf:(char*)buf startingAt:(int)idx unescapeCommas:(BOOL) bUnescapeCommas;
+- (void)setDefaultValues;
+
+// Finding or launching the helper
+- (BOOL)helperIsRunning;
+- (void)launchHelperIfNeeded;
+
+// Validation of user entries
+- (BOOL)control:(NSControl *)control isValidObject:(id) obj;
+- (BOOL)currentTabIsValid;
+- (void)alertForControl:(NSControl *)control;
+
+// Alert delegate method(s)
+- (void)alertDidEnd:(NSAlert *)alert returnCode:(int)returnCode contextInfo:(void *)contextInfo;
+- (void)applySheetDidEnd:(NSAlert *)alert returnCode:(int)returnCode contextInfo:(void *)contextInfo;
+
+// Tab view delegate method(s)
+- (BOOL)tabView:(NSTabView *)tabView shouldSelectTabViewItem:(NSTabViewItem *)tabViewItem;
+
+// Browse panel delegate method(s)
+- (void)browsePanelEnded:(NSOpenPanel *)panel returnCode:(int)panelResult contextInfo:(void *)contextInfo;
+
+// Methods for dealing with the IPC proxy
+- (BOOL)makeProxyConnection;
+- (BOOL)checkProxyConnection;
+- (void)proxyTimerFired:(NSTimer *) timer;
+- (FireflyStartResult)startFirefly;
+- (FireflyStopResult)stopFirefly;
+- (FireflyRestartResult)restartFirefly;
+- (FireflyRescanResult)rescanLibrary;
+- (FireflyServerStatus)fireflyStatus;
+- (BOOL)fireflyIsRunning;
+- (NSString*)fireflyVersion;
+- (NSString*)fireflyConfigURL;
+- (void)showHelperMenu:(BOOL)bShowMenu;
+
+// Log view stuff
+- (void)updateLogTextView;
+- (void)logTimerFired:(NSTimer *) timer;
+
+@end
diff --git a/osx/FireflyPrefs/OrgFireflyMediaServerPrefs.m b/osx/FireflyPrefs/OrgFireflyMediaServerPrefs.m
new file mode 100644
index 00000000..b4678b40
--- /dev/null
+++ b/osx/FireflyPrefs/OrgFireflyMediaServerPrefs.m
@@ -0,0 +1,2268 @@
+// NOTE: The canonical Cocoa way to do things is with a strict Model-View-Controller
+// organization. However, that seems a bit silly for a simple case like a prefs
+// pane, so the OrgFireflyMediaServerPrefs object is both model and controller.
+
+#import
+#import
+#include
+#include
+#include
+#include
+#import "OrgFireflyMediaServerPrefs.h"
+#include "../FireflyCommon.h"
+
+// Here we define some constants used when testing the existence of and accessing
+// the components of our installation.
+#define FIREFLY_HELPER_NAME "Firefly Helper.app"
+#define FIREFLY_HELPER_PROC_N "Firefly Helper"
+#define FIREFLY_PLUGIN_DIR "plugins"
+#define FIREFLY_LOG_FILE "firefly.log"
+#define FIREFLY_PLAYLIST_FILE "firefly.playlist"
+
+@implementation OrgFireflyMediaServerPrefs
+
+// ===========================================================================
+// Initialization and deallocation
+// ===========================================================================
+- (id)initWithBundle:(NSBundle *)bundle
+{
+ if( ( self = [super initWithBundle:bundle] ) != nil )
+ {
+ appID = CFSTR( "org.fireflymediaserver.prefs" );
+
+ // Init our instance variables
+ configFileStrings = [[NSMutableArray arrayWithCapacity:100] retain];
+ configError = [[NSMutableString stringWithCapacity:20] retain];
+ fireflyFolderPath = [[NSMutableString stringWithCapacity:20] retain];
+ fireflyHelperPath = [[NSMutableString stringWithCapacity:20] retain];
+ serverURL = [[NSMutableString stringWithCapacity:20] retain];
+ logFilePath = [[NSMutableString stringWithCapacity:20] retain];
+ playlistPath = [[NSMutableString stringWithCapacity:20] retain];
+ userName = nil;
+ configFilePath = [[NSMutableString stringWithCapacity:20] retain];
+ serverName = [[NSMutableString stringWithCapacity:20] retain];
+ serverPassword = [[NSMutableString stringWithCapacity:20] retain];
+ libraryPath = [[NSMutableString stringWithCapacity:20] retain];
+ serverProxy = nil;
+ protocolChecker = nil;
+ ipcTimer = nil;
+ logTimer = nil;
+ logDate = nil;
+ srand((unsigned int)time(NULL));
+ }
+
+ return self;
+}
+
+- (void)dealloc
+{
+ [configFileStrings release];
+ [configError release];
+ [fireflyFolderPath release];
+ [fireflyHelperPath release];
+ [serverURL release];
+ [logFilePath release];
+ [playlistPath release];
+ [userName release];
+ [configFilePath release];
+ [serverName release];
+ [serverPassword release];
+ [libraryPath release];
+ [serverProxy release];
+ [protocolChecker release];
+ [ipcTimer release];
+ [logTimer release];
+ [logDate release];
+ [super dealloc];
+}
+
+// ===========================================================================
+// NSPreferencePane methods for handling the installation and removal of
+// the panel. We use these to read our prefs, set up our UI, and start
+// and stop our scan for the server, as well as confirming whether a user
+// wants to apply changes.
+// ===========================================================================
+
+// ---------------------------------------------------------------------------
+// willSelect
+//
+// NSPreferencePane instance method. We're about to be put on screen.
+// ---------------------------------------------------------------------------
+- (void)willSelect
+{
+ // NOTE: docs say default impl does nothing, so not necessary to call [super willSelect];
+
+ // Set up our user name (used for the library name as as IPC). Must do
+ // this early, because setDefaultValues will need it to make the library
+ // name. ("Copy" function name means no need to retain but we do need
+ // to release later. CSStringRef is toll-free bridged to NSString*)
+ [userName autorelease]; // in case we are being re-loaded within one Prefs session
+ userName = (NSString*)CSCopyUserName( false );
+
+ // We're about to be loaded. Set up everything
+ [self setDefaultValues];
+
+ // This is a bit of a hack. bConfigNeedsSaving will be set to YES upon
+ // exit from validateInstall if validateInstall had to create a new
+ // prefs file. We use this as a cue that it's a fresh install, and we
+ // need to get the startup item installed (by calling saveSettings)
+ bConfigNeedsSaving = NO;
+
+ if( ![self validateInstall] )
+ {
+ [self disableAllControls];
+ bConfigNeedsSaving = NO;
+ }
+ else
+ {
+ if(![self loadSettings])
+ {
+ [configError setString:NSLocalizedString( @"Unable to read configuration information",
+ @"Error message related to invalid config" ) ];
+ configAppearsValid = NO;
+ }
+ else
+ {
+ // If ValidateInstall told us it created a new file, then we are
+ // going to do some hacky things. First, set bStartServerOnLogin to
+ // false and save settings. This ensures that when we start firefly
+ // Helper in a few seconds, it does not launch the server before the
+ // user has a chance to set their settings. Then, we'll set it
+ // back to its original value, and leave bConfigNeedsSaving
+ // set. This way, when the user closes the panel or starts the server,
+ // their changes will be set. Ugh.
+ if( bConfigNeedsSaving )
+ {
+ BOOL priorVal = bStartServerOnLogin;
+ bStartServerOnLogin = NO;
+ [self saveSettings];
+ CFPreferencesAppSynchronize( CFSTR(FF_PREFS_DOMAIN) ); // flush changes
+ bConfigNeedsSaving = YES; // saveSettings sets to NO
+ bStartServerOnLogin = priorVal;
+ }
+ }
+ }
+
+ // Snag our current version
+ NSString *versionString = [[NSBundle bundleForClass:[self class]]
+ objectForInfoDictionaryKey:@"CFBundleShortVersionString"];
+ [panelVersionText setStringValue:versionString];
+
+
+ if( configAppearsValid )
+ {
+ // GUI setup to initial state (note that although some of these are set in
+ // the nib, we may be closed and then re-opened, so we need to set them
+ // here.
+ [browseButton setEnabled:YES];
+ [nameField setEnabled:YES];
+ [passwordCheckbox setEnabled:YES];
+ [helperMenuCheckbox setEnabled:YES];
+ [serverStartOptions setEnabled:YES];
+ [mainTabView selectFirstTabViewItem:self];
+ [nameField setStringValue:serverName];
+ [libraryField setStringValue:libraryPath];
+ [self setIconForPath];
+ [passwordField setStringValue:serverPassword];
+ if( [serverPassword length] > 0 )
+ {
+ [passwordCheckbox setState:NSOnState];
+ [passwordField setEnabled:YES];
+ }
+ else
+ {
+ [passwordCheckbox setState:NSOffState];
+ [passwordField setEnabled:NO];
+ }
+ [portField setIntValue:serverPort];
+ if( 0 != serverPort )
+ {
+ [portField setEnabled:YES];
+ [portPopup selectItemAtIndex:1];
+ }
+ else
+ {
+ [portField setEnabled:NO];
+ [portPopup selectItemAtIndex:0];
+ }
+ if( bStartServerOnLogin )
+ [serverStartOptions selectItemAtIndex:1];
+ else
+ [serverStartOptions selectItemAtIndex:0];
+
+ // bConfigNeedsSaving is configured above
+ [applyNowButton setEnabled:bConfigNeedsSaving];
+
+ [helperMenuCheckbox setState:(bShowHelperMenu ? NSOnState : NSOffState)];
+
+ // Member setup to initial state (note, these are not our actual
+ // preferences, which are set above). Rather, these are members for running
+ // the prefs pane.
+ serverProxy = nil;
+ ipcTimer = nil;
+ logTimer = nil;
+ [logDate autorelease];
+ logDate = [[NSDate distantPast] retain];
+
+ // Start by assuming that the server is not running.
+ [self updateServerStatus:kFireflyStatusStopped];
+
+ // We always need the helper running when the panel is running,
+ // so launch it if it's not already running
+ [self launchHelperIfNeeded];
+ }
+}
+
+// ---------------------------------------------------------------------------
+// didSelect
+//
+// NSPreferencePane instance method. We're now on screen.
+// ---------------------------------------------------------------------------
+- (void)didSelect
+{
+ // NOTE: docs say default impl does nothing, so not necessary to call [super didSelect];
+
+ // We've been loaded and are on screen.
+
+ // Did we encounter any errors at startup that will prevent us from doing work? If so,
+ // here's where we put up a sheet to explain.
+ if( configAppearsValid )
+ {
+ // No errors. We could go ahead and try right now to establish
+ // Connection. BUT, since we may be being opened in response to the
+ // Helper application's menu choice, we avoid a possible (temporary)
+ // deadlock by doing our first proxy attempt in the timer function,
+ // which allows didSelect to return and let the Apple Event complete.
+#if 0
+ //
+ if( [self makeProxyConnection] )
+ {
+ [self updateServerStatus:[self fireflyStatus]];
+ NSString *string = [self fireflyVersion];
+ if( nil != string )
+ [self versionChanged:string];
+ string = [self fireflyConfigURL];
+ if( nil != string )
+ [self configUrlChanged:string];
+ }
+ else
+#endif
+ {
+ [startStopButton setEnabled:NO];
+ [statusText setStringValue:NSLocalizedString( @"Checking Firefly statusÉ",
+ @"Status text for when Firefly state is not known" )];
+ [progressSpinner startAnimation:self];
+ ipcTimer = [[NSTimer scheduledTimerWithTimeInterval:1.0
+ target:self
+ selector:@selector(proxyTimerFired:)
+ userInfo:nil
+ repeats:YES] retain];
+ }
+ }
+ else
+ {
+ NSString *errorIntro = NSLocalizedString( @"Firefly appears to be incorrectly installed or damaged. "
+ "Please consult the documentation.\n\n",
+ @"Explanatory text for the failure-to-apply alert" );
+ NSString *errorString = [errorIntro stringByAppendingString:configError];
+ NSBeginCriticalAlertSheet( NSLocalizedString( @"Configuration error",
+ @"Alert message notifying the user of config error" ),
+ @"OK",
+ NULL,
+ NULL,
+ [[self mainView] window],
+ nil,
+ NULL,
+ NULL,
+ NULL,
+ errorString );
+ }
+}
+
+// ---------------------------------------------------------------------------
+// shouldUnselect
+//
+// NSPreferencePane delegate method
+// ---------------------------------------------------------------------------
+- (NSPreferencePaneUnselectReply)shouldUnselect
+{
+ // We write our config when the user clicks "Apply Now". If they've made changes
+ // but not clicked the button, we need to ask them here if they want to save.
+
+ // if changes need saving, we want to put up a sheet asking if they want to apply
+ // NOTE: Sheets are complicated to deal with, because you have to handle their results
+ // in delegate methods, and it gets a bit wonky if handling the result in turn
+ // requires another modal dialog. Anyway, we post the sheet here. Look for sheetDidEnd to
+ // see the handling of the result.
+ if( bConfigNeedsSaving )
+ {
+ // Even more complicated than the average sheet (where we could call NSBeginAlertSheet),
+ // because we offer Cmd-D for "Don't apply"
+ NSAlert *alert = [[NSAlert alloc] init];
+ [alert setMessageText:NSLocalizedString( @"Apply configuration changes?",
+ @"Prompt to save changes when exiting prefs pane" )];
+ [alert addButtonWithTitle:NSLocalizedString( @"Apply", @"Label for apply button in save prompt dialog" )];
+ [alert addButtonWithTitle:NSLocalizedString( @"Cancel", @"Label for cancel button in save prompt dialog" )];
+ NSButton *button;
+ button = [alert addButtonWithTitle:NSLocalizedString( @"Don't Apply",
+ @"Label for dont' apply button in save prompt dialog" )];
+ [button setKeyEquivalent:@"d"];
+ [button setKeyEquivalentModifierMask:NSCommandKeyMask];
+ [alert beginSheetModalForWindow:[[self mainView] window]
+ modalDelegate:self
+ didEndSelector:@selector(applySheetDidEnd:returnCode:contextInfo:)
+ contextInfo:nil];
+ return NSUnselectLater;
+ }
+ else
+ {
+ return NSUnselectNow;
+ }
+}
+
+// ---------------------------------------------------------------------------
+// willUnselect
+//
+// NSPreferencePane delegate method
+// ---------------------------------------------------------------------------
+- (void)willUnselect
+{
+ // NOTE: docs say default impl does nothing, so not necessary to call
+ // [super willUnselect];
+
+ // We could be unselected, then reselected, so there are a few objects
+ // where we need to go ahead and disconnect them and then release
+ // them so we can re-create if we're reloaded.
+ [ipcTimer invalidate];
+ [ipcTimer autorelease];
+ ipcTimer = nil;
+ [logTimer invalidate];
+ [logTimer autorelease];
+ logTimer = nil;
+
+ if( nil != serverProxy )
+ {
+ @try
+ {
+ [serverProxy unregisterClientId:clientIdent];
+ }
+ @catch( NSException *exception )
+ {
+ NSLog(@"willUnselect caught %@: %@",
+ [exception name], [exception reason]);
+ }
+ @finally
+ {
+ [serverProxy autorelease];
+ serverProxy = nil;
+ }
+ }
+
+ // Flush our prefs
+ CFPreferencesAppSynchronize( CFSTR(FF_PREFS_DOMAIN) );
+
+ // Last, make sure the login item is set up appropriately, no matter how
+ // we are getting out of here.
+ [self updateLoginItem];
+}
+
+// ===========================================================================
+// Functions to handle user input and interaction
+// ===========================================================================
+
+// ---------------------------------------------------------------------------
+// browseButtonClicked:
+//
+// User wants to change the library location. Pop up an "Open" sheet
+// ---------------------------------------------------------------------------
+- (IBAction)browseButtonClicked:(id)sender
+{
+ NSOpenPanel *panel = [NSOpenPanel openPanel];
+ [panel setCanChooseDirectories:YES];
+ [panel setCanChooseFiles:NO];
+ [panel setResolvesAliases:YES];
+ [panel setPrompt:NSLocalizedString( @"Choose", @"The Choose button in the library browser dialog" )];
+ [panel setTitle:NSLocalizedString( @"Choose Library Location", @"Title of the library browser dialog" )];
+ [panel setMessage:NSLocalizedString(
+ @"Please select the folder containing your music library, then click Choose.",
+ @"Info text for the library browse dialog" )];
+ NSString *path = [@"~/" stringByExpandingTildeInPath]; // default
+ NSString *file = nil;
+ NSFileManager *mgr = [NSFileManager defaultManager];
+ BOOL bIsDir = NO;
+ if( [mgr fileExistsAtPath:libraryPath isDirectory:&bIsDir] && bIsDir )
+ {
+ file = [libraryPath lastPathComponent];
+ path = [libraryPath stringByDeletingLastPathComponent];
+ }
+
+ [panel beginSheetForDirectory:path
+ file:file
+ types:nil
+ modalForWindow:[[self mainView] window]
+ modalDelegate:self
+ didEndSelector:@selector(browsePanelEnded:returnCode:contextInfo:)
+ contextInfo:nil];
+}
+
+// ---------------------------------------------------------------------------
+// browsePanelEnded:returnCode:contextInfo:
+//
+// Delegate method for the "Open" sheet. Handle the user's choice.
+// ---------------------------------------------------------------------------
+- (void)browsePanelEnded:(NSOpenPanel *)panel returnCode:(int)panelResult contextInfo:(void *)contextInfo
+{
+ if( NSOKButton == panelResult )
+ {
+ NSArray *selectedDirArray = [panel filenames];
+ if( 0 < [selectedDirArray count] )
+ {
+ [libraryPath setString:[selectedDirArray objectAtIndex:0]];
+ [libraryField setStringValue:libraryPath];
+ [self setIconForPath];
+ [self setConfigNeedsSaving:YES];
+ }
+ }
+}
+
+// ---------------------------------------------------------------------------
+// passwordChanged:
+// ---------------------------------------------------------------------------
+- (IBAction)passwordChanged:(id)sender
+{
+ if( NSOrderedSame != [serverPassword compare:[passwordField stringValue]] )
+ {
+ [serverPassword setString:[passwordField stringValue]];
+ [self setConfigNeedsSaving:YES];
+ }
+}
+
+// ---------------------------------------------------------------------------
+// shareNameChanged:
+// ---------------------------------------------------------------------------
+- (IBAction)shareNameChanged:(id)sender
+{
+ if( NSOrderedSame != [serverName compare:[nameField stringValue]] )
+ {
+ [serverName setString:[nameField stringValue]];
+ [self setConfigNeedsSaving:YES];
+ }
+}
+
+
+// ---------------------------------------------------------------------------
+// portPopupChanged:
+// ---------------------------------------------------------------------------
+- (IBAction)portPopupChanged:(id)sender
+{
+ if( 0 == [portPopup indexOfSelectedItem] )
+ {
+#if 0
+ [portField abortEditing];
+ [portField setIntValue:currentServerPort];
+ [portField setEnabled:false];
+#endif
+ }
+ else
+ {
+ [portField setEnabled:true];
+ [[[self mainView] window] makeFirstResponder:portField];
+ }
+ [self setConfigNeedsSaving:YES];
+}
+
+// ---------------------------------------------------------------------------
+// portChanged:
+//
+// The value of the port changed
+// ---------------------------------------------------------------------------
+- (IBAction)portChanged:(id)sender
+{
+ if( serverPort != [portField intValue] )
+ {
+ serverPort = [portField intValue];
+ [self setConfigNeedsSaving:YES];
+ }
+}
+
+// ---------------------------------------------------------------------------
+// pwCheckBoxChanged:
+//
+// User changed the state of the "Require Password" checkbox.
+// ---------------------------------------------------------------------------
+- (IBAction)pwCheckBoxChanged:(id)sender
+{
+ if( NSOffState == [passwordCheckbox state] )
+ {
+ [passwordField validateEditing];
+ [passwordField setStringValue:@""];
+ [serverPassword setString:@""];
+ [passwordField setEnabled:false];
+ [self setConfigNeedsSaving:YES];
+ }
+ else
+ {
+ [passwordField setEnabled:true];
+ [[[self mainView] window] makeFirstResponder:passwordField];
+ if( 0 < [serverPassword length] )
+ [self setConfigNeedsSaving:YES]; // Only enable if there's a password
+ }
+}
+
+// ---------------------------------------------------------------------------
+// serverStartOptionChanged:
+//
+// User changed the popup menu of server options.
+// ---------------------------------------------------------------------------
+- (IBAction)serverStartOptionChanged:(id)sender
+{
+ bStartServerOnLogin = ( 1 == [serverStartOptions indexOfSelectedItem] );
+ [self setConfigNeedsSaving:YES];
+}
+
+// ---------------------------------------------------------------------------
+// startStopButtonClicked:
+//
+// Start or stop the server.
+// ---------------------------------------------------------------------------
+- (IBAction)startStopButtonClicked:(id)sender
+{
+ if( ![self fireflyIsRunning] )
+ {
+ // Server is not running, so we need to start it. First, let's see
+ // if we have unsaved changes
+ BOOL bOKToStart = !bConfigNeedsSaving;
+ if( bConfigNeedsSaving &&
+ [[[self mainView] window] makeFirstResponder:[[self mainView] window]] &&
+ [self currentTabIsValid] )
+ {
+ if( [self saveSettings] )
+ {
+ [applyNowButton setEnabled:NO];
+ bOKToStart = YES;
+ }
+ else
+ {
+ NSBeginCriticalAlertSheet( NSLocalizedString( @"Failed to save changes",
+ @"Alert message notifying the user of failure to save" ),
+ @"OK",
+ NULL,
+ NULL,
+ [[self mainView] window],
+ nil,
+ NULL,
+ NULL,
+ NULL,
+ NSLocalizedString( @"Firefly could not be started because your changes "
+ "could not be saved",
+ @"Explanatory text for the failure-to-save alert" ) );
+ }
+ }
+
+ if( bOKToStart )
+ {
+ [self updateServerStatus:kFireflyStatusStarting];
+ if( ![self startFirefly] )
+ {
+ [self updateServerStatus:kFireflyStatusInvalid];
+ NSBeginCriticalAlertSheet( NSLocalizedString( @"Unable to start Firefly",
+ @"Alert message notifying the user of failure to stop" ),
+ @"OK",
+ NULL,
+ NULL,
+ [[self mainView] window],
+ nil,
+ NULL,
+ NULL,
+ NULL,
+ NSLocalizedString( @"An unexpected error occurred when trying to start Firefly. "
+ "Please close and re-open this Preference pane, and try again.",
+ @"Explanatory text for the failure-to-stop alert" ) );
+ }
+ }
+ }
+ else
+ {
+ // Server is running, so stop it.
+ if( [self stopFirefly] )
+ {
+ [self updateServerStatus:kFireflyStatusStopping];
+ }
+ else
+ {
+ [self updateServerStatus:kFireflyStatusInvalid];
+ NSBeginCriticalAlertSheet( NSLocalizedString( @"Unable to stop Firefly",
+ @"Alert message notifying the user of failure to stop" ),
+ @"OK",
+ NULL,
+ NULL,
+ [[self mainView] window],
+ nil,
+ NULL,
+ NULL,
+ NULL,
+ NSLocalizedString( @"An unexpected error occurred when trying to stop Firefly. "
+ "Please close and re-open this Preference pane, and try again.",
+ @"Explanatory text for the failure-to-stop alert" ) );
+ }
+ }
+}
+
+
+// ---------------------------------------------------------------------------
+// webPageButtonClicked:
+//
+// User clicked the Open Web Page button, so we want to open the server's
+// config page.
+// ---------------------------------------------------------------------------
+- (IBAction)webPageButtonClicked:(id)sender
+{
+ // User clicked the Show web page button. Open the firefly internal page.
+ [[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:serverURL]];
+}
+
+// ---------------------------------------------------------------------------
+// applyNowButtonClicked:
+//
+// Time to save our settings!
+// ---------------------------------------------------------------------------
+- (IBAction)applyNowButtonClicked:(id)sender
+{
+ if( [[[self mainView] window] makeFirstResponder:[[self mainView] window]] &&
+ [self currentTabIsValid] )
+ {
+ if( [self saveSettings] )
+ {
+ [applyNowButton setEnabled:NO];
+ if( [self fireflyIsRunning] )
+ [self restartFirefly];
+ }
+ else
+ NSBeginCriticalAlertSheet( NSLocalizedString( @"Failed to apply changes",
+ @"Alert message notifying the user of failure to apply" ),
+ @"OK",
+ NULL,
+ NULL,
+ [[self mainView] window],
+ nil,
+ NULL,
+ NULL,
+ NULL,
+ NSLocalizedString( @"Due to an unexpected error, your changes could not "
+ "be applied.",
+ @"Explanatory text for the failure-to-apply alert" ) );
+ }
+}
+
+// ---------------------------------------------------------------------------
+// helperMenuCheckboxClicked:
+//
+// User clicked the checkbox to show or hide the firefly menu. This happens
+// right away. The helper writes the pref for us.
+// ---------------------------------------------------------------------------
+- (IBAction)helperMenuCheckboxClicked:(id)sender
+{
+ if( NSOffState == [helperMenuCheckbox state] )
+ [self showHelperMenu:NO];
+ else
+ [self showHelperMenu:YES];
+}
+
+// ---------------------------------------------------------------------------
+// logoButtonClicked:
+//
+// User clicked the logo button, so we want to open the Firefly web site
+// ---------------------------------------------------------------------------
+- (IBAction)logoButtonClicked:(id)sender
+{
+ // User clicked the Firefly logo in the prefs pane. Open the web page.
+ [[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:@"http://fireflymediaserver.org"]];
+}
+
+
+// ---------------------------------------------------------------------------
+// controlTextDidChange:
+//
+// If the text in the control changes at all (the first time a user adds
+// or removes a character), we want to mark the configuration as needing
+// to be saved
+// ---------------------------------------------------------------------------
+- (void)controlTextDidChange:(NSNotification *)notification
+{
+ // If any of our text fields have changed text, we need to enable the "Apply" button.
+ [self setConfigNeedsSaving:YES];
+}
+
+
+// ---------------------------------------------------------------------------
+// validateInstall:
+//
+// Called when the prefs pane is first being loaded. Locates the pieces we
+// need to do our work (specifically, the config file and the Firefly Helper
+// application). Creates the Firefly directory and a default config file
+// if none is present. Makes note of any errors encountered for reporting
+// to the user when the panel finishes loading.
+// ---------------------------------------------------------------------------
+- (BOOL)validateInstall
+{
+ configAppearsValid = NO;
+
+ do // while( false )
+ {
+ // First up, locate or create the Firefly directory in Application Support.
+ NSFileManager *mgr = [NSFileManager defaultManager];
+ NSArray * appSupportDirArray = nil;
+ NSString * appSupportPath = nil;
+
+ // If we were guaranteed to be on 10.4 or later, we could call this:
+ //appSupportDirArray = NSSearchPathForDirectoriesInDomains( NSApplicationSupportDirectory,
+ // NSUserDomainMask,
+ // YES );
+
+ // But, we're not on 10.4; we have to go back to 10.3. So, we look in
+ // the Library directory.
+ appSupportDirArray = NSSearchPathForDirectoriesInDomains( NSLibraryDirectory,
+ NSUserDomainMask,
+ YES );
+ if( [appSupportDirArray count] > 0 )
+ {
+ appSupportPath = [[appSupportDirArray objectAtIndex:0]
+ stringByAppendingPathComponent:@"Application Support"];
+ }
+ else
+ {
+ [configError setString:NSLocalizedString( @"Library directory could not be found in user folder",
+ @"Error message displayed at panel load" )];
+ break;
+ }
+
+ BOOL isDir = YES;
+ if( ![mgr fileExistsAtPath:appSupportPath isDirectory:&isDir] || !isDir )
+ {
+ BOOL bFail = YES;
+ // If this is still true, it means that the directory is missing
+ // (Otherwise, there's a *file* called Application Support here!
+ if( isDir )
+ {
+ bFail = ( 0 != mkdir( [mgr fileSystemRepresentationWithPath:appSupportPath],
+ 0755 ) );
+ }
+
+ if( bFail )
+ {
+ [configError setString:NSLocalizedString( @"Unable to find or create Application Support folder",
+ @"Error message displayed at panel load" )];
+ break;
+ }
+ }
+
+ [fireflyFolderPath setString:[appSupportPath stringByAppendingPathComponent:@FIREFLY_DIR_NAME]];
+ if( ![mgr fileExistsAtPath:fireflyFolderPath isDirectory:&isDir] || !isDir )
+ {
+ // As above, except that this is less unexpected
+ BOOL bFail = YES;
+ // If this is still true, it means that the directory is missing
+ // (Otherwise, there's a *file* called Application Support here!
+ if( isDir )
+ {
+ bFail = ( 0 != mkdir( [mgr fileSystemRepresentationWithPath:fireflyFolderPath],
+ 0755 ) );
+ }
+
+ if( bFail )
+ {
+ // We're done. If we can't find the Firefly directory, notify the user and disable
+ // everything. Yes, maybe the server might be running and we could locate it, but somebody
+ // who has installed the server in a non-standard location doesn't need us.
+ NSString *formatString = NSLocalizedString( @"Firefly directory could not be found or created at: %@",
+ "Format string for error message" );
+ [configError setString:[NSString stringWithFormat:formatString, fireflyFolderPath]];
+ break;
+ }
+ }
+
+ // Check for the config file
+ [configFilePath setString:[fireflyFolderPath stringByAppendingPathComponent:@FIREFLY_CONF_NAME]];
+ if( [mgr fileExistsAtPath:configFilePath] )
+ {
+ // It exists. Can we write to it?
+ if( ![mgr isWritableFileAtPath:configFilePath] )
+ {
+ // This is bad. If we can't write to the config file, all we can do is open the web page and
+ // start/stop the server
+ NSString *formatString = NSLocalizedString( @"The configuration file is present, but is not writable: %@",
+ "Format string for error message" );
+ [configError setString:[NSString stringWithFormat:formatString, configFilePath]];
+ break;
+ }
+ }
+ else
+ {
+ // No config file, so let's create the default one
+ if( ![self createDefaultConfigFile] )
+ {
+ // Fatal error. Alert the user and disable everything.
+ NSString *formatString = NSLocalizedString( @"Unable to create a default configuration file at: %@",
+ "Format string for error message upon invalid install" );
+ [configError setString:[NSString stringWithFormat:formatString, configFilePath]];
+ break;
+ }
+
+ // This lets willSelect know that we wrote a new config file
+ bConfigNeedsSaving = YES;
+ }
+
+ // Check to make sure the helper app is present (also required)
+ [fireflyHelperPath setString:[[NSBundle bundleForClass:[self class]] pathForResource:@FIREFLY_HELPER_NAME
+ ofType:nil]];
+ if( ![mgr isExecutableFileAtPath:fireflyHelperPath] )
+ {
+ // As above, this is a fatal error
+ [configError setString:NSLocalizedString( @"The Firefly installation appears to be damaged. Unable to"
+ " locate Firefly Helper.",
+ @"Format string for error message upon invalid install" )];
+ break;
+ }
+
+ // Phew!
+ configAppearsValid = YES;
+
+ } while( false );
+
+ return configAppearsValid;
+}
+
+// ---------------------------------------------------------------------------
+// readConfigFromPath:
+//
+// Reading and writing the config file
+// ---------------------------------------------------------------------------
+- (BOOL)readConfigFromPath:(NSString*)path
+{
+ // I'm sure there's a nice Cocoa/Carbon/MacOS way to do this, but the docs
+ // are not forthcoming. So, we do it the Unix way.
+ FILE *configFile = fopen( [path UTF8String], "r" );
+ if( NULL == configFile )
+ return NO;
+
+ // Set up our members in case we've been run before
+ idxOfServerName = 0;
+ idxOfPassword = 0;
+ idxOfPort = 0;
+ idxOfLibraryPath = 0;
+ idxOfNextSection = 0;
+ idxOfDbPath = 0;
+ idxOfLogPath = 0;
+
+ // Now, read the file
+ BOOL bInGeneral = NO;
+ char buf[1024]; // yes, a hardcoded limit, but seriously, this is for one line.
+ [configFileStrings removeAllObjects];
+
+ while( NULL != fgets( buf, 1024, configFile ) )
+ {
+ buf[1023] = 0;
+ NSString *line = [NSString stringWithUTF8String:buf];
+ [configFileStrings addObject:line];
+
+ // Check to see if this is one of the lines we care about
+ if( 0 == idxOfNextSection )
+ {
+ if( bInGeneral )
+ {
+ if( 0 == idxOfServerName && 0 == strncasecmp( buf, "servername", 10 ) )
+ {
+ idxOfServerName = [configFileStrings count] - 1;
+ [serverName setString:[self readValueFromBuf:buf startingAt:10 unescapeCommas:NO]];
+ }
+ else if( 0 == idxOfPassword && 0 == strncasecmp( buf, "password", 8 ) )
+ {
+ idxOfPassword = [configFileStrings count] - 1;
+ [serverPassword setString:[self readValueFromBuf:buf startingAt:8 unescapeCommas:NO]];
+ }
+ else if( 0 == idxOfPort && 0 == strncasecmp( buf, "port", 4 ) )
+ {
+ idxOfPort = [configFileStrings count] - 1;
+ NSString *tmp = [self readValueFromBuf:buf startingAt:4 unescapeCommas:NO];
+ unsigned long num = atol( [tmp UTF8String] );
+ if( num < 65536 )
+ serverPort = num;
+ }
+ else if( 0 == idxOfLibraryPath && 0 == strncasecmp( buf, "mp3_dir", 7 ) )
+ {
+ idxOfLibraryPath = [configFileStrings count] - 1;
+ [libraryPath setString:[self readValueFromBuf:buf startingAt:7 unescapeCommas:YES]];
+ }
+ else if( 0 == idxOfDbPath && 0 == strncasecmp( buf, "db_parms", 8 ) )
+ {
+ // We only save the index of this if we're going to need to write the path
+ // out. We need to write it out if the string is empty (which means that
+ // it's coming from a default file).
+ NSString *string = [self readValueFromBuf:buf startingAt:8 unescapeCommas:NO];
+ if( 0 == [string length] )
+ idxOfDbPath = [configFileStrings count] - 1;
+ }
+ else if( 0 == idxOfLogPath && 0 == strncasecmp( buf, "logfile", 7 ) )
+ {
+ // as above
+ [logFilePath setString:[self readValueFromBuf:buf startingAt:7 unescapeCommas:NO]];
+ if( 0 == [logFilePath length] )
+ idxOfLogPath = [configFileStrings count] - 1;
+ }
+ else if( 0 == idxOfPlaylistPath && 0 == strncasecmp( buf, "playlist", 8 ) )
+ {
+ // as above
+ NSString *string = [self readValueFromBuf:buf startingAt:8 unescapeCommas:NO];
+ if( 0 == [string length] )
+ idxOfPlaylistPath = [configFileStrings count] - 1;
+ }
+
+ else if( buf[0] == '[' )
+ {
+ idxOfNextSection = [configFileStrings count] - 1;
+ }
+ }
+ else
+ {
+ if( 0 == strncasecmp( buf, "[general]", 9 ) )
+ bInGeneral = YES;
+ }
+ }
+ }
+
+ fclose( configFile );
+ return YES;
+}
+
+// ---------------------------------------------------------------------------
+// WriteCommaEscapedStringToFile:
+//
+// Utility function. Takes a const char* as input and copies the string,
+// escaping any commas as ",,". Writes the resulting string to the
+// supplied FILE*, which is assumed to be open for writing
+//
+// NOTE: This function could be smarter. For example, if a string
+// is passed in that is 1023 characters in length but contains commas,
+// this function will truncate the string silently.
+// ---------------------------------------------------------------------------
+static void
+WriteCommaEscapedStringToFile( FILE *inFile, const char *inStringToEscape )
+{
+ if( NULL == inFile || NULL == inStringToEscape )
+ return;
+
+ char escapingBuf[1025]; // 1 extra in case of a final comma
+ int i = 0;
+ int j = 0;
+ while( '\0' != inStringToEscape[i] && j < 1023 )
+ {
+ // Extra comma for any comma we find
+ if( ',' == inStringToEscape[i] )
+ escapingBuf[j++] = ',';
+ escapingBuf[j++] = inStringToEscape[i++];
+ }
+ escapingBuf[j] = '\0';
+ fputs( escapingBuf, inFile );
+}
+
+// ---------------------------------------------------------------------------
+// writeConfigToPath:
+//
+// Writes our configuration to the supplied path. readConfigFromPath
+// MUST have been called first!
+// ---------------------------------------------------------------------------
+- (BOOL)writeConfigToPath:(NSString*)path
+{
+ if( nil == configFileStrings || 0 == [configFileStrings count] )
+ return NO;
+
+ FILE *configFile = fopen( [path UTF8String], "w" );
+ if( NULL == configFile )
+ return NO;
+
+ char buf[1024];
+
+ unsigned i = 0;
+ for( ; i < [configFileStrings count]; i++ )
+ {
+ if( 0 == i )
+ {
+ // 0 is special-cased since it's also a special token for "this line wasn't
+ // in the original". Since we only use the new file format, though, we're guaranteed
+ // that none of our tokens is at the first (0th) line, because [general] has to
+ // come before any of our tokens
+ fputs( [[configFileStrings objectAtIndex:0] UTF8String], configFile );
+ }
+ else if( i == idxOfNextSection )
+ {
+ // We've reached the end of our general section, so it's now time to write
+ // out anything that the user has set, but that wasn't found in the config
+ // file before. Note that this is basically error recovery for somebody
+ // mucking with the file, since all lines should be present but with emtpy
+ // values if an optional setting isn't set. For example:
+ // password =
+ // If we didn't find it before, its index will be 0.
+
+ // servername is required
+ if( 0 == idxOfServerName )
+ {
+ sprintf( buf, "servername = %s\n", [serverName UTF8String] );
+ fputs( buf, configFile );
+ }
+
+ // so is the library path
+ if( 0 == idxOfLibraryPath )
+ {
+ sprintf( buf, "mp3_dir = %s\n", [libraryPath UTF8String] );
+ WriteCommaEscapedStringToFile( configFile, buf );
+ }
+
+ // password and port are optional, so don't write an empty entry
+ if( 0 == idxOfPassword && [serverPassword length] > 0 )
+ {
+ sprintf( buf, "password = %s\n", [serverPassword UTF8String] );
+ fputs( buf, configFile );
+ }
+ if( 0 == idxOfPort && 0 != serverPort )
+ {
+ sprintf( buf, "port = %u\n", serverPort );
+ fputs( buf, configFile );
+ }
+
+ // Don't forget the section header for that next section!
+ fputs( [[configFileStrings objectAtIndex:i] UTF8String], configFile );
+ }
+ else if( i == idxOfServerName )
+ {
+ sprintf( buf, "servername = %s\n", [serverName UTF8String] );
+ fputs( buf, configFile );
+ }
+ else if( i == idxOfPassword ) // NOTE: This will write an empty password if none is set
+ {
+ sprintf( buf, "password = %s\n", [serverPassword UTF8String] );
+ fputs( buf, configFile );
+ }
+ else if( i == idxOfPort )
+ {
+ if( 0 == serverPort )
+ fputs( "port =\n", configFile ); // no port is set, so write an empty value
+ else
+ {
+ sprintf( buf, "port = %u\n", serverPort );
+ fputs( buf, configFile );
+ }
+ }
+ else if( i == idxOfLibraryPath )
+ {
+ sprintf( buf, "mp3_dir = %s\n", [libraryPath UTF8String] );
+ WriteCommaEscapedStringToFile( configFile, buf );
+ }
+ else if ( i == idxOfDbPath )
+ {
+ sprintf( buf, "db_parms = %s\n", [fireflyFolderPath UTF8String] );
+ WriteCommaEscapedStringToFile( configFile, buf );
+ }
+ else if( i == idxOfLogPath )
+ {
+ sprintf( buf, "logfile = %s\n", [logFilePath UTF8String] );
+ WriteCommaEscapedStringToFile( configFile, buf );
+ }
+ else if( i == idxOfPlaylistPath )
+ {
+ sprintf( buf, "playlist = %s\n", [playlistPath UTF8String] );
+ WriteCommaEscapedStringToFile( configFile, buf );
+ }
+ else
+ {
+ // Just output our stored line
+ fputs( [[configFileStrings objectAtIndex:i] UTF8String], configFile );
+ }
+ }
+
+ fclose( configFile );
+ [self setConfigNeedsSaving:NO];
+ return YES;
+}
+
+
+// ---------------------------------------------------------------------------
+// readValueFromBuf:startingAt:unescapeCommas:
+//
+// Read the value from a key/value pair string. idx is the start point, which
+// is assumed to be the next character after the key. NOTE that this function
+// may modify buf if it contains trailing whitespace.
+// ---------------------------------------------------------------------------
+- (NSString *)readValueFromBuf:(char*)buf startingAt:(int)idx unescapeCommas:(BOOL)bUnescapeCommas
+{
+ char *retVal = NULL;
+ BOOL bFoundEquals = NO;
+
+ // skip over whitespace and = characters
+ while( buf[idx] )
+ {
+ if( buf[idx] == '=' )
+ bFoundEquals = YES;
+ else if( buf[idx] != ' ' && buf[idx] != '\t' )
+ break;
+
+ idx++;
+ }
+
+ // Okay, we found whitespace or the end of the line. If we didn't find an equals sign, then the
+ // value is empty. If there's nothing there but a newline, that's also considered empty.
+ if( !bFoundEquals || buf[idx] == '\0' || buf[idx] == '\n' || buf[idx] == '\r' )
+ return [NSString string];
+
+ // We found an equals, so retVal will point at our string to return. Now it's time
+ // to clip off any trailing whitespace. We work back from the end of the line, since
+ // whitespace is permitted in some places like the path and server name.
+
+ retVal = &buf[idx];
+ idx = strlen( retVal ); // this is at least 1, because we tested for empty above
+
+ while( idx-- && (retVal[idx] == ' ' || retVal[idx] == '\t' || retVal[idx] == '\r' || retVal[idx] == '\n') )
+ retVal[idx] = '\0';
+
+ // And, finally, if bUnescapeCommas is true, walk the string and
+ // convert ",," to "," in place
+ if( bUnescapeCommas)
+ {
+ int readIdx = 0;
+ int writeIdx = 0;
+ while( '\0' != retVal[readIdx] )
+ {
+ if( ',' == retVal[readIdx] && ',' == retVal[readIdx+1] )
+ readIdx++;
+ retVal[writeIdx++] = retVal[readIdx++];
+ }
+ retVal[writeIdx] = '\0';
+ }
+
+ return [NSString stringWithUTF8String:retVal];
+}
+
+// ---------------------------------------------------------------------------
+// createDefaultConfigFile:
+//
+// Read the value from a key/value pair string. idx is the start point, which
+// is assumed to be the next character after the key. NOTE that this function
+// may modify buf if it contains trailing whitespace.
+// ---------------------------------------------------------------------------
+- (BOOL)createDefaultConfigFile
+{
+ // Read the default file from our bundle
+ NSString *configPath = nil;
+ NSBundle *thisBundle = [NSBundle bundleForClass:[self class]];
+ if( nil == (configPath = [thisBundle pathForResource:@FIREFLY_CONF_NAME ofType:nil]) ||
+ ![self readConfigFromPath:configPath] )
+ return NO;
+
+ // Set the default values. This takes the items that are deliberately
+ // left blank in the "starter" config file (because they're specific
+ // to the particular installation location) and fills them in.
+ [self setDefaultValues];
+
+ // Write it out
+ return [self writeConfigToPath:configFilePath];
+}
+
+// ---------------------------------------------------------------------------
+// setDefaultValues:
+//
+// Model utility sets up the members representing server configuration to
+// their proper initial defaults (which are host- and user-specific!)
+// ---------------------------------------------------------------------------
+- (void)setDefaultValues
+{
+ // easy ones first
+ serverPort = 0;
+ [serverPassword setString:@""]; // FIXME: really no better way to clear a string?
+ bStartServerOnLogin = YES;
+
+ // Get the host name and make the default library name in a localization-friendly way
+ NSString *hostname = (NSString*)CSCopyMachineName();
+ NSString *format = NSLocalizedString( @"%@'s Firefly on %@",
+ @"Format string for default library name" );
+
+ [serverName setString:[NSString stringWithFormat:format, userName, hostname]];
+ [hostname release];
+
+ // Defaults for the log file and playlist paths. These get used only
+ // when we first write out our default config file.
+ [logFilePath setString:[fireflyFolderPath stringByAppendingPathComponent:
+ @FIREFLY_LOG_FILE]];
+ [playlistPath setString:[fireflyFolderPath stringByAppendingPathComponent:
+ @FIREFLY_PLAYLIST_FILE]];
+
+ // Finally, the default Music directory
+ [libraryPath setString:[@"~/Music" stringByExpandingTildeInPath]];
+}
+
+
+// ---------------------------------------------------------------------------
+// setConfigNeedsSaving:
+//
+// Tracking the need to save the config.
+// ---------------------------------------------------------------------------
+-(void)setConfigNeedsSaving:(BOOL)needsSaving
+{
+ [applyNowButton setEnabled:needsSaving];
+ bConfigNeedsSaving = needsSaving;
+}
+
+// ===========================================================================
+// UI utility functions for setting the UI into certain common states
+// (sets up control enabling, text, etc.)
+// ===========================================================================
+
+// ---------------------------------------------------------------------------
+// disableAllControls:
+//
+// Disables all the controls in the prefs pane. Used when the configuration
+// is invalid.
+// ---------------------------------------------------------------------------
+- (void)disableAllControls
+{
+ [browseButton setEnabled:false];
+ [libraryField setEnabled:false];
+ [nameField setEnabled:false];
+ [passwordCheckbox setEnabled:false];
+ [passwordField setEnabled:false];
+ [portField setEnabled:false];
+ [serverStartOptions setEnabled:false];
+ [startStopButton setEnabled:false];
+ [webPageButton setEnabled:false];
+ [helperMenuCheckbox setEnabled:false];
+}
+
+// ---------------------------------------------------------------------------
+// updateServerStatus:
+//
+// Handles updating all relevant UI elements according to the server status
+// ---------------------------------------------------------------------------
+- (void)updateServerStatus:(FireflyServerStatus) status
+{
+ BOOL bAnimateProgress = NO;
+ BOOL bActivateStartStop = YES;
+ BOOL bButtonIsStart = YES;
+ BOOL bClearWebAndVersion = NO;
+
+ switch( status )
+ {
+ case kFireflyStatusStartFailed:
+ case kFireflyStatusCrashed:
+ case kFireflyStatusStopped:
+ bClearWebAndVersion = YES;
+ break;
+
+ case kFireflyStatusRestarting:
+ case kFireflyStatusStarting:
+ bAnimateProgress = YES;
+ bButtonIsStart = NO;
+ break;
+
+ case kFireflyStatusActive:
+ case kFireflyStatusScanning:
+ bButtonIsStart = NO;
+ break;
+
+ case kFireflyStatusStopping:
+ bActivateStartStop = NO;
+ bAnimateProgress = YES;
+ break;
+
+ case kFireflyStatusInvalid:
+ default:
+ bActivateStartStop = NO;
+ bClearWebAndVersion = YES;
+ break;
+ }
+
+ [startStopButton setEnabled:bActivateStartStop];
+ [statusText setStringValue:StringForFireflyStatus(status)];
+ if( bAnimateProgress )
+ [progressSpinner startAnimation:self];
+ else
+ [progressSpinner stopAnimation:self];
+
+ if( bButtonIsStart )
+ [startStopButton setTitle:NSLocalizedString( @"Start Firefly",
+ @"One of several titles for the start/stop button" )];
+ else
+ [startStopButton setTitle:NSLocalizedString( @"Stop Firefly",
+ @"One of several titles for the start/stop button" )];
+
+ if( bClearWebAndVersion )
+ {
+ [serverVersionText setStringValue:NSLocalizedString( @"(available when Firefly is running)",
+ @"Displayed in place of server version when server "
+ "is not running" )];
+ [webPageButton setEnabled:NO];
+ [webPageInfoText setStringValue:NSLocalizedString( @"Additional configuration options are "
+ "available from Firefly's built-in web page. "
+ "Available when Firefly is running.",
+ @"Info text for the web page button when server "
+ "is not running" )];
+ }
+}
+
+
+// ===========================================================================
+// Alert delegate method(s)
+// ===========================================================================
+
+// ---------------------------------------------------------------------------
+// alertDidEnd:returnCode:contextInfo:
+//
+// This is called for our "OK"-type alerts, and we don't need to do anything
+// extra.
+// ---------------------------------------------------------------------------
+- (void)alertDidEnd:(NSAlert *)alert returnCode:(int)returnCode contextInfo:(void *)contextInfo
+{
+}
+
+// ---------------------------------------------------------------------------
+// applySheetDidEnd:returnCode:contextInfo:
+//
+// Sheet delegate method, specially for the "apply changes" sheet. Depending
+// upon the user's answer, we may need to write out our config file and
+// restart the server. We definitely have to send the replyToShouldUnselect
+// message, since we deferred a reply in shouldUnselect, and that's what
+// prompts this sheet.
+// ---------------------------------------------------------------------------
+- (void) applySheetDidEnd:(NSAlert *)alert returnCode:(int)returnCode contextInfo:(void *)contextInfo
+{
+ BOOL bResponse = YES; // what do we say to shouldUnselect?
+
+ // We want the sheet closed in case we have to post another alert
+ [[alert window] orderOut:self];
+
+ if( NSAlertSecondButtonReturn == returnCode ) // "Cancel" button
+ {
+ bResponse = NO;
+ }
+ else if( NSAlertThirdButtonReturn == returnCode ) // "Don't Apply" button
+ {
+ // bResponse is already YES
+ }
+ else if( NSAlertFirstButtonReturn == returnCode ) // "Apply" button
+ {
+ if( [self currentTabIsValid] )
+ {
+ bResponse = YES;
+ if( ![self saveSettings] )
+ {
+ bResponse = NO;
+ NSBeginCriticalAlertSheet( NSLocalizedString( @"Failed to apply changes",
+ @"Alert message notifying the user of failure to apply" ),
+ @"OK",
+ NULL,
+ NULL,
+ [[self mainView] window],
+ nil,
+ NULL,
+ NULL,
+ NULL,
+ NSLocalizedString( @"Due to an unexpected error, your changes could not "
+ "be applied.",
+ @"Explanatory text for the failure-to-apply alert" ) );
+ }
+ else
+ {
+ // If the server is running, we need to restart it. Happily,
+ // the Firefly Helper will take care of that, so we can
+ // go ahead and exit
+ if( [self fireflyIsRunning] )
+ [self restartFirefly];
+ }
+ }
+ else
+ {
+ // Our tab data wasn't valid, so now there's an alert on the
+ // screen. Cancel closing the sheet.
+ bResponse = NO;
+ }
+ }
+
+ [self replyToShouldUnselect:bResponse];
+}
+
+// ===========================================================================
+// Tab view delegate method(s)
+// ===========================================================================
+
+// ---------------------------------------------------------------------------
+// -tabView:shouldSelectTabViewItem:(NSTabViewItem *)tabViewItem
+//
+// Our job is to return false if the tab view should not be able to switch
+// panes. We shouldn't switch if there's invalid text in any of our
+// fields. By trying to get the main window to become first responder, we
+// make sure that any field with editing in process will call its delegate
+// to see if the field value is valid. If it's not, then it won't be able
+// to give up first responder status, and makeFirstResponder will return
+// false.
+// ---------------------------------------------------------------------------
+- (BOOL)tabView:(NSTabView *)tabView shouldSelectTabViewItem:(NSTabViewItem *)tabViewItem
+{
+ BOOL bRetVal = [[[self mainView] window] makeFirstResponder:[[self mainView] window]];
+ if( bRetVal )
+ bRetVal = [self currentTabIsValid];
+
+ if( bRetVal )
+ {
+ // See whether we need to start or stop our log update timer
+ if( 3 == [tabView numberOfTabViewItems] )
+ {
+ NSTabViewItem *logTab = [tabView tabViewItemAtIndex:2];
+ if( [tabViewItem isEqual:logTab] )
+ {
+ // Update the view and start the timer
+ [self updateLogTextView];
+ logTimer = [[NSTimer scheduledTimerWithTimeInterval:1.0
+ target:self
+ selector:@selector(logTimerFired:)
+ userInfo:nil
+ repeats:YES] retain];
+
+ }
+ else if( [[tabView selectedTabViewItem] isEqual:logTab] )
+ {
+ // stop timer
+ [logTimer invalidate];
+ [logTimer autorelease];
+ logTimer = nil;
+ }
+ }
+ }
+ return bRetVal;
+}
+
+// ---------------------------------------------------------------------------
+// control:isValidObject
+//
+// NSControl delegate method, called when a control is about to commit its
+// newly-edited value. We return NO if the new value is not allowed, so
+// it will not allow editing to leave.
+// ---------------------------------------------------------------------------
+- (BOOL)control:(NSControl *)control isValidObject:(id) obj
+{
+ BOOL bRetVal = YES;
+ if( [obj isKindOfClass:[NSString class]] )
+ {
+ NSString *string = (NSString *)obj;
+ if( control == portField )
+ {
+ bRetVal = ( 1023 < [string intValue] && 65536 > [string intValue] );
+ }
+ else if( control == nameField )
+ {
+ bRetVal = ( 0 < [string length] );
+ }
+ else if( control == passwordField )
+ {
+ bRetVal = ( NSOffState == [passwordCheckbox state] || 0 < [string length] );
+ }
+ }
+
+ if( NO == bRetVal )
+ [self alertForControl:control];
+
+ return bRetVal;
+}
+
+// ---------------------------------------------------------------------------
+// currentTabIsValid
+//
+// Here's the deal. Annoyingly, Cocoa text fields don't call their delegate
+// methods when they lose focus *if* they haven't changed. So, this method
+// is our backstop, in case the other delegates don't catch this case. It
+// figures out the current tab, and then checks that the fields are valid.
+//
+// NOTE: We assume that the window has already been set to the first
+// responder, so that we can query the controls directly for their values.
+// ---------------------------------------------------------------------------
+- (BOOL)currentTabIsValid
+{
+ BOOL bRetVal = YES;
+ NSTabViewItem *selectedTab = [mainTabView selectedTabViewItem];
+ int idx;
+ if( nil != selectedTab )
+ {
+ idx = [mainTabView indexOfTabViewItem:selectedTab];
+ if( 0 == idx )
+ {
+ // General
+ if( ! (bRetVal = [self control:nameField isValidObject:[nameField objectValue]]) )
+ [self alertForControl:nameField];
+ else if( ! (bRetVal = [self control:passwordField isValidObject:[passwordField objectValue]]) )
+ [self alertForControl:nameField];
+ }
+ else if( 1 == idx )
+ {
+ // Advanced
+ // If "Manual" is selected, but the value of the field is not kosher, we must say no
+ if( 1 == [portPopup indexOfSelectedItem] &&
+ !(bRetVal = [self control:portField isValidObject:[portField objectValue]]) )
+ [self alertForControl:portField];
+ }
+ }
+
+ return bRetVal;
+}
+
+// ========================================================================
+// Private utilities
+// ========================================================================
+
+// ---------------------------------------------------------------------------
+// alertForControl
+//
+// There are a couple of places where we may need to pop up a modal alert
+// because a control's value is not valid. So, we have this utility. It
+// displays a modal "OK" style alert sheet with text specific to the
+// control, then returns.
+// ---------------------------------------------------------------------------
+- (void)alertForControl:(NSControl *)control
+{
+ NSString *alertTitle;
+ NSString *alertMessage;
+ if( control == nameField )
+ {
+ alertTitle = NSLocalizedString( @"Missing library name", "@Alert title when library name is invalid" );
+ alertMessage = NSLocalizedString( @"Please enter a library name",
+ @"Error message if library name is invalid" );
+ }
+ else if( control == passwordField )
+ {
+ alertTitle = NSLocalizedString( @"Missing password", "@Alert title when password is invalid" );
+ alertMessage = NSLocalizedString( @"Please enter a password, or un-check the password checkbox",
+ @"Error message if password is empty" );
+ }
+ else if( control == portField )
+ {
+ alertTitle = NSLocalizedString( @"Invalid port number", "@Alert title when port number is invalid" );
+ alertMessage = NSLocalizedString( @"Please enter a port number between 1024 and 65535, or choose "
+ "\"Automatic\" from the pop-up menu",
+ @"Error message if invalid port entered" );
+ }
+ else
+ {
+ alertTitle = NSLocalizedString( @"Invalid value", @"Generic alert string for an invalid control" );
+ alertMessage = @"";
+ }
+
+ NSBeginAlertSheet( alertTitle,
+ @"OK", NULL, NULL, [[self mainView] window],
+ nil, NULL, NULL, NULL,
+ alertMessage );
+}
+
+// ---------------------------------------------------------------------------
+// setIconForPath
+//
+// This function takes our library path and sets the icon in the Advanced tab
+// to be that path's icon. It's complicated a bit by the need to have a
+// special icon in case the path can't be found.
+// ---------------------------------------------------------------------------
+- (void)setIconForPath
+{
+ NSFileManager *mgr = [NSFileManager defaultManager];
+ BOOL isDir = NO;
+ if( [mgr fileExistsAtPath:libraryPath isDirectory:&isDir] && isDir )
+ {
+ NSWorkspace *workspace = [NSWorkspace sharedWorkspace];
+ [libraryIcon setImage:[workspace iconForFile:libraryPath]];
+ }
+ else
+ {
+ // we want a default "?" image, and IconServices is kind enough to oblige
+ IconRef unknownIcon;
+ if( 0 == GetIconRef( kOnSystemDisk, kSystemIconsCreator, kUnknownFSObjectIcon, &unknownIcon ) )
+ {
+ NSImage* image = [[NSImage alloc] initWithSize:NSMakeSize(32,32)];
+ [image lockFocus];
+ CGRect iconRect = CGRectMake(0,0,32,32);
+ PlotIconRefInContext((CGContextRef)[[NSGraphicsContext currentContext] graphicsPort],
+ &iconRect,
+ kAlignNone,
+ kTransformNone,
+ NULL /*labelColor*/,
+ kPlotIconRefNormalFlags,
+ unknownIcon);
+ [image unlockFocus];
+ [libraryIcon setImage:image];
+ [image release];
+ ReleaseIconRef( unknownIcon );
+ }
+ else
+ {
+ [libraryIcon setImage:nil];
+ }
+ }
+}
+
+// ---------------------------------------------------------------------------
+// loadSettings
+//
+// Read the config file and also fetch our server startup preference from
+// MacOS's preference mechanism
+// ---------------------------------------------------------------------------
+- (BOOL)loadSettings
+{
+ [self readSettingsForHelper:&bShowHelperMenu
+ andServer:&bStartServerOnLogin];
+ return [self readConfigFromPath:configFilePath];
+}
+
+// ---------------------------------------------------------------------------
+// saveSettings
+//
+// Writes out the config file and sets our prefs for whether we need to
+// launch the server at login
+//
+// Returns NO if this fails.
+// ---------------------------------------------------------------------------
+- (BOOL)saveSettings
+{
+ BOOL bSuccess = NO;
+
+ CFPreferencesSetAppValue( CFSTR(FF_PREFS_LAUNCH_AT_LOGIN),
+ bStartServerOnLogin ? kCFBooleanTrue : kCFBooleanFalse,
+ CFSTR(FF_PREFS_DOMAIN) );
+
+ // Now the server config file
+ bSuccess = [self writeConfigToPath:configFilePath];
+ return bSuccess;
+}
+
+// ---------------------------------------------------------------------------
+// updateLoginItem
+//
+// Based upon the current state of the persistent prefs (NOT our locals),
+// either set or un-set the Helper as a login item.
+//
+// NOTE: If bStartOnLogin is true, or if bShowMenu is true, then
+// we want the firefly helper to be in the startup items (because it handles
+// both of those tasks). But, if bShowMenu is true and bStartOnLogin
+// isn't, we'll start the helper but not the server.
+// ---------------------------------------------------------------------------
+- (BOOL)updateLoginItem
+{
+ BOOL bSuccess = NO;
+ NSString *scriptSource = nil;
+ BOOL bStartOnLogin = NO;
+ BOOL bShowMenu = NO;
+ [self readSettingsForHelper:&bShowMenu andServer:&bStartOnLogin];
+ if( bStartOnLogin || bShowMenu )
+ {
+ scriptSource = [NSString stringWithFormat:
+ @"tell application \"System Events\"\n"
+ "if \"Firefly Helper\" is not in (name of every login item) then\n"
+ "make login item at end with properties {hidden:false, path:\"%@\"}\n"
+ "end if\n"
+ "end tell",
+ fireflyHelperPath];
+ }
+ else
+ {
+ scriptSource = [NSString stringWithFormat:
+ @"tell application \"System Events\"\n"
+ "if \"Firefly Helper\" is in (name of every login item) then\n"
+ "delete (every login item whose name is \"Firefly Helper\")\n"
+ "end if\n"
+ "end tell\n"];
+ }
+
+ NSDictionary *errorDict = nil;
+ NSAppleScript *myScript = [[NSAppleScript alloc] initWithSource:scriptSource];
+
+ if( nil != myScript )
+ {
+ bSuccess = (nil != [myScript executeAndReturnError:&errorDict]);
+ [myScript release];
+ }
+ return bSuccess;
+}
+
+// ---------------------------------------------------------------------------
+// readSettingsForHelper:andServer:
+//
+// Utility to read the helper and server launch settings, since we need to
+// do it in more than one place
+// ---------------------------------------------------------------------------
+- (void)readSettingsForHelper:(BOOL*)outHelper andServer:(BOOL*)outServer
+{
+ if( NULL != outHelper )
+ {
+ CFBooleanRef showHelper =
+ CFPreferencesCopyAppValue( CFSTR(FF_PREFS_SHOW_MENU_EXTRA),
+ CFSTR(FF_PREFS_DOMAIN) );
+ if( nil != showHelper )
+ {
+ *outHelper = CFBooleanGetValue( showHelper );
+ CFRelease( showHelper );
+ }
+ else
+ {
+ // default value
+ *outHelper = NO;
+ }
+ }
+
+ if( NULL != outServer )
+ {
+ CFBooleanRef shouldLaunch =
+ CFPreferencesCopyAppValue( CFSTR(FF_PREFS_LAUNCH_AT_LOGIN),
+ CFSTR(FF_PREFS_DOMAIN) );
+ if( nil != shouldLaunch )
+ {
+ *outServer = CFBooleanGetValue( shouldLaunch );
+ CFRelease( shouldLaunch );
+ }
+ else
+ {
+ // default value
+ *outServer = YES;
+ }
+ }
+}
+
+// ------------------------------------------------------------------------
+// helperIsRunning
+//
+// Returns YES if "Firefly Helper" is running under our UID.
+// ------------------------------------------------------------------------
+- (BOOL)helperIsRunning
+{
+ bool bRetVal = NO;
+ kinfo_proc *result;
+ size_t length;
+ GetProcesses( &result, &length );
+
+ // Okay, now we have our list of processes. Let's find OUR copy of
+ // firefly. Note that Firefly runs as two processes, so we look
+ // for the higher-numbered one.
+ if( NULL != result )
+ {
+ int procCount = length / sizeof(kinfo_proc);
+ int i = 0;
+ uid_t ourUID = getuid();
+ for( ; i < procCount; i++ )
+ {
+ if( ourUID == result[i].kp_eproc.e_pcred.p_ruid &&
+ 0 == strcasecmp( result[i].kp_proc.p_comm, FIREFLY_HELPER_PROC_N ) )
+ {
+ bRetVal = YES;
+ break;
+ }
+ }
+ free( result );
+
+ }
+
+ return bRetVal;
+}
+
+// ------------------------------------------------------------------------
+// launchHelperIfNeeded
+//
+// Checks to see if our helper app is already running. If not, launch
+// it using NSTask (which doesn't have the issue in NSWorkspace where
+// launching another app, even background-only, causes us to lose our
+// window focus!).
+// ------------------------------------------------------------------------
+- (void)launchHelperIfNeeded
+{
+ if( ![self helperIsRunning] )
+ {
+ NSBundle *bundle = [NSBundle bundleWithPath:fireflyHelperPath];
+ NSString *path = [bundle executablePath];
+ NSTask *task = [[NSTask alloc] init];
+ [task setLaunchPath:path];
+ [task launch];
+ [task release];
+ }
+}
+
+// ------------------------------------------------------------------------
+// makeProxyConnection
+//
+// Try to connect up our serverProxy object by looking it up by name.
+// Returns a BOOL to indicate whether it has succeeded
+// ------------------------------------------------------------------------
+- (BOOL)makeProxyConnection
+{
+ BOOL bRetVal = NO;
+ NSString *serviceName = [@"FireflyHelper" stringByAppendingString:(NSString*)userName];
+ serverProxy = [NSConnection rootProxyForConnectionWithRegisteredName:serviceName
+ host:nil];
+ if( nil != serverProxy )
+ {
+ // This will notify us if the helper quits out from under us
+ [[NSNotificationCenter defaultCenter] addObserver:self
+ selector:@selector(connectionDied:)
+ name:NSConnectionDidDieNotification
+ object:nil];
+
+ [serverProxy retain];
+ [serverProxy setProtocolForProxy:@protocol(FireflyPrefsServerProtocol)];
+ clientIdent = rand();
+ [protocolChecker autorelease]; // in case we're being re-run
+ protocolChecker = [[NSProtocolChecker
+ protocolCheckerWithTarget:self
+ protocol:@protocol(FireflyPrefsClientProtocol)] retain];
+
+ @try
+ {
+ bRetVal = [serverProxy registerClient:protocolChecker withIdentifier:clientIdent];
+ }
+ @catch( NSException *exception )
+ {
+ NSLog(@"makeProxyConnection caught %@: %@",
+ [exception name], [exception reason]);
+ }
+
+ // If we fail to register, we will ditch our server proxy and fail
+ if( !bRetVal )
+ {
+ [serverProxy autorelease];
+ serverProxy = nil;
+ }
+ }
+
+ return bRetVal;
+}
+
+
+// ------------------------------------------------------------------------
+// checkProxyConnection
+//
+// Checks to see if we have a valid proxy connection. If we don't,
+// this disables the controls in the panel, posts a dialog, and returns NO.
+//
+// Because of the dialog, this should only be called when a connection is
+// believed to exist.
+// ------------------------------------------------------------------------
+- (BOOL)checkProxyConnection
+{
+ BOOL bRetVal = NO;
+ if( nil != serverProxy )
+ {
+ @try
+ {
+ [serverProxy fireflyStatus];
+ bRetVal = YES;
+ }
+ @catch( NSException *exception )
+ {
+ NSLog(@"checkProxyConnection caught %@: %@",
+ [exception name], [exception reason]);
+ [serverProxy autorelease];
+ serverProxy = nil;
+
+ NSBeginCriticalAlertSheet( NSLocalizedString( @"Lost contact with Firefly Helper",
+ @"Alert message notifying the user of failure to get status" ),
+ @"OK",
+ NULL,
+ NULL,
+ [[self mainView] window],
+ nil,
+ NULL,
+ NULL,
+ NULL,
+ NSLocalizedString( @"Communication has been lost with the Firefly Helper. "
+ "Please close and re-open this Preference pane, and try again.",
+ @"Explanatory text for the connection-lost alert" ) );
+ [self disableAllControls];
+ [self updateServerStatus:kFireflyStatusInvalid];
+ }
+ }
+
+ return bRetVal;
+}
+
+// ------------------------------------------------------------------------
+// connectionDied
+//
+// This notification fires if an NSConnection dies. We don't bother to
+// save our server connection. Rather, if this notification comes in, we
+// check our connection.
+// ------------------------------------------------------------------------
+- (void)connectionDied:(NSNotification *)notification
+{
+ [self checkProxyConnection];
+}
+
+
+// ------------------------------------------------------------------------
+// proxyTimerFired
+//
+// If the helper wasn't ready when we first checked, we try once a second
+// for 10 seconds, using a timer
+// ------------------------------------------------------------------------
+- (void)proxyTimerFired:(NSTimer *) timer
+{
+#ifdef FIREFLY_DEBUG
+ NSBeep();
+#endif
+ if( [self makeProxyConnection] )
+ {
+ [self updateServerStatus:[self fireflyStatus]];
+ NSString *string = [self fireflyVersion];
+ if( nil != string )
+ [self versionChanged:string];
+ string = [self fireflyConfigURL];
+ if( nil != string )
+ [self configUrlChanged:string];
+ [ipcTimer invalidate];
+ [ipcTimer autorelease];
+ ipcTimer = nil;
+ }
+ else if( 10 < ++ipcTries )
+ {
+ [ipcTimer invalidate];
+ [ipcTimer autorelease];
+ ipcTimer = nil;
+ [self updateServerStatus:kFireflyStatusInvalid];
+ NSBeginCriticalAlertSheet( NSLocalizedString( @"Unable to get server status",
+ @"Alert message notifying the user of failure to get status" ),
+ @"OK",
+ NULL,
+ NULL,
+ [[self mainView] window],
+ nil,
+ NULL,
+ NULL,
+ NULL,
+ NSLocalizedString( @"An unexpected error occurred when trying to get the "
+ "status of the Firefly server. "
+ "Please close and re-open this Preference pane, and try again.",
+ @"Explanatory text for the failure-to-get-status alert" ) );
+ }
+ // else we try again when the timer fires
+}
+
+// ------------------------------------------------------------------------
+// updateLogTextView
+// ------------------------------------------------------------------------
+- (void)updateLogTextView
+{
+ NSFileManager *mgr = [NSFileManager defaultManager];
+ if( [mgr isReadableFileAtPath:logFilePath] )
+ {
+ NSDictionary *dict = [mgr fileAttributesAtPath:logFilePath traverseLink:YES];
+ NSDate *modDate = [dict objectForKey:NSFileModificationDate];
+ if( nil != modDate && ![logDate isEqualTo:modDate] )
+ {
+ // log date is the last time we processed an update
+ [logDate autorelease];
+ logDate = [modDate retain];
+ NSString *newContents = nil;
+ NSData *data = [NSData dataWithContentsOfFile:logFilePath];
+ if( nil != data );
+ {
+ newContents = [[NSString alloc]
+ initWithData:data encoding:NSUTF8StringEncoding];
+ }
+ if( nil == newContents || 0 == [newContents length] )
+ {
+ [logTextView setString:NSLocalizedString( @"The log file is empty.",
+ @"Text for empty log file" )];
+ }
+ else
+ {
+ // We're going to figure out our current scroll position and
+ // the current selection (if any). After we set the text,
+ // we'll re-select the same range, and if we were at the bottom
+ // of the scroller, we'll make sure we stay there.
+ NSRange selection = [logTextView selectedRange];
+ float scrollPos = 0.0;
+ scrollPos = [[[logTextView enclosingScrollView] verticalScroller] floatValue];
+
+ // Actually set the new text
+ [logTextView setString:newContents];
+
+ // Restore selection (it's lost when setString is called)
+ [logTextView setSelectedRange:selection];
+
+ // If we were previously scrolled to the end, scroll to the end.
+ // Otherwise, leave the window as it is. (This makes a range
+ // of the very end of the view).
+ if( 1.0 == scrollPos )
+ [logTextView scrollRangeToVisible:NSMakeRange([[logTextView string] length], 0)];
+ }
+ if( nil != newContents )
+ [newContents autorelease];
+ }
+ }
+ else
+ {
+ [logTextView setString:NSLocalizedString( @"The log file has not been created.",
+ @"Text for missing log file" )];
+ }
+}
+
+// ------------------------------------------------------------------------
+// logTimerFired
+// ------------------------------------------------------------------------
+- (void)logTimerFired:(NSTimer *) timer
+{
+ [self updateLogTextView];
+}
+
+// ========================================================================
+// These functions wrap our IPC calls, so we can catch the exception that
+// will be thrown if we try to access the server with the connection
+// broken
+// ========================================================================
+
+// ------------------------------------------------------------------------
+// startFirefly
+// ------------------------------------------------------------------------
+- (FireflyStartResult)startFirefly
+{
+ FireflyStartResult retVal = kFireflyStartFail;
+ if( nil != serverProxy )
+ {
+ @try
+ {
+ retVal = [serverProxy startFirefly];
+ }
+ @catch( NSException *exception )
+ {
+ NSLog(@"startFirefly caught %@: %@", [exception name], [exception reason]);
+ [self checkProxyConnection];
+ }
+ }
+ return retVal;
+}
+
+// ------------------------------------------------------------------------
+// stopFirefly
+// ------------------------------------------------------------------------
+- (FireflyStopResult)stopFirefly
+{
+ FireflyStopResult retVal = kFireflyStopFail;
+ if( nil != serverProxy )
+ {
+ @try
+ {
+ retVal = [serverProxy stopFirefly];
+ }
+ @catch( NSException *exception )
+ {
+ NSLog(@"stopFirefly caught %@: %@", [exception name], [exception reason]);
+ [self checkProxyConnection];
+ }
+ }
+ return retVal;
+}
+
+// ------------------------------------------------------------------------
+// restartFirefly
+// ------------------------------------------------------------------------
+- (FireflyRestartResult)restartFirefly;
+{
+ FireflyRestartResult retVal = kFireflyRestartFail;
+ if( nil != serverProxy )
+ {
+ @try
+ {
+ retVal = [serverProxy restartFirefly];
+ }
+ @catch( NSException *exception )
+ {
+ NSLog(@"restartFirefly caught %@: %@", [exception name], [exception reason]);
+ [self checkProxyConnection];
+ }
+ }
+ return retVal;
+}
+
+// ------------------------------------------------------------------------
+// rescanLibrary
+// ------------------------------------------------------------------------
+- (FireflyRescanResult)rescanLibrary;
+{
+ FireflyRescanResult retVal = kFireflyRescanFail;
+ if( nil != serverProxy )
+ {
+ @try
+ {
+ retVal = [serverProxy rescanLibrary];
+ }
+ @catch( NSException *exception )
+ {
+ NSLog(@"rescanLibrary caught %@: %@", [exception name], [exception reason]);
+ [self checkProxyConnection];
+ }
+ }
+ return retVal;
+}
+
+// ------------------------------------------------------------------------
+// fireflyStatus
+// ------------------------------------------------------------------------
+- (FireflyServerStatus)fireflyStatus;
+{
+ FireflyServerStatus retVal = kFireflyStatusInvalid;
+ if( nil != serverProxy )
+ {
+ @try
+ {
+ retVal = [serverProxy fireflyStatus];
+ }
+ @catch( NSException *exception )
+ {
+ NSLog(@"fireflyStatus caught %@: %@", [exception name], [exception reason]);
+ [self checkProxyConnection];
+ }
+ }
+ return retVal;
+}
+
+// ------------------------------------------------------------------------
+// fireflyIsRunning
+// ------------------------------------------------------------------------
+- (BOOL)fireflyIsRunning;
+{
+ BOOL retVal = NO;
+ if( nil != serverProxy )
+ {
+ @try
+ {
+ retVal = [serverProxy fireflyIsRunning];
+ }
+ @catch( NSException *exception )
+ {
+ NSLog(@"fireflyStatus caught %@: %@", [exception name], [exception reason]);
+ [self checkProxyConnection];
+ }
+ }
+ return retVal;
+}
+
+// ------------------------------------------------------------------------
+// fireflyVersion
+// ------------------------------------------------------------------------
+- (NSString*)fireflyVersion;
+{
+ NSString *retVal = nil;
+ if( nil != serverProxy )
+ {
+ @try
+ {
+ retVal = [serverProxy fireflyVersion];
+ }
+ @catch( NSException *exception )
+ {
+ NSLog(@"fireflyVersion caught %@: %@", [exception name], [exception reason]);
+ [self checkProxyConnection];
+ }
+ }
+ return retVal;
+}
+
+// ------------------------------------------------------------------------
+// fireflyConfigURL
+// ------------------------------------------------------------------------
+- (NSString*)fireflyConfigURL;
+{
+ NSString *retVal = nil;
+ if( nil != serverProxy )
+ {
+ @try
+ {
+ retVal = [serverProxy fireflyConfigURL];
+ }
+ @catch( NSException *exception )
+ {
+ NSLog(@"fireflyConfigURL caught %@: %@", [exception name], [exception reason]);
+ [self checkProxyConnection];
+ }
+ }
+ return retVal;
+}
+
+// ------------------------------------------------------------------------
+// showHelperMenu
+// ------------------------------------------------------------------------
+- (void)showHelperMenu:(BOOL)bShowMenu
+{
+ if( nil != serverProxy )
+ {
+ @try
+ {
+ [serverProxy showHelperMenu:bShowMenu];
+ bShowHelperMenu = bShowMenu;
+ CFPreferencesSetAppValue( CFSTR(FF_PREFS_SHOW_MENU_EXTRA),
+ bShowMenu ? kCFBooleanTrue : kCFBooleanFalse,
+ CFSTR(FF_PREFS_DOMAIN) );
+ }
+ @catch( NSException *exception )
+ {
+ NSLog(@"fireflyConfigURL caught %@: %@", [exception name], [exception reason]);
+ [self checkProxyConnection];
+ [helperMenuCheckbox setState:(bShowHelperMenu ? NSOnState : NSOffState)];
+ }
+ }
+}
+
+
+// ========================================================================
+// Implementation of the FireflyPrefsClientProtocol
+// ========================================================================
+
+// ------------------------------------------------------------------------
+// configUrlChanged
+//
+// We're being told that the server's configuration URL has changed
+// ------------------------------------------------------------------------
+- (void)configUrlChanged:(NSString *)newUrl
+{
+ if( 0 != [newUrl length] )
+ {
+ [webPageButton setEnabled:YES];
+ [serverURL setString:newUrl];
+ [webPageInfoText setStringValue:NSLocalizedString( @"Additional configuration options are "
+ "available from Firefly's built-in web page. "
+ "Click to open the page in your browser.",
+ @"Info text for the web page button when server "
+ "is running" )];
+ }
+}
+
+// ------------------------------------------------------------------------
+// versionChanged
+//
+// We're being told that the server's version has changed
+// ------------------------------------------------------------------------
+- (void)versionChanged:(NSString *)newVersion
+{
+ if( 0 != [newVersion length] )
+ [serverVersionText setStringValue:newVersion];
+
+}
+
+// ------------------------------------------------------------------------
+// newStatus
+//
+// We're being told that the server's status has changed
+// ------------------------------------------------------------------------
+- (void)statusChanged:(FireflyServerStatus)newStatus
+{
+ [self updateServerStatus:newStatus];
+}
+
+// ------------------------------------------------------------------------
+// stillThere
+//
+// A "ping" to test the connection. If we received it, we're here!
+// ------------------------------------------------------------------------
+- (BOOL)stillThere
+{
+ return YES;
+}
+
+
+@end
diff --git a/osx/FireflyPrefs/firefly.conf b/osx/FireflyPrefs/firefly.conf
new file mode 100644
index 00000000..6752aac6
--- /dev/null
+++ b/osx/FireflyPrefs/firefly.conf
@@ -0,0 +1,351 @@
+# $Id: mt-daapd.conf.templ 1000 2006-05-01 08:07:56Z rpedde $
+#
+# This is the mt-daapd config file.
+#
+# If you have problems or questions with the format of this file,
+# direct your questions to rpedde@users.sourceforge.net.
+#
+# You can also check the website at http://mt-daapd.sourceforge.net,
+# as there is a growing documentation library there, peer-supported
+# forums and possibly more.
+#
+
+[general]
+
+#
+# web_root (required)
+#
+# Location of the admin web pages.
+#
+# If you installed from .RPM, .deb, or tarball with --prefix=/usr, then
+# this is correct.
+#
+# If you installed from tarball without --prefix=/usr, then the correct
+# path is probably /usr/local/share/mt-daapd/admin-root.
+#
+# In the default Mac install, this is a relative path, compared to the server
+
+web_root = admin-root
+
+#
+# port (required)
+#
+# What port to listen on. Leave blank to auto-assign. If the port is
+# specified, and that port is already taken, the server will not start.
+#
+
+port =
+
+#
+# admin_pw (required)
+#
+# This is the password to the administrative pages. If blank, access
+# will only be possible from the local host.
+#
+# In the default Mac install, this is left blank so that only local
+# host control is allowed
+
+admin_pw =
+
+
+#
+# db_type (required)
+#
+# This is what kind of backend database to store the song
+# info in. Valid choices are "sqlite" and "sqlite3".
+#
+
+db_type = sqlite
+
+#
+# db_parms
+#
+# This is any extra information the db needs to connect.
+# in the case of sqlite and sqlite3, this is the name
+# of the directory to store the database in
+#
+# If you installed from RPM or .deb, this path likely already
+# exists. If not, then you must create it. The directory itself
+# must be writable by the "runas" user.
+#
+# On the Mac, this lives in the Application Support folder, in our
+# Firefly folder.
+#
+
+db_parms =
+
+#
+# mp3_dir (required)
+#
+# Location of the mp3 files to share. Note that because the
+# files are stored in the database by inode, these must be
+# in the same physical filesystem.
+#
+
+mp3_dir =
+
+#
+# servername (required)
+#
+# This is both the name of the server as advertised
+# via rendezvous, and the name of the database
+# exported via DAAP. Also know as "What shows up in iTunes".
+#
+
+servername =
+
+#
+# runas (required)
+#
+# This is the user to drop privs to if running as
+# root. If mt-daapd is not started as root, this
+# configuration option is ignored. Notice that this
+# must be specified whether the server is running
+# as root or not.
+#
+# This is also ignored on Windows.
+#
+
+runas = nobody
+
+#
+# playlist (optional)
+#
+# This is the location of a playlist file.
+# This is for Apple-style "Smart Playlists"
+# See the mt-daapd.playlist file in the
+# contrib directory for syntax and examples
+#
+# This doesn't control static playlists... these
+# are controlled with the "process_m3u" directive
+# below.
+#
+#
+# On the Mac, this lives in our Firefly folder in Application Support
+
+playlist =
+
+#
+# password (optional)
+#
+# This is the password required to listen to MP3 files
+# i.e. the password that iTunes prompts for
+#
+
+password =
+
+#
+# extensions (optional)
+#
+# These are the file extensions that the daap server will
+# try to index and serve. By default, it only indexes and
+# serves .mp3 files. It can also server .m4a and .m4p files,
+# and just about any other files, really. Unfortunately, while
+# it can *attempt* to serve other files (.ogg?), iTunes won't
+# play them. Perhaps this would be useful on Linux with
+# Rhythmbox, once it understands daap. (hurry up!)
+#
+# Failing that, one can use server-side conversion to transcode
+# non-standard (.ogg, .flac) music to wav on the server side.
+# See the ssc_* options below.
+#
+# To be able to index .ogg files, you'll need to have configured
+# with --enable-oggvorbis. For .flac, --enable-flac, for .mpc,
+# --enable-musepack.
+#
+
+extensions = .mp3,.m4a,.m4p,.aiff
+
+#
+# ssc_codectypes (optional)
+#
+# List of codectypes for files that the daap server should
+# perform internal format conversion and present to clients
+# as WAV files. The file extensions that these codectypes correspond
+# to must also be present in 'extensions'
+# configuration value, or files are not probed in the first
+# place.
+#
+# Valid codectypes:
+#
+# mp4a - for AAC (.aac, .mp4, .m4a, .m4p)
+# mpeg - for mp3
+# wav - for wav
+# wma - for wma
+# ogg - for ogg
+# flac - for flac (.flac, .fla)
+# mpc for musepack (.mpc, .mpp, .mp+)
+# alac for alac (.m4a)
+#
+# NOTE: 1.0b3 of the Mac server does not have support for transcoding
+# Ogg and FLAC. Stay tuned
+
+ssc_codectypes = alac
+
+#
+# ssc_prog (optional)
+#
+# Program that is used in server side format conversion.
+# Program must accept following command line syntax:
+# ssc_prog filename offset length ...
+# Parameter filename is the real name of the file that is
+# to be converted and streamed, offset is number of bytes
+# that are skipped from the beginning of the _output_ file
+# before streaming is started, length is length of the song
+# in seconds (or zero). All other possible arguments must
+# be ignored. The resulting wav file (or the rest of
+# the file after initial seek) is written to the standard
+# output by the ssc_prog program. This is typically
+# a script that is a front end for different conversion tools
+# handling different formats.
+#
+# On the Mac, this is a relative path by default
+
+ssc_prog = ./mt-daapd-ssc.sh
+
+#
+# logfile (optional)
+#
+# This is the file to log to. If this is not configured,
+# then it will log to the syslog.
+#
+# Not that the -d switch will control the log verbosity.
+# By default, it runs at log level 1. Log level 9 will churn
+# out scads of useless debugging information. Values in between
+# will vary the amount of logging you get.
+#
+# On the Mac, this lives in our Firefly folder in Application Support
+
+logfile =
+
+#
+# truncate (optional)
+#
+# If logging is configured and this flag is enabled, the
+# server will truncate the log file each time it starts.
+# This is a good idea for both disk space and readability.
+#
+
+truncate = 1
+
+#
+# art_filename (optional)
+#
+# There is experimental support thanks to Hiren Joshi
+# (hirenj@mooh.org) for dynamically adding art to the id3v2
+# header as it is streamed (!!). If you were using a music system
+# like zina or andromeda, for example, with cover art called
+# "_folderOpenImage.jpg", you could use the parameter
+# art_file _folderOpenImage.jpg and if the file _folderOpenImage.jpg
+# was located in the same folder as the .mp3 file, it would appear
+# in iTunes. Cool, eh?
+#
+
+#art_filename = _folderOpenImage.jpg
+
+#
+# rescan_interval
+#
+# How often to check the file system to see if any mp3 files
+# have been added or removed.
+#
+# if not specified, the default is 0, which disables background scanning.
+#
+# If background rescanning is disabled, a scan can still be forced from the
+# "status" page of the administrative web interface
+#
+# Setting a rescan_interval lower than the time it takes to rescan
+# won't hurt anything, it will just waste CPU, and make connect times
+# to the daap server longer.
+#
+#
+
+rescan_interval = 300
+
+# always_scan
+#
+# The default behavior is not not do background rescans of the
+# filesystem unless there are clients connected. The thought is to
+# allow the drives to spin down unless they are in use. This might be
+# of more importance in IDE drives that aren't designed to be run
+# 24x7. Forcing a scan through the web interface will always work
+# though, even if no users are connected.
+
+# always_scan = 0
+
+#
+# process_m3u
+#
+# By default m3u processing is turned off, since most m3u files
+# sitting around in peoples mp3 directories have bad paths, and
+# I hear about it. :)
+#
+# If you are sure your m3u files have good paths (i.e. unixly pathed,
+# with relative paths relative to the directory the m3u is in), then
+# you can turn on m3u processing by setting this directive to 1.
+#
+# I'm not sure "unixly" is a word, but you get the idea.
+#
+# On the Mac, process_m3u needs to be on in order to scan the iTunes
+# database for playlists, so this is on in the default Mac install.
+
+process_m3u = 1
+
+#
+# scan_type
+#
+#
+# This sets how aggressively mp3 files should be scanned to determine
+# file length. There are three values:
+#
+# 0 (Normal)
+# Just scan the first mp3 frame to try and calculate size. This will
+# be accurate for most files, but VBR files without an Xing tag will
+# probably have wildly inaccurate file times. This is the default.
+#
+# 1 (Aggressive)
+# This checks the bitrates of 10 frames in the middle of the song.
+# This will still be inaccurate for VBR files without an Xing tag,
+# but they probably won't be quite as inaccurate as 0. This takes
+# more time, obviously, although the time hit will only happen the
+# first time you scan a particular file.
+#
+# 2 (Painfully aggressive)
+# This walks through the entire song, counting the number of frames.
+# This should result in accurate song times, but will take the most
+# time. Again, this will only have to be incurred the first time
+# the file is indexed.
+#
+
+scan_type = 2
+
+#
+# compress
+#
+# Whether to use gzip content-encoding when transferring playlists etc.
+# This was contributed as a patch by Ciamac Moallemi just prior to the 0.2.1
+# release, and as such, hasn't gotten as much testing as other features.
+#
+# This feature should substantially speed up transfers of large databases
+# and playlists.
+#
+# It will eventually default to 1, but currently it defaults to 0.
+#
+
+#compress = 0
+
+[scan]
+
+#
+# correct_order
+#
+# When set to 1, ensures that items in a playlist are returned in the
+# order in which they are set in the playlist. On platforms with
+# limited memory, this may impose an unacceptable performance penalty,
+# but on a PC or Mac, it's fine.
+#
+correct_order = 1
+
+[plugins]
+plugin_dir = plugins
+plugins = ssc-script.so,rsp.so
diff --git a/osx/FireflyPrefs/version.plist b/osx/FireflyPrefs/version.plist
new file mode 100644
index 00000000..c12b7080
--- /dev/null
+++ b/osx/FireflyPrefs/version.plist
@@ -0,0 +1,16 @@
+
+
+
+
+ BuildVersion
+ 54
+ CFBundleShortVersionString
+ 1.0
+ CFBundleVersion
+ 1.0
+ ProjectName
+ PreferencePaneTemplate
+ SourceVersion
+ 120000
+
+
diff --git a/osx/FireflyPrefsProtocol.h b/osx/FireflyPrefsProtocol.h
new file mode 100644
index 00000000..f8b70c69
--- /dev/null
+++ b/osx/FireflyPrefsProtocol.h
@@ -0,0 +1,43 @@
+/*
+ * FireflyPrefsProtocol.h
+ * Firefly Helper
+ *
+ * Created by Mike Kobb on 7/10/06.
+ * Copyright 2006 Roku LLC. All rights reserved.
+ *
+ * This file houses the declaration of the FireflyPrefsServerProtocol
+ * and FireflyPrefsClientProtocol, which are used on the Macintosh
+ * for communication between the Firefly prefs pane and the Firefly
+ * Helper background app.
+ *
+ */
+
+#include "FireflyCommon.h"
+
+// The protocol for functions exported by the server (the Firefly Helper)
+@protocol FireflyPrefsServerProtocol
+
+- (BOOL)registerClient:(byref id)client withIdentifier:(int)ident;
+- (oneway void)unregisterClientId:(int)ident;
+- (FireflyStartResult)startFirefly;
+- (FireflyStopResult)stopFirefly;
+- (FireflyRestartResult)restartFirefly;
+- (FireflyRescanResult)rescanLibrary;
+- (FireflyServerStatus)fireflyStatus;
+- (BOOL)fireflyIsRunning;
+- (bycopy NSString*)fireflyVersion;
+- (bycopy NSString*)fireflyConfigURL;
+- (oneway void)showHelperMenu:(BOOL)bShowMenu;
+
+@end
+
+
+// The protocol for functions exported by the client (the prefs pane)
+@protocol FireflyPrefsClientProtocol
+
+- (BOOL)stillThere;
+- (oneway void)statusChanged:(FireflyServerStatus)newStatus;
+- (oneway void)versionChanged:(bycopy NSString*)newVersion;
+- (oneway void)configUrlChanged:(bycopy NSString*)newUrl;
+
+@end
\ No newline at end of file
diff --git a/osx/Uninstall Firefly.app b/osx/Uninstall Firefly.app
new file mode 100755
index 00000000..d07649a6
Binary files /dev/null and b/osx/Uninstall Firefly.app differ
diff --git a/osx/makedist.sh.templ b/osx/makedist.sh.templ
new file mode 100755
index 00000000..3cacafcb
--- /dev/null
+++ b/osx/makedist.sh.templ
@@ -0,0 +1,63 @@
+#!/bin/sh
+
+if [ $# -eq 1 ]; then
+ # any argument leaves the server folder as-is
+ AR="Firefly Helper/Server/admin-root"
+
+ mkdir "Firefly Helper/Server/plugins"
+ mkdir "${AR}"
+ mkdir "${AR}/lib-js"
+ mkdir "${AR}/lib-js/script.aculo.us"
+
+ cp ../.build/ppc/mt-daapd FireflyHelper/Server/firefly
+ cp ../.build/ppc/*so FireflyHelper/Server/Plugins
+ cp ../.build/wavstreamer FireflyHelper/Server
+ cp ../.build/alac FireflyHelper/Server
+
+ cp ../admin-root/*\(html|xml|txt|jar|gif|js|png|jpg\) "${AR}"
+ cp ../admin-root/CREDITS "${AR}"
+ cp ../admin-root/lib-js/*js "${AR}/lib-js"
+ cp ../admin-root/lib-js/script.aculo.us/*js "${AR}/li-js/script.aculo.us"
+fi
+
+pushd "Firefly Helper"
+xcodebuild
+if [ "$?" -ne "0" ]; then
+ echo "Could not build Firefly Helper Project"
+ exit 1
+fi
+
+popd
+rm -rf "FireflyPrefs/Firefly Helper.app"
+mv "Firefly Helper/build/Release/Firefly Helper.app" FireflyPrefs
+
+pushd "FireflyPrefs"
+xcodebuild
+if [ "$?" -ne "0" ]; then
+ echo "Could not build FireflyPrefs panel"
+ exit 2
+fi
+
+# Now, build the image
+
+mkdir staging
+cp Install/root_DS_Store staging/.DS_Store
+mkdir staging/.background
+cp Install/background.png staging/.background/background.png
+cp Install/_background_DS_Store staging/.background/.DS_Store
+
+if [ ! -x FireflyPrefs/build/Release/Firefly.prefPane ]; then
+ echo "Wait... I can't find the pref pane"
+ exit 1
+fi
+
+mv FireflyPrefs/build/Release/Firefly.prefPane staging
+cp "Uninstall Firefly.app" staging
+cp -r "Install/Read Me First!.rtfd" staging
+
+hdiutil makehybrid -hfs -hfs-volume-name "Install Firefly" -hfs-openfolder staging staging -o tmp.dmg
+hdiutil convert -format UDZO tmp.dmg -o Firefly.dmg
+
+rm -rf staging
+rm tmp.dmg
+