vop 0.3.1 → 0.3.4

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.
Files changed (110) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +2 -0
  3. data/Gemfile +2 -1
  4. data/Gemfile.lock +34 -33
  5. data/README.md +184 -2
  6. data/bin/console +17 -0
  7. data/bin/setup +8 -0
  8. data/bin/vop.sh +6 -2
  9. data/exe/vop +3 -25
  10. data/lib/boot.rb +3 -0
  11. data/lib/core/meta/commands/new_command.rb +1 -0
  12. data/lib/core/meta/commands/new_plugin.rb +47 -0
  13. data/lib/core/meta/commands/search_gems_for_plugins.rb +38 -0
  14. data/lib/core/meta/commands/search_path.rb +6 -0
  15. data/lib/core/meta/commands/set.rb +12 -0
  16. data/lib/core/meta/commands/show.rb +6 -0
  17. data/lib/core/meta/commands/show_config.rb +9 -0
  18. data/lib/core/meta/commands/who_provides.rb +6 -0
  19. data/lib/core/meta/meta.plugin +1 -0
  20. data/lib/core/shell/commands/change_loglevel.rb +6 -0
  21. data/lib/{vop/plugins/core → core/shell}/commands/clear_context.rb +0 -0
  22. data/lib/core/shell/commands/edit.rb +12 -0
  23. data/lib/core/shell/commands/help.rb +49 -0
  24. data/lib/core/shell/commands/reset.rb +4 -0
  25. data/lib/{vop/plugins/core → core/shell}/commands/show_context.rb +0 -0
  26. data/lib/core/shell/commands/source.rb +21 -0
  27. data/lib/{vop/plugins/core/helpers/plugin_loader/plugin_syntax.rb → core/shell/shell.plugin} +0 -0
  28. data/lib/core/structure/commands/collect_contributions.rb +46 -0
  29. data/lib/core/structure/commands/disable_contributor.rb +16 -0
  30. data/lib/core/structure/commands/generate_entity_commands.rb +52 -0
  31. data/lib/core/structure/commands/generate_invalidation_commands.rb +26 -0
  32. data/lib/core/structure/commands/list_commands.rb +11 -0
  33. data/lib/core/structure/commands/list_contributors.rb +10 -0
  34. data/lib/core/structure/commands/list_entities.rb +3 -0
  35. data/lib/core/structure/commands/list_plugins.rb +3 -0
  36. data/lib/core/structure/commands/register_contributor.rb +14 -0
  37. data/lib/core/structure/structure.plugin +4 -0
  38. data/lib/vop/objects/chain.rb +25 -0
  39. data/lib/vop/objects/command.rb +86 -0
  40. data/lib/vop/objects/command_param.rb +39 -0
  41. data/lib/vop/objects/entities.rb +21 -0
  42. data/lib/vop/objects/entity.rb +75 -0
  43. data/lib/vop/objects/entity_definition.rb +33 -0
  44. data/lib/vop/objects/filter.rb +48 -0
  45. data/lib/vop/objects/plugin.rb +208 -0
  46. data/lib/vop/objects/request.rb +73 -0
  47. data/lib/vop/objects/response.rb +17 -0
  48. data/lib/vop/objects/thing_with_params.rb +17 -0
  49. data/lib/vop/{command_loader.rb → parts/command_loader.rb} +8 -12
  50. data/lib/vop/parts/dependency_resolver.rb +56 -0
  51. data/lib/vop/parts/entity_loader.rb +46 -0
  52. data/lib/vop/parts/executor.rb +155 -0
  53. data/lib/vop/parts/filter_loader.rb +41 -0
  54. data/lib/vop/parts/plugin_finder.rb +46 -0
  55. data/lib/vop/parts/plugin_loader.rb +72 -0
  56. data/lib/vop/shell/shell.rb +221 -0
  57. data/lib/vop/shell/shell_formatter.rb +110 -0
  58. data/lib/vop/shell/shell_input.rb +14 -0
  59. data/lib/vop/shell/shell_input_readline.rb +20 -0
  60. data/lib/vop/shell/shell_input_testable.rb +27 -0
  61. data/lib/vop/syntax/command_syntax.rb +90 -0
  62. data/lib/vop/syntax/entity_syntax.rb +35 -0
  63. data/lib/vop/syntax/filter_syntax.rb +11 -0
  64. data/lib/vop/syntax/plugin_syntax.rb +55 -0
  65. data/lib/vop/util/errors.rb +45 -0
  66. data/lib/vop/util/pluralizer.rb +26 -0
  67. data/lib/vop/util/worker.rb +24 -0
  68. data/lib/vop/version.rb +1 -1
  69. data/lib/vop/vop.rb +216 -0
  70. data/lib/vop.rb +16 -229
  71. data/vop.gemspec +18 -15
  72. metadata +95 -63
  73. data/bin/vop.rb +0 -28
  74. data/lib/vop/command.rb +0 -168
  75. data/lib/vop/entity.rb +0 -61
  76. data/lib/vop/loader.rb +0 -35
  77. data/lib/vop/plugin.rb +0 -141
  78. data/lib/vop/plugin_loader.rb +0 -88
  79. data/lib/vop/plugins/core/commands/collect_contributions.rb +0 -31
  80. data/lib/vop/plugins/core/commands/edit.rb +0 -12
  81. data/lib/vop/plugins/core/commands/help.rb +0 -38
  82. data/lib/vop/plugins/core/commands/identity.rb +0 -4
  83. data/lib/vop/plugins/core/commands/list_contributors.rb +0 -8
  84. data/lib/vop/plugins/core/commands/list_entities.rb +0 -3
  85. data/lib/vop/plugins/core/commands/pry.rb +0 -9
  86. data/lib/vop/plugins/core/commands/reset.rb +0 -5
  87. data/lib/vop/plugins/core/commands/source.rb +0 -5
  88. data/lib/vop/plugins/core/commands/system_call.rb +0 -5
  89. data/lib/vop/plugins/core/core.plugin +0 -4
  90. data/lib/vop/plugins/core/helpers/command_loader/command_syntax.rb +0 -45
  91. data/lib/vop/plugins/core/helpers/command_loader/contributions.rb +0 -28
  92. data/lib/vop/plugins/core/helpers/command_loader/entities.rb +0 -57
  93. data/lib/vop/plugins/core/helpers/helper.rb +0 -3
  94. data/lib/vop/plugins/meta/commands/add_search_path.rb +0 -6
  95. data/lib/vop/plugins/meta/commands/delete_plugin.rb +0 -13
  96. data/lib/vop/plugins/meta/commands/list_commands.rb +0 -17
  97. data/lib/vop/plugins/meta/commands/list_plugins.rb +0 -8
  98. data/lib/vop/plugins/meta/commands/new_command.rb +0 -14
  99. data/lib/vop/plugins/meta/commands/new_plugin.rb +0 -25
  100. data/lib/vop/plugins/meta/commands/show_search_path.rb +0 -3
  101. data/lib/vop/plugins/meta/commands/who_provides.rb +0 -5
  102. data/lib/vop/plugins/meta/meta.plugin +0 -1
  103. data/lib/vop/plugins/ssh/commands/scp.rb +0 -11
  104. data/lib/vop/plugins/ssh/commands/ssh.rb +0 -19
  105. data/lib/vop/plugins/ssh/ssh.plugin +0 -1
  106. data/lib/vop/shell/backend.rb +0 -28
  107. data/lib/vop/shell/base_shell.rb +0 -112
  108. data/lib/vop/shell/formatter.rb +0 -46
  109. data/lib/vop/shell/vop_shell_backend.rb +0 -257
  110. data/lib/vop/shell.rb +0 -52
