├── .gitignore ├── .gitmodules ├── AHLaunchCtl Tests ├── AHLaunchCtl_Tests.m └── Info.plist ├── AHLaunchCtl.podspec ├── AHLaunchCtl.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ ├── AHLaunchCtl.xccheckout │ │ └── IDEWorkspaceChecks.plist └── xcshareddata │ └── xcschemes │ └── AHLaunchCtl.xcscheme ├── AHLaunchCtl ├── AHAuthorizer.h ├── AHAuthorizer.m ├── AHLaunchCtl-Prefix.pch ├── AHLaunchCtl.h ├── AHLaunchCtl.m ├── AHLaunchJob.h ├── AHLaunchJob.m ├── AHLaunchJobSchedule.h ├── AHLaunchJobSchedule.m ├── AHServiceManagement.h ├── AHServiceManagement.m ├── AHServiceManagement_Private.h ├── NSFileManger+Privileged.h ├── NSFileManger+Privileged.m ├── NSString+ah_versionCompare.h └── NSString+ah_versionCompare.m ├── HelperTool-CodeSign.py ├── LICENSE ├── README.md └── rootTest └── main.m /.gitignore: -------------------------------------------------------------------------------- 1 | *.xcodeproj/project.xcworkspace/xcuserdata 2 | *.xcodeproj/xcuserdata -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eahrold/AHLaunchCtl/f0bfaaeb967c4cd1bcb4d469dd34986aeffba9b4/.gitmodules -------------------------------------------------------------------------------- /AHLaunchCtl Tests/AHLaunchCtl_Tests.m: -------------------------------------------------------------------------------- 1 | // 2 | // AHLaunchCtl_Tests.m 3 | // AHLaunchCtl Tests 4 | // 5 | // Created by Eldon on 10/16/14. 6 | // Copyright (c) 2014 Eldon Ahrold. All rights reserved. 7 | // 8 | 9 | #import 10 | #import 11 | #import "AHLaunchCtl.h" 12 | #import "AHServiceManagement.h" 13 | 14 | #import 15 | 16 | @interface AHLaunchCtl_Tests : XCTestCase 17 | 18 | @end 19 | 20 | @implementation AHLaunchCtl_Tests { 21 | AHLaunchDomain _domain; 22 | AHLaunchJob *_job; 23 | AHLaunchCtl *_controller; 24 | } 25 | 26 | - (void)setUp { 27 | // Put setup code here. This method is called before the invocation of each 28 | // test method in the class. 29 | [super setUp]; 30 | 31 | _domain = kAHUserLaunchAgent; 32 | _controller = [AHLaunchCtl new]; 33 | } 34 | 35 | - (void)tearDown { 36 | // Put teardown code here. This method is called after the invocation of 37 | // each 38 | // test method in the class. 39 | [super tearDown]; 40 | } 41 | 42 | #pragma mark - Default Test 43 | - (void)testAllUserLaunchAgent { 44 | [self testAdd]; 45 | [self testGetJob]; 46 | [self testUnload]; 47 | [self testLoad]; 48 | [self testGetJob]; 49 | [self testRestartJob]; 50 | [self testRemoveJob]; 51 | [self testStartCalendarInterval]; 52 | } 53 | 54 | #pragma mark - Privileged tests 55 | - (void)testAllAsGlobalLaunchDaemon { 56 | _domain = kAHGlobalLaunchDaemon; 57 | [self testAllUserLaunchAgent]; 58 | } 59 | 60 | - (void)testAllAsGlobalLaunchAgent { 61 | _domain = kAHGlobalLaunchAgent; 62 | [self testAllUserLaunchAgent]; 63 | } 64 | 65 | - (void)testKeepAliveGui { 66 | _job = [self guiJob]; 67 | _job.KeepAlive = @(YES); 68 | 69 | _domain = kAHUserLaunchAgent; 70 | [self testLoad]; 71 | [self testUnload]; 72 | } 73 | 74 | - (void)testCreateJobFromDict { 75 | _job = [AHLaunchJob jobFromDictionary:@{ 76 | @"Label" : @"com.me", 77 | @"ProgramArguments" : @[ @"/bin/echo", @"hello world!" ], 78 | @"SD" : @"gramps" 79 | } inDomain:kAHUserLaunchAgent]; 80 | 81 | printf("%s", _job.dictionary.description.UTF8String); 82 | 83 | XCTAssertTrue(_job.dictionary[@"Label"], @"Job was not created"); 84 | XCTAssertFalse(_job.dictionary[@"SD"], @"Job was not created"); 85 | } 86 | 87 | - (void)testAuthorizedController { 88 | if ([_controller authorize]) { 89 | [self testAllAsGlobalLaunchDaemon]; 90 | } 91 | [_controller deauthorize]; 92 | } 93 | 94 | - (void)testAddSchedule { 95 | _job = [self echoJob]; 96 | _job.StartCalendarInterval = 97 | [AHLaunchJobSchedule dailyRunAtHour:1 minute:10]; 98 | 99 | [self testAdd]; 100 | } 101 | 102 | #pragma mark - Tests 103 | - (void)testAdd { 104 | NSError *error; 105 | if (!_job) { 106 | _job = [self echoJob]; 107 | } 108 | 109 | BOOL success = [_controller add:_job toDomain:_domain error:&error]; 110 | 111 | NSLog(@"%@", _job); 112 | XCTAssertTrue(success, @"Error %@", error); 113 | 114 | success = [self verifyWithSM]; 115 | XCTAssertTrue(success, 116 | @"Could not verify job was loaded using service management."); 117 | } 118 | 119 | - (void)testGetJob { 120 | if (!_job) { 121 | _job = [self echoJob]; 122 | } 123 | 124 | AHLaunchJob *job = 125 | [AHLaunchCtl runningJobWithLabel:_job.Label inDomain:_domain]; 126 | 127 | NSLog(@"%@", job); 128 | BOOL success = (job && job.Label && job.ProgramArguments); 129 | XCTAssertTrue(success, @"Could not get job %@", job); 130 | 131 | success = jobIsRunning(job.Label, _domain); 132 | XCTAssertTrue(success, @"Could not get job %@", job); 133 | 134 | success = [self verifyWithSM]; 135 | XCTAssertTrue(success, 136 | @"Could not verify job was loaded using service management."); 137 | } 138 | 139 | - (void)testUnload { 140 | BOOL initialFileCheck = 141 | [[NSFileManager defaultManager] fileExistsAtPath:[self jobFile]]; 142 | 143 | NSError *error; 144 | if (!_job) { 145 | _job = [self echoJob]; 146 | } 147 | 148 | BOOL success = 149 | [_controller unload:_job.Label inDomain:_domain error:&error]; 150 | 151 | XCTAssertTrue(success, @"Error %@", error); 152 | 153 | if (initialFileCheck) { 154 | BOOL check2 = 155 | [[NSFileManager defaultManager] fileExistsAtPath:[self jobFile]]; 156 | XCTAssertTrue( 157 | check2, 158 | @"Unloading removed the agent file, but shouldn't have!!!."); 159 | } 160 | 161 | success = [self verifyWithSM]; 162 | XCTAssertFalse( 163 | success, @"Could not verify job was removed using service management."); 164 | } 165 | 166 | - (void)testLoad { 167 | NSError *error; 168 | if (!_job) { 169 | _job = [self echoJob]; 170 | } 171 | 172 | BOOL success = [_controller load:_job inDomain:_domain error:&error]; 173 | if (error.code != kAHErrorUserCanceledAuthorization) { 174 | XCTAssertTrue(success, @"Error %@", error); 175 | 176 | success = [self verifyWithSM]; 177 | XCTAssertTrue(success, @"Could not verify job was loaded using service management."); 178 | } 179 | 180 | } 181 | 182 | - (void)testRestartJob { 183 | NSError *error; 184 | 185 | if (!_job) { 186 | _job = [self echoJob]; 187 | } 188 | 189 | XCTAssertTrue( 190 | [_controller restart:_job.Label inDomain:_domain error:&error], 191 | @"Error: %@", 192 | error.localizedDescription); 193 | 194 | BOOL success = [self verifyWithSM]; 195 | XCTAssertTrue( 196 | success, 197 | @"Could not verify job was reloaded using service management."); 198 | } 199 | 200 | - (void)testRemoveJob { 201 | NSError *error; 202 | if (!_job) { 203 | _job = [self echoJob]; 204 | } 205 | 206 | XCTAssertTrue( 207 | [_controller remove:_job.Label fromDomain:_domain error:&error], 208 | @"Error: %@", 209 | error.localizedDescription); 210 | 211 | BOOL check2 = 212 | [[NSFileManager defaultManager] fileExistsAtPath:[self jobFile]]; 213 | XCTAssertFalse(check2, 214 | @"Did not remove the launch job file during user test."); 215 | 216 | BOOL success = [self verifyWithSM]; 217 | XCTAssertFalse( 218 | success, @"Could not verify job was loaded using service management."); 219 | } 220 | 221 | - (void)testStartCalendarInterval { 222 | AHLaunchJob *job = [[AHLaunchJob alloc] init]; 223 | NSError *error = nil; 224 | job.ProgramArguments = @[ @"/bin/echo", @"hello world!" ]; 225 | job.Label = @"com.eeaapps.ahlaunchctl.check.schedule"; 226 | 227 | AHLaunchJobSchedule *schedule = 228 | [AHLaunchJobSchedule dailyRunAtHour:1 minute:00]; 229 | job.StartCalendarInterval = schedule; 230 | 231 | [[AHLaunchCtl sharedController] load:job 232 | inDomain:kAHUserLaunchAgent 233 | error:&error]; 234 | 235 | XCTAssertNil(error, @"%@", error); 236 | 237 | XCTAssertEqualObjects(schedule, 238 | job.StartCalendarInterval, 239 | @"Thsese should be equal %@ and %@", 240 | schedule, 241 | job.StartCalendarInterval); 242 | 243 | error = nil; 244 | [[AHLaunchCtl sharedController] unload:job.Label 245 | inDomain:kAHUserLaunchAgent 246 | error:&error]; 247 | XCTAssertNil(error, @"%@", error); 248 | 249 | NSLog(@"%@", job.dictionary); 250 | NSLog(@"Dictionary Description: %@", job.StartCalendarInterval); 251 | } 252 | - (void)testCustomJobKeys { 253 | NSError *error; 254 | _job = [self echoJob]; 255 | 256 | NSMutableDictionary *dict = [[_job dictionary] mutableCopy]; 257 | [dict setValue:@{ 258 | @"one" : @"first", 259 | @"two" : @"second" 260 | } forKey:@"cccDict"]; 261 | 262 | AHLaunchJob *badJob = 263 | [AHLaunchJob jobFromDictionary:dict inDomain:kAHUserLaunchAgent]; 264 | 265 | XCTAssertTrue([[AHLaunchCtl sharedController] load:badJob 266 | inDomain:kAHUserLaunchAgent 267 | error:&error], 268 | @"%@", 269 | error); 270 | ; 271 | 272 | [[AHLaunchCtl sharedController] unload:badJob.Label 273 | inDomain:kAHUserLaunchAgent 274 | error:nil]; 275 | } 276 | 277 | #pragma mark - Setup Helpers 278 | - (AHLaunchJob *)echoJob { 279 | _job = [AHLaunchJob new]; 280 | _job.Label = @"com.eeaapps.echo.helloworld"; 281 | _job.ProgramArguments = @[ @"/bin/echo", @"hello world" ]; 282 | _job.StandardOutPath = @"/tmp/hello.txt"; 283 | _job.RunAtLoad = YES; 284 | return _job; 285 | } 286 | 287 | - (AHLaunchJob *)guiJob { 288 | _job = [AHLaunchJob new]; 289 | _job.Label = @"com.eeaapps.echo.open.preview"; 290 | _job.ProgramArguments = 291 | @[ @"/Applications/Preview.app/Contents/MacOS/Preview" ]; 292 | _job.RunAtLoad = YES; 293 | return _job; 294 | } 295 | 296 | - (NSString *)jobFile { 297 | NSString *path = nil; 298 | switch (_domain) { 299 | case kAHUserLaunchAgent: 300 | path = kAHUserLaunchAgentTildeDirectory.stringByExpandingTildeInPath; 301 | break; 302 | case kAHGlobalLaunchAgent: 303 | path = kAHGlobalLaunchAgentDirectory; 304 | break; 305 | case kAHSystemLaunchAgent: 306 | path = kAHSystemLaunchAgentDirectory; 307 | break; 308 | case kAHGlobalLaunchDaemon: 309 | path = kAHGlobalLaunchDaemonDirectory; 310 | break; 311 | case kAHSystemLaunchDaemon: 312 | path = kAHSystemLaunchDaemonDirectory; 313 | break; 314 | default: 315 | break; 316 | } 317 | 318 | NSString *jobFile = 319 | [path stringByAppendingPathComponent: 320 | [_job.Label stringByAppendingPathExtension:@"plist"]]; 321 | return jobFile; 322 | } 323 | 324 | - (BOOL)verifyWithSM { 325 | CFStringRef domainStr = NULL; 326 | if (_domain > kAHGlobalLaunchAgent) { 327 | domainStr = kSMDomainSystemLaunchd; 328 | } else { 329 | domainStr = kSMDomainUserLaunchd; 330 | } 331 | 332 | BOOL success = NO; 333 | 334 | NSDictionary *dict = CFBridgingRelease( 335 | SMJobCopyDictionary(domainStr, (__bridge CFStringRef)(_job.Label))); 336 | 337 | success = (dict != nil); 338 | 339 | if (domainStr) CFRelease(domainStr); 340 | 341 | return success; 342 | } 343 | 344 | @end 345 | -------------------------------------------------------------------------------- /AHLaunchCtl Tests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | com.eeaapps.launchctl.$(PRODUCT_NAME:rfc1034identifier) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | 24 | 25 | -------------------------------------------------------------------------------- /AHLaunchCtl.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |spec| 2 | spec.name = 'AHLaunchCtl' 3 | spec.version = '0.5.4' 4 | spec.license = 'MIT' 5 | spec.summary = 'A LaunchD framework for OSX Cocoa apps.' 6 | spec.homepage = 'https://github.com/eahrold/AHLaunchCtl' 7 | spec.authors = { 'Eldon Ahrold' => 'eldon.ahrold@gmail.com' } 8 | spec.source = { :git => 'https://github.com/eahrold/AHLaunchCtl.git', :tag => "v#{spec.version}"} 9 | spec.requires_arc = true 10 | spec.osx.deployment_target = '10.8' 11 | spec.frameworks = 'SystemConfiguration','ServiceManagement','Security' 12 | spec.public_header_files = 'AHLaunchCtl/*.h' 13 | spec.source_files = 'AHLaunchCtl/*.{h,m}' 14 | end 15 | -------------------------------------------------------------------------------- /AHLaunchCtl.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | BE190A51190C3A7300607787 /* AHLaunchJobSchedule.h in Headers */ = {isa = PBXBuildFile; fileRef = BE190A4F190C3A7300607787 /* AHLaunchJobSchedule.h */; settings = {ATTRIBUTES = (Public, ); }; }; 11 | BE190A52190C3A7300607787 /* AHLaunchJobSchedule.m in Sources */ = {isa = PBXBuildFile; fileRef = BE190A50190C3A7300607787 /* AHLaunchJobSchedule.m */; }; 12 | BE6F549A18B114CC00826656 /* AHServiceManagement.h in Headers */ = {isa = PBXBuildFile; fileRef = BE6F549818B114CC00826656 /* AHServiceManagement.h */; }; 13 | BE6F549B18B114CC00826656 /* AHServiceManagement.m in Sources */ = {isa = PBXBuildFile; fileRef = BE6F549918B114CC00826656 /* AHServiceManagement.m */; }; 14 | BE76C6FF18A19369003C94A0 /* AHLaunchCtl.m in Sources */ = {isa = PBXBuildFile; fileRef = BE76C6FE18A19369003C94A0 /* AHLaunchCtl.m */; }; 15 | BE854D8318AA71D6001548A8 /* AHAuthorizer.m in Sources */ = {isa = PBXBuildFile; fileRef = BE854D8118AA71D6001548A8 /* AHAuthorizer.m */; }; 16 | BE8C6BDF1A9AE29F00E3558C /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = BE8C6BDE1A9AE29F00E3558C /* main.m */; }; 17 | BE8C6BE51A9AE3A400E3558C /* libAHLaunchCtl.a in Frameworks */ = {isa = PBXBuildFile; fileRef = BE76C6F118A19369003C94A0 /* libAHLaunchCtl.a */; }; 18 | BE8C6BE61A9AE3B000E3558C /* ServiceManagement.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BE76C71B18A19B2C003C94A0 /* ServiceManagement.framework */; }; 19 | BE8C6BE71A9AE3B700E3558C /* Security.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BE5B001018A48412000D8603 /* Security.framework */; }; 20 | BE8C6BE81A9AE3C200E3558C /* SystemConfiguration.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BE76C71D18A19B42003C94A0 /* SystemConfiguration.framework */; }; 21 | BE8D5D551B48534200DBEAE3 /* NSString+ah_versionCompare.h in Headers */ = {isa = PBXBuildFile; fileRef = BE8D5D531B48534200DBEAE3 /* NSString+ah_versionCompare.h */; }; 22 | BE8D5D561B48534200DBEAE3 /* NSString+ah_versionCompare.m in Sources */ = {isa = PBXBuildFile; fileRef = BE8D5D541B48534200DBEAE3 /* NSString+ah_versionCompare.m */; }; 23 | BE8EA57318B7ABC9008BEE58 /* NSFileManger+Privileged.m in Sources */ = {isa = PBXBuildFile; fileRef = BEA75B4018B531D700CA2672 /* NSFileManger+Privileged.m */; }; 24 | BEAB0DD118AD4A0D004EE2FD /* AHLaunchCtl.h in Headers */ = {isa = PBXBuildFile; fileRef = BE76C6FD18A19369003C94A0 /* AHLaunchCtl.h */; settings = {ATTRIBUTES = (Public, ); }; }; 25 | BEB00C971987404500B6206E /* AHLaunchCtl.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = BE76C6FD18A19369003C94A0 /* AHLaunchCtl.h */; }; 26 | BEB00C981987404500B6206E /* AHLaunchJob.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = BEED2D6218A3117A00A1D6C0 /* AHLaunchJob.h */; }; 27 | BEB00C991987404500B6206E /* AHLaunchJobSchedule.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = BE190A4F190C3A7300607787 /* AHLaunchJobSchedule.h */; }; 28 | BEB00C9C1987404500B6206E /* AHAuthorizer.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = BE854D8018AA71D6001548A8 /* AHAuthorizer.h */; }; 29 | BEB00C9D1987404500B6206E /* AHServiceManagement.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = BE6F549818B114CC00826656 /* AHServiceManagement.h */; }; 30 | BEDDB53E18B1554700C16956 /* AHAuthorizer.h in Headers */ = {isa = PBXBuildFile; fileRef = BE854D8018AA71D6001548A8 /* AHAuthorizer.h */; }; 31 | BEED2D6418A3117A00A1D6C0 /* AHLaunchJob.h in Headers */ = {isa = PBXBuildFile; fileRef = BEED2D6218A3117A00A1D6C0 /* AHLaunchJob.h */; settings = {ATTRIBUTES = (Public, ); }; }; 32 | BEED2D6518A3117A00A1D6C0 /* AHLaunchJob.m in Sources */ = {isa = PBXBuildFile; fileRef = BEED2D6318A3117A00A1D6C0 /* AHLaunchJob.m */; }; 33 | BEFC28CB19F0D79D00C35A6F /* AHLaunchCtl_Tests.m in Sources */ = {isa = PBXBuildFile; fileRef = BEFC28CA19F0D79D00C35A6F /* AHLaunchCtl_Tests.m */; }; 34 | BEFC28D119F0D85200C35A6F /* libAHLaunchCtl.a in Frameworks */ = {isa = PBXBuildFile; fileRef = BE76C6F118A19369003C94A0 /* libAHLaunchCtl.a */; }; 35 | BEFC28D219F0D85B00C35A6F /* ServiceManagement.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BE76C71B18A19B2C003C94A0 /* ServiceManagement.framework */; }; 36 | BEFC28D319F0D86000C35A6F /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BE76C6F718A19369003C94A0 /* Foundation.framework */; }; 37 | BEFC28D419F0D86600C35A6F /* Security.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BE5B001018A48412000D8603 /* Security.framework */; }; 38 | BEFC28D519F0D8A500C35A6F /* SystemConfiguration.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BE76C71D18A19B42003C94A0 /* SystemConfiguration.framework */; }; 39 | /* End PBXBuildFile section */ 40 | 41 | /* Begin PBXContainerItemProxy section */ 42 | BE8C6BE31A9AE39C00E3558C /* PBXContainerItemProxy */ = { 43 | isa = PBXContainerItemProxy; 44 | containerPortal = BE76C6E918A19369003C94A0 /* Project object */; 45 | proxyType = 1; 46 | remoteGlobalIDString = BE76C6F018A19369003C94A0; 47 | remoteInfo = AHLaunchCtl; 48 | }; 49 | BEFC28CF19F0D84A00C35A6F /* PBXContainerItemProxy */ = { 50 | isa = PBXContainerItemProxy; 51 | containerPortal = BE76C6E918A19369003C94A0 /* Project object */; 52 | proxyType = 1; 53 | remoteGlobalIDString = BE76C6F018A19369003C94A0; 54 | remoteInfo = AHLaunchCtl; 55 | }; 56 | /* End PBXContainerItemProxy section */ 57 | 58 | /* Begin PBXCopyFilesBuildPhase section */ 59 | BE8C6BDA1A9AE29F00E3558C /* CopyFiles */ = { 60 | isa = PBXCopyFilesBuildPhase; 61 | buildActionMask = 2147483647; 62 | dstPath = /usr/share/man/man1/; 63 | dstSubfolderSpec = 0; 64 | files = ( 65 | ); 66 | runOnlyForDeploymentPostprocessing = 1; 67 | }; 68 | BEB00C961987402300B6206E /* CopyFiles */ = { 69 | isa = PBXCopyFilesBuildPhase; 70 | buildActionMask = 2147483647; 71 | dstPath = include/AHLaunchCtl; 72 | dstSubfolderSpec = 16; 73 | files = ( 74 | BEB00C971987404500B6206E /* AHLaunchCtl.h in CopyFiles */, 75 | BEB00C981987404500B6206E /* AHLaunchJob.h in CopyFiles */, 76 | BEB00C991987404500B6206E /* AHLaunchJobSchedule.h in CopyFiles */, 77 | BEB00C9C1987404500B6206E /* AHAuthorizer.h in CopyFiles */, 78 | BEB00C9D1987404500B6206E /* AHServiceManagement.h in CopyFiles */, 79 | ); 80 | runOnlyForDeploymentPostprocessing = 0; 81 | }; 82 | /* End PBXCopyFilesBuildPhase section */ 83 | 84 | /* Begin PBXFileReference section */ 85 | BE190A4F190C3A7300607787 /* AHLaunchJobSchedule.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AHLaunchJobSchedule.h; sourceTree = ""; }; 86 | BE190A50190C3A7300607787 /* AHLaunchJobSchedule.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AHLaunchJobSchedule.m; sourceTree = ""; }; 87 | BE2C317E1988397300F8044E /* HelperTool-CodeSign.py */ = {isa = PBXFileReference; lastKnownFileType = text.script.python; path = "HelperTool-CodeSign.py"; sourceTree = ""; }; 88 | BE5B001018A48412000D8603 /* Security.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Security.framework; path = System/Library/Frameworks/Security.framework; sourceTree = SDKROOT; }; 89 | BE5B001C18A55BAA000D8603 /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = System/Library/Frameworks/Foundation.framework; sourceTree = SDKROOT; }; 90 | BE6F549818B114CC00826656 /* AHServiceManagement.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AHServiceManagement.h; sourceTree = ""; }; 91 | BE6F549918B114CC00826656 /* AHServiceManagement.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AHServiceManagement.m; sourceTree = ""; }; 92 | BE76C6F118A19369003C94A0 /* libAHLaunchCtl.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libAHLaunchCtl.a; sourceTree = BUILT_PRODUCTS_DIR; }; 93 | BE76C6F418A19369003C94A0 /* Cocoa.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Cocoa.framework; path = System/Library/Frameworks/Cocoa.framework; sourceTree = SDKROOT; }; 94 | BE76C6F718A19369003C94A0 /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = System/Library/Frameworks/Foundation.framework; sourceTree = SDKROOT; }; 95 | BE76C6F818A19369003C94A0 /* CoreData.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreData.framework; path = System/Library/Frameworks/CoreData.framework; sourceTree = SDKROOT; }; 96 | BE76C6F918A19369003C94A0 /* AppKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AppKit.framework; path = System/Library/Frameworks/AppKit.framework; sourceTree = SDKROOT; }; 97 | BE76C6FC18A19369003C94A0 /* AHLaunchCtl-Prefix.pch */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "AHLaunchCtl-Prefix.pch"; sourceTree = ""; }; 98 | BE76C6FD18A19369003C94A0 /* AHLaunchCtl.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; lineEnding = 0; path = AHLaunchCtl.h; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objcpp; }; 99 | BE76C6FE18A19369003C94A0 /* AHLaunchCtl.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; lineEnding = 0; path = AHLaunchCtl.m; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objc; }; 100 | BE76C70518A19369003C94A0 /* XCTest.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = XCTest.framework; path = Library/Frameworks/XCTest.framework; sourceTree = DEVELOPER_DIR; }; 101 | BE76C71B18A19B2C003C94A0 /* ServiceManagement.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = ServiceManagement.framework; path = System/Library/Frameworks/ServiceManagement.framework; sourceTree = SDKROOT; }; 102 | BE76C71D18A19B42003C94A0 /* SystemConfiguration.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SystemConfiguration.framework; path = System/Library/Frameworks/SystemConfiguration.framework; sourceTree = SDKROOT; }; 103 | BE854D8018AA71D6001548A8 /* AHAuthorizer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; lineEnding = 0; path = AHAuthorizer.h; sourceTree = ""; }; 104 | BE854D8118AA71D6001548A8 /* AHAuthorizer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; lineEnding = 0; path = AHAuthorizer.m; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objc; }; 105 | BE8C6BD51A9976AD00E3558C /* AHServiceManagement_Private.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AHServiceManagement_Private.h; sourceTree = ""; }; 106 | BE8C6BDC1A9AE29F00E3558C /* rootTest */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = rootTest; sourceTree = BUILT_PRODUCTS_DIR; }; 107 | BE8C6BDE1A9AE29F00E3558C /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; 108 | BE8D5D531B48534200DBEAE3 /* NSString+ah_versionCompare.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSString+ah_versionCompare.h"; sourceTree = ""; }; 109 | BE8D5D541B48534200DBEAE3 /* NSString+ah_versionCompare.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSString+ah_versionCompare.m"; sourceTree = ""; }; 110 | BEA75B3F18B531D700CA2672 /* NSFileManger+Privileged.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSFileManger+Privileged.h"; sourceTree = ""; }; 111 | BEA75B4018B531D700CA2672 /* NSFileManger+Privileged.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSFileManger+Privileged.m"; sourceTree = ""; }; 112 | BEED2D6218A3117A00A1D6C0 /* AHLaunchJob.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; lineEnding = 0; path = AHLaunchJob.h; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objcpp; }; 113 | BEED2D6318A3117A00A1D6C0 /* AHLaunchJob.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; lineEnding = 0; path = AHLaunchJob.m; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objc; }; 114 | BEFC28C619F0D79D00C35A6F /* AHLaunchCtl Tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "AHLaunchCtl Tests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; 115 | BEFC28C919F0D79D00C35A6F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 116 | BEFC28CA19F0D79D00C35A6F /* AHLaunchCtl_Tests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AHLaunchCtl_Tests.m; sourceTree = ""; }; 117 | /* End PBXFileReference section */ 118 | 119 | /* Begin PBXFrameworksBuildPhase section */ 120 | BE76C6EE18A19369003C94A0 /* Frameworks */ = { 121 | isa = PBXFrameworksBuildPhase; 122 | buildActionMask = 2147483647; 123 | files = ( 124 | ); 125 | runOnlyForDeploymentPostprocessing = 0; 126 | }; 127 | BE8C6BD91A9AE29F00E3558C /* Frameworks */ = { 128 | isa = PBXFrameworksBuildPhase; 129 | buildActionMask = 2147483647; 130 | files = ( 131 | BE8C6BE81A9AE3C200E3558C /* SystemConfiguration.framework in Frameworks */, 132 | BE8C6BE71A9AE3B700E3558C /* Security.framework in Frameworks */, 133 | BE8C6BE61A9AE3B000E3558C /* ServiceManagement.framework in Frameworks */, 134 | BE8C6BE51A9AE3A400E3558C /* libAHLaunchCtl.a in Frameworks */, 135 | ); 136 | runOnlyForDeploymentPostprocessing = 0; 137 | }; 138 | BEFC28C319F0D79D00C35A6F /* Frameworks */ = { 139 | isa = PBXFrameworksBuildPhase; 140 | buildActionMask = 2147483647; 141 | files = ( 142 | BEFC28D519F0D8A500C35A6F /* SystemConfiguration.framework in Frameworks */, 143 | BEFC28D419F0D86600C35A6F /* Security.framework in Frameworks */, 144 | BEFC28D319F0D86000C35A6F /* Foundation.framework in Frameworks */, 145 | BEFC28D219F0D85B00C35A6F /* ServiceManagement.framework in Frameworks */, 146 | BEFC28D119F0D85200C35A6F /* libAHLaunchCtl.a in Frameworks */, 147 | ); 148 | runOnlyForDeploymentPostprocessing = 0; 149 | }; 150 | /* End PBXFrameworksBuildPhase section */ 151 | 152 | /* Begin PBXGroup section */ 153 | BE76C6E818A19369003C94A0 = { 154 | isa = PBXGroup; 155 | children = ( 156 | BE2C317E1988397300F8044E /* HelperTool-CodeSign.py */, 157 | BE76C6FA18A19369003C94A0 /* AHLaunchCtl */, 158 | BEFC28C719F0D79D00C35A6F /* AHLaunchCtl Tests */, 159 | BE8C6BDD1A9AE29F00E3558C /* AHLaunchCtl Root Test */, 160 | BE76C6F318A19369003C94A0 /* Frameworks */, 161 | BE76C6F218A19369003C94A0 /* Products */, 162 | ); 163 | sourceTree = ""; 164 | }; 165 | BE76C6F218A19369003C94A0 /* Products */ = { 166 | isa = PBXGroup; 167 | children = ( 168 | BE76C6F118A19369003C94A0 /* libAHLaunchCtl.a */, 169 | BEFC28C619F0D79D00C35A6F /* AHLaunchCtl Tests.xctest */, 170 | BE8C6BDC1A9AE29F00E3558C /* rootTest */, 171 | ); 172 | name = Products; 173 | sourceTree = ""; 174 | }; 175 | BE76C6F318A19369003C94A0 /* Frameworks */ = { 176 | isa = PBXGroup; 177 | children = ( 178 | BE5B001018A48412000D8603 /* Security.framework */, 179 | BE76C71D18A19B42003C94A0 /* SystemConfiguration.framework */, 180 | BE76C71B18A19B2C003C94A0 /* ServiceManagement.framework */, 181 | BE76C6F418A19369003C94A0 /* Cocoa.framework */, 182 | BE76C70518A19369003C94A0 /* XCTest.framework */, 183 | BE5B001C18A55BAA000D8603 /* Foundation.framework */, 184 | BE76C6F618A19369003C94A0 /* Other Frameworks */, 185 | ); 186 | name = Frameworks; 187 | sourceTree = ""; 188 | }; 189 | BE76C6F618A19369003C94A0 /* Other Frameworks */ = { 190 | isa = PBXGroup; 191 | children = ( 192 | BE76C6F718A19369003C94A0 /* Foundation.framework */, 193 | BE76C6F818A19369003C94A0 /* CoreData.framework */, 194 | BE76C6F918A19369003C94A0 /* AppKit.framework */, 195 | ); 196 | name = "Other Frameworks"; 197 | sourceTree = ""; 198 | }; 199 | BE76C6FA18A19369003C94A0 /* AHLaunchCtl */ = { 200 | isa = PBXGroup; 201 | children = ( 202 | BE76C6FD18A19369003C94A0 /* AHLaunchCtl.h */, 203 | BE76C6FE18A19369003C94A0 /* AHLaunchCtl.m */, 204 | BEED2D6218A3117A00A1D6C0 /* AHLaunchJob.h */, 205 | BEED2D6318A3117A00A1D6C0 /* AHLaunchJob.m */, 206 | BE190A4F190C3A7300607787 /* AHLaunchJobSchedule.h */, 207 | BE190A50190C3A7300607787 /* AHLaunchJobSchedule.m */, 208 | BE854D8018AA71D6001548A8 /* AHAuthorizer.h */, 209 | BE854D8118AA71D6001548A8 /* AHAuthorizer.m */, 210 | BE6F549818B114CC00826656 /* AHServiceManagement.h */, 211 | BE6F549918B114CC00826656 /* AHServiceManagement.m */, 212 | BE8C6BD51A9976AD00E3558C /* AHServiceManagement_Private.h */, 213 | BE8D5D531B48534200DBEAE3 /* NSString+ah_versionCompare.h */, 214 | BE8D5D541B48534200DBEAE3 /* NSString+ah_versionCompare.m */, 215 | BE76C6FB18A19369003C94A0 /* Supporting Files */, 216 | ); 217 | path = AHLaunchCtl; 218 | sourceTree = ""; 219 | }; 220 | BE76C6FB18A19369003C94A0 /* Supporting Files */ = { 221 | isa = PBXGroup; 222 | children = ( 223 | BE8EA57218B7A286008BEE58 /* Categories */, 224 | BE76C6FC18A19369003C94A0 /* AHLaunchCtl-Prefix.pch */, 225 | ); 226 | name = "Supporting Files"; 227 | sourceTree = ""; 228 | }; 229 | BE8C6BDD1A9AE29F00E3558C /* AHLaunchCtl Root Test */ = { 230 | isa = PBXGroup; 231 | children = ( 232 | BE8C6BDE1A9AE29F00E3558C /* main.m */, 233 | ); 234 | name = "AHLaunchCtl Root Test"; 235 | path = rootTest; 236 | sourceTree = ""; 237 | }; 238 | BE8EA57218B7A286008BEE58 /* Categories */ = { 239 | isa = PBXGroup; 240 | children = ( 241 | BEA75B3F18B531D700CA2672 /* NSFileManger+Privileged.h */, 242 | BEA75B4018B531D700CA2672 /* NSFileManger+Privileged.m */, 243 | ); 244 | name = Categories; 245 | sourceTree = ""; 246 | }; 247 | BEFC28C719F0D79D00C35A6F /* AHLaunchCtl Tests */ = { 248 | isa = PBXGroup; 249 | children = ( 250 | BEFC28CA19F0D79D00C35A6F /* AHLaunchCtl_Tests.m */, 251 | BEFC28C819F0D79D00C35A6F /* Supporting Files */, 252 | ); 253 | path = "AHLaunchCtl Tests"; 254 | sourceTree = ""; 255 | }; 256 | BEFC28C819F0D79D00C35A6F /* Supporting Files */ = { 257 | isa = PBXGroup; 258 | children = ( 259 | BEFC28C919F0D79D00C35A6F /* Info.plist */, 260 | ); 261 | name = "Supporting Files"; 262 | sourceTree = ""; 263 | }; 264 | /* End PBXGroup section */ 265 | 266 | /* Begin PBXHeadersBuildPhase section */ 267 | BE76C6EF18A19369003C94A0 /* Headers */ = { 268 | isa = PBXHeadersBuildPhase; 269 | buildActionMask = 2147483647; 270 | files = ( 271 | BEAB0DD118AD4A0D004EE2FD /* AHLaunchCtl.h in Headers */, 272 | BEED2D6418A3117A00A1D6C0 /* AHLaunchJob.h in Headers */, 273 | BE190A51190C3A7300607787 /* AHLaunchJobSchedule.h in Headers */, 274 | BEDDB53E18B1554700C16956 /* AHAuthorizer.h in Headers */, 275 | BE6F549A18B114CC00826656 /* AHServiceManagement.h in Headers */, 276 | BE8D5D551B48534200DBEAE3 /* NSString+ah_versionCompare.h in Headers */, 277 | ); 278 | runOnlyForDeploymentPostprocessing = 0; 279 | }; 280 | /* End PBXHeadersBuildPhase section */ 281 | 282 | /* Begin PBXNativeTarget section */ 283 | BE76C6F018A19369003C94A0 /* AHLaunchCtl */ = { 284 | isa = PBXNativeTarget; 285 | buildConfigurationList = BE76C71518A19369003C94A0 /* Build configuration list for PBXNativeTarget "AHLaunchCtl" */; 286 | buildPhases = ( 287 | BE76C6ED18A19369003C94A0 /* Sources */, 288 | BEB00C961987402300B6206E /* CopyFiles */, 289 | BE76C6EE18A19369003C94A0 /* Frameworks */, 290 | BE76C6EF18A19369003C94A0 /* Headers */, 291 | ); 292 | buildRules = ( 293 | ); 294 | dependencies = ( 295 | ); 296 | name = AHLaunchCtl; 297 | productName = AHLaunchCtl; 298 | productReference = BE76C6F118A19369003C94A0 /* libAHLaunchCtl.a */; 299 | productType = "com.apple.product-type.library.static"; 300 | }; 301 | BE8C6BDB1A9AE29F00E3558C /* rootTest */ = { 302 | isa = PBXNativeTarget; 303 | buildConfigurationList = BE8C6BE21A9AE29F00E3558C /* Build configuration list for PBXNativeTarget "rootTest" */; 304 | buildPhases = ( 305 | BE8C6BD81A9AE29F00E3558C /* Sources */, 306 | BE8C6BD91A9AE29F00E3558C /* Frameworks */, 307 | BE8C6BDA1A9AE29F00E3558C /* CopyFiles */, 308 | ); 309 | buildRules = ( 310 | ); 311 | dependencies = ( 312 | BE8C6BE41A9AE39C00E3558C /* PBXTargetDependency */, 313 | ); 314 | name = rootTest; 315 | productName = rootTest; 316 | productReference = BE8C6BDC1A9AE29F00E3558C /* rootTest */; 317 | productType = "com.apple.product-type.tool"; 318 | }; 319 | BEFC28C519F0D79D00C35A6F /* AHLaunchCtl Tests */ = { 320 | isa = PBXNativeTarget; 321 | buildConfigurationList = BEFC28CC19F0D79D00C35A6F /* Build configuration list for PBXNativeTarget "AHLaunchCtl Tests" */; 322 | buildPhases = ( 323 | BEFC28C219F0D79D00C35A6F /* Sources */, 324 | BEFC28C319F0D79D00C35A6F /* Frameworks */, 325 | BEFC28C419F0D79D00C35A6F /* Resources */, 326 | ); 327 | buildRules = ( 328 | ); 329 | dependencies = ( 330 | BEFC28D019F0D84A00C35A6F /* PBXTargetDependency */, 331 | ); 332 | name = "AHLaunchCtl Tests"; 333 | productName = "AHLaunchCtl Tests"; 334 | productReference = BEFC28C619F0D79D00C35A6F /* AHLaunchCtl Tests.xctest */; 335 | productType = "com.apple.product-type.bundle.unit-test"; 336 | }; 337 | /* End PBXNativeTarget section */ 338 | 339 | /* Begin PBXProject section */ 340 | BE76C6E918A19369003C94A0 /* Project object */ = { 341 | isa = PBXProject; 342 | attributes = { 343 | LastUpgradeCheck = 0600; 344 | ORGANIZATIONNAME = "Eldon Ahrold"; 345 | TargetAttributes = { 346 | BE8C6BDB1A9AE29F00E3558C = { 347 | CreatedOnToolsVersion = 6.1; 348 | }; 349 | BEFC28C519F0D79D00C35A6F = { 350 | CreatedOnToolsVersion = 6.0.1; 351 | }; 352 | }; 353 | }; 354 | buildConfigurationList = BE76C6EC18A19369003C94A0 /* Build configuration list for PBXProject "AHLaunchCtl" */; 355 | compatibilityVersion = "Xcode 3.2"; 356 | developmentRegion = English; 357 | hasScannedForEncodings = 0; 358 | knownRegions = ( 359 | en, 360 | Base, 361 | ); 362 | mainGroup = BE76C6E818A19369003C94A0; 363 | productRefGroup = BE76C6F218A19369003C94A0 /* Products */; 364 | projectDirPath = ""; 365 | projectRoot = ""; 366 | targets = ( 367 | BE76C6F018A19369003C94A0 /* AHLaunchCtl */, 368 | BEFC28C519F0D79D00C35A6F /* AHLaunchCtl Tests */, 369 | BE8C6BDB1A9AE29F00E3558C /* rootTest */, 370 | ); 371 | }; 372 | /* End PBXProject section */ 373 | 374 | /* Begin PBXResourcesBuildPhase section */ 375 | BEFC28C419F0D79D00C35A6F /* Resources */ = { 376 | isa = PBXResourcesBuildPhase; 377 | buildActionMask = 2147483647; 378 | files = ( 379 | ); 380 | runOnlyForDeploymentPostprocessing = 0; 381 | }; 382 | /* End PBXResourcesBuildPhase section */ 383 | 384 | /* Begin PBXSourcesBuildPhase section */ 385 | BE76C6ED18A19369003C94A0 /* Sources */ = { 386 | isa = PBXSourcesBuildPhase; 387 | buildActionMask = 2147483647; 388 | files = ( 389 | BE8EA57318B7ABC9008BEE58 /* NSFileManger+Privileged.m in Sources */, 390 | BE190A52190C3A7300607787 /* AHLaunchJobSchedule.m in Sources */, 391 | BE76C6FF18A19369003C94A0 /* AHLaunchCtl.m in Sources */, 392 | BEED2D6518A3117A00A1D6C0 /* AHLaunchJob.m in Sources */, 393 | BE6F549B18B114CC00826656 /* AHServiceManagement.m in Sources */, 394 | BE8D5D561B48534200DBEAE3 /* NSString+ah_versionCompare.m in Sources */, 395 | BE854D8318AA71D6001548A8 /* AHAuthorizer.m in Sources */, 396 | ); 397 | runOnlyForDeploymentPostprocessing = 0; 398 | }; 399 | BE8C6BD81A9AE29F00E3558C /* Sources */ = { 400 | isa = PBXSourcesBuildPhase; 401 | buildActionMask = 2147483647; 402 | files = ( 403 | BE8C6BDF1A9AE29F00E3558C /* main.m in Sources */, 404 | ); 405 | runOnlyForDeploymentPostprocessing = 0; 406 | }; 407 | BEFC28C219F0D79D00C35A6F /* Sources */ = { 408 | isa = PBXSourcesBuildPhase; 409 | buildActionMask = 2147483647; 410 | files = ( 411 | BEFC28CB19F0D79D00C35A6F /* AHLaunchCtl_Tests.m in Sources */, 412 | ); 413 | runOnlyForDeploymentPostprocessing = 0; 414 | }; 415 | /* End PBXSourcesBuildPhase section */ 416 | 417 | /* Begin PBXTargetDependency section */ 418 | BE8C6BE41A9AE39C00E3558C /* PBXTargetDependency */ = { 419 | isa = PBXTargetDependency; 420 | target = BE76C6F018A19369003C94A0 /* AHLaunchCtl */; 421 | targetProxy = BE8C6BE31A9AE39C00E3558C /* PBXContainerItemProxy */; 422 | }; 423 | BEFC28D019F0D84A00C35A6F /* PBXTargetDependency */ = { 424 | isa = PBXTargetDependency; 425 | target = BE76C6F018A19369003C94A0 /* AHLaunchCtl */; 426 | targetProxy = BEFC28CF19F0D84A00C35A6F /* PBXContainerItemProxy */; 427 | }; 428 | /* End PBXTargetDependency section */ 429 | 430 | /* Begin XCBuildConfiguration section */ 431 | BE76C71318A19369003C94A0 /* Debug */ = { 432 | isa = XCBuildConfiguration; 433 | buildSettings = { 434 | ALWAYS_SEARCH_USER_PATHS = NO; 435 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 436 | CLANG_CXX_LIBRARY = "libc++"; 437 | CLANG_ENABLE_OBJC_ARC = YES; 438 | CLANG_WARN_BOOL_CONVERSION = YES; 439 | CLANG_WARN_CONSTANT_CONVERSION = YES; 440 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 441 | CLANG_WARN_EMPTY_BODY = YES; 442 | CLANG_WARN_ENUM_CONVERSION = YES; 443 | CLANG_WARN_INT_CONVERSION = YES; 444 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 445 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 446 | COPY_PHASE_STRIP = NO; 447 | GCC_C_LANGUAGE_STANDARD = gnu99; 448 | GCC_DYNAMIC_NO_PIC = NO; 449 | GCC_ENABLE_OBJC_EXCEPTIONS = YES; 450 | GCC_OPTIMIZATION_LEVEL = 0; 451 | GCC_PREPROCESSOR_DEFINITIONS = ( 452 | "DEBUG=1", 453 | "$(inherited)", 454 | ); 455 | GCC_SYMBOLS_PRIVATE_EXTERN = NO; 456 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 457 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 458 | GCC_WARN_UNDECLARED_SELECTOR = YES; 459 | GCC_WARN_UNINITIALIZED_AUTOS = YES; 460 | GCC_WARN_UNUSED_FUNCTION = YES; 461 | GCC_WARN_UNUSED_VARIABLE = YES; 462 | MACOSX_DEPLOYMENT_TARGET = 10.8; 463 | ONLY_ACTIVE_ARCH = YES; 464 | SDKROOT = macosx; 465 | }; 466 | name = Debug; 467 | }; 468 | BE76C71418A19369003C94A0 /* Release */ = { 469 | isa = XCBuildConfiguration; 470 | buildSettings = { 471 | ALWAYS_SEARCH_USER_PATHS = NO; 472 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 473 | CLANG_CXX_LIBRARY = "libc++"; 474 | CLANG_ENABLE_OBJC_ARC = YES; 475 | CLANG_WARN_BOOL_CONVERSION = YES; 476 | CLANG_WARN_CONSTANT_CONVERSION = YES; 477 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 478 | CLANG_WARN_EMPTY_BODY = YES; 479 | CLANG_WARN_ENUM_CONVERSION = YES; 480 | CLANG_WARN_INT_CONVERSION = YES; 481 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 482 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 483 | COPY_PHASE_STRIP = YES; 484 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 485 | ENABLE_NS_ASSERTIONS = NO; 486 | GCC_C_LANGUAGE_STANDARD = gnu99; 487 | GCC_ENABLE_OBJC_EXCEPTIONS = YES; 488 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 489 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 490 | GCC_WARN_UNDECLARED_SELECTOR = YES; 491 | GCC_WARN_UNINITIALIZED_AUTOS = YES; 492 | GCC_WARN_UNUSED_FUNCTION = YES; 493 | GCC_WARN_UNUSED_VARIABLE = YES; 494 | MACOSX_DEPLOYMENT_TARGET = 10.8; 495 | SDKROOT = macosx; 496 | }; 497 | name = Release; 498 | }; 499 | BE76C71618A19369003C94A0 /* Debug */ = { 500 | isa = XCBuildConfiguration; 501 | buildSettings = { 502 | CODE_SIGN_IDENTITY = "Mac Developer"; 503 | COMBINE_HIDPI_IMAGES = YES; 504 | GCC_PRECOMPILE_PREFIX_HEADER = YES; 505 | GCC_PREFIX_HEADER = "AHLaunchCtl/AHLaunchCtl-Prefix.pch"; 506 | INSTALL_PATH = /usr/local/lib; 507 | MACOSX_DEPLOYMENT_TARGET = 10.8; 508 | OTHER_LDFLAGS = ""; 509 | PRIVATE_HEADERS_FOLDER_PATH = /usr/local/include; 510 | PRODUCT_NAME = "$(TARGET_NAME)"; 511 | PUBLIC_HEADERS_FOLDER_PATH = /usr/local/include; 512 | SDKROOT = macosx; 513 | }; 514 | name = Debug; 515 | }; 516 | BE76C71718A19369003C94A0 /* Release */ = { 517 | isa = XCBuildConfiguration; 518 | buildSettings = { 519 | CODE_SIGN_IDENTITY = "Mac Developer"; 520 | COMBINE_HIDPI_IMAGES = YES; 521 | GCC_PRECOMPILE_PREFIX_HEADER = YES; 522 | GCC_PREFIX_HEADER = "AHLaunchCtl/AHLaunchCtl-Prefix.pch"; 523 | INSTALL_PATH = /usr/local/lib; 524 | MACOSX_DEPLOYMENT_TARGET = 10.8; 525 | OTHER_LDFLAGS = ""; 526 | PRIVATE_HEADERS_FOLDER_PATH = /usr/local/include; 527 | PRODUCT_NAME = "$(TARGET_NAME)"; 528 | PUBLIC_HEADERS_FOLDER_PATH = /usr/local/include; 529 | SDKROOT = macosx; 530 | }; 531 | name = Release; 532 | }; 533 | BE8C6BE01A9AE29F00E3558C /* Debug */ = { 534 | isa = XCBuildConfiguration; 535 | buildSettings = { 536 | CLANG_ENABLE_MODULES = YES; 537 | CLANG_WARN_UNREACHABLE_CODE = YES; 538 | ENABLE_STRICT_OBJC_MSGSEND = YES; 539 | GCC_PREPROCESSOR_DEFINITIONS = ( 540 | "DEBUG=1", 541 | "$(inherited)", 542 | ); 543 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 544 | MACOSX_DEPLOYMENT_TARGET = 10.9; 545 | MTL_ENABLE_DEBUG_INFO = YES; 546 | PRODUCT_NAME = "$(TARGET_NAME)"; 547 | }; 548 | name = Debug; 549 | }; 550 | BE8C6BE11A9AE29F00E3558C /* Release */ = { 551 | isa = XCBuildConfiguration; 552 | buildSettings = { 553 | CLANG_ENABLE_MODULES = YES; 554 | CLANG_WARN_UNREACHABLE_CODE = YES; 555 | ENABLE_STRICT_OBJC_MSGSEND = YES; 556 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 557 | MACOSX_DEPLOYMENT_TARGET = 10.9; 558 | MTL_ENABLE_DEBUG_INFO = NO; 559 | PRODUCT_NAME = "$(TARGET_NAME)"; 560 | }; 561 | name = Release; 562 | }; 563 | BEFC28CD19F0D79D00C35A6F /* Debug */ = { 564 | isa = XCBuildConfiguration; 565 | buildSettings = { 566 | CLANG_ENABLE_MODULES = YES; 567 | CLANG_WARN_UNREACHABLE_CODE = YES; 568 | COMBINE_HIDPI_IMAGES = YES; 569 | ENABLE_STRICT_OBJC_MSGSEND = YES; 570 | FRAMEWORK_SEARCH_PATHS = ( 571 | "$(DEVELOPER_FRAMEWORKS_DIR)", 572 | "$(inherited)", 573 | ); 574 | GCC_PREPROCESSOR_DEFINITIONS = ( 575 | "DEBUG=1", 576 | "$(inherited)", 577 | ); 578 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 579 | INFOPLIST_FILE = "AHLaunchCtl Tests/Info.plist"; 580 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/../Frameworks"; 581 | MACOSX_DEPLOYMENT_TARGET = 10.9; 582 | MTL_ENABLE_DEBUG_INFO = YES; 583 | PRODUCT_NAME = "$(TARGET_NAME)"; 584 | }; 585 | name = Debug; 586 | }; 587 | BEFC28CE19F0D79D00C35A6F /* Release */ = { 588 | isa = XCBuildConfiguration; 589 | buildSettings = { 590 | CLANG_ENABLE_MODULES = YES; 591 | CLANG_WARN_UNREACHABLE_CODE = YES; 592 | COMBINE_HIDPI_IMAGES = YES; 593 | ENABLE_STRICT_OBJC_MSGSEND = YES; 594 | FRAMEWORK_SEARCH_PATHS = ( 595 | "$(DEVELOPER_FRAMEWORKS_DIR)", 596 | "$(inherited)", 597 | ); 598 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 599 | INFOPLIST_FILE = "AHLaunchCtl Tests/Info.plist"; 600 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/../Frameworks"; 601 | MACOSX_DEPLOYMENT_TARGET = 10.9; 602 | MTL_ENABLE_DEBUG_INFO = NO; 603 | PRODUCT_NAME = "$(TARGET_NAME)"; 604 | }; 605 | name = Release; 606 | }; 607 | /* End XCBuildConfiguration section */ 608 | 609 | /* Begin XCConfigurationList section */ 610 | BE76C6EC18A19369003C94A0 /* Build configuration list for PBXProject "AHLaunchCtl" */ = { 611 | isa = XCConfigurationList; 612 | buildConfigurations = ( 613 | BE76C71318A19369003C94A0 /* Debug */, 614 | BE76C71418A19369003C94A0 /* Release */, 615 | ); 616 | defaultConfigurationIsVisible = 0; 617 | defaultConfigurationName = Release; 618 | }; 619 | BE76C71518A19369003C94A0 /* Build configuration list for PBXNativeTarget "AHLaunchCtl" */ = { 620 | isa = XCConfigurationList; 621 | buildConfigurations = ( 622 | BE76C71618A19369003C94A0 /* Debug */, 623 | BE76C71718A19369003C94A0 /* Release */, 624 | ); 625 | defaultConfigurationIsVisible = 0; 626 | defaultConfigurationName = Release; 627 | }; 628 | BE8C6BE21A9AE29F00E3558C /* Build configuration list for PBXNativeTarget "rootTest" */ = { 629 | isa = XCConfigurationList; 630 | buildConfigurations = ( 631 | BE8C6BE01A9AE29F00E3558C /* Debug */, 632 | BE8C6BE11A9AE29F00E3558C /* Release */, 633 | ); 634 | defaultConfigurationIsVisible = 0; 635 | defaultConfigurationName = Release; 636 | }; 637 | BEFC28CC19F0D79D00C35A6F /* Build configuration list for PBXNativeTarget "AHLaunchCtl Tests" */ = { 638 | isa = XCConfigurationList; 639 | buildConfigurations = ( 640 | BEFC28CD19F0D79D00C35A6F /* Debug */, 641 | BEFC28CE19F0D79D00C35A6F /* Release */, 642 | ); 643 | defaultConfigurationIsVisible = 0; 644 | defaultConfigurationName = Release; 645 | }; 646 | /* End XCConfigurationList section */ 647 | }; 648 | rootObject = BE76C6E918A19369003C94A0 /* Project object */; 649 | } 650 | -------------------------------------------------------------------------------- /AHLaunchCtl.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /AHLaunchCtl.xcodeproj/project.xcworkspace/xcshareddata/AHLaunchCtl.xccheckout: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDESourceControlProjectFavoriteDictionaryKey 6 | 7 | IDESourceControlProjectIdentifier 8 | F6C5EDF5-2B75-469F-A897-7E8EF35FAAD7 9 | IDESourceControlProjectName 10 | AHLaunchCtl 11 | IDESourceControlProjectOriginsDictionary 12 | 13 | 32DCFAAC2A3841429E93FB3695A9B4C5B7E9CEB6 14 | github.com:eahrold/AHLaunchCtl.git 15 | 16 | IDESourceControlProjectPath 17 | AHLaunchCtl.xcodeproj 18 | IDESourceControlProjectRelativeInstallPathDictionary 19 | 20 | 32DCFAAC2A3841429E93FB3695A9B4C5B7E9CEB6 21 | ../.. 22 | 23 | IDESourceControlProjectURL 24 | github.com:eahrold/AHLaunchCtl.git 25 | IDESourceControlProjectVersion 26 | 111 27 | IDESourceControlProjectWCCIdentifier 28 | 32DCFAAC2A3841429E93FB3695A9B4C5B7E9CEB6 29 | IDESourceControlProjectWCConfigurations 30 | 31 | 32 | IDESourceControlRepositoryExtensionIdentifierKey 33 | public.vcs.git 34 | IDESourceControlWCCIdentifierKey 35 | 32DCFAAC2A3841429E93FB3695A9B4C5B7E9CEB6 36 | IDESourceControlWCCName 37 | AHLaunchCtl 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /AHLaunchCtl.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /AHLaunchCtl.xcodeproj/xcshareddata/xcschemes/AHLaunchCtl.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 42 | 43 | 49 | 50 | 51 | 52 | 53 | 54 | 60 | 61 | 63 | 64 | 67 | 68 | 69 | -------------------------------------------------------------------------------- /AHLaunchCtl/AHAuthorizer.h: -------------------------------------------------------------------------------- 1 | // AHAuthorizer.h 2 | // Copyright (c) 2014 Eldon Ahrold ( https://github.com/eahrold/AHLaunchCtl ) 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | 22 | #import 23 | 24 | /** 25 | * Authorization Class for AHLaunchCtl jobs where authorization is required. 26 | */ 27 | @interface AHAuthorizer : NSObject 28 | 29 | /** 30 | * Prompt for authorization to the System Daemon 31 | * 32 | * @param label Unique name of the job getting authorized. Should be similar to 33 | *a reverse domain. 34 | * @param prompt String used during the authorization dialog. 35 | * 36 | * @return AuthorizationRef 37 | */ 38 | + (OSStatus)authorizeSystemDaemonWithLabel:(NSString *)label 39 | prompt:(NSString *)prompt 40 | authRef:(AuthorizationRef *)authRef; 41 | 42 | /** 43 | * Prompt for authorization to the Service Management system 44 | * 45 | * @param prompt string to include in the prompt dialog 46 | * 47 | * @return AuthorizationRef 48 | */ 49 | + (OSStatus)authorizeSMJobBlessWithPrompt:(NSString *)prompt 50 | authRef:(AuthorizationRef *)authRef; 51 | 52 | /** 53 | * Free the AuthorizationRef object 54 | * 55 | * @param authRef the AuthorizationRef to be freed 56 | */ 57 | + (void)authorizationFree:(AuthorizationRef)authRef; 58 | 59 | @end 60 | -------------------------------------------------------------------------------- /AHLaunchCtl/AHAuthorizer.m: -------------------------------------------------------------------------------- 1 | // AHAuthorizer.m 2 | // Copyright (c) 2014 Eldon Ahrold ( https://github.com/eahrold/AHLaunchCtl ) 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | 22 | #import "AHAuthorizer.h" 23 | 24 | static NSString *kNSAuthorizationJobBless = 25 | @"com.apple.ServiceManagement.blesshelper"; 26 | static NSString *kNSAuthorizationSystemDaemon = 27 | @"com.apple.ServiceManagement.daemons.modify"; 28 | 29 | @implementation AHAuthorizer 30 | 31 | + (AuthorizationFlags)defaultFlags { 32 | static dispatch_once_t onceToken; 33 | static AuthorizationFlags authFlags; 34 | dispatch_once(&onceToken, ^{ 35 | authFlags = 36 | kAuthorizationFlagInteractionAllowed | 37 | kAuthorizationFlagPreAuthorize | 38 | kAuthorizationFlagExtendRights; 39 | }); 40 | return authFlags; 41 | } 42 | 43 | + (OSStatus)authorizeSystemDaemonWithLabel:(NSString *)label 44 | prompt:(NSString *)prompt 45 | authRef:(AuthorizationRef *)authRef { 46 | AuthorizationItem authItem = { 47 | kNSAuthorizationSystemDaemon.UTF8String, 0, NULL, 0}; 48 | 49 | if (!prompt || !prompt.length) { 50 | prompt = [NSString 51 | stringWithFormat:@"%@ is trying to perform a privileged operation.", 52 | [[NSProcessInfo processInfo] processName]]; 53 | } 54 | 55 | //Get userID 56 | uid_t uid = getuid(); 57 | if (uid != 0) { 58 | // Only setup custom auth name if not running as root. 59 | authItem.name = label.UTF8String; 60 | } 61 | 62 | return [self authorizePrompt:prompt authItems:authItem authRef:authRef]; 63 | } 64 | 65 | + (OSStatus)authorizeSMJobBlessWithPrompt:(NSString *)prompt 66 | authRef:(AuthorizationRef *)authRef { 67 | AuthorizationItem authItem = { 68 | kNSAuthorizationJobBless.UTF8String, 0, NULL, 0}; 69 | return [self authorizePrompt:prompt authItems:authItem authRef:authRef]; 70 | }; 71 | 72 | + (OSStatus)authorizePrompt:(NSString *)prompt 73 | authItems:(AuthorizationItem)authItem 74 | authRef:(AuthorizationRef *)authRef { 75 | AuthorizationRights authRights = {1, &authItem}; 76 | AuthorizationEnvironment environment = {0, NULL}; 77 | AuthorizationFlags flags = [[self class] defaultFlags]; 78 | 79 | if(prompt.length){ 80 | AuthorizationItem envItem = {kAuthorizationEnvironmentPrompt, 81 | prompt.length, 82 | (void *)prompt.UTF8String, 83 | 0}; 84 | environment.count = 1; 85 | environment.items = &envItem; 86 | 87 | // NOTE: envItems gets optimized out outside of the 88 | // if statement so just do the return right here 89 | return AuthorizationCreate( 90 | &authRights, &environment, flags, authRef); 91 | } 92 | 93 | return AuthorizationCreate( 94 | &authRights, &environment, flags, authRef);; 95 | } 96 | 97 | 98 | + (void)authorizationFree:(AuthorizationRef)authRef { 99 | if (authRef != NULL) { 100 | OSStatus junk = 101 | AuthorizationFree(authRef, kAuthorizationFlagDestroyRights); 102 | assert(junk == errAuthorizationSuccess); 103 | } 104 | } 105 | 106 | @end 107 | -------------------------------------------------------------------------------- /AHLaunchCtl/AHLaunchCtl-Prefix.pch: -------------------------------------------------------------------------------- 1 | // 2 | // Prefix header 3 | // 4 | // The contents of this file are implicitly included at the beginning of every source file. 5 | // 6 | 7 | #ifdef __OBJC__ 8 | #import 9 | #endif 10 | -------------------------------------------------------------------------------- /AHLaunchCtl/AHLaunchCtl.h: -------------------------------------------------------------------------------- 1 | // AHLaunchCtl.h 2 | // 3 | // Copyright (c) 2014 Eldon Ahrold ( https://github.com/eahrold/AHLaunchCtl ) 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in 13 | // all copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | // THE SOFTWARE. 22 | 23 | #import 24 | #import 25 | 26 | /** 27 | * AHLaunchCtlErrorCodes 28 | */ 29 | typedef NS_ENUM(NSInteger, AHLaunchCtlErrorCodes) { 30 | /** 31 | * No Error 32 | */ 33 | kAHErrorSuccess, 34 | /** 35 | * Error encountered when job label that is not in dot syntax or has spaces 36 | */ 37 | kAHErrorJobLabelNotValid, 38 | /** 39 | * job requires Label and Program Arguments 40 | */ 41 | kAHErrorJobMissingRequiredKeys, 42 | /** 43 | * Error encountered when job is not Loaded 44 | */ 45 | kAHErrorJobNotLoaded, 46 | /** 47 | * Error encountered when job already exists 48 | */ 49 | kAHErrorJobAlreadyExists, 50 | /** 51 | * Error Encountered when job already loaded 52 | */ 53 | kAHErrorJobAlreadyLoaded, 54 | /** 55 | * Error Encountered when trying to load a job 56 | */ 57 | kAHErrorCouldNotLoadJob, 58 | /** 59 | * Error Encountered when trying to load a helper tool 60 | */ 61 | kAHErrorCouldNotLoadHelperTool, 62 | /** 63 | * Error Encountered when trying a helper tool could not be removed 64 | */ 65 | kAHErrorCouldNotUnloadHelperTool, 66 | /** 67 | * Error Encountered when trying a helper tool could not be removed 68 | */ 69 | kAHErrorHelperToolNotLoaded, 70 | /** 71 | * Error Encountered when files associated with helper tool could not be 72 | * removed 73 | */ 74 | kAHErrorCouldNotRemoveHelperToolFiles, 75 | /** 76 | * Error Encountered when a job could not be unloaded 77 | */ 78 | kAHErrorCouldNotUnloadJob, 79 | /** 80 | * Error Encountered when a job could not be reloaded 81 | */ 82 | kAHErrorJobCouldNotReload, 83 | /** 84 | * Error Encountered when the launchd.plist file could not be located 85 | */ 86 | kAHErrorFileNotFound, 87 | /** 88 | * Error Encountered when the launchd.plist is actually a directory not a file 89 | */ 90 | kAHErrorFileIsDirectory, 91 | /** 92 | * Error Encountered when the launchd.plist file could not be written or 93 | * insufficient privileges 94 | */ 95 | kAHErrorCouldNotWriteFile, 96 | /** 97 | * Error Encountered when more than one job with the same label exist 98 | */ 99 | kAHErrorMultipleJobsMatching, 100 | /** 101 | * Error Encountered when user is not privileged to install into domain 102 | */ 103 | kAHErrorInsufficientPrivileges, 104 | /** 105 | * Error Encountered when a user is trying to unload another's launch job 106 | */ 107 | kAHErrorExecutingAsIncorrectUser, 108 | /** 109 | * Error Encountered when the program to be loaded is not executable 110 | */ 111 | kAHErrorProgramNotExecutable, 112 | /** 113 | * User Canceled authorization. 114 | */ 115 | kAHErrorUserCanceledAuthorization = errAuthorizationCanceled, 116 | }; 117 | 118 | /** 119 | * Objective-C Framework For LaunchAgents and LaunchDaemons 120 | */ 121 | @interface AHLaunchCtl : NSObject 122 | 123 | /** 124 | * Singleton Object to handle AHLaunchJobs 125 | * 126 | * @return Shared Controller 127 | */ 128 | + (AHLaunchCtl *)sharedController; 129 | #pragma mark - Public Methods 130 | /** 131 | * Create session wide authorization linked to the controller. 132 | * 133 | * @param prompt String to display for the Authorization creation dialog. 134 | * 135 | * @return Returns `YES` on success, or `NO` on failure. 136 | */ 137 | - (BOOL) authorizeWithPrompt:(NSString *)prompt; 138 | - (BOOL) authorize; 139 | 140 | /** 141 | * Deauthorize the session. 142 | * @note This is called when the controller is deallocated. 143 | */ 144 | - (void)deauthorize; 145 | 146 | /** 147 | * Write the launchd.plist and load the job into context 148 | * 149 | * @param job AHLaunchJob with desired keys. 150 | * @param domain Corresponding AHLaunchDomain. 151 | * @param error Populated should an error occur. 152 | * 153 | * @return Returns `YES` on success, or `NO` on failure. 154 | */ 155 | - (BOOL)add:(AHLaunchJob *)job 156 | toDomain:(AHLaunchDomain)domain 157 | error:(NSError **)error; 158 | 159 | /** 160 | * Remove launchd.plist and unload the job 161 | * 162 | * @param label Name of the running launchctl job. 163 | * @param domain Corresponding AHLaunchDomain 164 | * @param error Populated should an error occur. 165 | * 166 | * @return Returns `YES` on success, or `NO` on failure. 167 | */ 168 | - (BOOL)remove:(NSString *)label 169 | fromDomain:(AHLaunchDomain)domain 170 | error:(NSError **)error; 171 | 172 | /** 173 | * Loads launchd job (will not write file) 174 | * 175 | * @param job AHLaunchJob Object, Label and Program keys required. 176 | * @param domain Corresponding AHLaunchDomain 177 | * @param error Populated should an error occur. 178 | * 179 | * @return Returns `YES` on success, or `NO` on failure. 180 | */ 181 | - (BOOL)load:(AHLaunchJob *)job 182 | inDomain:(AHLaunchDomain)domain 183 | error:(NSError **)error; 184 | 185 | /** 186 | * Unloads a launchd job (will not remove file) 187 | * 188 | * @param label Name of the running launchctl job. 189 | * @param domain Corresponding AHLaunchDomain 190 | * @param error Populated should an error occur. 191 | * 192 | * @return Returns `YES` on success, or `NO` on failure. 193 | */ 194 | - (BOOL)unload:(NSString *)label 195 | inDomain:(AHLaunchDomain)domain 196 | error:(NSError **)error; 197 | 198 | /** 199 | * Loads and existing launchd.plist 200 | * 201 | * @param label Name of the launchctl file. 202 | * @param domain Corresponding AHLaunchDomain 203 | * @param error Populated should an error occur. 204 | * 205 | * @return Returns `YES` on success, or `NO` on failure. 206 | */ 207 | - (BOOL)start:(NSString *)label 208 | inDomain:(AHLaunchDomain)domain 209 | error:(NSError **)error; 210 | 211 | /** 212 | * Stops a running launchd job, synonomus with unload 213 | * 214 | * @param label Name of the running launchctl job. 215 | * @param error Populated should an error occur. 216 | * @param domain Corresponding AHLaunchDomain 217 | * 218 | * @return Returns `YES` on success, or `NO` on failure. 219 | */ 220 | - (BOOL)stop:(NSString *)label 221 | inDomain:(AHLaunchDomain)domain 222 | error:(NSError **)error; 223 | 224 | /** 225 | * Restarts a launchd job with an existsing launchd.plist file. 226 | * 227 | * @param label Name of the running launchctl job. 228 | * @param domain Corresponding AHLaunchDomain 229 | * @param error Populated should an error occur. 230 | * 231 | * @return Returns `YES` on success, or `NO` on failure. 232 | */ 233 | - (BOOL)restart:(NSString *)label 234 | inDomain:(AHLaunchDomain)domain 235 | error:(NSError **)error; 236 | 237 | #pragma mark - Class Methods 238 | /** 239 | * Launch an application at login. 240 | * 241 | * @param app Path to the Application 242 | * @param launch YES to launch, NO to stop launching 243 | * @param global YES to launch for all users, NO to launch for current user. 244 | * @param keepAlive YES to relaunch in the event of a crash or an attempt to quit 245 | * @param error Populated should an error occur. 246 | * 247 | * @return Returns `YES` on success, or `NO` on failure. 248 | */ 249 | + (BOOL)launchAtLogin:(NSString *)app 250 | launch:(BOOL)launch 251 | global:(BOOL)global 252 | keepAlive:(BOOL)keepAlive 253 | error:(NSError **)error; 254 | 255 | /** 256 | * Schedule a LaunchD Job to run at an interval. 257 | * 258 | * @param label uniquely identifier for launchd. This should be in the form of a reverse domain 259 | * @param program Path to the executable to run 260 | * @param interval How often (in seconds) to run. 261 | * @param domain Corresponding AHLaunchDomain 262 | * @param reply Reply block executed on completion that has no return value and 263 | *takes on argument NSError. 264 | */ 265 | + (void)scheduleJob:(NSString *)label 266 | program:(NSString *)program 267 | interval:(int)interval 268 | domain:(AHLaunchDomain)domain 269 | reply:(void (^)(NSError *error))reply; 270 | /** 271 | * Schedule a LaunchD Job to run at an interval. 272 | * 273 | * @param label uniquely identifier for launchd. This should be in the form of a reverse domain 274 | * @param program Path to the executable to run 275 | * @param programArguments Array of arguments to pass to the executable. 276 | * @param interval How often (in seconds) to run. 277 | * @param domain Corresponding AHLaunchDomain 278 | * @param reply Reply block executed on completion that has no return value and 279 | *takes on argument NSError. 280 | */ 281 | + (void)scheduleJob:(NSString *)label 282 | program:(NSString *)program 283 | programArguments:(NSArray *)programArguments 284 | interval:(int)interval 285 | domain:(AHLaunchDomain)domain 286 | reply:(void (^)(NSError *error))reply; 287 | 288 | /** 289 | * Create a job object based on a launchd.plist file 290 | * 291 | * @param label uniquely identifier for launchd. This should be in the form of a reverse domain 292 | * @param domain Corresponding AHLaunchDomain 293 | * 294 | * @return an allocated AHLaunchJob with the corresponding keys 295 | */ 296 | + (AHLaunchJob *)jobFromFileNamed:(NSString *)label 297 | inDomain:(AHLaunchDomain)domain; 298 | 299 | /** 300 | * Create a job object based on currently running Launchd Job 301 | * 302 | * @param label uniquely identifier for launchd. This should be in the form of a reverse domain 303 | * @param domain Corresponding AHLaunchDomain 304 | * 305 | * @return an allocated AHLaunchJob with the corresponding keys 306 | */ 307 | + (AHLaunchJob *)runningJobWithLabel:(NSString *)label 308 | inDomain:(AHLaunchDomain)domain; 309 | 310 | /** 311 | * List with all Jobs available based of files in the specified domain 312 | * 313 | * @param domain Corresponding AHLaunchDomain 314 | * 315 | * @return Array of allocated AHLaunchJob with the corresponding keys 316 | */ 317 | + (NSArray *)allJobsFromFilesInDomain:(AHLaunchDomain)domain; 318 | 319 | /** 320 | * List with all currently running jobs in the specified domain 321 | * 322 | * @param domain Corresponding AHLaunchDomain 323 | * 324 | * @return Array of allocated AHLaunchJob with the corresponding keys 325 | */ 326 | + (NSArray *)allRunningJobsInDomain:(AHLaunchDomain)domain; 327 | 328 | /** 329 | * List of running Jobs based on criteria 330 | * 331 | * @param match string to match. 332 | * @param domain AHLaunchDomain 333 | * 334 | * @return Array of allocated AHLaunchJob with the corresponding keys 335 | */ 336 | + (NSArray *)runningJobsMatching:(NSString *)match 337 | inDomain:(AHLaunchDomain)domain; 338 | 339 | /** 340 | * Installs a privileged helper tool with the specified label using a custom prompt. 341 | * 342 | * @param label label of the Helper Tool 343 | * @param prompt message to prefix the authorization prompt 344 | * @param error populated should error occur 345 | * 346 | * @return YES for success NO on failure; 347 | * @warning Must be code singed properly, and have an embedded Info.plist and 348 | *Launchd.plist, and located in the applications 349 | *MainBundle/Library/LaunchServices 350 | */ 351 | + (BOOL)installHelper:(NSString *)label 352 | prompt:(NSString *)prompt 353 | error:(NSError **)error; 354 | 355 | /** 356 | * Uninstalls HelperTool with specified label using a custom prompt. 357 | * 358 | * @param label Label of the Helper Tool. 359 | * @param prompt Message to prefix the authorization prompt. 360 | * @param error Error object populated if an error occurs. 361 | * @return YES for success NO on failure; 362 | */ 363 | + (BOOL)uninstallHelper:(NSString *)label 364 | prompt:(NSString *)prompt 365 | error:(NSError *__autoreleasing *)error; 366 | /** 367 | * Uninstalls HelperTool with specified label. 368 | * 369 | * @param label label of the Helper Tool 370 | * @param error error object populated if an error occurs. 371 | * 372 | * @return YES for success NO on failure; 373 | */ 374 | + (BOOL)uninstallHelper:(NSString *)label 375 | error:(NSError *__autoreleasing *)error 376 | __attribute__((deprecated)); 377 | 378 | /** 379 | * Cleans up files associated with the helper tool that SMJobBless leaves behind 380 | * 381 | * @param label label of the Helper Tool 382 | * @param error error object populated if an error occurs. 383 | * 384 | * @return YES for success NO on failure; 385 | */ 386 | + (BOOL)removeFilesForHelperWithLabel:(NSString *)label 387 | error:(NSError *__autoreleasing *)error; 388 | 389 | #pragma mark - Domain Error 390 | /** 391 | * Convenience Method for populating an NSError using message and code. It 392 | *also can be used to provide a return value for escaping another method. e.g. 393 | *on failure of a previous condition you could do "return [AHLaunchCtl 394 | *errorWithMessage:@"your message" andCode:1 error:error]" and you'll get 395 | *escaped out, if method return you're using on has BOOL return and error is 396 | *already an __autoreleasing error pointer 397 | * 398 | * @param message Human readable error message 399 | * @param code error Code 400 | * @param error error pointer 401 | * 402 | * @return YES if error code passed is 0, NO on all other error codes passed 403 | *into; 404 | */ 405 | + (BOOL)errorWithMessage:(NSString *)message 406 | andCode:(NSInteger)code 407 | error:(NSError **)error; 408 | 409 | @end 410 | -------------------------------------------------------------------------------- /AHLaunchCtl/AHLaunchCtl.m: -------------------------------------------------------------------------------- 1 | // AHLaunchCtl.m 2 | // Copyright (c) 2014 Eldon Ahrold ( https://github.com/eahrold/AHLaunchCtl ) 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | 22 | #import 23 | #import 24 | #import 25 | #import "AHServiceManagement_Private.h" 26 | 27 | #import 28 | 29 | #import 30 | 31 | static NSString *const kAHAuthorizationLoadJobPrompt = 32 | @"Loading the job requires authorization."; 33 | static NSString *const kAHAuthorizationUnloadJobPrompt = 34 | @"Unloading the job requires authorization."; 35 | static NSString *const kAHAuthorizationReloadJobPrompt = 36 | @"Reloading the job requires authorization."; 37 | static NSString *const kAHAuthorizationSessionPrompt = 38 | @"Performing administrative tasks requires authorization."; 39 | 40 | static NSString *const kAHSessionAuthorizationKey = 41 | @"com.eeaapps.ahlaunchctl.controller.session.authorization"; 42 | 43 | static NSString *const kAHLaunchCtlDomain = @"com.eeaapps.ahlaunchctl"; 44 | 45 | static NSString *errorMsgFromCode(NSInteger code); 46 | 47 | @interface AHLaunchJob () 48 | @property (nonatomic, readwrite) AHLaunchDomain domain; // 49 | @end 50 | 51 | #pragma mark - Launch Controller 52 | @implementation AHLaunchCtl { 53 | AuthorizationRef _authRef; 54 | } 55 | 56 | + (AHLaunchCtl *)sharedController { 57 | static dispatch_once_t onceToken; 58 | static AHLaunchCtl *shared; 59 | dispatch_once(&onceToken, ^{ 60 | shared = [AHLaunchCtl new]; 61 | }); 62 | return shared; 63 | } 64 | 65 | - (void)dealloc { 66 | [self deauthorize]; 67 | } 68 | 69 | #pragma mark - Session authorization. 70 | - (BOOL)authorizeWithPrompt:(NSString *)prompt { 71 | OSStatus status = errAuthorizationSuccess; 72 | if (_authRef == NULL) { 73 | if ([self isEqual:[[self class] sharedController]]) { 74 | status = errAuthorizationDenied; 75 | NSLog(@"You cannot create session authorization for the shared " 76 | @"controller."); 77 | } else { 78 | status = [AHAuthorizer 79 | authorizeSystemDaemonWithLabel:kAHSessionAuthorizationKey 80 | prompt:kAHAuthorizationSessionPrompt 81 | authRef:&_authRef]; 82 | } 83 | } 84 | 85 | return (status == errAuthorizationSuccess); 86 | } 87 | - (BOOL)authorize { 88 | return [self authorizeWithPrompt:kAHAuthorizationSessionPrompt]; 89 | } 90 | 91 | - (void)deauthorize { 92 | [AHAuthorizer authorizationFree:_authRef]; 93 | _authRef = NULL; 94 | } 95 | 96 | #pragma mark--- Add/Remove --- 97 | - (BOOL)add:(AHLaunchJob *)job 98 | toDomain:(AHLaunchDomain)domain 99 | error:(NSError *__autoreleasing *)error { 100 | if (![self jobIsValid:job error:error]) { 101 | return NO; 102 | } 103 | 104 | BOOL success = NO; 105 | AuthorizationRef authRef = NULL; 106 | 107 | if (domain > kAHUserLaunchAgent) { 108 | // Get userID 109 | success = [self createAuthorization:&authRef 110 | domain:domain 111 | label:job.Label 112 | prompt:kAHAuthorizationLoadJobPrompt 113 | error:error]; 114 | if (success) { 115 | success = AHCreatePrivilegedLaunchdPlist(domain, job.dictionary, 116 | authRef, error); 117 | } 118 | } else { 119 | if ((success = 120 | [job.dictionary writeToFile:launchdJobFile(job.Label, domain) 121 | atomically:YES]) == NO) { 122 | [[self class] errorWithCode:kAHErrorCouldNotWriteFile error:error]; 123 | } 124 | } 125 | 126 | if (success) { 127 | success = [self load:job inDomain:domain authRef:authRef error:error]; 128 | } 129 | 130 | if (_authRef == NULL) { 131 | [AHAuthorizer authorizationFree:authRef]; 132 | } 133 | authRef = NULL; 134 | 135 | return success; 136 | } 137 | 138 | - (BOOL)remove:(NSString *)label 139 | fromDomain:(AHLaunchDomain)domain 140 | error:(NSError *__autoreleasing *)error { 141 | BOOL success = NO; 142 | AuthorizationRef authRef = NULL; 143 | 144 | if (domain > kAHUserLaunchAgent) { 145 | // Get userID 146 | success = [self createAuthorization:&authRef 147 | domain:domain 148 | label:label 149 | prompt:kAHAuthorizationUnloadJobPrompt 150 | error:error]; 151 | if (success) { 152 | success = AHJobRemoveIncludingFile(domain, label, authRef, error); 153 | } 154 | } else { 155 | if ((success = AHJobRemove(domain, label, authRef, error))) { 156 | success = [[NSFileManager defaultManager] 157 | removeItemAtPath:launchdJobFile(label, domain) 158 | error:error]; 159 | } 160 | } 161 | 162 | if (_authRef == NULL) { 163 | [AHAuthorizer authorizationFree:authRef]; 164 | } 165 | authRef = NULL; 166 | return success; 167 | } 168 | 169 | #pragma mark--- Load / Unload Jobs --- 170 | - (BOOL)load:(AHLaunchJob *)job 171 | inDomain:(AHLaunchDomain)domain 172 | authRef:(AuthorizationRef)authRef 173 | error:(NSError *__autoreleasing *)error { 174 | BOOL success; 175 | NSString *consoleUser; 176 | 177 | if (domain <= kAHSystemLaunchAgent) { 178 | // If this is a launch agent and no user is logged in no reason to load; 179 | consoleUser = 180 | CFBridgingRelease(SCDynamicStoreCopyConsoleUser(NULL, NULL, NULL)); 181 | if (!consoleUser || [consoleUser isEqualToString:@"loginwindow"]) { 182 | #if DEBUG 183 | NSLog(@"No User Logged in"); 184 | #endif 185 | return YES; 186 | } 187 | } 188 | 189 | success = AHJobSubmit(domain, job.dictionary, authRef, error); 190 | 191 | if (success) { 192 | job.domain = domain; 193 | } 194 | 195 | return success; 196 | } 197 | 198 | - (BOOL)load:(AHLaunchJob *)job 199 | inDomain:(AHLaunchDomain)domain 200 | error:(NSError *__autoreleasing *)error { 201 | BOOL success = YES; 202 | AuthorizationRef authRef = NULL; 203 | 204 | success = [self createAuthorization:&authRef 205 | domain:domain 206 | label:job.Label 207 | prompt:kAHAuthorizationLoadJobPrompt 208 | error:error]; 209 | 210 | if (success) { 211 | success = [self load:job inDomain:domain authRef:authRef error:error]; 212 | } 213 | 214 | // Clean Up 215 | if (_authRef == NULL) { 216 | [AHAuthorizer authorizationFree:authRef]; 217 | } 218 | authRef = NULL; 219 | 220 | return success; 221 | } 222 | 223 | - (BOOL)unload:(NSString *)label 224 | inDomain:(AHLaunchDomain)domain 225 | authRef:(AuthorizationRef)authRef 226 | error:(NSError *__autoreleasing *)error { 227 | BOOL success = YES; 228 | 229 | if (!jobIsRunning(label, domain)) { 230 | return [[self class] errorWithCode:kAHErrorJobNotLoaded error:error]; 231 | } 232 | 233 | // If this is a launch agent and no user is logged in no reason to unload; 234 | if (domain <= kAHSystemLaunchAgent) { 235 | NSString *result = 236 | CFBridgingRelease(SCDynamicStoreCopyConsoleUser(NULL, NULL, NULL)); 237 | if ([result isEqualToString:@"loginwindow"] || !result) { 238 | NSLog(@"No User Logged in"); 239 | return YES; 240 | } 241 | } 242 | 243 | success = AHJobRemove(domain, label, authRef, error); 244 | return success; 245 | } 246 | 247 | - (BOOL)unload:(NSString *)label 248 | inDomain:(AHLaunchDomain)domain 249 | error:(NSError *__autoreleasing *)error { 250 | AuthorizationRef authRef = NULL; 251 | BOOL success = YES; 252 | 253 | success = [self createAuthorization:&authRef 254 | domain:domain 255 | label:label 256 | prompt:kAHAuthorizationUnloadJobPrompt 257 | error:error]; 258 | 259 | if (success) { 260 | success = 261 | [self unload:label inDomain:domain authRef:authRef error:error]; 262 | } 263 | 264 | // Clean Up 265 | if (_authRef == NULL) { 266 | [AHAuthorizer authorizationFree:authRef]; 267 | } 268 | authRef = NULL; 269 | 270 | return success; 271 | } 272 | 273 | - (BOOL)reload:(AHLaunchJob *)job 274 | inDomain:(AHLaunchDomain)domain 275 | error:(NSError *__autoreleasing *)error { 276 | AuthorizationRef authRef = NULL; 277 | BOOL success = YES; 278 | 279 | // Get userID 280 | success = [self createAuthorization:&authRef 281 | domain:domain 282 | label:job.Label 283 | prompt:kAHAuthorizationReloadJobPrompt 284 | error:error]; 285 | 286 | if (jobIsRunning2(job.Label, domain) && success) { 287 | if (![self unload:job.Label 288 | inDomain:domain 289 | authRef:authRef 290 | error:error]) { 291 | success = [[self class] errorWithCode:kAHErrorJobCouldNotReload 292 | error:error]; 293 | } 294 | } 295 | 296 | if (success) { 297 | success = [self load:job inDomain:domain authRef:authRef error:error]; 298 | } 299 | 300 | // Clean Up 301 | if (_authRef == NULL) { 302 | [AHAuthorizer authorizationFree:authRef]; 303 | } 304 | authRef = NULL; 305 | 306 | return success; 307 | } 308 | 309 | #pragma mark--- Start / Stop / Restart --- 310 | - (BOOL)start:(NSString *)label 311 | inDomain:(AHLaunchDomain)domain 312 | error:(NSError *__autoreleasing *)error { 313 | if (jobIsRunning(label, domain)) { 314 | return 315 | [[self class] errorWithCode:kAHErrorJobAlreadyLoaded error:error]; 316 | } 317 | 318 | AHLaunchJob *job = [[self class] jobFromFileNamed:label inDomain:domain]; 319 | if (job) { 320 | return [self load:job inDomain:domain error:error]; 321 | } else { 322 | return [[self class] errorWithCode:kAHErrorFileNotFound error:error]; 323 | } 324 | } 325 | 326 | - (BOOL)stop:(NSString *)label 327 | inDomain:(AHLaunchDomain)domain 328 | error:(NSError *__autoreleasing *)error { 329 | return [self unload:label inDomain:domain error:error]; 330 | } 331 | 332 | - (BOOL)restart:(NSString *)label 333 | inDomain:(AHLaunchDomain)domain 334 | error:(NSError *__autoreleasing *)error { 335 | AHLaunchJob *job = [[self class] runningJobWithLabel:label inDomain:domain]; 336 | if (!job) { 337 | return [[self class] errorWithCode:kAHErrorJobNotLoaded error:error]; 338 | } 339 | return [self reload:job inDomain:domain error:error]; 340 | } 341 | 342 | #pragma mark - Helper Tool Installation / Removal 343 | + (BOOL)installHelper:(NSString *)label 344 | prompt:(NSString *)prompt 345 | error:(NSError *__autoreleasing *)error { 346 | NSString *currentVersion; 347 | NSString *availableVersion; 348 | 349 | AHLaunchJob *job = 350 | [[self class] runningJobWithLabel:label inDomain:kAHGlobalLaunchDaemon]; 351 | 352 | if (job) { 353 | currentVersion = job.executableVersion; 354 | 355 | NSString *xpcToolPath = [@"Contents/Library/LaunchServices" 356 | stringByAppendingPathComponent:label]; 357 | NSURL *appBundleURL = [[NSBundle mainBundle] bundleURL]; 358 | 359 | NSURL *helperTool = 360 | [appBundleURL URLByAppendingPathComponent:xpcToolPath]; 361 | NSDictionary *helperPlist = CFBridgingRelease( 362 | CFBundleCopyInfoDictionaryForURL((__bridge CFURLRef)(helperTool))); 363 | 364 | availableVersion = helperPlist[@"CFBundleVersion"]; 365 | 366 | if ([availableVersion ah_version_isLessThanOrEqualTo:currentVersion]) { 367 | return YES; 368 | } 369 | } 370 | 371 | BOOL success = YES; 372 | 373 | AuthorizationRef authRef = NULL; 374 | OSStatus status = 375 | [AHAuthorizer authorizeSMJobBlessWithPrompt:prompt authRef:&authRef]; 376 | 377 | if (status == errAuthorizationCanceled) { 378 | return [[self class] errorWithCode:kAHErrorUserCanceledAuthorization 379 | error:error]; 380 | } else if (authRef == NULL) { 381 | return [[self class] errorWithCode:kAHErrorInsufficientPrivileges 382 | error:error]; 383 | } else { 384 | // If the job is running un-bless it in order to re-bless it. 385 | // This addresses a condition where when replaced, the binary has an 386 | // inconsistent code signature and causes paging failures. 387 | if (jobIsRunning(label, kAHGlobalLaunchDaemon)) { 388 | AHJobUnbless(kAHGlobalLaunchDaemon, label, authRef, nil); 389 | } 390 | 391 | // Run the job bless. 392 | if (!AHJobBless(kAHGlobalLaunchDaemon, label, authRef, error)) { 393 | success = [[self class] errorWithCode:kAHErrorCouldNotLoadHelperTool 394 | error:error]; 395 | } 396 | } 397 | 398 | [AHAuthorizer authorizationFree:authRef]; 399 | return success; 400 | } 401 | 402 | + (BOOL)uninstallHelper:(NSString *)label 403 | prompt:(NSString *)prompt 404 | error:(NSError *__autoreleasing *)error { 405 | BOOL success = YES; 406 | 407 | if (jobIsRunning(label, kAHGlobalLaunchDaemon)) { 408 | AuthorizationRef authRef = NULL; 409 | OSStatus status = 410 | [AHAuthorizer authorizeSystemDaemonWithLabel:label 411 | prompt:prompt 412 | authRef:&authRef]; 413 | 414 | if (status == errAuthorizationCanceled) { 415 | success = 416 | [[self class] errorWithCode:kAHErrorUserCanceledAuthorization 417 | error:error]; 418 | } else if (authRef == NULL) { 419 | return [[self class] errorWithCode:kAHErrorInsufficientPrivileges 420 | error:error]; 421 | } else { 422 | if (!AHJobUnbless(kAHGlobalLaunchDaemon, label, authRef, error)) { 423 | success = 424 | [[self class] errorWithCode:kAHErrorCouldNotUnloadHelperTool 425 | error:error]; 426 | } 427 | } 428 | [AHAuthorizer authorizationFree:authRef]; 429 | } else { 430 | success = [self errorWithCode:kAHErrorHelperToolNotLoaded error:error]; 431 | } 432 | return success; 433 | } 434 | 435 | + (BOOL)uninstallHelper:(NSString *)label 436 | error:(NSError *__autoreleasing *)error { 437 | NSString *defaultPrompt = [NSString 438 | stringWithFormat: 439 | @"%@ is trying to uninstall a the helper tool and it's components.", 440 | [[NSProcessInfo processInfo] processName]]; 441 | return [self uninstallHelper:label prompt:defaultPrompt error:error]; 442 | } 443 | 444 | + (BOOL)removeFilesForHelperWithLabel:(NSString *)label 445 | error:(NSError *__autoreleasing *)error { 446 | // This is depreciated and now happens during AHJobUnbless() 447 | return YES; 448 | } 449 | 450 | #pragma mark - Convenience Accessors 451 | + (BOOL)launchAtLogin:(NSString *)app 452 | launch:(BOOL)launch 453 | global:(BOOL)global 454 | keepAlive:(BOOL)keepAlive 455 | error:(NSError *__autoreleasing *)error { 456 | NSBundle *appBundle = [NSBundle bundleWithPath:app]; 457 | NSString *appIdentifier = 458 | [appBundle.bundleIdentifier stringByAppendingPathExtension:@"launcher"]; 459 | 460 | AHLaunchDomain domain = global ? kAHGlobalLaunchAgent : kAHUserLaunchAgent; 461 | 462 | if (launch) { 463 | AHLaunchJob *job = [AHLaunchJob new]; 464 | job.Label = appIdentifier; 465 | job.Program = appBundle.executablePath; 466 | job.RunAtLoad = YES; 467 | job.KeepAlive = 468 | @{ @"SuccessfulExit" : [NSNumber numberWithBool:keepAlive] }; 469 | 470 | return [[AHLaunchCtl new] add:job toDomain:domain error:error]; 471 | } else { 472 | return [[AHLaunchCtl new] remove:appIdentifier 473 | fromDomain:domain 474 | error:error]; 475 | } 476 | } 477 | 478 | + (void)scheduleJob:(NSString *)label 479 | program:(NSString *)program 480 | interval:(int)seconds 481 | domain:(AHLaunchDomain)domain 482 | reply:(void (^)(NSError *error))reply { 483 | [self scheduleJob:label 484 | program:program 485 | programArguments:nil 486 | interval:seconds 487 | domain:domain 488 | reply:reply]; 489 | } 490 | 491 | + (void)scheduleJob:(NSString *)label 492 | program:(NSString *)program 493 | programArguments:(NSArray *)programArguments 494 | interval:(int)seconds 495 | domain:(AHLaunchDomain)domain 496 | reply:(void (^)(NSError *error))reply { 497 | AHLaunchCtl *controller = [AHLaunchCtl new]; 498 | AHLaunchJob *job = [AHLaunchJob new]; 499 | job.Label = label; 500 | job.Program = program; 501 | job.ProgramArguments = programArguments; 502 | job.RunAtLoad = YES; 503 | job.StartInterval = seconds; 504 | 505 | NSError *error; 506 | [controller add:job toDomain:domain error:&error]; 507 | reply(error); 508 | } 509 | 510 | #pragma mark--- Get Job --- 511 | + (AHLaunchJob *)jobFromFileNamed:(NSString *)label 512 | inDomain:(AHLaunchDomain)domain { 513 | NSArray *jobs = [self allJobsFromFilesInDomain:domain]; 514 | if ([label.pathExtension isEqualToString:@"plist"]) 515 | label = [label stringByDeletingPathExtension]; 516 | 517 | NSPredicate *predicate = 518 | [NSPredicate predicateWithFormat:@"%@ == SELF.Label ", label]; 519 | 520 | for (AHLaunchJob *job in jobs) { 521 | if ([predicate evaluateWithObject:job]) { 522 | return job; 523 | } 524 | } 525 | return nil; 526 | } 527 | 528 | + (AHLaunchJob *)runningJobWithLabel:(NSString *)label 529 | inDomain:(AHLaunchDomain)domain { 530 | AHLaunchJob *job = nil; 531 | NSDictionary *dict = AHJobCopyDictionary(domain, label); 532 | // for some system processes the dict can return nil, so we have a more 533 | // expensive back-up in that case; 534 | if (dict.count) { 535 | job = [AHLaunchJob jobFromDictionary:dict inDomain:domain]; 536 | } else { 537 | job = [[[self class] runningJobsMatching:label 538 | inDomain:domain] lastObject]; 539 | } 540 | 541 | return job; 542 | } 543 | 544 | #pragma mark--- Get Array Of Jobs --- 545 | + (NSArray *)allRunningJobsInDomain:(AHLaunchDomain)domain { 546 | return [self jobMatch:nil domain:domain]; 547 | } 548 | 549 | + (NSArray *)runningJobsMatching:(NSString *)match 550 | inDomain:(AHLaunchDomain)domain { 551 | NSPredicate *predicate = [NSPredicate 552 | predicateWithFormat: 553 | @"SELF.Label CONTAINS[c] %@ OR SELF.Program CONTAINS[c] %@", match, 554 | match]; 555 | return [self jobMatch:predicate domain:domain]; 556 | } 557 | 558 | + (NSArray *)allJobsFromFilesInDomain:(AHLaunchDomain)domain { 559 | AHLaunchJob *job; 560 | NSMutableArray *jobs; 561 | NSString *launchDirectory = launchdJobFileDirectory(domain); 562 | NSArray *launchFiles = [[NSFileManager defaultManager] 563 | contentsOfDirectoryAtPath:launchDirectory 564 | error:nil]; 565 | 566 | if (launchFiles.count) { 567 | jobs = [[NSMutableArray alloc] initWithCapacity:launchFiles.count]; 568 | } 569 | 570 | for (NSString *file in launchFiles) { 571 | NSString *filePath = 572 | [NSString stringWithFormat:@"%@/%@", launchDirectory, file]; 573 | NSDictionary *dict = 574 | [NSDictionary dictionaryWithContentsOfFile:filePath]; 575 | if (dict) { 576 | @try { 577 | job = [AHLaunchJob jobFromDictionary:dict inDomain:domain]; 578 | if (job) job.domain = domain; 579 | [jobs addObject:job]; 580 | } @catch (NSException *exception) { 581 | NSLog(@"error %@", exception); 582 | } 583 | } 584 | } 585 | return jobs; 586 | } 587 | 588 | + (NSArray *)jobMatch:(NSPredicate *)predicate domain:(AHLaunchDomain)domain { 589 | NSArray *array = AHCopyAllJobDictionaries(domain); 590 | if (!array.count) return nil; 591 | 592 | NSMutableArray *jobs = 593 | [[NSMutableArray alloc] initWithCapacity:array.count]; 594 | for (NSDictionary *dict in array) { 595 | AHLaunchJob *job; 596 | if (predicate) { 597 | if ([predicate evaluateWithObject:dict]) { 598 | job = [AHLaunchJob jobFromDictionary:dict inDomain:domain]; 599 | } 600 | } else { 601 | job = [AHLaunchJob jobFromDictionary:dict inDomain:domain]; 602 | } 603 | if (job) { 604 | job.domain = domain; 605 | [jobs addObject:job]; 606 | } 607 | } 608 | return [NSArray arrayWithArray:jobs]; 609 | } 610 | 611 | #pragma mark - Private 612 | - (BOOL)createAuthorization:(AuthorizationRef *)authRef 613 | domain:(AHLaunchDomain)domain 614 | label:(NSString *)label 615 | prompt:(NSString *)prompt 616 | error:(NSError *__autoreleasing *)error { 617 | OSStatus status = errSecSuccess; 618 | 619 | // If domain is greater than the user domain, 620 | if ((domain > kAHUserLaunchAgent)) { 621 | if (_authRef != NULL) { 622 | *authRef = _authRef; 623 | status = errSecSuccess; 624 | } else { 625 | status = [AHAuthorizer authorizeSystemDaemonWithLabel:label 626 | prompt:prompt 627 | authRef:authRef]; 628 | } 629 | 630 | if (status != errSecSuccess) { 631 | if (status == errAuthorizationCanceled) { 632 | [[self class] errorWithCode:kAHErrorUserCanceledAuthorization 633 | error:error]; 634 | } else { 635 | [[self class] errorWithCode:kAHErrorInsufficientPrivileges 636 | error:error]; 637 | } 638 | } 639 | } 640 | return (status == errSecSuccess); 641 | } 642 | 643 | - (BOOL)jobIsValid:(AHLaunchJob *)job error:(NSError *__autoreleasing *)error { 644 | NSFileManager *fm = [NSFileManager defaultManager]; 645 | 646 | // The first argument needs to be executable 647 | if (![fm isExecutableFileAtPath:[job.ProgramArguments firstObject]]) { 648 | return [[self class] errorWithCode:kAHErrorProgramNotExecutable 649 | error:error]; 650 | } 651 | 652 | // LaunchD's need a label 653 | if (!job.Label || !job.Label.length) { 654 | return [[self class] errorWithCode:kAHErrorJobMissingRequiredKeys 655 | error:error]; 656 | } 657 | 658 | return YES; 659 | } 660 | 661 | - (BOOL)removeJobFileWithLabel:(NSString *)label 662 | domain:(AHLaunchDomain)domain 663 | authRef:(AuthorizationRef)authRef 664 | error:(NSError *__autoreleasing *)error { 665 | NSFileManager *fm = [NSFileManager new]; 666 | NSString *file = launchdJobFile(label, domain); 667 | BOOL isDir; 668 | if ([fm fileExistsAtPath:file isDirectory:&isDir] && !isDir) { 669 | return [fm removeItemAtPath:file error:error]; 670 | } else if (isDir) { 671 | return [[self class] errorWithCode:kAHErrorFileIsDirectory error:error]; 672 | } else { 673 | return YES; 674 | } 675 | } 676 | 677 | #pragma mark - Error Codes 678 | + (BOOL)errorWithCode:(NSInteger)code error:(NSError *__autoreleasing *)error { 679 | BOOL rc = code != 0 ? NO : YES; 680 | NSString *msg = errorMsgFromCode(code); 681 | NSError *err = 682 | [NSError errorWithDomain:kAHLaunchCtlDomain 683 | code:code 684 | userInfo:@{NSLocalizedDescriptionKey : msg}]; 685 | if (error) { 686 | *error = err; 687 | } else { 688 | NSLog(@"Error: %@", msg); 689 | } 690 | 691 | return rc; 692 | } 693 | 694 | + (BOOL)errorWithMessage:(NSString *)message 695 | andCode:(NSInteger)code 696 | error:(NSError *__autoreleasing *)error { 697 | BOOL rc = code != 0 ? NO : YES; 698 | NSError *err = 699 | [NSError errorWithDomain:kAHLaunchCtlDomain 700 | code:code 701 | userInfo:@{NSLocalizedDescriptionKey : message}]; 702 | 703 | if (error) { 704 | *error = err; 705 | } else { 706 | NSLog(@"Error: %@", message); 707 | } 708 | return rc; 709 | } 710 | 711 | + (BOOL)errorWithCFError:(CFErrorRef)cfError 712 | code:(int)code 713 | error:(NSError *__autoreleasing *)error { 714 | BOOL rc = code != 0 ? NO : YES; 715 | 716 | NSError *err = CFBridgingRelease(cfError); 717 | if (error) { 718 | *error = err; 719 | } else { 720 | NSLog(@"Error: %@", err.localizedDescription); 721 | } 722 | 723 | return rc; 724 | } 725 | 726 | @end 727 | 728 | #pragma mark - Utility Functions 729 | 730 | static NSString *errorMsgFromCode(NSInteger code) { 731 | NSString *msg; 732 | switch (code) { 733 | case kAHErrorJobNotLoaded: 734 | msg = 735 | NSLocalizedStringFromTable(@"Job not loaded", @"AHLaunchCtl", 736 | @"Error when the job is not loaded"); 737 | break; 738 | case kAHErrorFileNotFound: 739 | msg = NSLocalizedStringFromTable( 740 | @"Could not find the specified launchd.plist to load the job", 741 | @"AHLaunchCtl", @"Error when the job file is not found."); 742 | break; 743 | case kAHErrorFileIsDirectory: 744 | msg = NSLocalizedStringFromTable( 745 | @"The suggested file to remove is a directory.", @"AHLaunchCtl", 746 | @"Error when the FILE that is suppose to be removed is a " 747 | @"directory"); 748 | break; 749 | case kAHErrorCouldNotLoadJob: 750 | msg = NSLocalizedStringFromTable( 751 | @"Could not load job", @"AHLaunchCtl", 752 | @"Error when the job is not loaded"); 753 | break; 754 | case kAHErrorCouldNotLoadHelperTool: 755 | msg = NSLocalizedStringFromTable( 756 | @"Unable to install the privileged helper tool", @"AHLaunchCtl", 757 | @"Error when the helper tool cannot be loaded."); 758 | break; 759 | case kAHErrorCouldNotUnloadHelperTool: 760 | msg = NSLocalizedStringFromTable( 761 | @"Unable to remove the privileged helper tool", @"AHLaunchCtl", 762 | @"Error when cannot unload helper tool"); 763 | break; 764 | case kAHErrorHelperToolNotLoaded: 765 | msg = NSLocalizedStringFromTable( 766 | @"Cannot unload The helper tool, it is not currently loaded.", 767 | @"AHLaunchCtl", @"Error when the helper tool is not loaded"); 768 | break; 769 | case kAHErrorCouldNotRemoveHelperToolFiles: 770 | msg = NSLocalizedStringFromTable( 771 | @"Unable to remove some files associated with the privileged " 772 | @"helper tool", 773 | @"AHLaunchCtl", @"Error when a removing privileged helper."); 774 | break; 775 | case kAHErrorJobAlreadyExists: 776 | msg = NSLocalizedStringFromTable( 777 | @"The specified job already exists", @"AHLaunchCtl", 778 | @"Error when the job is alreay exists"); 779 | break; 780 | case kAHErrorJobAlreadyLoaded: 781 | msg = NSLocalizedStringFromTable( 782 | @"The specified job is already loaded", @"AHLaunchCtl", 783 | @"Error when the job is already loaded"); 784 | break; 785 | case kAHErrorJobCouldNotReload: 786 | msg = NSLocalizedStringFromTable( 787 | @"There were problems reloading the job", @"AHLaunchCtl", 788 | @"Error when the could not be reloaded"); 789 | break; 790 | case kAHErrorJobLabelNotValid: 791 | msg = NSLocalizedStringFromTable( 792 | @"The job label is not valid.", @"AHLaunchCtl", 793 | @"Error when the job label is not valid"); 794 | break; 795 | case kAHErrorCouldNotUnloadJob: 796 | msg = NSLocalizedStringFromTable( 797 | @"Could not unload job", @"AHLaunchCtl", 798 | @"Error when the job could not be unloaded"); 799 | break; 800 | case kAHErrorMultipleJobsMatching: 801 | msg = NSLocalizedStringFromTable( 802 | @"More than one job matched that description", @"AHLaunchCtl", 803 | @"Error when the job matching returned more than one result"); 804 | break; 805 | case kAHErrorCouldNotWriteFile: 806 | msg = NSLocalizedStringFromTable( 807 | @"There were problem writing to the file", @"AHLaunchCtl", 808 | @"Error when the the launchd.plist file could not be written"); 809 | break; 810 | case kAHErrorInsufficientPrivileges: 811 | msg = NSLocalizedStringFromTable( 812 | @"You are not authorized to to perform this action", 813 | @"AHLaunchCtl", @"Error when user is not authorized."); 814 | break; 815 | case kAHErrorJobMissingRequiredKeys: 816 | msg = NSLocalizedStringFromTable( 817 | @"The Submitted Job was missing some required keys", 818 | @"AHLaunchCtl", @"Error when the job is missing required keys"); 819 | break; 820 | case kAHErrorExecutingAsIncorrectUser: 821 | msg = NSLocalizedStringFromTable( 822 | @"Could not set the Job to run in the proper context", 823 | @"AHLaunchCtl", @"Error when job context could not be set."); 824 | break; 825 | case kAHErrorProgramNotExecutable: 826 | msg = NSLocalizedStringFromTable( 827 | @"The path specified doesn’t appear to be executable.", 828 | @"AHLaunchCtl", 829 | @"Error when the submitted job's executable could not be set."); 830 | break; 831 | case kAHErrorUserCanceledAuthorization: 832 | msg = NSLocalizedStringFromTable( 833 | @"Authorizaton canceled by user.", @"AHLaunchCtl", 834 | @"Error when authorization is canceled by user"); 835 | default: 836 | msg = (__bridge_transfer NSString *)SecCopyErrorMessageString( 837 | (int)code, NULL) 838 | ?: NSLocalizedStringFromTable( 839 | @"An unknown problem occurred", @"AHLaunchCtl", 840 | @"Generic error"); 841 | break; 842 | } 843 | return msg; 844 | } 845 | -------------------------------------------------------------------------------- /AHLaunchCtl/AHLaunchJob.h: -------------------------------------------------------------------------------- 1 | // AHLaunchJob.h 2 | // Copyright (c) 2014 Eldon Ahrold ( https://github.com/eahrold/AHLaunchCtl ) 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | 22 | #import 23 | #import 24 | 25 | /** 26 | * AHLaunchDomain Launch Domain specifying which context the job should run 27 | * under and where the launchd.plist file is located 28 | */ 29 | typedef NS_ENUM(NSInteger, AHLaunchDomain) { 30 | /** 31 | * User Launch Agents ~/Library/LaunchAgents. Loaded by the Console user. 32 | */ 33 | kAHUserLaunchAgent = 1, 34 | /** 35 | * Administrator provided LaunchAgents /Library/LaunchAgents/. Loaded by 36 | * the console user 37 | */ 38 | kAHGlobalLaunchAgent, 39 | /** 40 | * Apple provided LaunchAgents /System/Library/LaunchAgents/. Loaded by 41 | * root user 42 | */ 43 | kAHSystemLaunchAgent, 44 | /** 45 | * Administrator provided LaunchDaemon /Library/LaunchDaemons/. Loaded by 46 | * root user 47 | */ 48 | kAHGlobalLaunchDaemon, 49 | /** 50 | * Apple provided LaunchDaemon /System/Library/LaunchDaemons/. Loaded by 51 | * root user. 52 | */ 53 | kAHSystemLaunchDaemon, 54 | }; 55 | 56 | /** 57 | * Job Object to loaded by AHLaunchCtl. Conforms to the keys of launchd.plist. 58 | * See the Apple documentation for more info 59 | */ 60 | @interface AHLaunchJob : NSObject 61 | /** 62 | * Label 63 | */ 64 | @property (copy, nonatomic) NSString *Label; 65 | /** 66 | * Disabled 67 | */ 68 | @property (nonatomic) BOOL Disabled; 69 | 70 | #pragma mark - 71 | /** 72 | * Program 73 | */ 74 | @property (copy, nonatomic) NSString *Program; 75 | /** 76 | * see man launchd.plist 77 | */ 78 | @property (copy, nonatomic) NSArray *ProgramArguments; 79 | /** 80 | * see man launchd.plist 81 | */ 82 | @property (nonatomic) NSInteger StartInterval; 83 | 84 | /** 85 | * see man launchd.plist 86 | */ 87 | @property (copy, nonatomic) NSString *ServiceDescription; 88 | 89 | #pragma mark - 90 | /** 91 | * see man launchd.plist 92 | */ 93 | @property (copy, nonatomic) NSString *UserName; 94 | /** 95 | * see man launchd.plist 96 | */ 97 | @property (copy, nonatomic) NSString *GroupName; 98 | /** 99 | * see man launchd.plist 100 | */ 101 | @property (copy, nonatomic) NSDictionary *inetdCompatibility; 102 | 103 | #pragma mark - 104 | /** 105 | * see man launchd.plist 106 | */ 107 | @property (copy, nonatomic) NSArray *LimitLoadToHosts; 108 | /** 109 | * see man launchd.plist 110 | */ 111 | @property (copy, nonatomic) NSArray *LimitLoadFromHosts; 112 | /** 113 | * see man launchd.plist 114 | */ 115 | @property (copy, nonatomic) NSString *LimitLoadToSessionType; 116 | 117 | #pragma mark - 118 | /** 119 | * see man launchd.plist 120 | */ 121 | @property (nonatomic) BOOL EnableGlobbing; 122 | /** 123 | * see man launchd.plist 124 | */ 125 | @property (nonatomic) BOOL EnableTransactions; 126 | /** 127 | * see man launchd.plist 128 | */ 129 | @property (nonatomic) BOOL BeginTransactionAtShutdown; 130 | 131 | #pragma mark - 132 | /** 133 | * KeepAlive dictionary or Number use @YES and @NO 134 | */ 135 | @property (nonatomic) id KeepAlive; 136 | /** 137 | * see man launchd.plist 138 | */ 139 | @property (nonatomic) BOOL OnDemand; 140 | /** 141 | * see man launchd.plist 142 | */ 143 | @property (nonatomic) BOOL RunAtLoad; 144 | 145 | #pragma mark - 146 | /** 147 | * see man launchd.plist 148 | */ 149 | @property (copy, nonatomic) NSString *RootDirectory; 150 | /** 151 | * see man launchd.plist 152 | */ 153 | @property (copy, nonatomic) NSString *WorkingDirectory; 154 | 155 | #pragma mark - 156 | /** 157 | * see man launchd.plist 158 | */ 159 | @property (copy, nonatomic) NSDictionary *EnvironmentVariables; 160 | /** 161 | * see man launchd.plist 162 | */ 163 | @property (nonatomic) NSInteger Umask; 164 | /** 165 | * see man launchd.plist 166 | */ 167 | @property (nonatomic) NSInteger TimeOut; 168 | /** 169 | * see man launchd.plist 170 | */ 171 | @property (nonatomic) NSInteger ExitTimeOut; 172 | /** 173 | * see man launchd.plist 174 | */ 175 | @property (nonatomic) NSInteger ThrottleInterval; 176 | 177 | #pragma mark - 178 | /** 179 | * see man launchd.plist 180 | */ 181 | @property (nonatomic) BOOL InitGroups; 182 | /** 183 | * see man launchd.plist 184 | */ 185 | @property (copy, nonatomic) NSArray *WatchPaths; 186 | /** 187 | * see man launchd.plist 188 | */ 189 | @property (copy, nonatomic) NSArray *QueueDirectories; 190 | /** 191 | * see man launchd.plist 192 | */ 193 | @property (nonatomic) BOOL StartOnMount; 194 | 195 | #pragma mark - Schedule 196 | /** 197 | * StartCalendarInterval AHLaunchJobSchedule of integers or array of 198 | * AHLaunchJobSchedules 199 | */ 200 | @property (copy, nonatomic) id StartCalendarInterval; 201 | 202 | /** 203 | * Array Of AHLaunchJobSchedule for scheduling multiple runs with the same 204 | * Launch Job 205 | */ 206 | @property (copy, nonatomic) NSArray *StartCalendarIntervalArray __deprecated_msg("Use StartCalendarInterval instead."); 207 | 208 | #pragma mark - In/Out 209 | /** 210 | * see man launchd.plist 211 | */ 212 | @property (copy, nonatomic) NSString *StandardInPath; 213 | /** 214 | * see man launchd.plist 215 | */ 216 | @property (copy, nonatomic) NSString *StandardOutPath; 217 | /** 218 | * see man launchd.plist 219 | */ 220 | @property (copy, nonatomic) NSString *StandardErrorPath; 221 | /** 222 | * see man launchd.plist 223 | */ 224 | @property (nonatomic) BOOL Debug; 225 | /** 226 | * see man launchd.plist 227 | */ 228 | @property (nonatomic) BOOL WaitForDebugger; 229 | 230 | #pragma mark - 231 | /** 232 | * see man launchd.plist 233 | */ 234 | @property (copy, nonatomic) NSDictionary *SoftResourceLimits; 235 | /** 236 | * see man launchd.plist 237 | */ 238 | @property (copy, nonatomic) NSDictionary *HardResourceLimits; 239 | 240 | #pragma mark - 241 | /** 242 | * see man launchd.plist 243 | */ 244 | @property (nonatomic) NSInteger Nice; 245 | /** 246 | * see man launchd.plist 247 | */ 248 | @property (copy, nonatomic) NSString *ProcessType; 249 | 250 | #pragma mark - 251 | /** 252 | * see man launchd.plist 253 | */ 254 | @property (nonatomic) BOOL AbandonProcessGroup; 255 | /** 256 | * see man launchd.plist 257 | */ 258 | @property (nonatomic) BOOL LowPriorityIO; 259 | /** 260 | * see man launchd.plist 261 | */ 262 | @property (nonatomic) BOOL LowPriorityBackgroundIO; 263 | /** 264 | * see man launchd.plist 265 | */ 266 | @property (nonatomic) BOOL LaunchOnlyOnce; 267 | 268 | #pragma mark - 269 | /** 270 | * see man launchd.plist 271 | */ 272 | @property (copy, nonatomic) NSDictionary *MachServices; 273 | /** 274 | * see man launchd.plist 275 | */ 276 | @property (copy, nonatomic) NSDictionary *Sockets; 277 | 278 | #pragma mark - Specialized / Undocumented Apple Keys 279 | /** 280 | * see man launchd.plist 281 | */ 282 | @property (copy, nonatomic) NSDictionary *LaunchEvents; 283 | /** 284 | * see man launchd.plist 285 | */ 286 | @property (copy, nonatomic) NSDictionary *PerJobMachServices; 287 | /** 288 | * see man launchd.plist 289 | */ 290 | @property (copy, nonatomic) NSString *MachExceptionHandler; 291 | 292 | #pragma mark - 293 | /** 294 | * see man launchd.plist 295 | */ 296 | @property (copy, nonatomic) NSString *POSIXSpawnType; 297 | /** 298 | * see man launchd.plist 299 | */ 300 | @property (copy, nonatomic) NSString *PosixSpawnType; 301 | 302 | #pragma mark - 303 | /** 304 | * see man launchd.plist 305 | */ 306 | @property (nonatomic) BOOL ServiceIPC; 307 | /** 308 | * see man launchd.plist 309 | */ 310 | @property (nonatomic) BOOL XPCDomainBootstrapper; 311 | 312 | #pragma mark - 313 | /** 314 | * see man launchd.plist 315 | */ 316 | @property (copy, nonatomic) NSString *CFBundleIdentifier; 317 | /** 318 | * see man launchd.plist 319 | */ 320 | @property (copy, nonatomic) NSString *SHAuthorizationRight; 321 | 322 | #pragma mark - 323 | /** 324 | * see man launchd.plist 325 | */ 326 | @property (copy, nonatomic) NSDictionary *JetsamProperties; 327 | /** 328 | * see man launchd.plist 329 | */ 330 | @property (copy, nonatomic) NSArray *BinaryOrderPreference; 331 | /** 332 | * see man launchd.plist 333 | */ 334 | @property (nonatomic) BOOL SessionCreate; 335 | /** 336 | * see man launchd.plist 337 | */ 338 | @property (nonatomic) BOOL MultipleInstances; 339 | 340 | #pragma mark - 341 | /** 342 | * see man launchd.plist 343 | */ 344 | @property (nonatomic) BOOL HopefullyExitsLast; 345 | /** 346 | * see man launchd.plist 347 | */ 348 | @property (nonatomic) BOOL ShutdownMonitor; 349 | /** 350 | * see man launchd.plist 351 | */ 352 | @property (nonatomic) BOOL EventMonitor; 353 | /** 354 | * see man launchd.plist 355 | */ 356 | @property (nonatomic) BOOL IgnoreProcessGroupAtShutdown; 357 | 358 | #pragma mark - Read only properties... 359 | /** 360 | * Associated Launch Domain 361 | */ 362 | @property (nonatomic, readonly) AHLaunchDomain domain; 363 | /** 364 | * Process ID for the managed executable 365 | */ 366 | @property (nonatomic, readonly) NSInteger PID; 367 | /** 368 | * Last exit status of the managed executable 369 | */ 370 | @property (nonatomic, readonly) NSInteger LastExitStatus; 371 | /** 372 | * wether or not the current job is loaded 373 | */ 374 | @property (nonatomic, readonly) BOOL isCurrentlyLoaded; 375 | 376 | #pragma mark; 377 | /** 378 | * The dictionary value of the Launch Job. 379 | * 380 | * @return The dictionary that will be submitted to launchd 381 | */ 382 | - (NSDictionary *)dictionary; 383 | 384 | /** 385 | * The version number of the executable if it was compiled with an embedded 386 | * Info.plist This is primarily used for determining the version on a 387 | * privileged helper application 388 | * 389 | * @return Version String Value 390 | */ 391 | - (NSString *)executableVersion; 392 | 393 | #pragma mark - Class Methods 394 | /** 395 | * Create a job from a dictionary 396 | * 397 | * @param dict dictionary with KVC attributes 398 | * 399 | * @return allocated AHLaunchJob with corresponding keys 400 | */ 401 | + (AHLaunchJob *)jobFromDictionary:(NSDictionary *)dict 402 | inDomain:(AHLaunchDomain)domain; 403 | 404 | /** 405 | * Create a job from a launchd.plist 406 | * 407 | * @param file path to launchd.plist 408 | * 409 | * @return allocated AHLaunchJob with corresponding keys 410 | */ 411 | + (AHLaunchJob *)jobFromFile:(NSString *)file; 412 | 413 | @end 414 | -------------------------------------------------------------------------------- /AHLaunchCtl/AHLaunchJob.m: -------------------------------------------------------------------------------- 1 | // AHLaunchJob.m 2 | // Copyright (c) 2014 Eldon Ahrold ( https://github.com/eahrold/AHLaunchCtl ) 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | 22 | #import "AHLaunchJob.h" 23 | #import 24 | 25 | #import 26 | 27 | @interface AHLaunchJob () 28 | @property (strong, atomic, readwrite) NSMutableDictionary *internalDictionary; 29 | @property (nonatomic, readwrite) AHLaunchDomain domain; // 30 | @property (nonatomic, readwrite) NSInteger LastExitStatus; // 31 | @property (nonatomic, readwrite) NSInteger PID; // 32 | @property (nonatomic, readwrite) BOOL isCurrentlyLoaded; // 33 | @end 34 | 35 | #pragma mark - AHLaunchJob 36 | @implementation AHLaunchJob { 37 | unsigned int _count; 38 | } 39 | 40 | @synthesize StartCalendarInterval = _StartCalendarInterval; 41 | @synthesize StartCalendarIntervalArray = _StartCalendarIntervalArray; 42 | 43 | - (void)dealloc { 44 | [self removeObservingOnAllProperties]; 45 | } 46 | 47 | - (instancetype)init { 48 | if (self = [super init]) { 49 | [self startObservingOnAllProperties]; 50 | } 51 | return self; 52 | } 53 | 54 | - (instancetype)initWithoutObservers { 55 | if (self = [super init]) { 56 | _count = 33; 57 | _internalDictionary = 58 | [[NSMutableDictionary alloc] initWithCapacity:_count]; 59 | } 60 | return self; 61 | } 62 | 63 | #pragma mark - Secure Coding 64 | - (id)initWithCoder:(NSCoder *)aDecoder { 65 | NSSet *SAND = [NSSet setWithObjects:[NSArray class], 66 | [NSDictionary class], 67 | [NSString class], 68 | [NSNumber class], 69 | nil]; 70 | if (self = [super init]) { 71 | _internalDictionary = [aDecoder 72 | decodeObjectOfClasses:SAND 73 | forKey:NSStringFromSelector(@selector(dictionary))]; 74 | _Label = [aDecoder 75 | decodeObjectOfClass:[NSString class] 76 | forKey:NSStringFromSelector(@selector(Label))]; 77 | _Program = [aDecoder 78 | decodeObjectOfClass:[NSString class] 79 | forKey:NSStringFromSelector(@selector(Program))]; 80 | _ProgramArguments = 81 | [aDecoder decodeObjectOfClasses:SAND 82 | forKey:NSStringFromSelector( 83 | @selector(ProgramArguments))]; 84 | } 85 | return self; 86 | } 87 | 88 | + (BOOL)supportsSecureCoding { 89 | return YES; 90 | } 91 | 92 | - (void)encodeWithCoder:(NSCoder *)aEncoder { 93 | [aEncoder encodeObject:_internalDictionary 94 | forKey:NSStringFromSelector(@selector(dictionary))]; 95 | [aEncoder encodeObject:_Label 96 | forKey:NSStringFromSelector(@selector(Label))]; 97 | [aEncoder encodeObject:_Program 98 | forKey:NSStringFromSelector(@selector(Program))]; 99 | [aEncoder encodeObject:_ProgramArguments 100 | forKey:NSStringFromSelector(@selector(ProgramArguments))]; 101 | } 102 | 103 | #pragma mark - Instance Methods 104 | - (NSDictionary *)dictionary { 105 | return [_internalDictionary copy]; 106 | } 107 | 108 | - (NSString *)executableVersion { 109 | NSString *helperVersion; 110 | if (_ProgramArguments.count) { 111 | NSURL *execURL = 112 | [NSURL fileURLWithPath:[self.ProgramArguments objectAtIndex:0]]; 113 | NSDictionary *helperPlist = (NSDictionary *)CFBridgingRelease( 114 | CFBundleCopyInfoDictionaryForURL((__bridge CFURLRef)(execURL))); 115 | if (helperPlist) helperVersion = helperPlist[@"CFBundleVersion"]; 116 | } 117 | return helperVersion; 118 | }; 119 | 120 | - (NSSet *)ignoredProperties { 121 | NSSet *const ignoredProperties = [NSSet 122 | setWithObjects:NSStringFromSelector(@selector(StartCalendarInterval)), 123 | NSStringFromSelector(@selector(PID)), 124 | NSStringFromSelector(@selector(LastExitStatus)), 125 | NSStringFromSelector(@selector(isCurrentlyLoaded)), 126 | NSStringFromSelector(@selector(domain)), 127 | nil]; 128 | 129 | return ignoredProperties; 130 | } 131 | 132 | #pragma mark--- Observing --- 133 | - (void)startObservingOnAllProperties { 134 | objc_property_t *properties = class_copyPropertyList([self class], &_count); 135 | for (int i = 0; i < _count; ++i) { 136 | const char *property = property_getName(properties[i]); 137 | NSString *keyPath = [NSString stringWithUTF8String:property]; 138 | if (![[self ignoredProperties] member:keyPath]) { 139 | [self addObserver:self 140 | forKeyPath:keyPath 141 | options:NSKeyValueObservingOptionNew 142 | context:NULL]; 143 | } 144 | } 145 | free(properties); 146 | } 147 | 148 | - (void)removeObservingOnAllProperties { 149 | unsigned int count; 150 | objc_property_t *properties = class_copyPropertyList([self class], &count); 151 | for (int i = 0; i < count; ++i) { 152 | const char *property = property_getName(properties[i]); 153 | NSString *keyPath = [NSString stringWithUTF8String:property]; 154 | @try { 155 | if (![[self ignoredProperties] member:keyPath]) { 156 | [self removeObserver:self forKeyPath:keyPath]; 157 | } 158 | } 159 | @catch (NSException *exception) { 160 | } 161 | } 162 | free(properties); 163 | } 164 | 165 | - (void)observeValueForKeyPath:(NSString *)keyPath 166 | ofObject:(id)object 167 | change:(NSDictionary *)change 168 | context:(void *)context { 169 | if (!_internalDictionary) { 170 | _internalDictionary = 171 | [[NSMutableDictionary alloc] initWithCapacity:_count]; 172 | } 173 | 174 | id new = change[@"new"]; 175 | 176 | objc_property_t property = 177 | class_getProperty([self class], keyPath.UTF8String); 178 | const char *p = property_getAttributes(property); 179 | 180 | if (p != NULL) { 181 | if (!strncmp("Tc", p, 2)) { 182 | [self writeBoolValueToDict:new forKey:keyPath]; 183 | } else { 184 | [self writeObjectValueToDict:new forKey:keyPath]; 185 | } 186 | } 187 | } 188 | 189 | #pragma mark--- Accessors --- 190 | - (id)StartCalendarInterval { 191 | if (!_StartCalendarInterval) { 192 | id sci = _internalDictionary[NSStringFromSelector( 193 | @selector(StartCalendarInterval))]; 194 | if ([sci isKindOfClass:[NSDictionary class]]) { 195 | _StartCalendarInterval = 196 | [[AHLaunchJobSchedule alloc] initWithDictionary:sci]; 197 | } else if ([sci isKindOfClass:[NSArray class]]) { 198 | NSMutableArray *scheduleArray = 199 | [NSMutableArray arrayWithCapacity:[sci count]]; 200 | for (NSDictionary *d in sci) { 201 | AHLaunchJobSchedule *scheudle = 202 | [[AHLaunchJobSchedule alloc] initWithDictionary:d]; 203 | if (scheudle) { 204 | [scheduleArray addObject:scheudle]; 205 | }; 206 | } 207 | _StartCalendarInterval = scheduleArray; 208 | } 209 | } 210 | return _StartCalendarInterval; 211 | } 212 | 213 | - (void)setStartCalendarInterval:(id)StartCalendarInterval { 214 | NSString *startCalendarKey = 215 | NSStringFromSelector(@selector(StartCalendarInterval)); 216 | 217 | // Handle as single schedule. 218 | if ([StartCalendarInterval isKindOfClass:[AHLaunchJobSchedule class]]) { 219 | _internalDictionary[startCalendarKey] = 220 | [StartCalendarInterval dictionary]; 221 | } 222 | 223 | // Handle as array of schedules 224 | else if ([StartCalendarInterval isKindOfClass:[NSArray class]]) { 225 | NSMutableArray *scheduleArray = [[NSMutableArray alloc] 226 | initWithCapacity:[StartCalendarInterval count]]; 227 | for (id schedule in StartCalendarInterval) { 228 | AHLaunchJobSchedule *sch; 229 | if ([schedule isKindOfClass:[NSDictionary class]]){ 230 | sch = [[AHLaunchJobSchedule alloc] initWithDictionary:schedule]; 231 | } else if ([schedule isKindOfClass:[AHLaunchJobSchedule class]]){ 232 | sch = schedule; 233 | } 234 | if (sch) { 235 | [scheduleArray addObject:sch.dictionary]; 236 | } 237 | } 238 | _internalDictionary[startCalendarKey] = [scheduleArray copy]; 239 | } 240 | } 241 | 242 | - (NSArray *)StartCalendarIntervalArray { 243 | NSArray *array; 244 | if ((array = self.StartCalendarInterval) && 245 | [array isKindOfClass:[NSArray class]]) { 246 | _StartCalendarIntervalArray = array; 247 | } 248 | return _StartCalendarIntervalArray; 249 | } 250 | 251 | - (void)setStartCalendarIntervalArray:(NSArray *)StartCalendarIntervalArray { 252 | self.StartCalendarInterval = StartCalendarIntervalArray; 253 | } 254 | 255 | #pragma mark 256 | - (NSInteger)LastExitStatus { 257 | if (_LastExitStatus) { 258 | return _LastExitStatus; 259 | } 260 | id value = [self serviceManagementValueForKey:NSStringFromSelector(@selector(LastExitStatus))]; 261 | if (!value || ![value isKindOfClass:[NSNumber class]]) { 262 | return -1; 263 | } 264 | return [value integerValue]; 265 | } 266 | 267 | - (NSInteger)PID { 268 | if (_PID) { 269 | return _PID; 270 | } 271 | id value = [self serviceManagementValueForKey:NSStringFromSelector(@selector(PID))]; 272 | if (!value || ![value isKindOfClass:[NSNumber class]]) { 273 | return -1; 274 | } 275 | return [value integerValue]; 276 | } 277 | 278 | - (BOOL)isCurrentlyLoaded { 279 | id test = [self serviceManagementValueForKey:NSStringFromSelector(@selector(Label))]; 280 | if (test) return YES; 281 | return NO; 282 | } 283 | 284 | - (NSString *)description { 285 | if (!_internalDictionary.count) { 286 | return @"No Job Set"; 287 | } else { 288 | NSInteger pid = self.PID; 289 | NSString *pidStr; 290 | if (pid == -1) { 291 | pidStr = @"--"; 292 | } else { 293 | pidStr = [NSString stringWithFormat:@"%ld", pid]; 294 | } 295 | NSInteger lastExitStatus = self.LastExitStatus; 296 | NSString *lastExitString; 297 | if (lastExitStatus == -1) { 298 | lastExitString = @"--"; 299 | } else { 300 | lastExitString = @(lastExitStatus).stringValue; 301 | } 302 | NSString *loaded = self.isCurrentlyLoaded ? @"YES" : @"NO"; 303 | 304 | NSString *format = [NSString 305 | stringWithFormat:@"Is Loaded:%@\tLastExit:%@\tPID:%@\tLabel:%@", 306 | loaded, 307 | lastExitString, 308 | pidStr, 309 | self.Label]; 310 | return format; 311 | } 312 | } 313 | 314 | #pragma mark--- Private Methods --- 315 | - (id)serviceManagementValueForKey:(NSString *)key { 316 | if (self.Label && (self.domain != 0)) { 317 | NSDictionary *dict = AHJobCopyDictionary(self.domain, self.Label); 318 | return [dict objectForKey:key]; 319 | } else { 320 | return nil; 321 | } 322 | } 323 | 324 | - (void)writeBoolValueToDict:(id)value forKey:(NSString *)keyPath { 325 | if ([value isKindOfClass:[NSNumber class]]) { 326 | [_internalDictionary setValue:[NSNumber numberWithBool:[value boolValue]] 327 | forKey:keyPath]; 328 | } else { 329 | [_internalDictionary removeObjectForKey:keyPath]; 330 | } 331 | } 332 | 333 | - (void)writeObjectValueToDict:(id)value forKey:(NSString *)keyPath { 334 | BOOL write = YES; 335 | if ([value isKindOfClass:[NSString class]] && ([value length] == 0)){ 336 | write = NO; 337 | } else if ([value isKindOfClass:[NSDictionary class]] || [value isKindOfClass:[NSArray class]]){ 338 | if ([value count] == 0) { 339 | write = NO; 340 | } 341 | } else if ([value isKindOfClass:[NSNull class]]){ 342 | write = NO; 343 | } 344 | 345 | if (write) { 346 | [_internalDictionary setValue:value forKey:keyPath]; 347 | } else { 348 | [_internalDictionary removeObjectForKey:keyPath]; 349 | } 350 | } 351 | 352 | #pragma mark - Class Methods 353 | + (AHLaunchJob *)jobFromDictionary:(NSDictionary *)dict 354 | inDomain:(AHLaunchDomain)domain { 355 | assert(dict != nil); 356 | AHLaunchJob *job = [[AHLaunchJob alloc] initWithoutObservers]; 357 | job.domain = domain; 358 | 359 | for (id key in dict) { 360 | if ([key isKindOfClass:[NSString class]] && 361 | [job respondsToSelector:NSSelectorFromString(key)]) { 362 | @try { 363 | [job setValue:dict[key] forKey:key]; 364 | } 365 | @catch (NSException *exception) { 366 | #if DEBUG 367 | NSLog(@"[DEBUG] there was a problem parsing launchd.plist of " 368 | @"%@: %@", 369 | dict[NSStringFromSelector(@selector(Label))], 370 | exception); 371 | #endif 372 | } 373 | [job.internalDictionary setValue:dict[key] forKey:key]; 374 | } 375 | } 376 | [job startObservingOnAllProperties]; 377 | return job; 378 | } 379 | 380 | + (AHLaunchJob *)jobFromFile:(NSString *)file { 381 | // Normalize the string // 382 | NSString *filePath = file.stringByExpandingTildeInPath; 383 | AHLaunchDomain domain = 0; 384 | 385 | if ([filePath hasPrefix:kAHGlobalLaunchAgentDirectory]) { 386 | domain = kAHGlobalLaunchAgent; 387 | } else if ([filePath hasPrefix:kAHGlobalLaunchDaemonDirectory]) { 388 | domain = kAHGlobalLaunchDaemon; 389 | } else if ([filePath hasPrefix:kAHSystemLaunchAgentDirectory]) { 390 | domain = kAHSystemLaunchAgent; 391 | } else if ([filePath hasPrefix:kAHSystemLaunchDaemonDirectory]) { 392 | domain = kAHSystemLaunchDaemon; 393 | } else if ([filePath hasPrefix:NSHomeDirectory()]) { 394 | domain = kAHUserLaunchAgent; 395 | } 396 | 397 | NSDictionary *dict; 398 | // Check the file returns a dict, and that the dictionary returned 399 | // has both a label and program arguments keys. 400 | if ((dict = [NSDictionary dictionaryWithContentsOfFile:file]) && 401 | dict[NSStringFromSelector(@selector(Label))] && 402 | dict[NSStringFromSelector(@selector(ProgramArguments))]) { 403 | return [self jobFromDictionary:dict inDomain:domain]; 404 | }; 405 | 406 | return nil; 407 | } 408 | 409 | @end 410 | #pragma mark - Functions 411 | -------------------------------------------------------------------------------- /AHLaunchCtl/AHLaunchJobSchedule.h: -------------------------------------------------------------------------------- 1 | // 2 | // NSDateComponents+AHLaunchCtlSchedule.h 3 | // AHLaunchCtl 4 | // 5 | // Created by Eldon on 4/26/14. 6 | // Copyright (c) 2014 Eldon Ahrold. All rights reserved. 7 | // 8 | 9 | #import 10 | /** 11 | * Used as a wildcard in schedule 12 | */ 13 | extern NSInteger AHUndefinedScheduleComponent; 14 | 15 | /** 16 | * Schedule Class for StartCalendarIntervals 17 | */ 18 | @interface AHLaunchJobSchedule : NSDateComponents 19 | 20 | // Init with a dictionary of keys, such as a dictionary 21 | - (instancetype)initWithDictionary:(NSDictionary *)dictionary; 22 | 23 | - (NSDictionary *)dictionary; 24 | 25 | /** 26 | * Set up a custom AHLaunchCtl Schedule 27 | * @discussion Pass AHUndefinedScheduleComponent to any unused parameter. 28 | * 29 | * @param minute minute of the hour 30 | * @param hour hour of the day 31 | * @param day day of the month 32 | * @param weekday day of the week (0 is sunday) 33 | * @param month month of the year 34 | * 35 | * @return Initialized AHLaunchJobSchedule object 36 | */ 37 | + (instancetype)scheduleWithMinute:(NSInteger)minute 38 | hour:(NSInteger)hour 39 | day:(NSInteger)day 40 | weekday:(NSInteger)weekday 41 | month:(NSInteger)month; 42 | /** 43 | * setup a daily run 44 | * 45 | * @param hour hour of the day 46 | * @param minute minute of the hour 47 | * 48 | * @return Initialized AHLaunchJobSchedule object 49 | */ 50 | + (instancetype)dailyRunAtHour:(NSInteger)hour minute:(NSInteger)minute; 51 | 52 | /** 53 | * setup a weekly run 54 | * 55 | * @param weekday day of the week (0 is sunday) 56 | * @param hour hour of the day 57 | * 58 | * @return Initialized AHLaunchJobSchedule object 59 | */ 60 | + (instancetype)weeklyRunOnWeekday:(NSInteger)weekday hour:(NSInteger)hour; 61 | 62 | /** 63 | * setup a monthly run 64 | * 65 | * @param day day of the month 66 | * @param hour hour of the day 67 | * 68 | * @return Initialized AHLaunchJobSchedule object 69 | */ 70 | + (instancetype)monthlyRunOnDay:(NSInteger)day hour:(NSInteger)hour; 71 | 72 | @end 73 | -------------------------------------------------------------------------------- /AHLaunchCtl/AHLaunchJobSchedule.m: -------------------------------------------------------------------------------- 1 | // 2 | // NSDateComponents+AHLaunchCtlSchedule.m 3 | // AHLaunchCtl 4 | // 5 | // Created by Eldon on 4/26/14. 6 | // Copyright (c) 2014 Eldon Ahrold. All rights reserved. 7 | // 8 | 9 | #import "AHLaunchJobSchedule.h" 10 | NSInteger AHUndefinedScheduleComponent = NSUndefinedDateComponent; 11 | 12 | @implementation AHLaunchJobSchedule 13 | 14 | - (instancetype)initWithDictionary:(NSDictionary *)dictionary { 15 | if (self = [super init]) { 16 | [dictionary enumerateKeysAndObjectsUsingBlock:^(NSString *key, 17 | NSNumber *obj, 18 | BOOL *stop) { 19 | if ([key.lowercaseString 20 | isEqualToString:NSStringFromSelector(@selector(minute))]) { 21 | self.minute = obj.integerValue; 22 | } else if ([key.lowercaseString 23 | isEqualToString:NSStringFromSelector( 24 | @selector(hour))]) { 25 | self.hour = obj.integerValue; 26 | } else if ([key.lowercaseString 27 | isEqualToString:NSStringFromSelector( 28 | @selector(weekday))]) { 29 | self.weekday = obj.integerValue; 30 | } else if ([key.lowercaseString 31 | isEqualToString:NSStringFromSelector( 32 | @selector(weekday))]) { 33 | self.day = obj.integerValue; 34 | } else if ([key.lowercaseString 35 | isEqualToString:NSStringFromSelector( 36 | @selector(month))]) { 37 | self.month = obj.integerValue; 38 | } 39 | }]; 40 | } 41 | return self; 42 | } 43 | 44 | - (NSString *)description { 45 | return self.dictionary.description; 46 | } 47 | 48 | - (NSDictionary *)dictionary { 49 | NSMutableDictionary *dict = 50 | [[NSMutableDictionary alloc] initWithCapacity:5]; 51 | 52 | if (self.minute != AHUndefinedScheduleComponent) { 53 | dict[@"Minute"] = @(self.minute); 54 | } 55 | 56 | if (self.hour != AHUndefinedScheduleComponent) { 57 | dict[@"Hour"] = @(self.hour); 58 | } 59 | 60 | if (self.day != AHUndefinedScheduleComponent) { 61 | dict[@"Day"] = @(self.day); 62 | } 63 | 64 | if (self.weekday != AHUndefinedScheduleComponent) { 65 | dict[@"Weekday"] = @(self.weekday); 66 | } 67 | 68 | if (self.month != AHUndefinedScheduleComponent) { 69 | dict[@"Month"] = @(self.month); 70 | } 71 | 72 | return [NSDictionary dictionaryWithDictionary:dict]; 73 | } 74 | 75 | + (instancetype)scheduleWithMinute:(NSInteger)minute 76 | hour:(NSInteger)hour 77 | day:(NSInteger)day 78 | weekday:(NSInteger)weekday 79 | month:(NSInteger)month { 80 | AHLaunchJobSchedule *components = [AHLaunchJobSchedule new]; 81 | 82 | if (minute != AHUndefinedScheduleComponent) { 83 | components.minute = minute; 84 | } 85 | if (hour != AHUndefinedScheduleComponent) { 86 | components.hour = hour; 87 | } 88 | if (day != AHUndefinedScheduleComponent) { 89 | components.day = day; 90 | } 91 | if (weekday != AHUndefinedScheduleComponent) { 92 | components.weekday = weekday; 93 | } 94 | if (month != AHUndefinedScheduleComponent) { 95 | components.month = month; 96 | } 97 | return components; 98 | } 99 | 100 | + (instancetype)dailyRunAtHour:(NSInteger)hour minute:(NSInteger)minute { 101 | return [self scheduleWithMinute:minute 102 | hour:hour 103 | day:AHUndefinedScheduleComponent 104 | weekday:AHUndefinedScheduleComponent 105 | month:AHUndefinedScheduleComponent]; 106 | } 107 | 108 | + (instancetype)weeklyRunOnWeekday:(NSInteger)weekday hour:(NSInteger)hour { 109 | return [self scheduleWithMinute:00 110 | hour:hour 111 | day:AHUndefinedScheduleComponent 112 | weekday:weekday 113 | month:AHUndefinedScheduleComponent]; 114 | } 115 | 116 | + (instancetype)monthlyRunOnDay:(NSInteger)day hour:(NSInteger)hour { 117 | return [self scheduleWithMinute:00 118 | hour:hour 119 | day:day 120 | weekday:AHUndefinedScheduleComponent 121 | month:AHUndefinedScheduleComponent]; 122 | } 123 | 124 | @end 125 | -------------------------------------------------------------------------------- /AHLaunchCtl/AHServiceManagement.h: -------------------------------------------------------------------------------- 1 | // AHServiceManagement.h 2 | // AHLaunchCtl 3 | // 4 | // Copyright (c) 2014 Eldon Ahrold ( https://github.com/eahrold/AHLaunchCtl ) 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | // THE SOFTWARE. 23 | 24 | #import 25 | #import 26 | 27 | /** ~/Library/LaunchAgents/ 28 | * @warning Make sure to expand the tilde if necissary for checking! 29 | */ 30 | extern NSString *const kAHUserLaunchAgentTildeDirectory; 31 | 32 | /** /Library/LaunchDaemons/ */ 33 | extern NSString *const kAHGlobalLaunchDaemonDirectory; 34 | 35 | /** /Library/LaunchAgents/ */ 36 | extern NSString *const kAHGlobalLaunchAgentDirectory; 37 | 38 | /** /System/Library/LaunchDaemons/ */ 39 | extern NSString *const kAHSystemLaunchDaemonDirectory; 40 | 41 | /** /System/Library/LaunchAgents/ */ 42 | extern NSString *const kAHSystemLaunchAgentDirectory; 43 | 44 | 45 | 46 | /** 47 | * Function to test whether the job is currently running 48 | * 49 | * @param label Label of the LaunchD job 50 | * @param domain AHLaunchDomain 51 | * 52 | * @return YES if Loaded, NO otherwise 53 | */ 54 | extern BOOL jobIsRunning(NSString *label, AHLaunchDomain domain); 55 | 56 | /** 57 | * More expensive version of jobIsRunning(), but will not cause "launchadd: FAILURE:" messages to appear in the system.log when the job is not loaded. 58 | * 59 | * @param label Label of the LaunchD job 60 | * @param domain AHLaunchDomain 61 | * 62 | * @return YES if Loaded, NO otherwise 63 | */ 64 | extern BOOL jobIsRunning2(NSString *label, AHLaunchDomain domain); 65 | 66 | #pragma mark - Bridged ServiceManagement 67 | 68 | /** 69 | * Dictionary representation of the LaunchD job 70 | * 71 | * @param domain Domain of the job 72 | * @param label Label of the job 73 | * 74 | * @return Populated dictionary if job exists 75 | */ 76 | extern NSDictionary *AHJobCopyDictionary(AHLaunchDomain domain, 77 | NSString *label); 78 | 79 | /** 80 | * Array of Jobs 81 | * 82 | * @param domain Domain to get jobs from 83 | * 84 | * @return Array of Jobs 85 | */ 86 | extern NSArray *AHCopyAllJobDictionaries(AHLaunchDomain domain); 87 | 88 | /** 89 | * Submit a job to load 90 | * 91 | * @param domain Domain for the job 92 | * @param dictionary Job object and keys 93 | * @param authRef Authorization data for job 94 | * @param error Pointer to error to populate should one occur 95 | * 96 | * @return YES if job was successfully loaded, NO otherwise 97 | */ 98 | extern BOOL AHJobSubmit(AHLaunchDomain domain, 99 | NSDictionary *dictionary, 100 | AuthorizationRef authRef, 101 | NSError **error); 102 | 103 | /** 104 | * Submit a job to load and create requisite files for persistence. 105 | * 106 | * @param domain Domain for the job 107 | * @param dictionary Job object and keys 108 | * @param authRef Authorization data for job 109 | * @param error Pointer to error to populate should one occur 110 | * 111 | * @return YES if job was successfully loaded, NO otherwise 112 | */ 113 | BOOL AHJobSubmitCreatingFile(AHLaunchDomain domain, 114 | NSDictionary *dictionary, 115 | AuthorizationRef authRef, 116 | NSError *__autoreleasing *error); 117 | 118 | /** 119 | * Remove a loaded job 120 | * 121 | * @param domain Domain for the job 122 | * @param label Label of the job 123 | * @param authRef Authorization data for job 124 | * @param error Pointer to error to populate should one occur 125 | * 126 | * @return YES if job was successfully loaded, NO otherwise 127 | */ 128 | extern BOOL AHJobRemove(AHLaunchDomain domain, 129 | NSString *label, 130 | AuthorizationRef authRef, 131 | NSError **error); 132 | 133 | /** 134 | * Remove a loaded job from the registry and delete the file. 135 | * 136 | * @param domain Domain for the job 137 | * @param label Label of the job 138 | * @param authRef Authorization data for job 139 | * @param error Pointer to error to populate should one occur 140 | * 141 | * @return YES if job was successfully loaded, NO otherwise 142 | */ 143 | extern BOOL AHJobRemoveIncludingFile(AHLaunchDomain domain, 144 | NSString *label, 145 | AuthorizationRef authRef, 146 | NSError **error); 147 | 148 | /** 149 | * Submits the executable for the given label as a launchd job. 150 | * @param domain Domain for the job 151 | * @param label Label of the executable 152 | * @param authRef Authorization data for job 153 | * @param error Pointer to error to populate should one occur 154 | * 155 | * @return YES if job was successfully loaded, NO otherwise 156 | */ 157 | extern BOOL AHJobBless(AHLaunchDomain domain, 158 | NSString *label, 159 | AuthorizationRef authRef, 160 | NSError **error); 161 | 162 | /** 163 | * Removes the executable, and unloads the job. 164 | * @param domain Domain for the job 165 | * @param label Label of the executable 166 | * @param authRef Authorization data for job 167 | * @param error Pointer to error to populate should one occur 168 | * 169 | * @return YES if job was successfully loaded, NO otherwise 170 | */ 171 | extern BOOL AHJobUnbless(AHLaunchDomain domain, 172 | NSString *label, 173 | AuthorizationRef authRef, 174 | NSError **error); 175 | -------------------------------------------------------------------------------- /AHLaunchCtl/AHServiceManagement.m: -------------------------------------------------------------------------------- 1 | // 2 | // AHServiceManagement.m 3 | // AHLaunchCtl 4 | // 5 | // Created by Eldon on 2/16/14. 6 | // Copyright (c) 2014 Eldon Ahrold. All rights reserved. 7 | // 8 | 9 | #import "AHServiceManagement.h" 10 | #import "AHServiceManagement_Private.h" 11 | #import "AHLaunchJob.h" 12 | 13 | #import 14 | 15 | /** ~/Library/LaunchAgents/ */ 16 | NSString *const kAHUserLaunchAgentTildeDirectory = @"~/Library/LaunchAgents/"; 17 | 18 | /** /Library/LaunchDaemons/ */ 19 | NSString *const kAHGlobalLaunchDaemonDirectory = @"/Library/LaunchDaemons/"; 20 | 21 | /** /Library/LaunchAgents/ */ 22 | NSString *const kAHGlobalLaunchAgentDirectory = @"/Library/LaunchAgents/"; 23 | 24 | /** /System/Library/LaunchDaemons/ */ 25 | NSString *const kAHSystemLaunchDaemonDirectory = 26 | @"/System/Library/LaunchDaemons/"; 27 | 28 | /** /System/Library/LaunchAgents/ */ 29 | NSString *const kAHSystemLaunchAgentDirectory = 30 | @"/System/Library/LaunchAgents/"; 31 | 32 | static NSString *const kAHChownJobPrefix = @"com.eeaapps.ahlaunchctl.chown"; 33 | static NSString *const kAHCopyJobPrefix = @"com.eeaapps.ahlaunchctl.copy"; 34 | static NSString *const kAHRemoveJobPrefix = @"com.eeaapps.ahlaunchctl.remove"; 35 | 36 | BOOL jobIsRunning(NSString *label, AHLaunchDomain domain) { 37 | NSDictionary *dict = AHJobCopyDictionary(domain, label); 38 | return dict ? YES : NO; 39 | } 40 | 41 | BOOL jobIsRunning2(NSString *label, AHLaunchDomain domain) { 42 | NSArray *runningJobs = AHCopyAllJobDictionaries(domain); 43 | NSPredicate *check = [NSPredicate 44 | predicateWithFormat:@"%K == %@", NSStringFromSelector(@selector(Label)), 45 | label]; 46 | 47 | return ([runningJobs filteredArrayUsingPredicate:check].count > 0); 48 | } 49 | 50 | NSDictionary *AHJobCopyDictionary(AHLaunchDomain domain, NSString *label) { 51 | NSDictionary *dict; 52 | if (label && domain != 0) { 53 | dict = CFBridgingRelease( 54 | SMJobCopyDictionary((__bridge CFStringRef)(SMDomain(domain)), 55 | (__bridge CFStringRef)(label))); 56 | return dict; 57 | } else { 58 | return nil; 59 | } 60 | } 61 | 62 | BOOL AHJobSubmit(AHLaunchDomain domain, 63 | NSDictionary *dictionary, 64 | AuthorizationRef authRef, 65 | NSError *__autoreleasing *error) { 66 | CFErrorRef cfError; 67 | if (domain == 0) return NO; 68 | cfError = NULL; 69 | 70 | BOOL rc = 71 | SMJobSubmit((__bridge CFStringRef)(SMDomain(domain)), 72 | (__bridge CFDictionaryRef)dictionary, authRef, &cfError); 73 | 74 | if (!rc) { 75 | NSError *err = CFBridgingRelease(cfError); 76 | if (error) *error = err; 77 | } 78 | 79 | return rc; 80 | } 81 | 82 | BOOL AHJobSubmitCreatingFile(AHLaunchDomain domain, 83 | NSDictionary *dictionary, 84 | AuthorizationRef authRef, 85 | NSError *__autoreleasing *error) { 86 | BOOL success = NO; 87 | 88 | if ((success = AHJobSubmit(domain, dictionary, authRef, error))) { 89 | success = 90 | AHCreatePrivilegedLaunchdPlist(domain, dictionary, authRef, error); 91 | } 92 | return success; 93 | } 94 | 95 | BOOL AHJobRemove(AHLaunchDomain domain, 96 | NSString *label, 97 | AuthorizationRef authRef, 98 | NSError *__autoreleasing *error) { 99 | CFErrorRef cfError; 100 | if (domain == 0) return NO; 101 | cfError = NULL; 102 | 103 | BOOL rc = 104 | SMJobRemove((__bridge CFStringRef)(SMDomain(domain)), 105 | (__bridge CFStringRef)(label), authRef, YES, &cfError); 106 | 107 | if (!rc) { 108 | NSError *err = CFBridgingRelease(cfError); 109 | if (error) *error = err; 110 | } 111 | return rc; 112 | } 113 | 114 | extern BOOL AHJobRemoveIncludingFile(AHLaunchDomain domain, 115 | NSString *label, 116 | AuthorizationRef authRef, 117 | NSError **error) { 118 | BOOL success = NO; 119 | 120 | if ((success = AHJobRemove(domain, label, authRef, error))) { 121 | success = AHRemovePrivilegedFile(domain, launchdJobFile(label, domain), 122 | authRef, error); 123 | } 124 | return success; 125 | } 126 | 127 | BOOL AHJobBless(AHLaunchDomain domain, 128 | NSString *label, 129 | AuthorizationRef authRef, 130 | NSError *__autoreleasing *error) { 131 | if (domain == 0) return NO; 132 | 133 | CFErrorRef cfError = NULL; 134 | BOOL rc = NO; 135 | 136 | if (jobIsRunning(label, domain)) { 137 | AHJobUnbless(domain, label, authRef, error); 138 | } 139 | 140 | rc = SMJobBless(kSMDomainSystemLaunchd, (__bridge CFStringRef)(label), 141 | authRef, &cfError); 142 | if (!rc) { 143 | NSError *err = CFBridgingRelease(cfError); 144 | if (error) *error = err; 145 | } 146 | return rc; 147 | } 148 | 149 | BOOL AHJobUnbless(AHLaunchDomain domain, 150 | NSString *label, 151 | AuthorizationRef authRef, 152 | NSError *__autoreleasing *error) { 153 | if (domain == 0) return NO; 154 | 155 | CFErrorRef cfError = NULL; 156 | BOOL success = NO; 157 | 158 | success = AHJobRemove(domain, label, authRef, error); 159 | 160 | // Remove the launchd plist 161 | NSString *const launchJobFile = launchdJobFile(label, domain); 162 | if (!AHRemovePrivilegedFile(domain, launchJobFile, authRef, error)) { 163 | NSLog(@"There was a problem removing the launchd.plist of the helper " 164 | @"tool."); 165 | } 166 | 167 | // Remove the helper tool binary 168 | NSString *const privilegedToolBinary = [@"/Library/PrivilegedHelperTools/" 169 | stringByAppendingPathComponent:label]; 170 | 171 | if (!AHRemovePrivilegedFile(domain, privilegedToolBinary, authRef, error)) { 172 | NSLog(@"There was a problem removing binary file of the helper tool."); 173 | } 174 | 175 | if (!success) { 176 | NSError *err = CFBridgingRelease(cfError); 177 | if (error) *error = err; 178 | } 179 | return success; 180 | } 181 | 182 | NSArray *AHCopyAllJobDictionaries(AHLaunchDomain domain) { 183 | return CFBridgingRelease( 184 | SMCopyAllJobDictionaries((__bridge CFStringRef)(SMDomain(domain)))); 185 | } 186 | 187 | #pragma mark Private 188 | BOOL AHCreatePrivilegedLaunchdPlist(AHLaunchDomain domain, 189 | NSDictionary *dictionary, 190 | AuthorizationRef authRef, 191 | NSError *__autoreleasing *error) { 192 | BOOL success = NO; 193 | 194 | AHLaunchJob *copyJob; 195 | AHLaunchJob *chownJob; 196 | 197 | // File path to for the launchd.plist 198 | NSString *filePath; 199 | 200 | // tmp file path the current under privileged user has access to 201 | NSString *tmpFilePath; 202 | 203 | NSFileManager *fileManager = [NSFileManager new]; 204 | NSString *label = dictionary[@"Label"]; 205 | 206 | if (label.length) { 207 | filePath = launchdJobFile(label, domain); 208 | 209 | NSDictionary *launchPlistPermissions = 210 | @{NSFilePosixPermissions : [NSNumber numberWithShort:0644]}; 211 | 212 | if (getuid() == 0) { 213 | if ((success = [dictionary writeToFile:filePath atomically:YES])) { 214 | success = [fileManager setAttributes:launchPlistPermissions 215 | ofItemAtPath:filePath 216 | error:error]; 217 | } 218 | } else { 219 | tmpFilePath = [@"/tmp" 220 | stringByAppendingPathComponent: 221 | [[NSProcessInfo processInfo] globallyUniqueString]]; 222 | 223 | if ([dictionary writeToFile:tmpFilePath atomically:YES]) { 224 | [fileManager setAttributes:launchPlistPermissions 225 | ofItemAtPath:tmpFilePath 226 | error:nil]; 227 | 228 | copyJob = [AHLaunchJob new]; 229 | copyJob.Label = 230 | [kAHCopyJobPrefix stringByAppendingPathExtension:label]; 231 | copyJob.ProgramArguments = 232 | @[ @"/bin/mv", @"-f", tmpFilePath, filePath ]; 233 | copyJob.RunAtLoad = YES; 234 | copyJob.LaunchOnlyOnce = YES; 235 | 236 | if ((success = 237 | AHJobSubmit(kAHGlobalLaunchDaemon, copyJob.dictionary, 238 | authRef, error))) { 239 | [NSThread sleepForTimeInterval:0.5]; 240 | 241 | // This should exit fast. If it's still alive unload it. 242 | if (jobIsRunning2(copyJob.Label, kAHGlobalLaunchDaemon)) { 243 | AHJobRemove(kAHGlobalLaunchDaemon, copyJob.Label, 244 | authRef, nil); 245 | } 246 | 247 | chownJob = [AHLaunchJob new]; 248 | chownJob.Label = [kAHChownJobPrefix 249 | stringByAppendingPathExtension:label]; 250 | chownJob.ProgramArguments = 251 | @[ @"/usr/sbin/chown", @"root:wheel", filePath ]; 252 | chownJob.RunAtLoad = YES; 253 | chownJob.LaunchOnlyOnce = YES; 254 | 255 | success = AHJobSubmit(kAHGlobalLaunchDaemon, 256 | chownJob.dictionary, authRef, error); 257 | // This should exit fast. If it's still alive unload it. 258 | if (jobIsRunning2(chownJob.Label, kAHGlobalLaunchDaemon)) { 259 | AHJobRemove(kAHGlobalLaunchDaemon, chownJob.Label, 260 | authRef, nil); 261 | } 262 | } 263 | } 264 | } 265 | } 266 | return success; 267 | } 268 | 269 | BOOL AHRemovePrivilegedFile(AHLaunchDomain domain, 270 | NSString *filePath, 271 | AuthorizationRef authRef, 272 | NSError *__autoreleasing *error) { 273 | BOOL success = YES; 274 | NSFileManager *fm = [NSFileManager defaultManager]; 275 | 276 | if ([fm fileExistsAtPath:filePath]) { 277 | if (getuid() == 0) { 278 | [fm removeItemAtPath:filePath error:error]; 279 | } else { 280 | NSString *const label = [kAHRemoveJobPrefix 281 | stringByAppendingPathExtension:filePath.lastPathComponent]; 282 | 283 | AHLaunchJob *removeJob = [AHLaunchJob new]; 284 | removeJob.Label = label; 285 | removeJob.ProgramArguments = @[ @"/bin/rm", filePath ]; 286 | 287 | removeJob.RunAtLoad = YES; 288 | removeJob.LaunchOnlyOnce = YES; 289 | 290 | if ((success = AHJobSubmit(kAHGlobalLaunchDaemon, 291 | removeJob.dictionary, 292 | authRef, 293 | error))) { 294 | 295 | [NSThread sleepForTimeInterval:0.5]; 296 | } 297 | 298 | // This should exit fast. If it's still alive unload it. 299 | if (jobIsRunning2(label, kAHGlobalLaunchDaemon)) { 300 | AHJobRemove(kAHGlobalLaunchDaemon, label, authRef, nil); 301 | } 302 | } 303 | } 304 | return success; 305 | } 306 | 307 | NSString *launchdJobFileDirectory(AHLaunchDomain domain) { 308 | NSString *type; 309 | switch (domain) { 310 | case kAHGlobalLaunchAgent: 311 | type = kAHGlobalLaunchAgentDirectory; 312 | break; 313 | case kAHGlobalLaunchDaemon: 314 | type = kAHGlobalLaunchDaemonDirectory; 315 | break; 316 | case kAHSystemLaunchAgent: 317 | type = kAHSystemLaunchAgentDirectory; 318 | break; 319 | case kAHSystemLaunchDaemon: 320 | type = kAHSystemLaunchDaemonDirectory; 321 | break; 322 | case kAHUserLaunchAgent: 323 | default: 324 | type = 325 | kAHUserLaunchAgentTildeDirectory.stringByExpandingTildeInPath; 326 | break; 327 | } 328 | return type; 329 | } 330 | 331 | NSString *launchdJobFile(NSString *label, AHLaunchDomain domain) { 332 | NSString *file; 333 | if (domain == 0 || !label) return nil; 334 | file = [launchdJobFileDirectory(domain) 335 | stringByAppendingPathComponent: 336 | [label stringByAppendingPathExtension:@"plist"]]; 337 | 338 | return file; 339 | } 340 | 341 | NSString *SMDomain(AHLaunchDomain domain) { 342 | if (domain > kAHGlobalLaunchAgent) { 343 | return (__bridge NSString *)kSMDomainSystemLaunchd; 344 | } else { 345 | return (__bridge NSString *)kSMDomainUserLaunchd; 346 | } 347 | } -------------------------------------------------------------------------------- /AHLaunchCtl/AHServiceManagement_Private.h: -------------------------------------------------------------------------------- 1 | // 2 | // AHServiceManagement_Private.h 3 | // AHLaunchCtl 4 | // 5 | // Created by Eldon on 2/21/15. 6 | // Copyright (c) 2015 Eldon Ahrold. All rights reserved. 7 | // 8 | 9 | #ifndef AHLaunchCtl_AHServiceManagement_Private_h 10 | #define AHLaunchCtl_AHServiceManagement_Private_h 11 | 12 | /** 13 | * Directory path for launchd.plist files based on the supplied domain. 14 | * 15 | * @param domain AHLaunchDomain. 16 | * 17 | * @return Directory path for domain. 18 | */ 19 | extern NSString *launchdJobFileDirectory(AHLaunchDomain domain); 20 | 21 | /** 22 | * Get the file path to the launchd.plist based on the supplied domain. 23 | * 24 | * @param label label of the launchd plist 25 | * @param domain AHLaunchDomain 26 | * 27 | * @return File path to the launchd.plist 28 | */ 29 | extern NSString *launchdJobFile(NSString *label, AHLaunchDomain domain); 30 | 31 | /** 32 | * Convert an AHLaunchDomain to an Service Management domain 33 | * 34 | * @param domain AHLaunchDomain 35 | * 36 | * @return the Service management domain to pass into SM functions. 37 | */ 38 | extern NSString *SMDomain(AHLaunchDomain domain); 39 | 40 | BOOL AHCreatePrivilegedLaunchdPlist(AHLaunchDomain domain, 41 | NSDictionary *dictionary, 42 | AuthorizationRef authRef, 43 | NSError *__autoreleasing *error); 44 | 45 | BOOL AHRemovePrivilegedFile(AHLaunchDomain domain, 46 | NSString *filePath, 47 | AuthorizationRef authRef, 48 | NSError *__autoreleasing *error); 49 | 50 | #endif 51 | -------------------------------------------------------------------------------- /AHLaunchCtl/NSFileManger+Privileged.h: -------------------------------------------------------------------------------- 1 | // 2 | // NSFileManager+authorized.h 3 | // AHLaunchCtl 4 | // 5 | // Created by Eldon on 2/19/14. 6 | // Copyright (c) 2014 Eldon Ahrold. All rights reserved. 7 | // 8 | 9 | #import 10 | #import "AHLaunchCtl.h" 11 | 12 | /** 13 | * A Category for NSFileManger that extends it to perform privileged operations using AHLaunchCtl library. 14 | */ 15 | @interface NSFileManager (Privileged) 16 | /** 17 | * Move a file to an protected path 18 | * 19 | * @param path source file 20 | * @param location destination folder 21 | * @param message message to show when prompting rights 22 | * @param overwrite YES to overwrite, NO to ignore 23 | * @param error populate should error occur 24 | * 25 | * @return YES on success NO on failure 26 | */ 27 | - (BOOL)moveItemAtPath:(NSString *)path 28 | toPrivilegedLocation:(NSString *)location 29 | message:(NSString *)message 30 | overwrite:(BOOL)overwrite 31 | error:(NSError **)error; 32 | /** 33 | * Move a file to an protected path 34 | * 35 | * @param path source file 36 | * @param location destination folder 37 | * @param overwrite YES to overwrite, NO to ignore 38 | * @param error populate should error occur 39 | * 40 | * @return YES on success NO on failure 41 | */ 42 | - (BOOL)moveItemAtPath:(NSString *)path 43 | toPrivilegedLocation:(NSString *)location 44 | overwrite:(BOOL)overwrite 45 | error:(NSError **)error; 46 | 47 | /** 48 | * Move a file to an protected path 49 | * 50 | * @param path source file 51 | * @param location destination folder 52 | * @param message message to show when prompting rights 53 | * @param overwrite YES to overwrite, NO to ignore 54 | * @param error populate should error occur 55 | * 56 | * @return YES on success NO on failure 57 | */ 58 | - (BOOL)copyItemAtPath:(NSString *)path 59 | toPrivilegedLocation:(NSString *)location 60 | message:(NSString *)message 61 | overwrite:(BOOL)overwrite 62 | error:(NSError **)error; 63 | 64 | /** 65 | * Move a file to an protected path 66 | * 67 | * @param path source file 68 | * @param error populate should error occur 69 | * 70 | * @return YES on success NO on failure 71 | */ 72 | - (BOOL)deleteItemAtPrivilegedPath:(NSString *)path error:(NSError **)error; 73 | 74 | /** 75 | * Set permissions on a protected file 76 | * 77 | * @param attributes Dictionary of attributes. Conform to NSFileManger set 78 | *attribute keys 79 | * @param path file path 80 | * @param error populate should error occur 81 | * 82 | * @return YES on success NO on failure 83 | */ 84 | - (BOOL)setAttributes:(NSDictionary *)attributes ofItemAtPrivilegedPath:(NSString *)path error:(NSError **)error; 85 | 86 | @end 87 | -------------------------------------------------------------------------------- /AHLaunchCtl/NSFileManger+Privileged.m: -------------------------------------------------------------------------------- 1 | // 2 | // NSFileManager+authorized.m 3 | // AHLaunchCtl 4 | // 5 | // Created by Eldon on 2/19/14. 6 | // Copyright (c) 2014 Eldon Ahrold. All rights reserved. 7 | // 8 | 9 | #import "NSFileManger+Privileged.h" 10 | #import "AHAuthorizer.h" 11 | #import "AHServiceManagement.h" 12 | 13 | static NSString *kNSFileManagerCopyFile = @"com.eeaapps.launchctl.filecopy"; 14 | static NSString *kNSFileManagerMoveFile = @"com.eeaapps.launchctl.filemove"; 15 | static NSString *kNSFileManagerDeleteFile = @"com.eeaapps.launchctl.filedelete"; 16 | 17 | static NSString *kNSFileManagerChownFile = @"com.eeaapps.launchctl.filechown"; 18 | static NSString *kNSFileManagerChmodFile = @"com.eeaapps.launchctl.filechmod"; 19 | 20 | static NSString *kNSFileManagerErrFileNotDirectory = 21 | @"You specified a file not a directory"; 22 | static NSString *kNSFileManagerErrDirectoryNotFile = 23 | @"You specified a directory not a file"; 24 | static NSString *kNSFileManagerErrNoFileAtLocation = 25 | @"there was no file found at specified location"; 26 | static NSString *kNSFileManagerErrNoDirectoryAtLocation = 27 | @"there was no folder found at that location"; 28 | 29 | @implementation NSFileManager (Privileged) 30 | 31 | - (BOOL)moveItemAtPath:(NSString *)path 32 | toPrivilegedLocation:(NSString *)location 33 | message:(NSString *)message 34 | overwrite:(BOOL)overwrite 35 | error:(NSError *__autoreleasing *)error { 36 | AHLaunchJob *job; 37 | if (![self testSource:path andDest:location error:error]) { 38 | } 39 | 40 | AuthorizationRef authRef = NULL; 41 | [AHAuthorizer authorizeSystemDaemonWithLabel:kNSFileManagerMoveFile 42 | prompt:message 43 | authRef:&authRef]; 44 | if (authRef == NULL) { 45 | return NO; 46 | }; 47 | 48 | job = [self FileManagerJob]; 49 | job.Label = kNSFileManagerMoveFile; 50 | job.ProgramArguments = @[ @"/bin/mv", path, location ]; 51 | 52 | return AHJobSubmit(kAHSystemLaunchDaemon, job.dictionary, authRef, error) == 0; 53 | } 54 | 55 | - (BOOL)moveItemAtPath:(NSString *)path 56 | toPrivilegedLocation:(NSString *)location 57 | overwrite:(BOOL)overwrite 58 | error:(NSError *__autoreleasing *)error { 59 | return [self moveItemAtPath:path 60 | toPrivilegedLocation:location 61 | message:@"Trying to copy a file to a privileged location" 62 | overwrite:overwrite 63 | error:error]; 64 | } 65 | 66 | - (BOOL)copyItemAtPath:(NSString *)path 67 | toPrivilegedLocation:(NSString *)location 68 | message:(NSString *)message 69 | overwrite:(BOOL)overwrite 70 | error:(NSError **)error { 71 | AHLaunchJob *job; 72 | if (![self testSource:path andDest:location error:error]) { 73 | return NO; 74 | } 75 | 76 | AuthorizationRef authRef = NULL; 77 | [AHAuthorizer authorizeSystemDaemonWithLabel:kNSFileManagerCopyFile 78 | prompt:message 79 | authRef:&authRef]; 80 | if (authRef == NULL) { 81 | return NO; 82 | }; 83 | job = [self FileManagerJob]; 84 | job.Label = kNSFileManagerCopyFile; 85 | job.ProgramArguments = @[ @"/bin/cp", @"-a", path, location ]; 86 | 87 | return AHJobSubmit(kAHSystemLaunchDaemon, job.dictionary, authRef, error); 88 | } 89 | 90 | - (BOOL)copyItemAtPath:(NSString *)path 91 | toPrivilegedLocation:(NSString *)location 92 | overwrite:(BOOL)overwrite 93 | error:(NSError **)error { 94 | return [self copyItemAtPath:path 95 | toPrivilegedLocation:location 96 | message:@"Trying to copy a file to a privileged location" 97 | overwrite:overwrite 98 | error:error]; 99 | } 100 | 101 | - (BOOL)deleteItemAtPrivilegedPath:(NSString *)path 102 | error:(NSError *__autoreleasing *)error { 103 | AHLaunchJob *job; 104 | BOOL rc = NO; 105 | if (![self fileExistsAtPath:path]) { 106 | return NO; 107 | } 108 | 109 | AuthorizationRef authRef = NULL; 110 | [AHAuthorizer authorizeSystemDaemonWithLabel:kNSFileManagerDeleteFile 111 | prompt:@"Trying to remove a file " 112 | @"from a privileged location" 113 | authRef:&authRef]; 114 | if (authRef == NULL) { 115 | return NO; 116 | }; 117 | 118 | job = [self FileManagerJob]; 119 | job.Label = kNSFileManagerDeleteFile; 120 | job.ProgramArguments = @[ @"/bin/rm", path ]; 121 | 122 | rc = AHJobSubmit(kAHSystemLaunchDaemon, job.dictionary, authRef, error); 123 | [AHAuthorizer authorizationFree:authRef]; 124 | return rc; 125 | } 126 | 127 | - (BOOL)setAttributes:(NSDictionary *)attributes 128 | ofItemAtPrivilegedPath:(NSString *)path 129 | error:(NSError *__autoreleasing *)error { 130 | AHLaunchJob *job; 131 | BOOL rc = NO; 132 | if ([self fileExistsAtPath:path isDirectory:nil]) { 133 | AuthorizationRef authRef = NULL; 134 | [AHAuthorizer 135 | authorizeSystemDaemonWithLabel:kNSFileManagerChownFile 136 | prompt:@"Trying to modify attributes of a " 137 | @"file in a privileged location" 138 | authRef:&authRef]; 139 | if (authRef == NULL) { 140 | return NO; 141 | }; 142 | 143 | NSNumber *permissions = attributes[NSFilePosixPermissions]; 144 | NSString *owner = attributes[NSFileOwnerAccountName]; 145 | NSString *group = attributes[NSFileGroupOwnerAccountName]; 146 | 147 | NSMutableString *chown; 148 | if (permissions) { 149 | job = [self FileManagerJob]; 150 | 151 | job.Label = kNSFileManagerChmodFile; 152 | job.ProgramArguments = 153 | @[ @"/bin/chmod", [permissions stringValue], path ]; 154 | rc = AHJobSubmit( 155 | kAHSystemLaunchDaemon, job.dictionary, authRef, error); 156 | } 157 | 158 | if (owner || group) { 159 | job = [self FileManagerJob]; 160 | job.Label = kNSFileManagerChownFile; 161 | 162 | chown = [[NSMutableString alloc] 163 | initWithCapacity:owner.length + group.length + 1]; 164 | if (owner) [chown appendString:owner]; 165 | if (group) [chown appendFormat:@":%@", group]; 166 | 167 | job.ProgramArguments = @[ @"/usr/sbin/chown", chown, path ]; 168 | rc = AHJobSubmit( 169 | kAHSystemLaunchDaemon, job.dictionary, authRef, error); 170 | } 171 | [AHAuthorizer authorizationFree:authRef]; 172 | } 173 | return rc; 174 | } 175 | 176 | - (BOOL)testSource:(NSString *)source 177 | andDest:(NSString *)dest 178 | error:(NSError *__autoreleasing *)error { 179 | BOOL rc; 180 | if ([self fileExistsAtPath:source isDirectory:&rc]) { 181 | if (rc) { 182 | return 183 | [AHLaunchCtl errorWithMessage:kNSFileManagerErrDirectoryNotFile 184 | andCode:1 185 | error:error]; 186 | } 187 | } else { 188 | return [AHLaunchCtl errorWithMessage:kNSFileManagerErrNoFileAtLocation 189 | andCode:1 190 | error:error]; 191 | } 192 | if ([self fileExistsAtPath:dest isDirectory:&rc]) { 193 | if (!rc) { 194 | return 195 | [AHLaunchCtl errorWithMessage:kNSFileManagerErrFileNotDirectory 196 | andCode:1 197 | error:error]; 198 | } 199 | } else { 200 | return 201 | [AHLaunchCtl errorWithMessage:kNSFileManagerErrNoDirectoryAtLocation 202 | andCode:1 203 | error:error]; 204 | } 205 | return YES; 206 | } 207 | 208 | - (AHLaunchJob *)FileManagerJob { 209 | AHLaunchJob *job = [AHLaunchJob new]; 210 | job.LaunchOnlyOnce = YES; 211 | job.RunAtLoad = YES; 212 | return job; 213 | } 214 | @end 215 | -------------------------------------------------------------------------------- /AHLaunchCtl/NSString+ah_versionCompare.h: -------------------------------------------------------------------------------- 1 | // 2 | // NSString+valueCompare.h 3 | // AutoPkgr 4 | // 5 | // Created by Eldon Ahrold on 5/14/15. 6 | // Copyright 2015 Eldon Ahrold. 7 | // 8 | // Licensed under the Apache License, Version 2.0 (the "License"); 9 | // you may not use this file except in compliance with the License. 10 | // You may obtain a copy of the License at 11 | // 12 | // http://www.apache.org/licenses/LICENSE-2.0 13 | // 14 | // Unless required by applicable law or agreed to in writing, software 15 | // distributed under the License is distributed on an "AS IS" BASIS, 16 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 | // See the License for the specific language governing permissions and 18 | // limitations under the License. 19 | // 20 | 21 | #import 22 | 23 | @interface NSString (ah_versionCompare) 24 | 25 | - (NSComparisonResult)ah_compareToVersion:(NSString *)version; 26 | 27 | - (BOOL)ah_version_isGreaterThan:(NSString *)version; 28 | - (BOOL)ah_version_isGreaterThanOrEqualTo:(NSString *)version; 29 | - (BOOL)ah_version_isEqualTo:(NSString *)version; 30 | 31 | - (BOOL)ah_version_isLessThan:(NSString *)version; 32 | - (BOOL)ah_version_isLessThanOrEqualTo:(NSString *)version; 33 | 34 | @end 35 | -------------------------------------------------------------------------------- /AHLaunchCtl/NSString+ah_versionCompare.m: -------------------------------------------------------------------------------- 1 | // 2 | // NSString+valueCompare.m 3 | // AutoPkgr 4 | // 5 | // Created by Eldon Ahrold on 5/14/15. 6 | // Copyright 2015 Eldon Ahrold. 7 | // 8 | // Licensed under the Apache License, Version 2.0 (the "License"); 9 | // you may not use this file except in compliance with the License. 10 | // You may obtain a copy of the License at 11 | // 12 | // http://www.apache.org/licenses/LICENSE-2.0 13 | // 14 | // Unless required by applicable law or agreed to in writing, software 15 | // distributed under the License is distributed on an "AS IS" BASIS, 16 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 | // See the License for the specific language governing permissions and 18 | // limitations under the License. 19 | // 20 | 21 | #import "NSString+ah_versionCompare.h" 22 | 23 | @implementation NSString (lcVersionCompare) 24 | 25 | - (NSComparisonResult)ah_compareToVersion:(NSString *)version 26 | { 27 | // Break version into fields (separated by '.') 28 | NSMutableArray *selfArray = [[self componentsSeparatedByString:@"."] mutableCopy]; 29 | NSMutableArray *versionArray = [[version componentsSeparatedByString:@"."] mutableCopy]; 30 | 31 | // Balence the number of digits to compare 32 | if (selfArray.count < versionArray.count) { 33 | while ([selfArray count] != [versionArray count]) { 34 | [selfArray addObject:@"0"]; 35 | } 36 | } else if (versionArray.count){ 37 | while ([selfArray count] >= [versionArray count]) { 38 | [versionArray addObject:@"0"]; 39 | }; 40 | } 41 | 42 | __block NSComparisonResult results = NSOrderedSame; 43 | [selfArray enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) { 44 | results = [selfArray[idx] compare:versionArray[idx] options:NSNumericSearch]; 45 | if (results != NSOrderedSame) { 46 | *stop = YES; 47 | } 48 | }]; 49 | 50 | return results; 51 | } 52 | 53 | - (BOOL)ah_version_isGreaterThan:(NSString *)b 54 | { 55 | return ([self ah_compareToVersion:b] == NSOrderedDescending); 56 | } 57 | 58 | - (BOOL)ah_version_isGreaterThanOrEqualTo:(NSString *)b 59 | { 60 | NSComparisonResult res = [self ah_compareToVersion:b]; 61 | return ((res == NSOrderedDescending) || (res == NSOrderedSame)); 62 | } 63 | 64 | - (BOOL)ah_version_isEqualTo:(NSString *)b 65 | { 66 | return ([self ah_compareToVersion:b] == NSOrderedSame); 67 | } 68 | 69 | - (BOOL)ah_version_isLessThan:(NSString *)b 70 | { 71 | return ([self ah_compareToVersion:b] == NSOrderedAscending); 72 | } 73 | 74 | - (BOOL)ah_version_isLessThanOrEqualTo:(NSString *)b 75 | { 76 | NSComparisonResult res = [self ah_compareToVersion:b]; 77 | return ((res == NSOrderedAscending) || (res == NSOrderedSame)); 78 | } 79 | 80 | @end 81 | -------------------------------------------------------------------------------- /HelperTool-CodeSign.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | '''Place this file in a folder named 'scripts' at 4 | the root of your project. 5 | 6 | Add this next line (including quotes) as a "Run Script" 7 | right after Target Dependencies 8 | 9 | "${PROJECT_DIR}/scripts/helper-tool-codesing-config.py" 10 | ''' 11 | 12 | import os 13 | import subprocess 14 | import plistlib 15 | 16 | '''If the helper tool's root folder is in a folder named something other 17 | than helper At the root of your project make the adjustments here''' 18 | HELPER_INFO = 'helper/helper-Info.plist' 19 | 20 | HELPER_LAUNCHD = 'helper/helper-Launchd.plist' 21 | 22 | '''the name for the helper tool will be default to this format com.your.app.helper 23 | which is the main apps bundleID with a .helper extension. if the executable you're 24 | insatlling is something other than that specify it here''' 25 | HELPER_NAME_OVERRIDE = '' 26 | 27 | TMP_FILENAME = 'code_sign_tmp_file' 28 | 29 | def get_code_sing_ident(build_dir): 30 | '''Get code signing identity''' 31 | identity = os.getenv('CODE_SIGN_IDENTITY') 32 | check_var(identity) 33 | 34 | _file = os.path.join(build_dir, TMP_FILENAME) 35 | 36 | open(_file, 'w').close() 37 | result = subprocess.check_output(['codesign', '--force', '--sign', identity, _file]) 38 | result = subprocess.check_output(['codesign', '-d', '-r', '-', _file]) 39 | 40 | cid = result.split("=> ")[1] 41 | cid = cid.rstrip(os.linesep) 42 | 43 | os.remove(_file) 44 | 45 | return cid 46 | 47 | 48 | def edit_app_info_plist(cert_id, app_name): 49 | '''Edit app info.plist with correct cert identity''' 50 | app_info_plist = os.getenv('PRODUCT_SETTINGS_PATH') 51 | 52 | check_var(app_info_plist) 53 | try: 54 | plist = plistlib.readPlist(app_info_plist) 55 | 56 | 57 | bundle_id = plist['CFBundleIdentifier'].split("$")[0] 58 | 59 | if not HELPER_NAME_OVERRIDE: 60 | helper_id = ''.join([bundle_id, app_name, '.helper']) 61 | else: 62 | helper_id = HELPER_NAME_OVERRIDE 63 | 64 | csstring = cert_id.replace(TMP_FILENAME, helper_id) 65 | plist['SMPrivilegedExecutables'] = {helper_id:csstring} 66 | except Exception: 67 | print "There is No Info.plist for them main app, somethings really wrong" 68 | exit(1) 69 | 70 | plistlib.writePlist(plist, app_info_plist) 71 | return bundle_id, helper_id 72 | 73 | 74 | def edit_helper_info_plist(cert_id, project_path, bundle_id, app_name): 75 | '''Edit the helper.plist file to match the cert identity''' 76 | helper_info_plist = os.path.join(project_path, HELPER_INFO) 77 | check_var(helper_info_plist) 78 | 79 | app_id = ''.join([bundle_id, app_name]) 80 | csstring = cert_id.replace(TMP_FILENAME, app_id) 81 | 82 | try: 83 | plist = plistlib.readPlist(helper_info_plist) 84 | plist['SMAuthorizedClients'] = [csstring] 85 | except Exception: 86 | print "There is No Info.plist for helper tool, somethings really wrong" 87 | exit(1) 88 | 89 | 90 | plistlib.writePlist(plist, helper_info_plist) 91 | 92 | 93 | def edit_helper_launchd(project_path, helper_id): 94 | '''Edit helper launchd''' 95 | helper_launchd_plist = os.path.join(project_path, HELPER_LAUNCHD) 96 | check_var(helper_launchd_plist) 97 | try: 98 | plist = plistlib.readPlist(helper_launchd_plist) 99 | plist['Label'] = helper_id 100 | plist['MachServices'] = {helper_id:True} 101 | except Exception: 102 | plist = {'Label':helper_id, 'MachServices':{helper_id:True}} 103 | 104 | plistlib.writePlist(plist, helper_launchd_plist) 105 | 106 | 107 | 108 | def check_var(var): 109 | '''Check for empty string or None''' 110 | if var == "" or var == None: 111 | exit(1) 112 | 113 | def main(): 114 | '''main''' 115 | # Configure info from environment 116 | build_dir = os.getenv('BUILT_PRODUCTS_DIR') 117 | project_path = os.getenv('PROJECT_DIR') 118 | app_name = os.getenv('PRODUCT_NAME') 119 | 120 | # Get the existing cert values 121 | cs_ident = get_code_sing_ident(build_dir) 122 | 123 | # write out to the helper tool 124 | bundle_id, helper_id = edit_app_info_plist(cs_ident, app_name) 125 | edit_helper_info_plist(cs_ident, project_path, bundle_id, app_name) 126 | edit_helper_launchd(project_path, helper_id) 127 | 128 | if __name__ == "__main__": 129 | main() 130 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014 Eldon Ahrold ( https://github.com/eahrold/AHLaunchCtl ) 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | #AHLaunchCtl 2 | Objective-C library for managing launchd Daemons & Agents. 3 | 4 | ## Usage: 5 | * [Add Job](#add-job) 6 | * [Remove Job](#remove-job) 7 | * [Load Job](#load-job) 8 | * [Unload Job](#unload-job) 9 | * [Scheduling](#scheduling) 10 | 11 | - 12 | ####*__Notes__* 13 | * There are five members of AHLaunchDomain representing the common locations of LaunchDaemons and LaunchAgents. 14 | 15 | ```objective-c 16 | /* User Launch Agents `~/Library/LaunchAgents`. Loaded by the console user.*/ 17 | kAHUserLaunchAgent, 18 | 19 | /* Administrator provided LaunchAgents `/Library/LaunchAgents/`. Loaded by the console user */ 20 | kAHGlobalLaunchAgent, 21 | 22 | /* Apple provided LaunchAgents `/System/Library/LaunchAgents/`. Loaded by root user.*/ 23 | kAHSystemLaunchAgent, 24 | 25 | /* Administrator provided LaunchDaemon `/Library/LaunchDaemons/`. Loaded by root user.*/ 26 | kAHGlobalLaunchDaemon, 27 | 28 | /* Apple provided LaunchDaemon `/System/Library/LaunchDaemons/`. Loaded by root user.*/ 29 | kAHSystemLaunchDaemon, 30 | ``` 31 | 32 | - 33 | ### Add Job 34 | This will load a job and create the launchd.plist file in the appropriate location. 35 | 36 | ```objective-c 37 | AHLaunchJob* job = [AHLaunchJob new]; 38 | job.Program = @"/bin/echo"; 39 | job.Label = @"com.eeaapps.echo"; 40 | job.ProgramArguments = @[@"/bin/echo", @"hello world!"]; 41 | job.StandardOutPath = @"/tmp/hello.txt"; 42 | job.RunAtLoad = YES; 43 | job.StartCalendarInterval = [AHLaunchJobSchedule dailyRunAtHour:2 minute:00]; 44 | 45 | // All sharedController methods return BOOL values. 46 | // `YES` for success, `NO` on failure (which will also populate an NSError). 47 | [[AHLaunchCtl sharedController] add:job 48 | toDomain:kAHUserLaunchAgent 49 | error:&error]; 50 | ``` 51 | - 52 | ### Remove Job 53 | This will unload a job and remove associated launchd.plist file. 54 | ```objective-c 55 | [[AHLaunchCtl sharedController] remove:@"com.eeaapps.echo" 56 | fromDomain:kAHUserLaunchAgent 57 | error:&error]; 58 | ``` 59 | - 60 | ### Load Job 61 | Simply load a job, this is good for one off jobs you need executed. 62 | It will not create a launchd file, but it will run the specified launchd job as long as the user in logged in (for LaunchAgents) or until the system is rebooted (LaunchDaemons). 63 | ```objective-c 64 | AHLaunchJob* job = [AHLaunchJob new]; 65 | 66 | // build the job as you would for adding one ... 67 | 68 | [[AHLaunchCtl sharedController] load:job 69 | inDomain:kAHGlobalLaunchDaemon 70 | error:&error]; 71 | 72 | ``` 73 | - 74 | ### Unload Job 75 | Unload a job temporarily, this will not remove the launchd.plist file 76 | ```objective-c 77 | [[AHLaunchCtl sharedController] unload:@"com.eeaapps.echo.helloworld" 78 | inDomain:kAHGlobalLaunchDaemon 79 | error:&error]; 80 | ``` 81 | - 82 | ### Scheduling 83 | To set the StartCalendarInterval key in the job, use the AHLaunchJobSchedule class. 84 | 85 | ```objective-c 86 | + (instancetype)scheduleWithMinute:(NSInteger)minute 87 | hour:(NSInteger)hour 88 | day:(NSInteger)day 89 | weekday:(NSInteger)weekday 90 | month:(NSInteger)month 91 | ``` 92 | _Passing ```AHUndefinedScheduleComponent``` to any of the above parameters will make it behave like a wildcard for that parameter._ 93 | 94 | - 95 | 96 | **There are also some convenience methods** 97 | ```objective-c 98 | + (instancetype)dailyRunAtHour:(NSInteger)hour minute:(NSInteger)minute; 99 | + (instancetype)weeklyRunOnWeekday:(NSInteger)weekday hour:(NSInteger)hour; 100 | + (instancetype)monthlyRunOnDay:(NSInteger)day hour:(NSInteger)hour; 101 | 102 | ``` 103 | 104 | - 105 | #### Install Privileged Helper Tool 106 | Your helper tool must be properly code signed, and have an embedded Info.plist and Launchd.plist file.** 107 | ```objective-c 108 | NSError *error; 109 | [AHLaunchCtl installHelper:kYourHelperToolReverseDomain 110 | prompt:@"Install Helper?" 111 | error:&error]; 112 | if(error) 113 | NSLog(@"error: %@",error); 114 | ``` 115 | 116 | **_See the HelperTool-CodeSign.py script at the root of this repo, for more details, it's helpful for getting the proper certificate name and .plists created._ 117 | 118 | - 119 | 120 | _see the AHLaunchCtl.h for full usage._ 121 | 122 | 123 | -------------------------------------------------------------------------------- /rootTest/main.m: -------------------------------------------------------------------------------- 1 | // 2 | // main.m 3 | // rootTest 4 | // 5 | // Created by Eldon on 2/22/15. 6 | // Copyright (c) 2015 Eldon Ahrold. All rights reserved. 7 | // 8 | 9 | #import 10 | #import "AHLaunchCtl.h" 11 | 12 | int main(int argc, const char *argv[]) { 13 | @autoreleasepool { 14 | // insert code here... 15 | AHLaunchJob *_job = [[AHLaunchJob alloc] init]; 16 | NSLog(@"Check initialization. %@", _job); 17 | 18 | _job = [AHLaunchJob new]; 19 | _job.Label = @"com.eeaapps.echo.helloworld"; 20 | _job.ProgramArguments = @[ @"/bin/echo", @"hello world" ]; 21 | _job.StandardOutPath = @"/tmp/hello.txt"; 22 | _job.RunAtLoad = YES; 23 | 24 | NSLog(@"Check initialization after property set. %@", _job); 25 | 26 | NSError *error = nil; 27 | if (![[AHLaunchCtl sharedController] add:_job 28 | toDomain:kAHGlobalLaunchDaemon 29 | error:&error]) { 30 | NSLog(@"Error Adding Job: %@", error.localizedDescription); 31 | 32 | } else { 33 | NSLog(@"should be YES%@", _job); 34 | if (![[AHLaunchCtl sharedController] remove:_job.Label 35 | fromDomain:kAHGlobalLaunchDaemon 36 | error:&error]) { 37 | NSLog(@"Error Removing Job: %@", error.localizedDescription); 38 | } 39 | } 40 | } 41 | return 0; 42 | } 43 | --------------------------------------------------------------------------------