rb-scpt 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGES +497 -0
- data/doc/aem-manual/01_introduction.html +60 -0
- data/doc/aem-manual/02_apioverview.html +107 -0
- data/doc/aem-manual/03_packingandunpackingdata.html +135 -0
- data/doc/aem-manual/04_references.html +409 -0
- data/doc/aem-manual/05_targetingapplications.html +164 -0
- data/doc/aem-manual/06_buildingandsendingevents.html +229 -0
- data/doc/aem-manual/07_findapp.html +63 -0
- data/doc/aem-manual/08_examples.html +94 -0
- data/doc/aem-manual/aemreferenceinheritance.gif +0 -0
- data/doc/aem-manual/index.html +56 -0
- data/doc/appscript-manual/01_introduction.html +94 -0
- data/doc/appscript-manual/02_aboutappscripting.html +247 -0
- data/doc/appscript-manual/03_quicktutorial.html +167 -0
- data/doc/appscript-manual/04_gettinghelp.html +188 -0
- data/doc/appscript-manual/05_keywordconversion.html +106 -0
- data/doc/appscript-manual/06_classesandenums.html +192 -0
- data/doc/appscript-manual/07_applicationobjects.html +211 -0
- data/doc/appscript-manual/08_realvsgenericreferences.html +96 -0
- data/doc/appscript-manual/09_referenceforms.html +241 -0
- data/doc/appscript-manual/10_referenceexamples.html +154 -0
- data/doc/appscript-manual/11_applicationcommands.html +245 -0
- data/doc/appscript-manual/12_commandexamples.html +138 -0
- data/doc/appscript-manual/13_performanceissues.html +142 -0
- data/doc/appscript-manual/14_notes.html +80 -0
- data/doc/appscript-manual/application_architecture.gif +0 -0
- data/doc/appscript-manual/application_architecture2.gif +0 -0
- data/doc/appscript-manual/finder_to_textedit_event.gif +0 -0
- data/doc/appscript-manual/index.html +62 -0
- data/doc/appscript-manual/relationships_example.gif +0 -0
- data/doc/appscript-manual/ruby_to_itunes_event.gif +0 -0
- data/doc/full.css +106 -0
- data/doc/index.html +45 -0
- data/doc/mactypes-manual/01_introduction.html +54 -0
- data/doc/mactypes-manual/02_aliasclass.html +124 -0
- data/doc/mactypes-manual/03_fileurlclass.html +126 -0
- data/doc/mactypes-manual/04_unitsclass.html +100 -0
- data/doc/mactypes-manual/index.html +53 -0
- data/doc/osax-manual/01_introduction.html +67 -0
- data/doc/osax-manual/02_interface.html +147 -0
- data/doc/osax-manual/03_examples.html +73 -0
- data/doc/osax-manual/04_notes.html +61 -0
- data/doc/osax-manual/index.html +53 -0
- data/doc/rb-appscript-logo.png +0 -0
- data/extconf.rb +65 -0
- data/rb-scpt.gemspec +14 -0
- data/sample/AB_export_vcard.rb +31 -0
- data/sample/AB_list_people_with_emails.rb +13 -0
- data/sample/Add_iCal_event.rb +21 -0
- data/sample/Create_daily_iCal_todos.rb +75 -0
- data/sample/Export_Address_Book_phone_numbers.rb +59 -0
- data/sample/Hello_world.rb +21 -0
- data/sample/List_iTunes_playlist_names.rb +11 -0
- data/sample/Make_Mail_message.rb +33 -0
- data/sample/Open_file_in_TextEdit.rb +13 -0
- data/sample/Organize_Mail_messages.rb +61 -0
- data/sample/Print_folder_tree.rb +16 -0
- data/sample/Select_all_HTML_files.rb +14 -0
- data/sample/Set_iChat_status.rb +24 -0
- data/sample/Simple_Finder_GUI_Scripting.rb +18 -0
- data/sample/Stagger_Finder_windows.rb +25 -0
- data/sample/TextEdit_demo.rb +130 -0
- data/sample/iTunes_top40_to_html.rb +71 -0
- data/src/SendThreadSafe.c +380 -0
- data/src/SendThreadSafe.h +139 -0
- data/src/lib/_aem/aemreference.rb +1022 -0
- data/src/lib/_aem/codecs.rb +662 -0
- data/src/lib/_aem/connect.rb +205 -0
- data/src/lib/_aem/encodingsupport.rb +77 -0
- data/src/lib/_aem/findapp.rb +85 -0
- data/src/lib/_aem/mactypes.rb +251 -0
- data/src/lib/_aem/send.rb +279 -0
- data/src/lib/_aem/typewrappers.rb +59 -0
- data/src/lib/_appscript/defaultterminology.rb +277 -0
- data/src/lib/_appscript/referencerenderer.rb +245 -0
- data/src/lib/_appscript/reservedkeywords.rb +116 -0
- data/src/lib/_appscript/safeobject.rb +249 -0
- data/src/lib/_appscript/terminology.rb +471 -0
- data/src/lib/aem.rb +253 -0
- data/src/lib/appscript.rb +1075 -0
- data/src/lib/kae.rb +1489 -0
- data/src/lib/osax.rb +659 -0
- data/src/rbae.c +979 -0
- data/test/README +3 -0
- data/test/test_aemreference.rb +118 -0
- data/test/test_appscriptcommands.rb +152 -0
- data/test/test_appscriptreference.rb +106 -0
- data/test/test_codecs.rb +186 -0
- data/test/test_findapp.rb +26 -0
- data/test/test_mactypes.rb +79 -0
- data/test/test_osax.rb +54 -0
- data/test/testall.sh +10 -0
- metadata +145 -0
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
|
|
3
|
+
# Selects all .htm/.html files in the top Finder window.
|
|
4
|
+
|
|
5
|
+
# Note: if using the appscript gem, rubygems must be required first:
|
|
6
|
+
begin; require 'rubygems'; rescue LoadError; end
|
|
7
|
+
|
|
8
|
+
require "appscript"
|
|
9
|
+
include Appscript
|
|
10
|
+
|
|
11
|
+
finder = app('Finder')
|
|
12
|
+
finder.activate
|
|
13
|
+
folder = finder.Finder_windows[1].target.get
|
|
14
|
+
folder.files[its.name_extension.is_in(['htm', 'html'])].select
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
|
|
3
|
+
# Set iChat's status message to the name of the currently selected
|
|
4
|
+
# iTunes track.
|
|
5
|
+
#
|
|
6
|
+
# Based on an AppleScript example from:
|
|
7
|
+
# <http://developer.apple.com/cocoa/applescriptforapps.html>
|
|
8
|
+
|
|
9
|
+
# Note: if using the appscript gem, rubygems must be required first:
|
|
10
|
+
begin; require 'rubygems'; rescue LoadError; end
|
|
11
|
+
|
|
12
|
+
require "appscript"
|
|
13
|
+
include Appscript
|
|
14
|
+
|
|
15
|
+
begin
|
|
16
|
+
track_name = app("iTunes").current_track.name.get
|
|
17
|
+
rescue CommandError => e
|
|
18
|
+
if e.to_i == -1728 # Can't get reference.
|
|
19
|
+
track_name = 'No track selected.'
|
|
20
|
+
else
|
|
21
|
+
raise
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
app("iChat").status_message.set(track_name)
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
|
|
3
|
+
# Opens a new Finder smart folder that searches for 'ruby', via GUI Scripting
|
|
4
|
+
|
|
5
|
+
# (Note: to use GUI Scripting, 'Enable access for assistive devices' option must
|
|
6
|
+
# be enabled in the Universal Access panel of System Preferences.)
|
|
7
|
+
|
|
8
|
+
# Note: if using the appscript gem, rubygems must be required first:
|
|
9
|
+
begin; require 'rubygems'; rescue LoadError; end
|
|
10
|
+
|
|
11
|
+
require 'appscript'
|
|
12
|
+
include Appscript
|
|
13
|
+
|
|
14
|
+
se = app('System Events')
|
|
15
|
+
|
|
16
|
+
app('Finder').activate
|
|
17
|
+
se.keystroke('n', :using=>[:command_down, :option_down])
|
|
18
|
+
se.keystroke('ruby')
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
|
|
3
|
+
# Rearranges Finder windows diagonally across screen with title bars one above
|
|
4
|
+
# another. (Could easily be adapted to work with any scriptable application
|
|
5
|
+
# that uses standard window class terminology.)
|
|
6
|
+
|
|
7
|
+
# Note: if using the appscript gem, rubygems must be required first:
|
|
8
|
+
begin; require 'rubygems'; rescue LoadError; end
|
|
9
|
+
|
|
10
|
+
require "appscript"
|
|
11
|
+
include Appscript
|
|
12
|
+
|
|
13
|
+
x, y = 0, 44
|
|
14
|
+
offset = 22
|
|
15
|
+
|
|
16
|
+
# Get list of window references, ignoring any minimised windows
|
|
17
|
+
window_list = app('Finder').windows[its.collapsed.not].get
|
|
18
|
+
|
|
19
|
+
# Move windows while preserving their original sizes
|
|
20
|
+
window_list.reverse.each do |window|
|
|
21
|
+
x1, y1, x2, y2 = window.bounds.get
|
|
22
|
+
window.bounds.set([x, y, x2 - x1 + x, y2 - y1 + y])
|
|
23
|
+
x += offset
|
|
24
|
+
y += offset
|
|
25
|
+
end
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
|
|
3
|
+
# Demonstrates various references and commands in action.
|
|
4
|
+
|
|
5
|
+
# Note: if using the appscript gem, rubygems must be required first:
|
|
6
|
+
begin; require 'rubygems'; rescue LoadError; end
|
|
7
|
+
|
|
8
|
+
require "appscript"
|
|
9
|
+
include Appscript
|
|
10
|
+
|
|
11
|
+
textedit = app('TextEdit') # get an application object for TextEdit
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
# tell application "TextEdit" to activate
|
|
15
|
+
textedit.activate
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
# tell application "TextEdit" to make new document at end of documents
|
|
19
|
+
textedit.documents.end.make(:new => :document)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
# tell application "TextEdit" to set text of document 1 to "Hello World\n"
|
|
23
|
+
textedit.documents[1].text.set('Hello World\n')
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
# tell application "TextEdit" to get a reference to every window
|
|
27
|
+
p textedit.windows
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
# tell application "TextEdit" to get a reference to document 1
|
|
31
|
+
p textedit.documents
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
# tell application "TextEdit" to get every document
|
|
35
|
+
p textedit.documents.get
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
# tell application "TextEdit" to get a reference to document 1
|
|
39
|
+
p textedit.documents[1]
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
# tell application "TextEdit" to get document 1
|
|
43
|
+
p textedit.documents[1].get
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
# tell application "TextEdit" to get window id 3210
|
|
47
|
+
# p textedit.windows.ID(3210).get
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
# tell application "TextEdit" to get a reference to text of document 1
|
|
51
|
+
p textedit.documents[1].text
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
# tell application "TextEdit" to get text of document 1
|
|
55
|
+
p textedit.documents[1].text.get
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
# tell application "TextEdit" to make new document at end of documents
|
|
59
|
+
textedit.documents.end.make(:new => :document)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
# tell application "TextEdit" to set text of document 1 to "Happy Happy Joy Joy\n"
|
|
63
|
+
textedit.documents[1].text.set("Happy Happy Joy Joy\n")
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
# tell application "TextEdit" to get text of every document
|
|
67
|
+
p textedit.documents.text.get
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
# tell application "TextEdit" to count each word of text of document 1
|
|
71
|
+
p textedit.documents[1].text.count(:each => :word)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
# tell application "TextEdit" to get words 3 thru -1 of document 1
|
|
75
|
+
p textedit.documents[1].words[3, -1].get
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
# tell application "TextEdit" to set size of character 1 of every word
|
|
79
|
+
# of document 1 to 24
|
|
80
|
+
textedit.documents[1].words.characters[1].size.set(24)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
# tell application "TextEdit" to set color of any word of document 1 to {65535, 0, 0}
|
|
84
|
+
textedit.documents[1].words.any.color.set([65535, 0, 0])
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
# tell application "TextEdit" to make new paragraph at
|
|
88
|
+
# (after last paragraph of text of document 1) with data "Silly Rabbit\n"
|
|
89
|
+
textedit.documents[1].text.paragraphs.last.after.make(
|
|
90
|
+
:new => :paragraph, :with_data => "Silly Rabbit\n")
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
# tell application "TextEdit" to get paragraph after paragraph 1 of document 1
|
|
94
|
+
p textedit.documents[1].paragraphs[1].next(:paragraph).get
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
# tell application "TextEdit" to make new document at end of documents
|
|
98
|
+
# with properties {text:"foo\nbar\n\n\nbaz\n\nfub\n"}
|
|
99
|
+
textedit.documents.end.make(:new => :document,
|
|
100
|
+
:with_properties => {:text => "foo\nbar\n\n\nbaz\n\nfub\n"})
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
# tell application "TextEdit" to get every paragraph of text of document 1
|
|
104
|
+
p textedit.documents[1].text.paragraphs.get
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
# tell application "TextEdit" to get every paragraph of document 1
|
|
108
|
+
# where it is not "\n" -- get non-empty paragraphs
|
|
109
|
+
p textedit.documents[1].paragraphs[its.ne("\n")].get
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
# tell application "TextEdit" to get text of every document
|
|
113
|
+
# whose text begins with "H"
|
|
114
|
+
p textedit.documents[its.text.begins_with('H')].text.get
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
# The following examples don't work in TextEdit but will work in,
|
|
119
|
+
# for example, Tex-Edit Plus:
|
|
120
|
+
#
|
|
121
|
+
# # tell application "Tex-Edit Plus" to get words (character 5)
|
|
122
|
+
# # thru (paragraph 9) of document 1
|
|
123
|
+
# p app('Tex-Edit Plus').documents[1] \
|
|
124
|
+
# .words[con.characters[5], con.paragraphs[9]].get
|
|
125
|
+
#
|
|
126
|
+
#
|
|
127
|
+
# # tell application "Tex-Edit Plus" to get every word of text
|
|
128
|
+
# # of document 1 whose color is {0, 0, 0}
|
|
129
|
+
# p app('Tex-Edit Plus').documents[1].text.paragraphs \
|
|
130
|
+
# [its.color.equals((0, 0, 0))].get
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
|
|
3
|
+
# This script renders the track and album names of the top 40 most played
|
|
4
|
+
# iTunes tracks to an HTML file, then opens it in Safari.
|
|
5
|
+
#
|
|
6
|
+
# Requires the Amrita templating engine: http://amrita.sourceforge.jp
|
|
7
|
+
|
|
8
|
+
# Note: if using the appscript gem, rubygems must be required first:
|
|
9
|
+
begin; require 'rubygems'; rescue LoadError; end
|
|
10
|
+
|
|
11
|
+
require 'appscript'
|
|
12
|
+
include Appscript
|
|
13
|
+
require 'osax'
|
|
14
|
+
require "amrita/template"
|
|
15
|
+
include Amrita
|
|
16
|
+
|
|
17
|
+
# Amrita HTML template
|
|
18
|
+
tmpl = TemplateText.new <<END
|
|
19
|
+
<html>
|
|
20
|
+
<head>
|
|
21
|
+
<title> iTunes Top 40 Tracks</title>
|
|
22
|
+
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
|
23
|
+
<style type="text/css" media="all">
|
|
24
|
+
body {padding:0; margin:0;}
|
|
25
|
+
table {color:black; background-color:#448; width:100%; padding:2px;}
|
|
26
|
+
th, td {padding:2px; border-width:0;}
|
|
27
|
+
th {color:#fff; background-color:#114;}
|
|
28
|
+
td {color:black; background-color:#bbd;}
|
|
29
|
+
</style>
|
|
30
|
+
</head>
|
|
31
|
+
<body>
|
|
32
|
+
<table border="1">
|
|
33
|
+
<thead>
|
|
34
|
+
<tr><th>#</th><th>Track</th><th>Album</th></tr>
|
|
35
|
+
</thead>
|
|
36
|
+
<tbody>
|
|
37
|
+
<tr id="table_row"><td id="table_column"></td></tr>
|
|
38
|
+
</tbody>
|
|
39
|
+
</table>
|
|
40
|
+
</body>
|
|
41
|
+
</html>
|
|
42
|
+
END
|
|
43
|
+
|
|
44
|
+
# Choose the file to write
|
|
45
|
+
sa = OSAX.osax('StandardAdditions')
|
|
46
|
+
out_file = sa.choose_file_name(:default_name=>'My-iTunes-Top-40.html')
|
|
47
|
+
|
|
48
|
+
# Get the played count, name and album for every track in iTunes
|
|
49
|
+
tracks = app('iTunes').library_playlists[1].tracks
|
|
50
|
+
info = tracks.played_count.get.zip(tracks.name.get, tracks.album.get)
|
|
51
|
+
# Extract the top 40 most played entries
|
|
52
|
+
top40 = info.sort.reverse[0, 40]
|
|
53
|
+
|
|
54
|
+
# Assemble input data for Amrita
|
|
55
|
+
data = {
|
|
56
|
+
:table_row=>top40.collect { |row_data| {:table_column=>row_data} }
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
# Render HTML file
|
|
60
|
+
tmpl.prettyprint = true
|
|
61
|
+
File.open(out_file.to_s, 'w') { |f| tmpl.expand(f, data) }
|
|
62
|
+
|
|
63
|
+
# Open file in Safari for viewing
|
|
64
|
+
# safari = app('Safari')
|
|
65
|
+
# safari.activate
|
|
66
|
+
# safari.open(out_file)
|
|
67
|
+
|
|
68
|
+
# Open file in default web browser for viewing
|
|
69
|
+
sa.open_location(out_file.url)
|
|
70
|
+
|
|
71
|
+
|
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
/*
|
|
2
|
+
File: AESendThreadSafe.c
|
|
3
|
+
|
|
4
|
+
Contains: Code to send Apple events in a thread-safe manner.
|
|
5
|
+
|
|
6
|
+
Written by: DTS
|
|
7
|
+
|
|
8
|
+
Copyright: Copyright (c) 2007 Apple Inc. All Rights Reserved.
|
|
9
|
+
|
|
10
|
+
Disclaimer: IMPORTANT: This Apple software is supplied to you by Apple Inc.
|
|
11
|
+
("Apple") in consideration of your agreement to the following
|
|
12
|
+
terms, and your use, installation, modification or
|
|
13
|
+
redistribution of this Apple software constitutes acceptance of
|
|
14
|
+
these terms. If you do not agree with these terms, please do
|
|
15
|
+
not use, install, modify or redistribute this Apple software.
|
|
16
|
+
|
|
17
|
+
In consideration of your agreement to abide by the following
|
|
18
|
+
terms, and subject to these terms, Apple grants you a personal,
|
|
19
|
+
non-exclusive license, under Apple's copyrights in this
|
|
20
|
+
original Apple software (the "Apple Software"), to use,
|
|
21
|
+
reproduce, modify and redistribute the Apple Software, with or
|
|
22
|
+
without modifications, in source and/or binary forms; provided
|
|
23
|
+
that if you redistribute the Apple Software in its entirety and
|
|
24
|
+
without modifications, you must retain this notice and the
|
|
25
|
+
following text and disclaimers in all such redistributions of
|
|
26
|
+
the Apple Software. Neither the name, trademarks, service marks
|
|
27
|
+
or logos of Apple Inc. may be used to endorse or promote
|
|
28
|
+
products derived from the Apple Software without specific prior
|
|
29
|
+
written permission from Apple. Except as expressly stated in
|
|
30
|
+
this notice, no other rights or licenses, express or implied,
|
|
31
|
+
are granted by Apple herein, including but not limited to any
|
|
32
|
+
patent rights that may be infringed by your derivative works or
|
|
33
|
+
by other works in which the Apple Software may be incorporated.
|
|
34
|
+
|
|
35
|
+
The Apple Software is provided by Apple on an "AS IS" basis.
|
|
36
|
+
APPLE MAKES NO WARRANTIES, EXPRESS OR IMPLIED, INCLUDING
|
|
37
|
+
WITHOUT LIMITATION THE IMPLIED WARRANTIES OF NON-INFRINGEMENT,
|
|
38
|
+
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE, REGARDING
|
|
39
|
+
THE APPLE SOFTWARE OR ITS USE AND OPERATION ALONE OR IN
|
|
40
|
+
COMBINATION WITH YOUR PRODUCTS.
|
|
41
|
+
|
|
42
|
+
IN NO EVENT SHALL APPLE BE LIABLE FOR ANY SPECIAL, INDIRECT,
|
|
43
|
+
INCIDENTAL OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
|
|
44
|
+
TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
|
45
|
+
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) ARISING IN ANY WAY
|
|
46
|
+
OUT OF THE USE, REPRODUCTION, MODIFICATION AND/OR DISTRIBUTION
|
|
47
|
+
OF THE APPLE SOFTWARE, HOWEVER CAUSED AND WHETHER UNDER THEORY
|
|
48
|
+
OF CONTRACT, TORT (INCLUDING NEGLIGENCE), STRICT LIABILITY OR
|
|
49
|
+
OTHERWISE, EVEN IF APPLE HAS BEEN ADVISED OF THE POSSIBILITY OF
|
|
50
|
+
SUCH DAMAGE.
|
|
51
|
+
|
|
52
|
+
Change History (most recent first):
|
|
53
|
+
|
|
54
|
+
$Log: AESendThreadSafe.c,v $
|
|
55
|
+
Revision 1.3 2007/02/27 10:45:15
|
|
56
|
+
In the destructor, add an assert that the thread-local storage has been set to NULL. Also fixed a comment typo.
|
|
57
|
+
|
|
58
|
+
Revision 1.2 2007/02/12 11:59:09
|
|
59
|
+
Added a type cast for the malloc result.
|
|
60
|
+
|
|
61
|
+
Revision 1.1 2007/02/09 10:55:24
|
|
62
|
+
First checked in.
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
*/
|
|
66
|
+
|
|
67
|
+
/*
|
|
68
|
+
|
|
69
|
+
2007/06/24 -- Modified by HAS to make AESendMessageThreadSafeSynchronous API-compatible with AESendMessage; renamed SendMessageThreadSafe.
|
|
70
|
+
|
|
71
|
+
*/
|
|
72
|
+
|
|
73
|
+
/////////////////////////////////////////////////////////////////
|
|
74
|
+
|
|
75
|
+
#include "SendThreadSafe.h"
|
|
76
|
+
|
|
77
|
+
#include <pthread.h>
|
|
78
|
+
#include <mach/mach.h>
|
|
79
|
+
|
|
80
|
+
/////////////////////////////////////////////////////////////////
|
|
81
|
+
|
|
82
|
+
/*
|
|
83
|
+
How It Works
|
|
84
|
+
------------
|
|
85
|
+
The basic idea behind this module is that it uses per-thread storage to keep
|
|
86
|
+
track of an Apple event reply for any given thread. The first time that the
|
|
87
|
+
thread calls AESendMessageThreadSafeSynchronous, the per-thread storage will
|
|
88
|
+
not be initialised and the code will grab an Apple event reply port and
|
|
89
|
+
assign it to the per-thread storage. Subsequent calls to AESendMessageThreadSafeSynchronous
|
|
90
|
+
will continue to use that port. When the thread dies, pthreads will automatically
|
|
91
|
+
call the destructor for the per-thread storage, and that will clean up the port.
|
|
92
|
+
|
|
93
|
+
Because we can't dispose of the reply port (without triggering the Apple
|
|
94
|
+
Event Manager bug that's the reason we wrote this code in the first place),
|
|
95
|
+
the destructor doesn't actually dispose of the port. Rather, it adds the
|
|
96
|
+
port to a pool of ports that are available for reuse. The next time a thread
|
|
97
|
+
needs to allocate a port, it will grab it from the pool rather than allocating
|
|
98
|
+
it from scratch.
|
|
99
|
+
|
|
100
|
+
This technique means that the code still 'leaks' Apple event reply ports, but
|
|
101
|
+
the size of the leak is limited to the maximum number of threads that you run
|
|
102
|
+
simultaneously. This isn't a problem in practice.
|
|
103
|
+
*/
|
|
104
|
+
|
|
105
|
+
/////////////////////////////////////////////////////////////////
|
|
106
|
+
|
|
107
|
+
// The PerThreadStorage structure is a trivial wrapper around the Mach port.
|
|
108
|
+
// I added this because I need to attach this structure a thread using
|
|
109
|
+
// per-thread storage. The API for that (<x-man-page://3/pthread_setspecific>)
|
|
110
|
+
// is pointer based. I could've just cast the Mach port to a (void *), but
|
|
111
|
+
// that's ugly because of a) pointer size issues (are pointers always bigger than
|
|
112
|
+
// ints?), and b) because it implies an equivalent between NULL and MACH_PORT_NULL.
|
|
113
|
+
// Given this, I simply decided to create a structure to wrap the Mach port.
|
|
114
|
+
|
|
115
|
+
enum {
|
|
116
|
+
kPerThreadStorageMagic = 'PTSm'
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
struct PerThreadStorage {
|
|
120
|
+
OSType magic; // must be kPerThreadStorageMagic
|
|
121
|
+
mach_port_t port;
|
|
122
|
+
};
|
|
123
|
+
typedef struct PerThreadStorage PerThreadStorage;
|
|
124
|
+
|
|
125
|
+
// The following static variables manage the per-thread storage key
|
|
126
|
+
// (sPerThreadStorageKey) and the pool of Mach ports (wrapped in
|
|
127
|
+
// PerThreadStorage structures) that are not currently attached to a thread.
|
|
128
|
+
|
|
129
|
+
static pthread_once_t sInited = PTHREAD_ONCE_INIT; // covers initialisation of all of the
|
|
130
|
+
// following static variables
|
|
131
|
+
|
|
132
|
+
static OSStatus sPerThreadStorageKeyInitErrNum; // latches result of initialisation
|
|
133
|
+
|
|
134
|
+
static pthread_key_t sPerThreadStorageKey = 0; // key for our per-thread storage
|
|
135
|
+
|
|
136
|
+
static pthread_mutex_t sPoolMutex; // protects sPool
|
|
137
|
+
static CFMutableArrayRef sPool; // array of (PerThreadStorage *), holds
|
|
138
|
+
// the ports that aren't currently bound to
|
|
139
|
+
// a thread
|
|
140
|
+
|
|
141
|
+
static void PerThreadStorageDestructor(void *keyValue); // forward declaration
|
|
142
|
+
|
|
143
|
+
static void InitRoutine(void)
|
|
144
|
+
// Call once (via pthread_once) to initialise various static variables.
|
|
145
|
+
{
|
|
146
|
+
OSStatus err;
|
|
147
|
+
|
|
148
|
+
// Create the per-thread storage key. Note that we assign a destructor to this key;
|
|
149
|
+
// pthreads call the destructor to clean up that item of per-thread storage whenever
|
|
150
|
+
// a thread terminates.
|
|
151
|
+
|
|
152
|
+
err = (OSStatus) pthread_key_create(&sPerThreadStorageKey, PerThreadStorageDestructor);
|
|
153
|
+
|
|
154
|
+
// Create the pool of Mach ports that aren't bound to any thread, and its associated
|
|
155
|
+
// lock. The pool starts out empty.
|
|
156
|
+
|
|
157
|
+
if (err == noErr) {
|
|
158
|
+
err = (OSStatus) pthread_mutex_init(&sPoolMutex, NULL);
|
|
159
|
+
}
|
|
160
|
+
if (err == noErr) {
|
|
161
|
+
sPool = CFArrayCreateMutable(NULL, 0, NULL);
|
|
162
|
+
if (sPool == NULL) {
|
|
163
|
+
err = coreFoundationUnknownErr;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
assert(err == 0);
|
|
167
|
+
|
|
168
|
+
sPerThreadStorageKeyInitErrNum = err;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
static OSStatus AllocatePortFromPool(PerThreadStorage **storagePtr)
|
|
172
|
+
// Grab a Mach port from sPool; if sPool is empty, create one.
|
|
173
|
+
{
|
|
174
|
+
OSStatus err;
|
|
175
|
+
OSStatus junk;
|
|
176
|
+
PerThreadStorage * storage;
|
|
177
|
+
|
|
178
|
+
assert( storagePtr != NULL);
|
|
179
|
+
assert(*storagePtr == NULL);
|
|
180
|
+
|
|
181
|
+
storage = NULL;
|
|
182
|
+
|
|
183
|
+
// First try to get an entry from pool. We try to grab the last one because
|
|
184
|
+
// that minimises the amount of copying that CFArrayRemoveValueAtIndex has to
|
|
185
|
+
// do.
|
|
186
|
+
|
|
187
|
+
err = (OSStatus) pthread_mutex_lock(&sPoolMutex);
|
|
188
|
+
if (err == noErr) {
|
|
189
|
+
CFIndex poolCount;
|
|
190
|
+
|
|
191
|
+
poolCount = CFArrayGetCount(sPool);
|
|
192
|
+
if (poolCount > 0) {
|
|
193
|
+
storage = (PerThreadStorage *) CFArrayGetValueAtIndex(sPool, poolCount - 1);
|
|
194
|
+
CFArrayRemoveValueAtIndex(sPool, poolCount - 1);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
junk = (OSStatus) pthread_mutex_unlock(&sPoolMutex);
|
|
198
|
+
assert(junk == noErr);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// If we failed to find an entry in the pool, create a new one.
|
|
202
|
+
|
|
203
|
+
if ( (err == noErr) && (storage == NULL) ) {
|
|
204
|
+
storage = (PerThreadStorage *) malloc(sizeof(*storage));
|
|
205
|
+
if (storage == NULL) {
|
|
206
|
+
err = memFullErr;
|
|
207
|
+
} else {
|
|
208
|
+
storage->magic = kPerThreadStorageMagic;
|
|
209
|
+
storage->port = MACH_PORT_NULL;
|
|
210
|
+
|
|
211
|
+
err = (OSStatus) mach_port_allocate(mach_task_self(), MACH_PORT_RIGHT_RECEIVE, &storage->port);
|
|
212
|
+
if (err != noErr) {
|
|
213
|
+
assert(storage->port == MACH_PORT_NULL);
|
|
214
|
+
free(storage);
|
|
215
|
+
storage = NULL;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
if (err == noErr) {
|
|
220
|
+
*storagePtr = storage;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
assert( (err == noErr) == (*storagePtr != NULL) );
|
|
224
|
+
assert( (*storagePtr == NULL) || ((*storagePtr)->magic == kPerThreadStorageMagic) );
|
|
225
|
+
assert( (*storagePtr == NULL) || ((*storagePtr)->port != MACH_PORT_NULL) );
|
|
226
|
+
|
|
227
|
+
return err;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
static void ReturnPortToPool(PerThreadStorage * storage)
|
|
231
|
+
// Returns a port to sPool.
|
|
232
|
+
{
|
|
233
|
+
OSStatus err;
|
|
234
|
+
|
|
235
|
+
assert(storage != NULL);
|
|
236
|
+
assert(storage->magic == kPerThreadStorageMagic);
|
|
237
|
+
assert(storage->port != MACH_PORT_NULL);
|
|
238
|
+
|
|
239
|
+
err = (OSStatus) pthread_mutex_lock(&sPoolMutex);
|
|
240
|
+
if (err == noErr) {
|
|
241
|
+
CFArrayAppendValue(sPool, storage);
|
|
242
|
+
|
|
243
|
+
err = (OSStatus) pthread_mutex_unlock(&sPoolMutex);
|
|
244
|
+
}
|
|
245
|
+
assert(err == noErr);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Main Thread Notes
|
|
249
|
+
// -----------------
|
|
250
|
+
// There are two reasons why we don't assign a reply port to the main thread.
|
|
251
|
+
// First, the main thread already has a reply port created for it by Apple
|
|
252
|
+
// Event Manager. Thus, we don't need a specific reply port. Also, the
|
|
253
|
+
// destructor for per-thread storage isn't called for the main thread, so
|
|
254
|
+
// we wouldn't get a chance to clean up (although that's not really a problem
|
|
255
|
+
// in practice).
|
|
256
|
+
|
|
257
|
+
static OSStatus BindReplyMachPortToThread(mach_port_t *replyPortPtr)
|
|
258
|
+
// Get a reply port for this thread, remembering that we've done this
|
|
259
|
+
// in per-thread storage.
|
|
260
|
+
//
|
|
261
|
+
// On success, *replyPortPtr is the port to use for this thread's reply
|
|
262
|
+
// port. It will be MACH_PORT_NULL if you call it from the main thread.
|
|
263
|
+
{
|
|
264
|
+
OSStatus err;
|
|
265
|
+
|
|
266
|
+
assert( replyPortPtr != NULL);
|
|
267
|
+
assert(*replyPortPtr == MACH_PORT_NULL);
|
|
268
|
+
|
|
269
|
+
// Initialise ourselves the first time that we're called.
|
|
270
|
+
|
|
271
|
+
err = (OSStatus) pthread_once(&sInited, InitRoutine);
|
|
272
|
+
|
|
273
|
+
// If something went wrong, return the latched error.
|
|
274
|
+
|
|
275
|
+
if ( (err == noErr) && (sPerThreadStorageKeyInitErrNum != noErr) ) {
|
|
276
|
+
err = sPerThreadStorageKeyInitErrNum;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Now do the real work.
|
|
280
|
+
|
|
281
|
+
if (err == noErr) {
|
|
282
|
+
if ( pthread_main_np() ) {
|
|
283
|
+
// This is the main thread, so do nothing; leave *replyPortPtr set
|
|
284
|
+
// to MACH_PORT_NULL.
|
|
285
|
+
assert(*replyPortPtr == MACH_PORT_NULL);
|
|
286
|
+
} else {
|
|
287
|
+
PerThreadStorage * storage;
|
|
288
|
+
|
|
289
|
+
// Get the per-thread storage for this thread.
|
|
290
|
+
|
|
291
|
+
storage = (PerThreadStorage *) pthread_getspecific(sPerThreadStorageKey);
|
|
292
|
+
if (storage == NULL) {
|
|
293
|
+
|
|
294
|
+
// The per-thread storage hasn't been allocated yet for this specific
|
|
295
|
+
// thread. Let's go allocate it and attach it to this thread.
|
|
296
|
+
|
|
297
|
+
err = AllocatePortFromPool(&storage);
|
|
298
|
+
if (err == noErr) {
|
|
299
|
+
err = (OSStatus) pthread_setspecific(sPerThreadStorageKey, (void *) storage);
|
|
300
|
+
if (err != noErr) {
|
|
301
|
+
ReturnPortToPool(storage);
|
|
302
|
+
storage = NULL;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
assert( (err == noErr) == (storage != NULL) );
|
|
307
|
+
|
|
308
|
+
// If all went well, copy the port out to our client.
|
|
309
|
+
|
|
310
|
+
if (err == noErr) {
|
|
311
|
+
assert(storage->magic == kPerThreadStorageMagic);
|
|
312
|
+
assert(storage->port != MACH_PORT_NULL);
|
|
313
|
+
*replyPortPtr = storage->port;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// no error + MACH_PORT_NULL is a valid response if we're on the main
|
|
319
|
+
// thread.
|
|
320
|
+
//
|
|
321
|
+
// assert( (err == noErr) == (*replyPortPtr != MACH_PORT_NULL) );
|
|
322
|
+
assert( (*replyPortPtr == MACH_PORT_NULL) || (err == noErr) );
|
|
323
|
+
|
|
324
|
+
return err;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
static void PerThreadStorageDestructor(void *keyValue)
|
|
328
|
+
// Called by pthreads when a thread dies and it has a non-null value for our
|
|
329
|
+
// per-thread storage key. We use this callback to return the thread's
|
|
330
|
+
// Apple event reply port to the pool.
|
|
331
|
+
{
|
|
332
|
+
PerThreadStorage * storage;
|
|
333
|
+
|
|
334
|
+
storage = (PerThreadStorage *) keyValue;
|
|
335
|
+
assert(storage != NULL); // pthread won't call us if it's NULL
|
|
336
|
+
assert(storage->magic == kPerThreadStorageMagic);
|
|
337
|
+
assert(storage->port != MACH_PORT_NULL);
|
|
338
|
+
|
|
339
|
+
// Return the port associated with this thread to the pool.
|
|
340
|
+
|
|
341
|
+
ReturnPortToPool(storage);
|
|
342
|
+
|
|
343
|
+
// pthreads has already set this thread's per-thread storage for our key to
|
|
344
|
+
// NULL before calling us. So we don't need to do anything to remove it.
|
|
345
|
+
|
|
346
|
+
assert( pthread_getspecific(sPerThreadStorageKey) == NULL );
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
OSStatus SendMessageThreadSafe(
|
|
350
|
+
AppleEvent * eventPtr,
|
|
351
|
+
AppleEvent * replyPtr,
|
|
352
|
+
AESendMode sendMode,
|
|
353
|
+
long timeOutInTicks
|
|
354
|
+
)
|
|
355
|
+
// See comment in header.
|
|
356
|
+
{
|
|
357
|
+
OSStatus err = noErr;
|
|
358
|
+
mach_port_t replyPort;
|
|
359
|
+
assert(eventPtr != NULL);
|
|
360
|
+
assert(replyPtr != NULL);
|
|
361
|
+
|
|
362
|
+
if (sendMode && kAEWaitReply) {
|
|
363
|
+
replyPort = MACH_PORT_NULL;
|
|
364
|
+
|
|
365
|
+
// Set up the reply port if necessary.
|
|
366
|
+
|
|
367
|
+
err = BindReplyMachPortToThread(&replyPort);
|
|
368
|
+
if ( (err == noErr) && (replyPort != MACH_PORT_NULL) ) {
|
|
369
|
+
err = AEPutAttributePtr(eventPtr, keyReplyPortAttr, typeMachPort, &replyPort, sizeof(replyPort));
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// Call through to AESendMessage.
|
|
374
|
+
|
|
375
|
+
if (err == noErr) {
|
|
376
|
+
err = AESendMessage(eventPtr, replyPtr, sendMode, timeOutInTicks);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
return err;
|
|
380
|
+
}
|