PageRenderTime 25ms CodeModel.GetById 27ms RepoModel.GetById 1ms app.codeStats 0ms

/SharedSupport/lib/scriptmate.rb

http://github.com/drnic/macruby-tmbundle
Ruby | 315 lines | 289 code | 16 blank | 10 comment | 2 complexity | b174ea676a864cfb769b9b9064b8a9de MD5 | raw file
  1. SUPPORT_LIB = ENV['TM_SUPPORT_PATH'] + '/lib/'
  2. require SUPPORT_LIB + 'escape'
  3. require SUPPORT_LIB + 'web_preview'
  4. require SUPPORT_LIB + 'io'
  5. require SUPPORT_LIB + 'tm/tempfile'
  6. require 'cgi'
  7. require 'fcntl'
  8. $SCRIPTMATE_VERSION = "$Revision: 9925 $"
  9. def my_popen3(*cmd) # returns [stdin, stdout, strerr, pid]
  10. pw = IO::pipe # pipe[0] for read, pipe[1] for write
  11. pr = IO::pipe
  12. pe = IO::pipe
  13. # F_SETOWN = 6, ideally this would be under Fcntl::F_SETOWN
  14. pw[0].fcntl(6, ENV['TM_PID'].to_i) if ENV.has_key? 'TM_PID'
  15. pid = fork{
  16. pw[1].close
  17. STDIN.reopen(pw[0])
  18. pw[0].close
  19. pr[0].close
  20. STDOUT.reopen(pr[1])
  21. pr[1].close
  22. pe[0].close
  23. STDERR.reopen(pe[1])
  24. pe[1].close
  25. tm_interactive_input = SUPPORT_LIB + '/tm_interactive_input.dylib'
  26. if (File.exists? tm_interactive_input)
  27. dil = ENV['DYLD_INSERT_LIBRARIES']
  28. ENV['DYLD_INSERT_LIBRARIES'] = (dil) ? "#{tm_interactive_input}:#{dil}" : tm_interactive_input unless (dil =~ /#{tm_interactive_input}/)
  29. ENV['DYLD_FORCE_FLAT_NAMESPACE'] = "1"
  30. ENV['TM_INTERACTIVE_INPUT'] = 'AUTO|ECHO'
  31. end
  32. exec(*cmd)
  33. }
  34. pw[0].close
  35. pr[1].close
  36. pe[1].close
  37. pw[1].sync = true
  38. [pw[1], pr[0], pe[0], pid]
  39. end
  40. def cmd_mate(cmd)
  41. # cmd can be either a string or a list of strings to be passed to Popen3
  42. # this command will write the output of the `cmd` on STDOUT, formatted in
  43. # HTML.
  44. c = UserCommand.new(cmd)
  45. m = CommandMate.new(c)
  46. m.emit_html
  47. end
  48. class UserCommand
  49. attr_reader :display_name, :path
  50. def initialize(cmd)
  51. @cmd = cmd
  52. end
  53. def run
  54. stdin, stdout, stderr, pid = my_popen3(@cmd)
  55. return stdout, stderr, nil, pid
  56. end
  57. def to_s
  58. @cmd.to_s
  59. end
  60. end
  61. class CommandMate
  62. def initialize (command)
  63. # the object `command` needs to implement a method `run`. `run` should
  64. # return an array of three file descriptors [stdout, stderr, stack_dump].
  65. @error = ""
  66. @command = command
  67. STDOUT.sync = true
  68. @mate = self.class.name
  69. end
  70. protected
  71. def filter_stdout(str)
  72. # strings from stdout are passed through this method before being printed
  73. # txmt://open?line=3&url=file:///var/folders/Gx/Gxr7D8ILFba5bZaZC6rrCE%2B%2B%2BTQ/-Tmp-/untitled_m16p.py
  74. str = htmlize(str).gsub(/\<br\>/, "<br>\n")
  75. end
  76. def filter_stderr(str)
  77. # strings from stderr are passwed through this method before printing
  78. "<span style='color: red'>#{htmlize str}</span>".gsub(/\<br\>/, "<br>\n")
  79. end
  80. def emit_header
  81. puts html_head(:window_title => "#{@command}", :page_title => "#{@command}", :sub_title => "")
  82. puts "<pre>"
  83. end
  84. def emit_footer
  85. puts "</pre>"
  86. html_footer
  87. end
  88. public
  89. def emit_html
  90. @command.run do |stdout, stderr, stack_dump, pid|
  91. %w[INT TERM].each do |signal|
  92. trap(signal) do
  93. begin
  94. Process.kill("KILL", pid)
  95. sleep 0.5
  96. Process.kill("TERM", pid)
  97. rescue
  98. # process doesn't exist anymore
  99. end
  100. end
  101. end
  102. emit_header()
  103. TextMate::IO.exhaust(:out => stdout, :err => stderr, :stack => stack_dump) do |str, type|
  104. case type
  105. when :out then print filter_stdout(str)
  106. when :err then puts filter_stderr(str)
  107. when :stack then
  108. unless @command.temp_file.nil?
  109. str.gsub!(/(href=("|')(?:txmt:\/\/open\?(?:[a-z]+=[0-9]+)*?))(&url=.*)([a-z]+=[0-9]+)?(\2)/, '\1\2')
  110. ext = @command.default_extension
  111. str.gsub!(File.basename(@command.temp_file), "untitled")
  112. end
  113. @error << str
  114. end
  115. end
  116. emit_footer()
  117. Process.waitpid(pid)
  118. end
  119. end
  120. end
  121. class UserScript
  122. attr_reader :display_name, :path, :warning
  123. attr_reader :temp_file
  124. def initialize(content)
  125. @warning = ''
  126. @content = content
  127. @hashbang = $1 if @content =~ /\A#!(.*)$/
  128. @saved = true
  129. if ENV.has_key? 'TM_FILEPATH' then
  130. @path = ENV['TM_FILEPATH']
  131. @display_name = File.basename(@path)
  132. begin
  133. file = open(@path, 'w')
  134. file.write @content
  135. rescue Errno::EACCES
  136. @saved = false
  137. @warning = "Could not save #{@path} before running, using temp file..."
  138. ensure
  139. file.close unless file.nil?
  140. end
  141. else
  142. @saved = false
  143. end
  144. end
  145. public
  146. def executable
  147. # return the path to the executable that will run @content.
  148. end
  149. def args
  150. # return any arguments to be fed to the executable
  151. []
  152. end
  153. def filter_cmd(cmd)
  154. # this method is called with this list:
  155. # [executable, args, e_sh(@path), ARGV.to_a ].flatten
  156. cmd
  157. end
  158. def version_string
  159. # return the version string of the executable.
  160. end
  161. def default_extension
  162. # return the extension to use if the script has not yet been saved
  163. end
  164. def run(&block)
  165. rd, wr = IO.pipe
  166. rd.fcntl(Fcntl::F_SETFD, 1)
  167. ENV['TM_ERROR_FD'] = wr.to_i.to_s
  168. if @saved
  169. cmd = filter_cmd([executable, args, e_sh(@path), ARGV.to_a ].flatten)
  170. stdin, stdout, stderr, pid = my_popen3(cmd.join(" "))
  171. wr.close
  172. block.call(stdout, stderr, rd, pid)
  173. else
  174. TextMate::IO.tempfile(default_extension) do |f|
  175. f.write @content
  176. @display_name = "untitled"
  177. @temp_file = f.path
  178. cmd = filter_cmd([executable, args, e_sh(f.path), ARGV.to_a ].flatten)
  179. stdin, stdout, stderr, pid = my_popen3(cmd.join(" "))
  180. wr.close
  181. block.call(stdout, stderr, rd, pid)
  182. end
  183. end
  184. end
  185. end
  186. class ScriptMate < CommandMate
  187. protected
  188. def emit_header
  189. puts html_head(:window_title => "#{@command.display_name} — #{@mate}", :page_title => "#{@mate}", :sub_title => "#{@command.lang}")
  190. puts <<-HTML
  191. <!-- scriptmate javascripts -->
  192. <script type="text/javascript" charset="utf-8">
  193. function press(evt) {
  194. if (evt.keyCode == 67 && evt.ctrlKey == true) {
  195. TextMate.system("kill -s INT #{@pid}; sleep 0.5; kill -s TERM #{@pid}", null);
  196. }
  197. }
  198. document.body.addEventListener('keydown', press, false);
  199. function copyOutput(link) {
  200. output = document.getElementById('_scriptmate_output').innerText;
  201. cmd = TextMate.system('__CF_USER_TEXT_ENCODING=$UID:0x8000100:0x8000100 /usr/bin/pbcopy', function(){});
  202. cmd.write(output);
  203. cmd.close();
  204. link.innerText = 'output copied to clipboard';
  205. }
  206. </script>
  207. <!-- end javascript -->
  208. HTML
  209. puts <<-HTML
  210. <style type="text/css">
  211. /* =================== */
  212. /* = ScriptMate Styles = */
  213. /* =================== */
  214. div.scriptmate {
  215. }
  216. div.scriptmate > div {
  217. /*border-bottom: 1px dotted #666;*/
  218. /*padding: 1ex;*/
  219. }
  220. div.scriptmate pre em
  221. {
  222. /* used for stderr */
  223. font-style: normal;
  224. color: #FF5600;
  225. }
  226. div.scriptmate div#exception_report
  227. {
  228. /* background-color: rgb(210, 220, 255);*/
  229. }
  230. div.scriptmate p#exception strong
  231. {
  232. color: #E4450B;
  233. }
  234. div.scriptmate p#traceback
  235. {
  236. font-size: 8pt;
  237. }
  238. div.scriptmate blockquote {
  239. font-style: normal;
  240. border: none;
  241. }
  242. div.scriptmate table {
  243. margin: 0;
  244. padding: 0;
  245. }
  246. div.scriptmate td {
  247. margin: 0;
  248. padding: 2px 2px 2px 5px;
  249. font-size: 10pt;
  250. }
  251. div.scriptmate a {
  252. color: #FF5600;
  253. }
  254. div#exception_report pre.snippet {
  255. margin:4pt;
  256. padding:4pt;
  257. }
  258. </style>
  259. <strong class="warning" style="float:left; color:#B4AF00;">#{@command.warning}</strong>
  260. <div class="scriptmate #{@mate.downcase}">
  261. <div class="controls" style="text-align:right;">
  262. <a style="text-decoration: none;" href="#" onclick="copyOutput(document.getElementById('_script_output'))">copy output</a>
  263. </div>
  264. <!-- first box containing version info and script output -->
  265. <pre>
  266. <strong>#{@mate} r#{$SCRIPTMATE_VERSION[/\d+/]} running #{@command.version_string}</strong>
  267. <strong>>>> #{@command.display_name}</strong>
  268. <div id="_scriptmate_output" style="white-space: normal; -khtml-nbsp-mode: space; -khtml-line-break: after-white-space;"> <!-- Script output -->
  269. HTML
  270. end
  271. def emit_footer
  272. puts '</div></pre></div>'
  273. puts @error unless @error == ""
  274. puts '<div id="exception_report" class="framed">Program exited.</div>'
  275. html_footer
  276. end
  277. end