fix(launch-services): Use the same approach on all Mac OS X versions

This commit is contained in:
Ben Gotow 2015-02-09 16:15:08 -08:00
parent 70b260b285
commit 2fc89f7d6f
2 changed files with 127 additions and 284 deletions

View file

@ -2,7 +2,6 @@ exec = require('child_process').exec
fs = require('fs')
bundleIdentifier = 'com.inbox.edgehill'
launchServicesPlistPath = "#{process.env.HOME}/Library/Preferences/com.apple.LaunchServices/com.apple.launchservices.secure.plist"
module.exports =
class LaunchServices
@ -16,92 +15,48 @@ class LaunchServices
available: ->
@getPlatform() is 'darwin'
isYosemiteOrGreater: (callback) ->
fs.exists launchServicesPlistPath, (exists) =>
callback(exists)
getLaunchServicesPlistPath: (callback) ->
secure = "#{process.env.HOME}/Library/Preferences/com.apple.LaunchServices/com.apple.launchservices.secure.plist"
insecure = "#{process.env.HOME}/Library/Preferences/com.apple.LaunchServices.plist"
fs.exists secure, (exists) =>
if exists
callback(secure)
else
callback(insecure)
readDefaults: (callback) ->
return callback(@_defaults) if @_defaults
@isYosemiteOrGreater (result) =>
if result
@_readDefaultsSecure(callback)
else
@_readDefaultsPreYosemite(callback)
_readDefaultsSecure: (callback) ->
@secure = true
tmpPath = "#{launchServicesPlistPath}.#{Math.random()}"
exec "plutil -convert json \"#{launchServicesPlistPath}\" -o \"#{tmpPath}\"", (err, stdout, stderr) =>
return callback(err) if callback and err
fs.readFile tmpPath, (err, data) =>
@getLaunchServicesPlistPath (plistPath) =>
tmpPath = "#{plistPath}.#{Math.random()}"
exec "plutil -convert json \"#{plistPath}\" -o \"#{tmpPath}\"", (err, stdout, stderr) ->
return callback(err) if callback and err
try
data = JSON.parse(data)
callback(data['LSHandlers'], data)
fs.unlink(tmpPath)
catch e
callback(e) if callback and err
_readDefaultsPreYosemite: (callback) ->
@secure = false
exec "defaults read com.apple.launchservices LSHandlers", (err, stdout, stderr) =>
return callback(err) if callback and err
# Convert the defaults from Apple's plist format into
# JSON. It's nearly the same, just has different delimiters
plist = stdout.toString()
regex = /([a-zA-Z]*) = (.*);/
while (match = regex.exec(plist)) != null
[text, key, val] = match
val = "\"#{val}\"" unless val[0] is '"'
plist = plist.replace(text, "\"#{key}\":#{val},")
plist = plist.replace(/\(/g, '[')
plist = plist.replace(/\)/g, ']')
plist = plist.replace(/[\s]*,[\s]*\n[\s]*}/g, '\n}')
json = []
if plist.length > 0
json = JSON.parse(plist)
callback(json)
fs.readFile tmpPath, (err, data) ->
return callback(err) if callback and err
try
data = JSON.parse(data)
callback(data['LSHandlers'], data)
fs.unlink(tmpPath)
catch e
callback(e) if callback and err
writeDefaults: (defaults, callback) ->
@_defaults = defaults
if @secure
@_writeDefaultsSecure(defaults, callback)
else
@_writeDefaultsPreYosemite(defaults, callback)
@getLaunchServicesPlistPath (plistPath) ->
tmpPath = "#{plistPath}.#{Math.random()}"
exec "plutil -convert json \"#{plistPath}\" -o \"#{tmpPath}\"", (err, stdout, stderr) ->
return callback(err) if callback and err
try
data = fs.readFileSync(tmpPath)
data = JSON.parse(data)
data['LSHandlers'] = defaults
data = JSON.stringify(data)
fs.writeFileSync(tmpPath, data)
catch error
return callback(error) if callback and error
_writeDefaultsSecure: (newDefaults, callback) ->
@_readDefaultsSecure (currentDefaults, entireFileJSON) =>
entireFileJSON['LSHandlers'] = newDefaults
data = JSON.stringify(entireFileJSON)
tmpPath = "#{launchServicesPlistPath}.json"
fs.writeFile tmpPath, data, (err) =>
return callback(err) if callback and err
exec "plutil -convert binary1 \"#{tmpPath}\" -o \"#{launchServicesPlistPath}\"", =>
fs.unlink(tmpPath)
@triggerSystemReload(callback)
_writeDefaultsPreYosemite: (defaults, callback) ->
# Convert the defaults JSON back into Apple's json-like
# format. (I think it predates JSON?)
json = JSON.stringify(defaults)
plist = json.replace(/\[/g, '(')
plist = plist.replace(/\]/g, ')')
regex = /\"([a-zA-Z^"]*)\":\"([^"]*)\",?/
while (match = regex.exec(plist)) != null
[text, key, val] = match
plist = plist.replace(text, "#{key} = \"#{val}\";")
# Write the new defaults back to the system
exec "defaults write ~/Library/Preferences/com.apple.LaunchServices.plist LSHandlers '#{plist}'", (err, stdout, stderr) =>
return callback(err) if callback and err
@triggerSystemReload(callback)
triggerSystemReload: (callback) ->
exec "/System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister -kill -r -domain local -domain system -domain user", (err, stdout, stderr) ->
callback(err) if callback
exec "plutil -convert binary1 \"#{tmpPath}\" -o \"#{plistPath}\"", ->
fs.unlink(tmpPath)
exec "/System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister -kill -r -domain local -domain system -domain user", (err, stdout, stderr) ->
callback(err) if callback
isRegisteredForURLScheme: (scheme, callback) ->
throw new Error "isRegisteredForURLScheme is async, provide a callback" unless callback