@@ -0,0 +1,73 @@
1
+ require_relative "response"
2
+ require_relative "chain"
3
+ require_relative "../parts/executor"
4
+
5
+ module Vop
6
+
7
+ class Request
8
+
9
+ attr_reader :command_name, :param_values, :extra
10
+ attr_accessor :shell
11
+
12
+ def initialize(op, command_name, param_values = {}, extra = {})
13
+ @op = op
14
+ @command_name = command_name
15
+ raise "unknown command '#{command_name}'" if command.nil?
16
+ @param_values = param_values
17
+ @extra = extra
18
+ @shell = nil
19
+
20
+ @current_filter = nil
21
+ @filter_chain = @op.filter_chain.clone
22
+ end
23
+
24
+ def command
25
+ @op.commands[@command_name]
26
+ end
27
+
28
+ def cache_key
29
+ blacklist = %w|shell raw_params|
30
+
31
+ ex = Executor.new(@op)
32
+ prepared = ex.prepare_params(self)
33
+
34
+ param_string = prepared.map { |k,v|
35
+ next if blacklist.include? k
36
+ [k.to_s,v].join("=")
37
+ }.sort.compact.join(":")
38
+ "vop/request:#{command.name}:" + param_string + ":v1"
39
+ end
40
+
41
+ def self.from_json(op, json)
42
+ hash = JSON.parse(json)
43
+ self.new(op, hash["command"], hash["params"], hash["extra"])
44
+ end
45
+
46
+ def to_json
47
+ {
48
+ command: @command_name,
49
+ params: @param_values,
50
+ extra: @extra
51
+ }.to_json
52
+ end
53
+
54
+ def next_filter
55
+ @chain.next()
56
+ end
57
+
58
+ def execute
59
+ result = nil
60
+ context = nil
61
+
62
+ # build a chain out of all filters + the command itself
63
+ filter_chain = @op.filter_chain.clone.map {
64
+ |filter_name| @op.filters[filter_name.split(".").first]
65
+ }
66
+ filter_chain << Executor.new(@op)
67
+ @chain = Chain.new(@op, filter_chain)
68
+ @chain.execute(self)
69
+ end
70
+
71
+ end
72
+
73
+ end
@@ -0,0 +1,17 @@
1
+ module Vop
2
+
3
+ class Response
4
+
5
+ attr_reader :result, :context, :timestamp
6
+ attr_accessor :status
7
+
8
+ def initialize(result, context, timestamp = nil)
9
+ @result = result
10
+ @context = context
11
+ @timestamp = timestamp || Time.now
12
+ @status = "ok"
13
+ end
14
+
15
+ end
16
+
17
+ end
@@ -0,0 +1,17 @@
1
+ module Vop
2
+
3
+ class ThingWithParams
4
+
5
+ attr_reader :params
6
+
7
+ def initialize
8
+ @params = []
9
+ end
10
+
11
+ def param(name)
12
+ @params.select { |x| x.name == name }.first
13
+ end
14
+
15
+ end
16
+
17
+ end
@@ -1,4 +1,5 @@
1
- require 'vop/command'
1
+ require_relative "../syntax/command_syntax"
2
+ require_relative "../objects/command"
2
3
 
