detroit 0.3.0 → 0.4.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/.index +59 -0
- data/EXAMPLE.md +66 -64
- data/{HISTORY.rdoc → HISTORY.md} +32 -5
- data/{COPYING.rdoc → LICENSE.txt} +0 -0
- data/README.md +142 -0
- data/bin/detroit +1 -1
- data/lib/detroit.rb +112 -40
- data/lib/detroit.yml +44 -29
- data/lib/detroit/assembly.rb +49 -193
- data/lib/detroit/basic_tool.rb +200 -0
- data/lib/detroit/basic_utils.rb +66 -0
- data/lib/detroit/core_ext.rb +2 -136
- data/lib/detroit/{tool/core_ext → core_ext}/facets.rb +3 -0
- data/lib/detroit/{tool/core_ext → core_ext}/filetest.rb +0 -0
- data/lib/detroit/{tool/core_ext → core_ext}/shell_extensions.rb +0 -0
- data/lib/detroit/{tool/core_ext → core_ext}/to_actual_filename.rb +0 -0
- data/lib/detroit/{tool/core_ext → core_ext}/to_console.rb +1 -0
- data/lib/detroit/{tool/core_ext → core_ext}/to_list.rb +0 -0
- data/lib/detroit/{tool/core_ext → core_ext}/to_yamlfrag.rb +0 -0
- data/lib/detroit/{tool/core_ext → core_ext}/unfold_paragraphs.rb +0 -0
- data/lib/detroit/{tool/email_utils.rb → email_utils.rb} +3 -1
- data/lib/detroit/exec.rb +55 -0
- data/lib/detroit/project.rb +134 -0
- data/lib/detroit/ruby_utils.rb +29 -0
- data/lib/detroit/{tool/shell_utils.rb → shell_utils.rb} +10 -5
- data/lib/detroit/toolchain.rb +6 -0
- data/lib/detroit/toolchain/cli.rb +320 -0
- data/lib/detroit/toolchain/config.rb +223 -0
- data/lib/detroit/toolchain/runner.rb +678 -0
- data/lib/detroit/toolchain/script.rb +248 -0
- data/lib/detroit/toolchain/worker.rb +84 -0
- data/man/detroit.1 +116 -0
- data/man/detroit.1.html +171 -0
- data/man/detroit.1.ronn +99 -0
- metadata +90 -51
- data/.ruby +0 -44
- data/README.rdoc +0 -132
- data/lib/detroit/application.rb +0 -463
- data/lib/detroit/assembly_system.rb +0 -80
- data/lib/detroit/config.rb +0 -203
- data/lib/detroit/control.rb +0 -129
- data/lib/detroit/custom.rb +0 -102
- data/lib/detroit/dsl.rb +0 -55
- data/lib/detroit/service.rb +0 -78
- data/lib/detroit/standard_assembly.rb +0 -51
- data/lib/detroit/tool.rb +0 -295
- data/lib/detroit/tool/core_ext.rb +0 -3
- data/lib/detroit/tool/project_utils.rb +0 -41
@@ -1,80 +0,0 @@
|
|
1
|
-
module Detroit
|
2
|
-
|
3
|
-
# All assemblies and tracks have a maintenance sub-track.
|
4
|
-
# For this reason stop names `reset`, `clean` and `purge`
|
5
|
-
# are reserved names and MUST not be used as stop names
|
6
|
-
# in defining custom lines.
|
7
|
-
MAINTENANCE_TRACK = [:reset, :clean, :purge]
|
8
|
-
|
9
|
-
# Returns Hash of name and Circuit instance pairs.
|
10
|
-
def self.assembly_systems
|
11
|
-
@assembly_system ||= {}
|
12
|
-
end
|
13
|
-
|
14
|
-
# Define a new assembly system.
|
15
|
-
def self.assembly_system(name, &block)
|
16
|
-
assembly_systems[name.to_sym] = AssemblySystem.new(name, &block)
|
17
|
-
end
|
18
|
-
|
19
|
-
# The AssemblySystem class encapsulates a set of interrelated
|
20
|
-
# assembly lines, or _tracks_.
|
21
|
-
class AssemblySystem
|
22
|
-
|
23
|
-
# Name of the assembly system.
|
24
|
-
attr :name
|
25
|
-
|
26
|
-
# Returns a Hash of track names mapped to list of stops.
|
27
|
-
attr :lines
|
28
|
-
|
29
|
-
# Lines are also called `tracks`.
|
30
|
-
alias_method :tracks, :lines
|
31
|
-
|
32
|
-
# Create a new instance.
|
33
|
-
def initialize(name, &block)
|
34
|
-
@name = name.to_sym
|
35
|
-
@lines = {:maintenance => MAINTENANCE_TRACK}
|
36
|
-
instance_eval(&block) if block
|
37
|
-
end
|
38
|
-
|
39
|
-
# Define an assembly line.
|
40
|
-
def line(name, *stops)
|
41
|
-
if stops.empty?
|
42
|
-
@lines[name.to_sym]
|
43
|
-
else
|
44
|
-
@lines[name.to_sym] = stops.map{ |s| s.to_sym }
|
45
|
-
end
|
46
|
-
end
|
47
|
-
|
48
|
-
# Lines are also called tracks.
|
49
|
-
alias_method :track, :line
|
50
|
-
|
51
|
-
# Lookup track by name and (optional) stop. If the stop belongs
|
52
|
-
# to the maintenance sub-track then the maintenance sub-track will
|
53
|
-
# be returned instead of the track itself.
|
54
|
-
#
|
55
|
-
# The Application class uses this to simplify track lookup.
|
56
|
-
def get_track(name, stop=nil)
|
57
|
-
name = name.to_sym
|
58
|
-
if stop
|
59
|
-
stop = stop.to_sym
|
60
|
-
if MAINTENANCE_TRACK.include?(stop.to_sym)
|
61
|
-
track = MAINTENANCE_TRACK
|
62
|
-
else
|
63
|
-
track = tracks[name]
|
64
|
-
raise "Unknown track `#{name}'." unless track
|
65
|
-
unless track.include?(stop)
|
66
|
-
raise "Unknown stop `#{stop}` for track `#{name}'."
|
67
|
-
end
|
68
|
-
end
|
69
|
-
else
|
70
|
-
track = tracks[name]
|
71
|
-
end
|
72
|
-
track
|
73
|
-
end
|
74
|
-
|
75
|
-
# Did I mention that `line` and `track` are synonyms?
|
76
|
-
alias_method :get_line, :get_track
|
77
|
-
|
78
|
-
end
|
79
|
-
|
80
|
-
end
|
data/lib/detroit/config.rb
DELETED
@@ -1,203 +0,0 @@
|
|
1
|
-
module Detroit
|
2
|
-
|
3
|
-
# Detroit configuration. Configuration comes from a main +Routine+
|
4
|
-
# and/or +.routine+ files.
|
5
|
-
class Config
|
6
|
-
#instance_methods.each{ |m| private m unless /^__/ =~ m.to_s }
|
7
|
-
|
8
|
-
# Configuration directory name (most likely a hidden "dot" directory).
|
9
|
-
DIRECTORY = "detroit"
|
10
|
-
|
11
|
-
# File identifier used to find a project's Assembly(s).
|
12
|
-
FILE_EXTENSION = "assembly"
|
13
|
-
|
14
|
-
# Current POM::Project object.
|
15
|
-
#attr :project
|
16
|
-
|
17
|
-
# The list of a project's assembly files.
|
18
|
-
#
|
19
|
-
# @return [Array<String>] routine files
|
20
|
-
attr :assemblies
|
21
|
-
|
22
|
-
# Service configurations from Assembly or *.assembly files.
|
23
|
-
#
|
24
|
-
# @return [Hash] service settings
|
25
|
-
attr :services
|
26
|
-
|
27
|
-
# Service defaults. This is a mapping of service names to
|
28
|
-
# default settings. Very useful for when using the same
|
29
|
-
# service more than once.
|
30
|
-
#
|
31
|
-
# @return [Hash] default settings
|
32
|
-
attr :defaults
|
33
|
-
|
34
|
-
#
|
35
|
-
def initialize(assembly_files=nil)
|
36
|
-
if assembly_files && !assembly_files.empty?
|
37
|
-
@assembly_filenames = assembly_files
|
38
|
-
else
|
39
|
-
@assembly_filenames = nil
|
40
|
-
end
|
41
|
-
|
42
|
-
@assemblies = {}
|
43
|
-
@services = {}
|
44
|
-
@defaults = {}
|
45
|
-
|
46
|
-
@loaded_plugins = {}
|
47
|
-
|
48
|
-
load_plugins
|
49
|
-
load_defaults
|
50
|
-
load_assemblies
|
51
|
-
end
|
52
|
-
|
53
|
-
#--
|
54
|
-
# TODO: Use this, or pass in via initialize?
|
55
|
-
#++
|
56
|
-
def project
|
57
|
-
Detroit.project
|
58
|
-
end
|
59
|
-
|
60
|
-
# Load a plugin.
|
61
|
-
def load_plugin(name)
|
62
|
-
@loaded_plugins[name] ||= (
|
63
|
-
begin
|
64
|
-
require "detroit-#{name}"
|
65
|
-
rescue LoadError => e
|
66
|
-
$stderr.puts "ERROR: #{e.message.capitalize}"
|
67
|
-
$stderr.puts " Perhaps `gem install detroit-#{name}`?"
|
68
|
-
exit -1
|
69
|
-
end
|
70
|
-
name # true ?
|
71
|
-
)
|
72
|
-
end
|
73
|
-
|
74
|
-
# Pre-load plugins using `.detroit/plugins.rb`.
|
75
|
-
def load_plugins
|
76
|
-
if file = project.root.glob('{.,}#{DIRECTORY}/plugins{,.rb}').first
|
77
|
-
require file
|
78
|
-
else
|
79
|
-
self.defaults = {}
|
80
|
-
end
|
81
|
-
end
|
82
|
-
|
83
|
-
# Load defaults from `.detroit/defaults.yml`.
|
84
|
-
def load_defaults
|
85
|
-
if file = project.root.glob('{.,}#{DIRECTORY}/defaults{,.yml,.yaml}').first
|
86
|
-
self.defaults = YAML.load(File.new(file))
|
87
|
-
else
|
88
|
-
self.defaults = {}
|
89
|
-
end
|
90
|
-
end
|
91
|
-
|
92
|
-
#
|
93
|
-
def load_assemblies
|
94
|
-
assembly_filenames.each do |file|
|
95
|
-
load_assembly_file(file)
|
96
|
-
end
|
97
|
-
end
|
98
|
-
|
99
|
-
#
|
100
|
-
def load_assembly_file(file)
|
101
|
-
@assemblies[file] = Assembly.load(File.new(file))
|
102
|
-
@services.merge!(assemblies[file].services)
|
103
|
-
end
|
104
|
-
|
105
|
-
# Set defaults.
|
106
|
-
def defaults=(hash)
|
107
|
-
@defaults = hash.to_h
|
108
|
-
end
|
109
|
-
|
110
|
-
# If a `Assembly` or `.assembly` file exists, then it is returned. Otherwise
|
111
|
-
# all `*.assembly` files are loaded. To load `*.assembly` files from another
|
112
|
-
# directory add the directory to config options file.
|
113
|
-
def assembly_filenames
|
114
|
-
@assembly_filenames ||= (
|
115
|
-
files = []
|
116
|
-
## match 'Assembly' or '.assembly' file
|
117
|
-
files = project.root.glob("{,.,*.}#{FILE_EXTENSION}{,.rb,.yml,.yaml}", :casefold)
|
118
|
-
## only files
|
119
|
-
files = files.select{ |f| File.file?(f) }
|
120
|
-
##
|
121
|
-
if files.empty?
|
122
|
-
## match '.detroit/*.assembly' or 'detroit/*.assembly'
|
123
|
-
files += project.root.glob("{,.}#{DIRECTORY}/*.#{FILE_EXTENSION}", :casefold)
|
124
|
-
## match 'task/*.assembly' (OLD SCHOOL)
|
125
|
-
files += project.root.glob("{task,tasks}/*.#{FILE_EXTENSION}", :casefold)
|
126
|
-
## only files
|
127
|
-
files = files.select{ |f| File.file?(f) }
|
128
|
-
end
|
129
|
-
files
|
130
|
-
)
|
131
|
-
end
|
132
|
-
|
133
|
-
#
|
134
|
-
def each(&block)
|
135
|
-
services.each(&block)
|
136
|
-
end
|
137
|
-
|
138
|
-
#
|
139
|
-
def size
|
140
|
-
services.size
|
141
|
-
end
|
142
|
-
|
143
|
-
=begin
|
144
|
-
# If using a `Routine` file and want to import antoher file then use
|
145
|
-
# `import:` entry.
|
146
|
-
def load_detroit_file(file)
|
147
|
-
#@dir = File.dirname(file)
|
148
|
-
|
149
|
-
assemblies[file] =
|
150
|
-
|
151
|
-
# TODO: can we just read the first line of the file and go from there?
|
152
|
-
#text = File.read(file).strip
|
153
|
-
|
154
|
-
## if yaml vs. ruby file
|
155
|
-
#if (/\A---/ =~ text || /\.(yml|yaml)$/ =~ File.extname(file))
|
156
|
-
# #data = parse_detroit_file_yaml(text, file)
|
157
|
-
# YAML.load(text)
|
158
|
-
#else
|
159
|
-
# data = parse_detroit_file_ruby(text, file)
|
160
|
-
#end
|
161
|
-
|
162
|
-
## extract defaults
|
163
|
-
#if defaults = data.delete('defaults')
|
164
|
-
# @defaults.merge!(defaults)
|
165
|
-
#end
|
166
|
-
|
167
|
-
## import other files
|
168
|
-
#if import = data.delete('import')
|
169
|
-
# [import].flatten.each do |glob|
|
170
|
-
# routine(glob)
|
171
|
-
# end
|
172
|
-
#end
|
173
|
-
|
174
|
-
## require plugins
|
175
|
-
#if plugins = data.delete('plugins')
|
176
|
-
# [plugins].flatten.each do |file|
|
177
|
-
# require file
|
178
|
-
# end
|
179
|
-
#end
|
180
|
-
|
181
|
-
#@services.update(data)
|
182
|
-
end
|
183
|
-
=end
|
184
|
-
|
185
|
-
## Parse a YAML-based routine.
|
186
|
-
#def parse_detroit_file_yaml(text, file)
|
187
|
-
# YAMLParser.parse(self, text, file)
|
188
|
-
#end
|
189
|
-
|
190
|
-
## Parse a Ruby-based routine.
|
191
|
-
#def parse_detroit_file_ruby(text, file)
|
192
|
-
# RubyParser.parse(self, text, file)
|
193
|
-
#end
|
194
|
-
|
195
|
-
## TODO: Should the +dir+ be relative to the file or project.root?
|
196
|
-
#def routine(glob)
|
197
|
-
# pattern = File.join(@dir, glob)
|
198
|
-
# Dir[pattern].each{ |f| load_detroit_file(f) }
|
199
|
-
#end
|
200
|
-
|
201
|
-
end
|
202
|
-
|
203
|
-
end
|
data/lib/detroit/control.rb
DELETED
@@ -1,129 +0,0 @@
|
|
1
|
-
module Detroit
|
2
|
-
|
3
|
-
# The control module is a function module that extends
|
4
|
-
# the toplevel Detroit namespace module.
|
5
|
-
module Control
|
6
|
-
|
7
|
-
# Location of standard plugins.
|
8
|
-
#PLUGIN_DIRECTORY = File.dirname(__FILE__) + '/plugins'
|
9
|
-
|
10
|
-
# Returns Array of standard plugin file names.
|
11
|
-
#def standard_plugins
|
12
|
-
# Dir[PLUGIN_DIRECTORY + '/*.rb']
|
13
|
-
#end
|
14
|
-
|
15
|
-
# Universal acccess to the current project.
|
16
|
-
#
|
17
|
-
# TODO: Is Control#project being used?
|
18
|
-
def project
|
19
|
-
@project ||= POM::Project.find
|
20
|
-
end
|
21
|
-
|
22
|
-
# Returns Application given options.
|
23
|
-
def application(options={})
|
24
|
-
Application.new(options)
|
25
|
-
end
|
26
|
-
|
27
|
-
# Run the command line interface.
|
28
|
-
def cli(*argv)
|
29
|
-
cli_options = {
|
30
|
-
:system => nil, :assemblies => [],
|
31
|
-
:trace=>nil, :trial=>nil, :debug=>nil, :quiet=>nil, :verbose=>nil,
|
32
|
-
:force=>nil, :multitask=>nil, :skip=>[]
|
33
|
-
}
|
34
|
-
|
35
|
-
usage = cli_usage(cli_options)
|
36
|
-
usage.parse!(argv)
|
37
|
-
|
38
|
-
#if /\.assembly$/ =~ argv[0]
|
39
|
-
# job = argv[1]
|
40
|
-
# begin
|
41
|
-
# application(cli_options).runscript(argv[0], job)
|
42
|
-
# rescue => error
|
43
|
-
# $stderr.puts error.message
|
44
|
-
# exit -1
|
45
|
-
# end
|
46
|
-
#else
|
47
|
-
begin
|
48
|
-
application(cli_options).start(*argv)
|
49
|
-
rescue => error
|
50
|
-
if $DEBUG
|
51
|
-
raise error
|
52
|
-
else
|
53
|
-
$stderr.puts error.message
|
54
|
-
exit -1
|
55
|
-
end
|
56
|
-
end
|
57
|
-
#end
|
58
|
-
end
|
59
|
-
|
60
|
-
# Returns an instance of OptionParser.
|
61
|
-
def cli_usage(options)
|
62
|
-
@usage ||= (
|
63
|
-
OptionParser.new do |usage|
|
64
|
-
usage.banner = "Usage: detroit [<track>:]<stop> [options]"
|
65
|
-
usage.on('-m', '--multitask', "Run work elements in parallel.") do
|
66
|
-
options[:multitask] = true
|
67
|
-
end
|
68
|
-
usage.on('-S', '--skip [SERVICE]', 'Skip a service.') do |skip|
|
69
|
-
options[:skip] << skip
|
70
|
-
end
|
71
|
-
|
72
|
-
usage.on('-s', '--system=NAME', "Select assembly system. Default is `standard'.") do |system|
|
73
|
-
options[:system] = system
|
74
|
-
end
|
75
|
-
usage.on('-a', '--assembly [FILE]', 'Use specific assembly file(s).') do |file|
|
76
|
-
options[:assemblies] << file
|
77
|
-
end
|
78
|
-
|
79
|
-
usage.on('-F', '--force', "Force operations.") do
|
80
|
-
options[:force] = true
|
81
|
-
end
|
82
|
-
usage.on('--trace', "Run in TRACE mode.") do
|
83
|
-
#$TRACE = true
|
84
|
-
options[:trace] = true
|
85
|
-
end
|
86
|
-
usage.on('--trial', "Run in TRIAL mode (no disk writes).") do
|
87
|
-
#$TRIAL = true
|
88
|
-
options[:trial] = true
|
89
|
-
end
|
90
|
-
# TODO: do we really need verbose?
|
91
|
-
usage.on('--verbose', "Provide extra output.") do
|
92
|
-
options[:verbose] = true
|
93
|
-
end
|
94
|
-
usage.on('-q', '--quiet', "Run silently.") do
|
95
|
-
options[:quiet] = true
|
96
|
-
end
|
97
|
-
|
98
|
-
usage.on('-I=PATH', "Add directory to $LOAD_PATH") do |dirs|
|
99
|
-
dirs.to_list.each do |dir|
|
100
|
-
$LOAD_PATH.unshift(dir)
|
101
|
-
end
|
102
|
-
end
|
103
|
-
usage.on('--debug', "Run with $DEBUG set to true.") do
|
104
|
-
$DEBUG = true
|
105
|
-
options[:debug] = true # DEPRECATE?
|
106
|
-
end
|
107
|
-
usage.on('--warn', "Run with $VERBOSE set to true.") do
|
108
|
-
$VERBOSE = true # wish this were called $WARN
|
109
|
-
end
|
110
|
-
usage.on_tail('--help [TOOL]', "Display this help message.") do |tool|
|
111
|
-
if tool
|
112
|
-
application.display_help(tool)
|
113
|
-
else
|
114
|
-
puts usage
|
115
|
-
end
|
116
|
-
exit
|
117
|
-
end
|
118
|
-
usage.on_tail('-c', '--config TOOL', "Produce a configuration template.") do |tool|
|
119
|
-
puts application.config_template(tool).to_yaml
|
120
|
-
exit
|
121
|
-
end
|
122
|
-
end
|
123
|
-
)
|
124
|
-
end
|
125
|
-
|
126
|
-
end
|
127
|
-
|
128
|
-
extend Control
|
129
|
-
end
|
data/lib/detroit/custom.rb
DELETED
@@ -1,102 +0,0 @@
|
|
1
|
-
module Detroit
|
2
|
-
|
3
|
-
# Custom tool is used to create "quicky" services.
|
4
|
-
#
|
5
|
-
# This is a useful alternative to writing a full-blown plugin
|
6
|
-
# when the need is simple.
|
7
|
-
#
|
8
|
-
class Custom < Tool
|
9
|
-
|
10
|
-
# Default track(s) in which this plugin operates.
|
11
|
-
DEFAULT_TRACK = "main"
|
12
|
-
|
13
|
-
# Which track(s) to run this custom plugin.
|
14
|
-
attr_accessor :track
|
15
|
-
|
16
|
-
# Special writer to allow single track or a list of tracks.
|
17
|
-
def track=(val)
|
18
|
-
@track = val.to_list #[val].flatten
|
19
|
-
end
|
20
|
-
|
21
|
-
# Plural alias for #track.
|
22
|
-
alias_accessor :tracks, :track
|
23
|
-
|
24
|
-
alias_accessor :on, :track
|
25
|
-
|
26
|
-
private
|
27
|
-
|
28
|
-
SPECIAL_OPTIONS = %w{
|
29
|
-
service track tracks on active priority project
|
30
|
-
trial trace verbose force quiet
|
31
|
-
}
|
32
|
-
|
33
|
-
# Instantiate new custom plugin.
|
34
|
-
#
|
35
|
-
# FIXME: Custom#initialize seems to be running twice at startup. Why?
|
36
|
-
#
|
37
|
-
# This works by interpreting the service configuration as a hash of
|
38
|
-
# stop names to ruby code.
|
39
|
-
#
|
40
|
-
def initialize(options)
|
41
|
-
super(options)
|
42
|
-
options.each do |stop, script|
|
43
|
-
# skip specific names used for configuration
|
44
|
-
next if SPECIAL_OPTIONS.include? stop
|
45
|
-
# remaining options are names of track stops
|
46
|
-
#tracks.each do |t|
|
47
|
-
src = %{
|
48
|
-
def station_#{stop}
|
49
|
-
#{script}
|
50
|
-
end
|
51
|
-
}
|
52
|
-
(class << self; self; end).module_eval(src)
|
53
|
-
#end
|
54
|
-
end
|
55
|
-
end
|
56
|
-
|
57
|
-
# Set initial attribute defaults.
|
58
|
-
def initialize_defaults
|
59
|
-
@track = [DEFAULT_TRACK]
|
60
|
-
end
|
61
|
-
|
62
|
-
#
|
63
|
-
def method_missing(s, *a, &b)
|
64
|
-
if s.to_s.end_with?('=')
|
65
|
-
# stop = s.to_s.chomp('=')
|
66
|
-
# if !SPECIAL_OPTIONS.include?(stop)
|
67
|
-
# (class << self; self; end).module_eval %{
|
68
|
-
# def station_#{stop}
|
69
|
-
# #{a.first}
|
70
|
-
# end
|
71
|
-
# }
|
72
|
-
# end
|
73
|
-
else
|
74
|
-
if @context.respond_to?(s)
|
75
|
-
@context.__send__(s,*a,&b)
|
76
|
-
else
|
77
|
-
super(s, *a, &b)
|
78
|
-
end
|
79
|
-
end
|
80
|
-
end
|
81
|
-
|
82
|
-
# @todo should only respond to stop names and special options.
|
83
|
-
#def respond_to?(s)
|
84
|
-
# return true if SPECIAL_OPTIONS.include?(s.to_s)
|
85
|
-
# return true
|
86
|
-
#end
|
87
|
-
|
88
|
-
# RUBY 1.9
|
89
|
-
def respond_to_missing?(name, privy)
|
90
|
-
#return true if name.to_s.start_with?('station_')
|
91
|
-
return true if name.to_s.end_with?('=')
|
92
|
-
return true if @context.respond_to?(name)
|
93
|
-
false
|
94
|
-
end
|
95
|
-
|
96
|
-
def inspect
|
97
|
-
"#<Custom @on=#{track.join(',')}>"
|
98
|
-
end
|
99
|
-
|
100
|
-
end
|
101
|
-
|
102
|
-
end
|