View file

@ -2,19 +2,27 @@ _ = require 'underscore-plus'
proxyquire = require 'proxyquire'
stubDefaultsJSON = null
stubDefaults = null
execHitory = []
ChildProcess =
exec: (command, callback) ->
console.log(command)
execHitory.push(arguments)
if command is "defaults read com.apple.launchservices LSHandlers"
callback(null, stubDefaults, null)
else
callback(null, '', null)
callback(null, '', null)
fs =
exists: (path, callback) ->
callback(true)
readFile: (path, callback) ->
callback(null, JSON.stringify(stubDefaultsJSON))
readFileSync: (path) ->
JSON.stringify(stubDefaultsJSON)
writeFileSync: (path) ->
null
LaunchServices = proxyquire "../lib/launch-services",
"child_process": ChildProcess
"child_process": ChildProcess
"fs": fs
describe "LaunchServices", ->
beforeEach ->
@ -109,99 +117,7 @@ describe "LaunchServices", ->
LSHandlerURLScheme: 'mailto'
}
]
stubDefaults = """
(
{
LSHandlerRoleAll = "com.apple.dt.xcode";
LSHandlerURLScheme = xcdoc;
},
{
LSHandlerRoleAll = "com.fournova.tower";
LSHandlerURLScheme = "github-mac";
},
{
LSHandlerRoleAll = "com.fournova.tower";
LSHandlerURLScheme = sourcetree;
},
{
LSHandlerRoleAll = "com.google.chrome";
LSHandlerURLScheme = http;
},
{
LSHandlerRoleAll = "com.google.chrome";
LSHandlerURLScheme = https;
},
{
LSHandlerContentType = "public.html";
LSHandlerRoleViewer = "com.google.chrome";
},
{
LSHandlerContentType = "public.url";
LSHandlerRoleViewer = "com.google.chrome";
},
{
LSHandlerContentType = "com.apple.ical.backup";
LSHandlerRoleAll = "com.apple.ical";
},
{
LSHandlerContentTag = icalevent;
LSHandlerContentTagClass = "public.filename-extension";
LSHandlerRoleAll = "com.apple.ical";
},
{
LSHandlerContentTag = icaltodo;
LSHandlerContentTagClass = "public.filename-extension";
LSHandlerRoleAll = "com.apple.reminders";
},
{
LSHandlerRoleAll = "com.apple.ical";
LSHandlerURLScheme = webcal;
},
{
LSHandlerContentTag = coffee;
LSHandlerContentTagClass = "public.filename-extension";
LSHandlerRoleAll = "com.sublimetext.2";
},
{
LSHandlerRoleAll = "com.apple.facetime";
LSHandlerURLScheme = facetime;
},
{
LSHandlerRoleAll = "com.apple.dt.xcode";
LSHandlerURLScheme = xcdevice;
},
{
LSHandlerContentType = "public.png";
LSHandlerRoleAll = "com.macromedia.fireworks";
},
{
LSHandlerRoleAll = "com.apple.dt.xcode";
LSHandlerURLScheme = xcbot;
},
{
LSHandlerRoleAll = "com.microsoft.rdc.mac";
LSHandlerURLScheme = rdp;
},
{
LSHandlerContentTag = rdp;
LSHandlerContentTagClass = "public.filename-extension";
LSHandlerRoleAll = "com.microsoft.rdc.mac";
},
{
LSHandlerContentType = "public.json";
LSHandlerRoleAll = "com.sublimetext.2";
},
{
LSHandlerContentTag = cson;
LSHandlerContentTagClass = "public.filename-extension";
LSHandlerRoleAll = "com.sublimetext.2";
},
{
LSHandlerRoleAll = "com.apple.mail";
LSHandlerURLScheme = mailto;
}
)
"""
describe "when the platform is darwin", ->
beforeEach ->
@ -213,118 +129,90 @@ describe "LaunchServices", ->
it "should return true", ->
expect(@services.available()).toEqual(true)
describe "pre-Yosemite", ->
beforeEach ->
@services.isYosemiteOrGreater = (callback) -> callback(false)
describe "readDefaults", ->
describe "readDefaults", ->
it "should return the user defaults registered with the system via `defaults`", ->
response = null
runs ->
@services.readDefaults (defaults) ->
response = defaults
waitsFor ->
response
runs ->
expect(response).toEqual(stubDefaultsJSON)
describe "writeDefaults", ->
it "should `lsregister` to reload defaults after saving them", ->
callback = jasmine.createSpy('callback')
@services.writeDefaults(stubDefaultsJSON, callback)
callback.callCount is 1
command = execHitory[2][0]
expect(command).toBe("/System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister -kill -r -domain local -domain system -domain user")
describe "writeDefaults", ->
it "should covert the defaults to the plist format and call `defaults write`", ->
callback = jasmine.createSpy('callback')
@services.writeDefaults(stubDefaultsJSON, callback)
command = execHitory[0][0]
expect(command).toBe("""defaults write ~/Library/Preferences/com.apple.LaunchServices.plist LSHandlers '({LSHandlerRoleAll = "com.apple.dt.xcode";LSHandlerURLScheme = "xcdoc";},{LSHandlerRoleAll = "com.fournova.tower";LSHandlerURLScheme = "github-mac";},{LSHandlerRoleAll = "com.fournova.tower";LSHandlerURLScheme = "sourcetree";},{LSHandlerRoleAll = "com.google.chrome";LSHandlerURLScheme = "http";},{LSHandlerRoleAll = "com.google.chrome";LSHandlerURLScheme = "https";},{LSHandlerContentType = "public.html";LSHandlerRoleViewer = "com.google.chrome";},{LSHandlerContentType = "public.url";LSHandlerRoleViewer = "com.google.chrome";},{LSHandlerContentType = "com.apple.ical.backup";LSHandlerRoleAll = "com.apple.ical";},{LSHandlerContentTag = "icalevent";LSHandlerContentTagClass = "public.filename-extension";LSHandlerRoleAll = "com.apple.ical";},{LSHandlerContentTag = "icaltodo";LSHandlerContentTagClass = "public.filename-extension";LSHandlerRoleAll = "com.apple.reminders";},{LSHandlerRoleAll = "com.apple.ical";LSHandlerURLScheme = "webcal";},{LSHandlerContentTag = "coffee";LSHandlerContentTagClass = "public.filename-extension";LSHandlerRoleAll = "com.sublimetext.2";},{LSHandlerRoleAll = "com.apple.facetime";LSHandlerURLScheme = "facetime";},{LSHandlerRoleAll = "com.apple.dt.xcode";LSHandlerURLScheme = "xcdevice";},{LSHandlerContentType = "public.png";LSHandlerRoleAll = "com.macromedia.fireworks";},{LSHandlerRoleAll = "com.apple.dt.xcode";LSHandlerURLScheme = "xcbot";},{LSHandlerRoleAll = "com.microsoft.rdc.mac";LSHandlerURLScheme = "rdp";},{LSHandlerContentTag = "rdp";LSHandlerContentTagClass = "public.filename-extension";LSHandlerRoleAll = "com.microsoft.rdc.mac";},{LSHandlerContentType = "public.json";LSHandlerRoleAll = "com.sublimetext.2";},{LSHandlerContentTag = "cson";LSHandlerContentTagClass = "public.filename-extension";LSHandlerRoleAll = "com.sublimetext.2";},{LSHandlerRoleAll = "com.apple.mail";LSHandlerURLScheme = "mailto";})'""")
describe "isRegisteredForURLScheme", ->
it "should require a callback is provided", ->
expect( -> @services.isRegisteredForURLScheme('mailto')).toThrow()
it "should `lsregister` to reload defaults after saving them", ->
callback = jasmine.createSpy('callback')
@services.writeDefaults(stubDefaultsJSON, callback)
command = execHitory[1][0]
expect(command).toBe("/System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister -kill -r -domain local -domain system -domain user")
it "should return true if a matching `LSHandlerURLScheme` record exists for the bundle identifier", ->
spyOn(@services, 'readDefaults').andCallFake (callback) ->
callback([{
"LSHandlerRoleAll": "com.apple.dt.xcode",
"LSHandlerURLScheme": "xcdoc"
}, {
"LSHandlerContentTag": "cson",
"LSHandlerContentTagClass": "public.filename-extension",
"LSHandlerRoleAll": "com.sublimetext.2"
}, {
"LSHandlerRoleAll": "com.inbox.edgehill",
"LSHandlerURLScheme": "mailto"
}])
@services.isRegisteredForURLScheme 'mailto', (registered) ->
expect(registered).toBe(true)
describe "isRegisteredForURLScheme", ->
it "should require a callback is provided", ->
expect( -> @services.isRegisteredForURLScheme('mailto')).toThrow()
it "should return false when other records exist for the bundle identifier but do not match", ->
spyOn(@services, 'readDefaults').andCallFake (callback) ->
callback([{
LSHandlerRoleAll: "com.apple.dt.xcode",
LSHandlerURLScheme: "xcdoc"
},{
LSHandlerContentTag: "cson",
LSHandlerContentTagClass: "public.filename-extension",
LSHandlerRoleAll: "com.sublimetext.2"
},{
LSHandlerRoleAll: "com.inbox.edgehill",
LSHandlerURLScheme: "atom"
}])
@services.isRegisteredForURLScheme 'mailto', (registered) ->
expect(registered).toBe(false)
it "should return true if a matching `LSHandlerURLScheme` record exists for the bundle identifier", ->
stubDefaults = """
(
{
LSHandlerRoleAll = "com.apple.dt.xcode";
LSHandlerURLScheme = xcdoc;
},
{
LSHandlerContentTag = cson;
LSHandlerContentTagClass = "public.filename-extension";
LSHandlerRoleAll = "com.sublimetext.2";
},
{
LSHandlerRoleAll = "com.inbox.edgehill";
LSHandlerURLScheme = mailto;
}
)
"""
@services.isRegisteredForURLScheme 'mailto', (registered) ->
expect(registered).toBe(true)
it "should return false if another bundle identifier is registered for the `LSHandlerURLScheme`", ->
spyOn(@services, 'readDefaults').andCallFake (callback) ->
callback([{
LSHandlerRoleAll: "com.apple.dt.xcode",
LSHandlerURLScheme: "xcdoc"
},{
LSHandlerContentTag: "cson",
LSHandlerContentTagClass: "public.filename-extension",
LSHandlerRoleAll: "com.sublimetext.2"
},{
LSHandlerRoleAll: "com.apple.mail",
LSHandlerURLScheme: "mailto"
}])
@services.isRegisteredForURLScheme 'mailto', (registered) ->
expect(registered).toBe(false)
it "should return false when other records exist for the bundle identifier but do not match", ->
stubDefaults = """
(
{
LSHandlerRoleAll = "com.apple.dt.xcode";
LSHandlerURLScheme = xcdoc;
},
{
LSHandlerContentTag = cson;
LSHandlerContentTagClass = "public.filename-extension";
LSHandlerRoleAll = "com.sublimetext.2";
},
{
LSHandlerRoleAll = "com.inbox.edgehill";
LSHandlerURLScheme = atom;
}
)
"""
@services.isRegisteredForURLScheme 'mailto', (registered) ->
expect(registered).toBe(false)
describe "registerForURLScheme", ->
it "should remove any existing records for the `LSHandlerURLScheme`", ->
@services.registerForURLScheme 'mailto', =>
@services.readDefaults (values) ->
expect(JSON.stringify(values).indexOf('com.apple.mail')).toBe(-1)
it "should return false if another bundle identifier is registered for the `LSHandlerURLScheme`", ->
stubDefaults = """
(
{
LSHandlerRoleAll = "com.apple.dt.xcode";
LSHandlerURLScheme = xcdoc;
},
{
LSHandlerContentTag = cson;
LSHandlerContentTagClass = "public.filename-extension";
LSHandlerRoleAll = "com.sublimetext.2";
},
{
LSHandlerRoleAll = "com.apple.mail";
LSHandlerURLScheme = mailto;
}
)
"""
@services.isRegisteredForURLScheme 'mailto', (registered) ->
expect(registered).toBe(false)
it "should add a record for the `LSHandlerURLScheme` and the app's bundle identifier", ->
@services.registerForURLScheme 'mailto', =>
@services.readDefaults (defaults) ->
match = _.find defaults, (d) ->
d.LSHandlerURLScheme is 'mailto' and d.LSHandlerRoleAll is 'com.inbox.edgehill'
expect(match).not.toBe(null)
describe "registerForURLScheme", ->
it "should remove any existing records for the `LSHandlerURLScheme`", ->
@services.registerForURLScheme 'mailto', =>
@services.readDefaults (values) ->
expect(JSON.stringify(values).indexOf('com.apple.mail')).toBe(-1)
it "should add a record for the `LSHandlerURLScheme` and the app's bundle identifier", ->
@services.registerForURLScheme 'mailto', =>
@services.readDefaults (defaults) ->
match = _.find defaults, (d) ->
d.LSHandlerURLScheme is 'mailto' and d.LSHandlerRoleAll is 'com.inbox.edgehill'
expect(match).not.toBe(null)
it "should write the new defaults", ->
spyOn(@services, 'writeDefaults')
@services.registerForURLScheme('mailto')
expect(@services.writeDefaults).toHaveBeenCalled()
it "should write the new defaults", ->
spyOn(@services, 'readDefaults').andCallFake (callback) ->
callback([{
LSHandlerRoleAll: "com.apple.dt.xcode",
LSHandlerURLScheme: "xcdoc"
}])
spyOn(@services, 'writeDefaults')
@services.registerForURLScheme('mailto')
expect(@services.writeDefaults).toHaveBeenCalled()
describe "on other platforms", ->
describe "available", ->