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 +