rb-scpt 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
}
|