3
4
  module Vop
4
5
 
@@ -8,24 +9,19 @@ module Vop
8
9
  @plugin = plugin
9
10
  @op = plugin.op
10
11
 
11
- @commands = {}
12
+ @commands = []
12
13
 
13
- # we need to load both the general helpers of the plugin because they are
14
- # used inside commands as well as the helpers specific to the command_loader
15
14
  @plugin.inject_helpers(self)
16
- @plugin.inject_helpers(self, 'command_loader')
15
+
16
+ extend CommandSyntax
17
17
  end
18
18
 
19
19
  def new_command(name)
20
20
  @command = Command.new(@plugin, name)
21
- @commands[@command.name] = @command
21
+ @commands << @command
22
22
  @command
23
23
  end
24
24
 
25
- def run(&block)
26
- @command.block = block
27
- end
28
-
29
25
  def read_sources(named_commands)
30
26
  # reads a hash of <command_name> => <source string>
31
27
  named_commands.each do |name, source|
@@ -34,8 +30,8 @@ module Vop
34
30
 
35
31
  begin
36
32
  self.instance_eval(source[:code], source[:file_name])
37
- rescue => detail
38
- raise "problem loading plugin #{name} : #{detail.message}\n#{detail.backtrace.join("\n")}"
33
+ rescue Exception
34
+ raise Errors::CommandLoadError.new("problem loading command #{name}")
39
35
  end
40
36
  end
41
37
 
@@ -0,0 +1,56 @@
1
+ module Vop
2
+
3
+ class DependencyResolver
4
+
5
+ def initialize(op)
6
+ @op = op
7
+ end
8
+
9
+ def sort(plugins)
10
+ @plugins = {}
11
+ plugins.each do |plugin|
12
+ @plugins[plugin.name] = plugin
13
+ end
14
+
15
+ resolved = []
16
+ unresolved = []
17
+
18
+ root_plugin = Plugin.new(@op, "__root__", nil)
19
+ root_plugin.dependencies = @plugins.keys
20
+
21
+ $logger.debug "root dummy : #{root_plugin}"
22
+
23
+ resolve(root_plugin, resolved, unresolved)
24
+ resolved.delete_if { |x| x == root_plugin.name }
25
+
26
+ resolved.map { |x| @plugins[x] }
27
+ end
28
+
29
+ def resolve(plugin, resolved, unresolved, level = 0)
30
+ $logger.debug "#{' ' * level}checking dependencies for #{plugin.name}"
31
+ unresolved << plugin.name
32
+
33
+ plugin.dependencies.each do |dep|
34
+ $logger.debug "#{' ' * level}resolving #{dep}"
35
+ already_loaded = @op.plugins.map(&:name).include? dep
36
+ unless already_loaded
37
+ unless resolved.include? dep
38
+ if unresolved.include? dep
39
+ raise ::Vop::Errors::RunningInCircles, "circular dependency #{plugin.name} -> #{dep}"
40
+ else
41
+ unless @plugins.has_key? dep
42
+ raise ::Vop::Errors::MissingPlugin, "dependency not met: #{plugin.name} depends on #{dep}"
43
+ end
44
+ dependency = @plugins[dep]
45
+ resolve(dependency, resolved, unresolved, level + 1)
46
+ end
47
+ end
48
+ end
49
+ end
50
+
51
+ resolved << plugin.name
52
+ end
53
+
54
+ end
55
+
56
+ end
@@ -0,0 +1,46 @@
1
+ require_relative "../syntax/entity_syntax"
2
+ require_relative "../objects/entity_definition"
3
+
4
+ module Vop
5
+
6
+ class EntityLoader
7
+
8
+ def initialize(plugin)
9
+ @plugin = plugin
10
+ @op = plugin.op
11
+
12
+ @loaded = []
13
+
14
+ @plugin.inject_helpers(self)
15
+
16
+ #@command = @entity
17
+ #extend CommandSyntax
18
+ extend EntitySyntax
19
+ end
20
+
21
+ def prepare(name)
22
+ @entity = EntityDefinition.new(@plugin, name)
23
+ @command = @entity
24
+ @loaded << @entity
25
+ @entity
26
+ end
27
+
28
+ def read_sources(named_sources)
29
+ # reads a hash of <name> => <source string>
30
+ named_sources.each do |name, source|
31
+
32
+ prepare(name)
33
+
34
+ begin
35
+ self.instance_eval(source[:code], source[:file_name])
36
+ rescue SyntaxError => detail
37
+ raise Errors::EntityLoadError.new("problem loading entity #{name} : #{detail.message}")
38
+ end
39
+ end
40
+
41
+ @loaded
42
+ end
43
+
44
+ end
45
+
46
+ end
@@ -0,0 +1,155 @@
1
+ module Vop
2
+
3
+ class Executor
4
+
5
+ def initialize(vop)
6
+ @op = vop
7
+ end
8
+
9
+ # accepts arguments as handed in by :define_method and prepares them
10
+ # into the +params+ structure expected by command blocks
11
+ def prepare_params(request)
12
+ (ruby_args, extra) = request.param_values, request.extra
13
+ result = {}
14
+ if ruby_args
15
+ if ruby_args.is_a? Hash
16
+ result = ruby_args
17
+ else
18
+ # if there is a default param, it can be passed to execute as "scalar"
19
+ # param, but it will be converted into a "normal" named param
20
+ dp = request.command.default_param
21
+ if dp
22
+ result = {
23
+ dp.name => ruby_args
24
+ }
25
+ end
26
+ end
27
+ end
28
+
29
+ if extra.keys.size > 0
30
+ extra.each do |k,v|
31
+ param = request.command.param(k)
32
+ # TODO actually, this is not always context - it's used from the entities as well
33
+ if param && param.wants_context
34
+ result[k] = extra[k]
35
+ end
36
+ end
37
+ end
38
+
39
+ # add in defaults (for all params that have not been specified)
40
+ request.command.params.each do |p|
41
+ param_name = p.name.to_sym
42
+ unless result.has_key? param_name
43
+ if p.options.has_key? :default
44
+ result[param_name] = p.options[:default]
45
+ end
46
+ end
47
+ end
48
+
49
+ result.each do |k,v|
50
+ param = request.command.param(k.to_s)
51
+ if param.nil? && ! request.command.allows_extra
52
+ raise "no such param #{k.to_s} (in #{request.command.name})"
53
+ end
54
+ p = param && param.options
55
+ if p
56
+ # values are auto-boxed into an array if the param expects multiple values
57
+ if p[:multi] && ! v.is_a?(Array) then
58
+ v = [ v ]
59
+ # array values are auto-unboxed if the param does not want multi
60
+ elsif ! p[:multi] && v.is_a?(Array) && v.length == 1
61
+ v = v.first
62
+ end
63
+
64
+ # convert booleans
65
+ if p[:boolean] && ! v.nil?
66
+ unless [true, false].include? v
67
+ #$logger.debug("converting #{param.name} (#{v}) into boolean")
68
+ v = !! /[tT]rue|[yY]es|[oO]n/.match(v)
69
+ end
70
+ end
71
+ end
72
+ result[k] = v
73
+ end
74
+
75
+ result
76
+ end
77
+
78
+ def prepare_payload(request, context, block_param_names)
79
+ payload = []
80
+
81
+ prepared = prepare_params(request)
82
+ param_names = request.command.params.map { |x| x.name }
83
+
84
+ block_param_names.each do |name|
85
+ param = nil
86
+
87
+ case name.to_s
88
+ when "params"
89
+ param = prepared
90
+ when "plugin"
91
+ param = request.command.plugin
92
+ when "command"
93
+ param = request.command
94
+ when "request"
95
+ param = request
96
+ when "context"
97
+ param = context
98
+ when "shell"
99
+ raise "shell not supported" if request.shell.nil?
100
+ param = request.shell
101
+ else
102
+ if prepared.has_key? name.to_s
103
+ param = prepared[name.to_s]
104
+ elsif prepared.has_key? name
105
+ param = prepared[name]
106
+ else
107
+ unless param_names.include? name.to_s
108
+ raise "unknown block param name : >>#{name}<<"
109
+ end
110
+ end
111
+ end
112
+
113
+ unless param.nil?
114
+ command_param = request.command.param(name.to_s)
115
+ if command_param && command_param.options[:entity]
116
+ # auto-inflate entities
117
+ entity_list = @op.entities.values
118
+ entity = entity_list.select { |x| x.short_name == name.to_s }.first
119
+
120
+ unless entity.nil?
121
+ #$logger.debug "auto-inflating entity #{name.to_s} (#{param})"
122
+
123
+ list_command_name = entity.short_name.carefully_pluralize
124
+ the_list = @op.execute(list_command_name, {})
125
+ #$logger.debug "inflated entity list : #{the_list.size} entities"
126
+ param = the_list.select { |x| x[entity.key] == param }.first
127
+ end
128
+ end
129
+
130
+ payload << param
131
+ end
132
+ end
133
+
134
+ payload
135
+ end
136
+
137
+ def execute(request)
138
+ blacklist = %w|list_contributors collect_contributions machines rails_machines|
139
+ unless blacklist.include? request.command.short_name
140
+ $logger.debug "+++ #{request.command.short_name} (#{request.param_values}) +++"
141
+ end
142
+ command = request.command
143
+
144
+ context = {}
145
+ block_param_names = request.command.block.parameters.map { |x| x.last }
146
+ payload = prepare_payload(request, context, block_param_names)
147
+ result = command.execute(payload)
148
+
149
+ Response.new(result, context)
150
+ end
151
+
152
+ end
153
+
154
+
155
+ end
@@ -0,0 +1,41 @@
1
+ require_relative "../syntax/filter_syntax"
2
+ require_relative "../objects/filter"
3
+
4
+ module Vop
5
+
6
+ class FilterLoader
7
+
8
+ def initialize(plugin)
9
+ @plugin = plugin
10
+ @op = @plugin.op
11
+
12
+ @filters = []
13
+
14
+ @plugin.inject_helpers(self)
15
+ extend FilterSyntax
16
+ end
17
+
18
+ def new_filter(name)
19
+ @filter = Filter.new(@plugin, name)
20
+ @filters << @filter
21
+ @filter
22
+ end
23
+
24
+ def read_sources(named_sources)
25
+ # reads a hash of <name> => <source string>
26
+ named_sources.each do |name, source|
27
+ new_filter(name)
28
+
29
+ begin
30
+ self.instance_eval(source[:code], source[:file_name])
31
+ rescue => detail
32
+ raise Errors::LoadError.new("problem loading filter #{name}", detail)
33
+ end
34
+ end
35
+
36
+ @filters
37
+ end
38
+
39
+ end
40
+
41
+ end
@@ -0,0 +1,46 @@
1
+ require "pathname"
2
+
3
+ module Vop
4
+
5
+ class PluginFinder
6
+
7
+ attr_reader :plugins, :templates
8
+
9
+ def initialize
10
+ reset
11
+ end
12
+
13
+ def reset
14
+ @plugins = []
15
+ @templates = []
16
+ end
17
+
18
+ def find(paths)
19
+ reset
20
+
21
+ $logger.debug "scanning #{paths} for plugins..."
22
+ paths = [ paths ] unless paths.is_a? Array
23
+
24
+ if paths.size > 0
25
+ paths.each do |path|
26
+ begin
27
+ next unless File.exists? path
28
+ rescue => e
29
+ if e.message =~ /Fixnum/
30
+ $logger.warn "unexpected Fixnum path : #{path}"
31
+ end
32
+ raise e
33
+ end
34
+
35
+
36
+ @plugins += Dir.glob("#{path}/**/*.plugin").map { |x| Pathname.new(File.dirname(x)).realpath.to_s }
37
+ @templates += Dir.glob("#{path}/**/plugin.vop").map { |x| Pathname.new(x).realpath.to_s }
38
+ end
39
+ end
40
+
41
+ self
42
+ end
43
+
44
+ end
45
+
46
+ end
@@ -0,0 +1,72 @@
1
+ require_relative "../objects/plugin"
2
+ require_relative "../syntax/plugin_syntax"
3
+ require_relative "../util/errors"
4
+
5
+ module Vop
6
+
7
+ class PluginLoader
8
+
9
+ attr_reader :loaded
10
+
11
+ def initialize(op)
12
+ @op = op
13
+
14
+ extend PluginSyntax
15
+ end
16
+
17
+ def reset
18
+ @loaded = []
19
+ end
20
+
21
+ def new_plugin(plugin_name, plugin_path, plugin_options = {})
22
+ @plugin = Plugin.new(@op, plugin_name, plugin_path, plugin_options)
23
+ @plugin
24
+ end
25
+
26
+ def read_plugin(code, source_file = nil)
27
+ begin
28
+ instance_eval(code, source_file)
29
+ rescue => detail
30
+ $logger.warn "problem loading plugin #{@plugin.name} : #{detail.message}\n#{detail.backtrace.join("\n")}"
31
+ raise Errors::PluginLoadError.new(detail)
32
+ end
33
+ end
34
+
35
+ def load(found, plugin_options = {})
36
+ reset
37
+
38
+ (plugins, templates) = [found.plugins, found.templates]
39
+
40
+ plugins.each do |plugin_path|
41
+ name = File.basename(plugin_path)
42
+
43
+ $logger.debug "loading #{name} from #{plugin_path}"
44
+ plugin = new_plugin(name, plugin_path, plugin_options)
45
+
46
+ templates.each do |template|
47
+ template_path = File.dirname(template)
48
+ if plugin_path.start_with? template_path
49
+ $logger.debug " (applying template #{template_path})"
50
+ template_file = File.join(template_path, "plugin.vop")
51
+ code = File.read(template_file)
52
+ read_plugin(code, template_file)
53
+ end
54
+ end
55
+
56
+ plugin_file = File.join(plugin_path, "#{name}.plugin")
57
+ next unless File.exists?(plugin_file)
58
+ $logger.debug "reading plugin '#{name}' from '#{plugin_file}'"
59
+
60
+ code = File.read(plugin_file)
61
+ read_plugin(code, plugin_file)
62
+ @loaded << @plugin
63
+
64
+ $logger.debug "loaded plugin #{@plugin.name}"
65
+ end
66
+
67
+ self
68
+ end
69
+
70
+ end
71
+
72
+ end