aboutsummaryrefslogtreecommitdiffstats
path: root/src/services.js
blob: a5158149b25979ca0efac8ea876d1b1830902e16 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
var URL_MATCH = /^(file|ftp|http|https):\/\/(\w+:{0,1}\w*@)?([\w\.-]*)(:([0-9]+))?([^\?#]+)(\?([^#]*))?(#(.*))?$/,
    HASH_MATCH = /^([^\?]*)?(\?([^\?]*))?$/,
    DEFAULT_PORTS = {'http': 80, 'https': 443, 'ftp':21};

angularService("$window", bind(window, identity, window));
angularService("$document", function(window){
  return jqLite(window.document);
}, {inject:['$window']});

angularService("$location", function(browser){
  var scope = this,
      location = {parse:parseUrl, toString:toString, update:update},
      lastLocation = {};

  browser.watchUrl(function(url){
    update(url);
    scope.$root.$eval();
  });
  this.$onEval(PRIORITY_FIRST, update);
  this.$onEval(PRIORITY_LAST, update);
  update(browser.getUrl());
  return location;

  function update(href){
    if (href) {
      parseUrl(href);
    } else {
      href = check('href') || checkProtocol();
      var hash = check('hash');
      if (isUndefined(hash)) hash = checkHashPathSearch();
      if (isDefined(hash)) {
        href = (href || location.href).split('#')[0];
        href+= '#' + hash;
      }
      if (isDefined(href)) {
        parseUrl(href);
        browser.setUrl(href);
      }
    }
  }

  function check(param) {
    return lastLocation[param] == location[param] ? undefined : location[param];
  }

  function checkProtocol(){
    if (lastLocation.protocol === location.protocol &&
        lastLocation.host === location.host &&
        lastLocation.port === location.port &&
        lastLocation.path === location.path &&
        equals(lastLocation.search, location.search))
      return undefined;
    var url = toKeyValue(location.search);
    var port = (location.port == DEFAULT_PORTS[location.protocol] ? null : location.port);
    return location.protocol  + '://' + location.host +
          (port ? ':' + port : '') + location.path +
          (url ? '?' + url : '');
  }

  function checkHashPathSearch(){
    if (lastLocation.hashPath === location.hashPath &&
        equals(lastLocation.hashSearch, location.hashSearch) )
      return undefined;
    var url = toKeyValue(location.hashSearch);
    return escape(location.hashPath) + (url ? '?' + url : '');
  }

  function parseUrl(url){
    if (isDefined(url)) {
      var match = URL_MATCH.exec(url);
      if (match) {
        location.href = url.replace('#$', '');
        location.protocol = match[1];
        location.host = match[3] || '';
        location.port = match[5] || DEFAULT_PORTS[location.protocol] || null;
        location.path = match[6];
        location.search = parseKeyValue(match[8]);
        location.hash = match[10] || '';
        match = HASH_MATCH.exec(location.hash);
        location.hashPath = unescape(match[1] || '');
        location.hashSearch = parseKeyValue(match[3]);

        copy(location, lastLocation);
      }
    }
  }

  function toString() {
    update();
    return location.href;
  }
}, {inject: ['$browser']});

angularService("$log", function($window){
  var console = $window.console || {log: noop, warn: noop, info: noop, error: noop},
      log = console.log || noop;
  return {
    log: bind(console, log),
    warn: bind(console, console.warn || log),
    info: bind(console, console.info || log),
    error: bind(console, console.error || log)
  };
}, {inject:['$window']});

angularService('$exceptionHandler', function($log){
  return function(e) {
    $log.error(e);
  };
}, {inject:['$log']});

angularService("$hover", function(browser, document) {
  var tooltip, self = this, error, width = 300, arrowWidth = 10, body = jqLite(document[0].body);;
  browser.hover(function(element, show){
    if (show && (error = element.attr(NG_EXCEPTION) || element.attr(NG_VALIDATION_ERROR))) {
      if (!tooltip) {
        tooltip = {
            callout: jqLite('<div id="ng-callout"></div>'),
            arrow: jqLite('<div></div>'),
            title: jqLite('<div class="ng-title"></div>'),
            content: jqLite('<div class="ng-content"></div>')
        };
        tooltip.callout.append(tooltip.arrow);
        tooltip.callout.append(tooltip.title);
        tooltip.callout.append(tooltip.content);
        body.append(tooltip.callout);
      }
      var docRect = body[0].getBoundingClientRect(),
          elementRect = element[0].getBoundingClientRect(),
          leftSpace = docRect.right - elementRect.right - arrowWidth;
      tooltip.title.text(element.hasClass("ng-exception") ? "EXCEPTION:" : "Validation error...");
      tooltip.content.text(error);
      if (leftSpace < width) {
        tooltip.arrow.addClass('ng-arrow-right');
        tooltip.arrow.css({left: (width + 1)+'px'});
        tooltip.callout.css({
          position: 'fixed',
          left: (elementRect.left - arrowWidth - width - 4) + "px",
          top: (elementRect.top - 3) + "px",
          width: width + "px"
        });
      } else {
        tooltip.arrow.addClass('ng-arrow-left');
        tooltip.callout.css({
          position: 'fixed',
          left: (elementRect.right + arrowWidth) + "px",
          top: (elementRect.top - 3) + "px",
          width: width + "px"
        });
      }
    } else if (tooltip) {
      tooltip.callout.remove();
      tooltip = null;
    }
  });
}, {inject:['$browser', '$document']});

angularService("$invalidWidgets", function(){
  var invalidWidgets = [];
  invalidWidgets.markValid = function(element){
    var index = indexOf(invalidWidgets, element);
    if (index != -1)
      invalidWidgets.splice(index, 1);
  };
  invalidWidgets.markInvalid = function(element){
    var index = indexOf(invalidWidgets, element);
    if (index === -1)
      invalidWidgets.push(element);
  };
  invalidWidgets.visible = function() {
    var count = 0;
    foreach(invalidWidgets, function(widget){
      count = count + (isVisible(widget) ? 1 : 0);
    });
    return count;
  };
  invalidWidgets.clearOrphans = function() {
    for(var i = 0; i < invalidWidgets.length;) {
      var widget = invalidWidgets[i];
      if (isOrphan(widget[0])) {
        invalidWidgets.splice(i, 1);
      } else {
        i++;
      }
    }
  };
  function isOrphan(widget) {
    if (widget == window.document) return false;
    var parent = widget.parentNode;
    return !parent || isOrphan(parent);
  }
  return invalidWidgets;
});

function switchRouteMatcher(on, when, dstName) {
  var regex = '^' + when.replace(/[\.\\\(\)\^\$]/g, "\$1") + '$',
      params = [],
      dst = {};
  foreach(when.split(/\W/), function(param){
    if (param) {
      var paramRegExp = new RegExp(":" + param + "([\\W])");
      if (regex.match(paramRegExp)) {
        regex = regex.replace(paramRegExp, "([^\/]*)$1");
        params.push(param);
      }
    }
  });
  var match = on.match(new RegExp(regex));
  if (match) {
    foreach(params, function(name, index){
      dst[name] = match[index + 1];
    });
    if (dstName) this.$set(dstName, dst);
  }
  return match ? dst : null;
}

angularService('$route', function(location){
  var routes = {},
      onChange = [],
      matcher = switchRouteMatcher,
      parentScope = this,
      dirty = 0,
      $route = {
        routes: routes,
        onChange: bind(onChange, onChange.push),
        when:function (path, params){
          if (angular.isUndefined(path)) return routes;
          var route = routes[path];
          if (!route) route = routes[path] = {};
          if (params) angular.extend(route, params);
          dirty++;
          return route;
        }
      };
  function updateRoute(){
    var childScope;
    $route.current = null;
    angular.foreach(routes, function(routeParams, route) {
      if (!childScope) {
        var pathParams = matcher(location.hashPath, route);
        if (pathParams) {
          childScope = angular.scope(parentScope);
          $route.current = angular.extend({}, routeParams, {
            scope: childScope,
            params: angular.extend({}, location.hashSearch, pathParams)
          });
        }
      }
    });
    angular.foreach(onChange, parentScope.$tryEval);
    if (childScope) {
      childScope.$become($route.current.controller);
      parentScope.$tryEval(childScope.init);
    }
  }
  this.$watch(function(){return dirty + location.hash;}, updateRoute);
  return $route;
}, {inject: ['$location']});

angularService('$xhr', function($browser, $error, $log){
  var self = this;
  return function(method, url, post, callback){
    if (isFunction(post)) {
      callback = post;
      post = null;
    }
    if (post && isObject(post)) {
      post = toJson(post);
    }
    $browser.xhr(method, url, post, function(code, response){
      try {
        if (isString(response) && /^\s*[\[\{]/.exec(response) && /[\}\]]\s*$/.exec(response)) {
          response = fromJson(response);
        }
        if (code == 200) {
          callback(code, response);
        } else {
          $error(
            {method: method, url:url, data:post, callback:callback},
            {status: code, body:response});
        }
      } catch (e) {
        $log.error(e);
      } finally {
        self.$eval();
      }
    });
  };
}, {inject:['$browser', '$xhr.error', '$log']});

angularService('$xhr.error', function($log){
  return function(request, response){
    $log.error('ERROR: XHR: ' + request.url, request, response);
  };
}, {inject:['$log']});

angularService('$xhr.bulk', function($xhr, $error, $log){
  var requests = [],
      scope = this;
  function bulkXHR(method, url, post, callback) {
    if (isFunction(post)) {
      callback = post;
      post = null;
    }
    var currentQueue;
    foreach(bulkXHR.urls, function(queue){
      if (isFunction(queue.match) ? queue.match(url) : queue.match.exec(url)) {
        currentQueue = queue;
      }
    });
    if (currentQueue) {
      if (!currentQueue.requests) currentQueue.requests = [];
      currentQueue.requests.push({method: method, url: url, data:post, callback:callback});
    } else {
      $xhr(method, url, post, callback);
    }
  }
  bulkXHR.urls = {};
  bulkXHR.flush = function(callback){
    foreach(bulkXHR.urls, function(queue, url){
      var currentRequests = queue.requests;
      if (currentRequests && currentRequests.length) {
        queue.requests = [];
        queue.callbacks = [];
        $xhr('POST', url, {requests:currentRequests}, function(code, response){
          foreach(response, function(response, i){
            try {
              if (response.status == 200) {
                (currentRequests[i].callback || noop)(response.status, response.response);
              } else {
                $error(currentRequests[i], response);
              }
            } catch(e) {
              $log.error(e);
            }
          });
          (callback || noop)();
        });
        scope.$eval();
      }
    });
  };
  this.$onEval(PRIORITY_LAST, bulkXHR.flush);
  return bulkXHR;
}, {inject:['$xhr', '$xhr.error', '$log']});

angularService('$xhr.cache', function($xhr){
  var inflight = {}, self = this;;
  function cache(method, url, post, callback, verifyCache){
    if (isFunction(post)) {
      callback = post;
      post = null;
    }
    if (method == 'GET') {
      var data;
      if (data = cache.data[url]) {
        callback(200, copy(data.value));
        if (!verifyCache)
          return;
      }

      if (data = inflight[url]) {
        data.callbacks.push(callback);
      } else {
        inflight[url] = {callbacks: [callback]};
        cache.delegate(method, url, post, function(status, response){
          if (status == 200)
            cache.data[url] = { value: response };
          var callbacks = inflight[url].callbacks;
          delete inflight[url];
          foreach(callbacks, function(callback){
            try {
              (callback||noop)(status, copy(response));
            } catch(e) {
              self.$log.error(e);
            }
          });
        });
      }

    } else {
      cache.data = {};
      cache.delegate(method, url, post, callback);
    }
  }
  cache.data = {};
  cache.delegate = $xhr;
  return cache;
}, {inject:['$xhr.bulk']});

angularService('$resource', function($xhr){
  var resource = new ResourceFactory($xhr);
  return bind(resource, resource.route);
}, {inject: ['$xhr.cache']});
span>deps) inherited_options = Hash.new { |hash, key| hash[key] = Options.new } expanded_deps = Dependency.expand(formula, deps) do |dependent, dep| inherited_options[dep.name] |= inherited_options_for(dep) build = effective_build_options_for( dependent, inherited_options.fetch(dependent.name, []), ) if (dep.optional? || dep.recommended?) && build.without?(dep) Dependency.prune elsif dep.build? && install_bottle_for?(dependent, build) Dependency.prune elsif dep.satisfied?(inherited_options[dep.name]) Dependency.skip end end expanded_deps.map { |dep| [dep, inherited_options[dep.name]] } end def effective_build_options_for(dependent, inherited_options = []) args = dependent.build.used_options args |= dependent == formula ? options : inherited_options args |= Tab.for_formula(dependent).used_options args &= dependent.options BuildOptions.new(args, dependent.options) end def inherited_options_for(dep) inherited_options = Options.new u = Option.new("universal") if (options.include?(u) || formula.require_universal_deps?) && !dep.build? && dep.to_formula.option_defined?(u) inherited_options << u end inherited_options end def install_dependencies(deps) if deps.empty? && only_deps? puts "All dependencies for #{formula.full_name} are satisfied." elsif !deps.empty? oh1 "Installing dependencies for #{formula.full_name}: #{deps.map(&:first).map(&Formatter.method(:identifier)).join(", ")}", truncate: false deps.each { |dep, options| install_dependency(dep, options) } end @show_header = true unless deps.empty? end def install_dependency(dep, inherited_options) df = dep.to_formula tab = Tab.for_formula(df) if df.linked_keg.directory? linked_keg = Keg.new(df.linked_keg.resolved_path) linked_keg.unlink end if df.installed? installed_keg = Keg.new(df.prefix) tmp_keg = Pathname.new("#{installed_keg}.tmp") installed_keg.rename(tmp_keg) end fi = FormulaInstaller.new(df) fi.options |= tab.used_options fi.options |= Tab.remap_deprecated_options(df.deprecated_options, dep.options) fi.options |= inherited_options fi.options &= df.options fi.build_from_source = ARGV.build_formula_from_source?(df) fi.verbose = verbose? && !quieter? fi.debug = debug? fi.installed_as_dependency = true fi.installed_on_request = false fi.prelude oh1 "Installing #{formula.full_name} dependency: #{Formatter.identifier(dep.name)}" fi.install fi.finish rescue Exception ignore_interrupts do tmp_keg.rename(installed_keg) if tmp_keg && !installed_keg.directory? linked_keg.link if linked_keg end raise else ignore_interrupts { tmp_keg.rmtree if tmp_keg && tmp_keg.directory? } end def caveats return if only_deps? audit_installed if ARGV.homebrew_developer? && !formula.keg_only? c = Caveats.new(formula) return if c.empty? @show_summary_heading = true ohai "Caveats", c.caveats end def finish return if only_deps? ohai "Finishing up" if verbose? install_plist keg = Keg.new(formula.prefix) link(keg) unless @poured_bottle && formula.bottle_specification.skip_relocation? fix_dynamic_linkage(keg) end if formula.post_install_defined? if build_bottle? ohai "Not running post_install as we're building a bottle" puts "You can run it manually using `brew postinstall #{formula.full_name}`" else post_install end end caveats ohai "Summary" if verbose? || show_summary_heading? puts summary # let's reset Utils.git_available? if we just installed git Utils.clear_git_available_cache if formula.name == "git" ensure unlock end def summary s = "" s << "#{Emoji.install_badge} " if Emoji.enabled? s << "#{formula.prefix.resolved_path}: #{formula.prefix.abv}" s << ", built in #{pretty_duration build_time}" if build_time s end def build_time @build_time ||= Time.now - @start_time if @start_time && !interactive? end def sanitized_argv_options args = [] args << "--ignore-dependencies" if ignore_deps? if build_bottle? args << "--build-bottle" args << "--bottle-arch=#{ARGV.bottle_arch}" if ARGV.bottle_arch end args << "--git" if git? args << "--interactive" if interactive? args << "--verbose" if verbose? args << "--debug" if debug? args << "--cc=#{ARGV.cc}" if ARGV.cc args << "--default-fortran-flags" if ARGV.include? "--default-fortran-flags" args << "--keep-tmp" if ARGV.keep_tmp? if ARGV.env args << "--env=#{ARGV.env}" elsif formula.env.std? || formula.deps.select(&:build?).any? { |d| d.name == "scons" } args << "--env=std" end if formula.head? args << "--HEAD" elsif formula.devel? args << "--devel" end formula.options.each do |opt| name = opt.name[/^([^=]+)=$/, 1] value = ARGV.value(name) if name args << "--#{name}=#{value}" if value end args end def build_argv sanitized_argv_options + options.as_flags end def build FileUtils.rm_rf(formula.logs) @start_time = Time.now # 1. formulae can modify ENV, so we must ensure that each # installation has a pristine ENV when it starts, forking now is # the easiest way to do this args = %W[ nice #{RUBY_PATH} -W0 -I #{HOMEBREW_LOAD_PATH} -- #{HOMEBREW_LIBRARY_PATH}/build.rb #{formula.specified_path} ].concat(build_argv) Sandbox.print_sandbox_message if Sandbox.formula?(formula) Utils.safe_fork do # Invalidate the current sudo timestamp in case a build script calls sudo system "/usr/bin/sudo", "-k" if Sandbox.formula?(formula) sandbox = Sandbox.new formula.logs.mkpath sandbox.record_log(formula.logs/"build.sandbox.log") sandbox.allow_write_temp_and_cache sandbox.allow_write_log(formula) sandbox.allow_write_xcode sandbox.allow_write_cellar(formula) sandbox.exec(*args) else exec(*args) end end formula.update_head_version if !formula.prefix.directory? || Keg.new(formula.prefix).empty_installation? raise "Empty installation" end rescue Exception ignore_interrupts do # any exceptions must leave us with nothing installed formula.update_head_version formula.prefix.rmtree if formula.prefix.directory? formula.rack.rmdir_if_possible end raise end def link(keg) if formula.keg_only? begin keg.optlink rescue Keg::LinkError => e onoe "Failed to create #{formula.opt_prefix}" puts "Things that depend on #{formula.full_name} will probably not build." puts e Homebrew.failed = true end return end if keg.linked? opoo "This keg was marked linked already, continuing anyway" keg.remove_linked_keg_record end link_overwrite_backup = {} # Hash: conflict file -> backup file backup_dir = HOMEBREW_CACHE/"Backup" begin keg.link rescue Keg::ConflictError => e conflict_file = e.dst if formula.link_overwrite?(conflict_file) && !link_overwrite_backup.key?(conflict_file) backup_file = backup_dir/conflict_file.relative_path_from(HOMEBREW_PREFIX).to_s backup_file.parent.mkpath conflict_file.rename backup_file link_overwrite_backup[conflict_file] = backup_file retry end onoe "The `brew link` step did not complete successfully" puts "The formula built, but is not symlinked into #{HOMEBREW_PREFIX}" puts e puts puts "Possible conflicting files are:" mode = OpenStruct.new(dry_run: true, overwrite: true) keg.link(mode) @show_summary_heading = true Homebrew.failed = true rescue Keg::LinkError => e onoe "The `brew link` step did not complete successfully" puts "The formula built, but is not symlinked into #{HOMEBREW_PREFIX}" puts e puts puts "You can try again using:" puts " brew link #{formula.name}" @show_summary_heading = true Homebrew.failed = true rescue Exception => e onoe "An unexpected error occurred during the `brew link` step" puts "The formula built, but is not symlinked into #{HOMEBREW_PREFIX}" puts e puts e.backtrace if debug? @show_summary_heading = true ignore_interrupts do keg.unlink link_overwrite_backup.each do |origin, backup| origin.parent.mkpath backup.rename origin end end Homebrew.failed = true raise end return if link_overwrite_backup.empty? opoo "These files were overwritten during `brew link` step:" puts link_overwrite_backup.keys puts puts "They have been backed up in #{backup_dir}" @show_summary_heading = true end def install_plist return unless formula.plist formula.plist_path.atomic_write(formula.plist) formula.plist_path.chmod 0644 log = formula.var/"log" log.mkpath if formula.plist.include? log.to_s rescue Exception => e onoe "Failed to install plist file" ohai e, e.backtrace if debug? Homebrew.failed = true end def fix_dynamic_linkage(keg) keg.fix_dynamic_linkage rescue Exception => e onoe "Failed to fix install linkage" puts "The formula built, but you may encounter issues using it or linking other" puts "formula against it." ohai e, e.backtrace if debug? Homebrew.failed = true @show_summary_heading = true end def clean ohai "Cleaning" if verbose? Cleaner.new(formula).clean rescue Exception => e opoo "The cleaning step did not complete successfully" puts "Still, the installation was successful, so we will link it into your prefix" ohai e, e.backtrace if debug? Homebrew.failed = true @show_summary_heading = true end def post_install Homebrew.run_post_install(formula) rescue Exception => e opoo "The post-install step did not complete successfully" puts "You can try again using `brew postinstall #{formula.full_name}`" ohai e, e.backtrace if debug? Homebrew.failed = true @show_summary_heading = true end def pour if Homebrew::Hooks::Bottles.formula_has_bottle?(formula) return if Homebrew::Hooks::Bottles.pour_formula_bottle(formula) end if (bottle_path = formula.local_bottle_path) downloader = LocalBottleDownloadStrategy.new(bottle_path) else downloader = formula.bottle downloader.verify_download_integrity(downloader.fetch) end HOMEBREW_CELLAR.cd do downloader.stage end keg = Keg.new(formula.prefix) tab = Tab.for_keg(keg) Tab.clear_cache skip_linkage = formula.bottle_specification.skip_relocation? keg.replace_placeholders_with_locations tab.changed_files, skip_linkage: skip_linkage Pathname.glob("#{formula.bottle_prefix}/{etc,var}/**/*") do |path| path.extend(InstallRenamed) path.cp_path_sub(formula.bottle_prefix, HOMEBREW_PREFIX) end FileUtils.rm_rf formula.bottle_prefix tab = Tab.for_keg(keg) CxxStdlib.check_compatibility( formula, formula.recursive_dependencies, Keg.new(formula.prefix), tab.compiler ) tab.tap = formula.tap tab.poured_from_bottle = true tab.time = Time.now.to_i tab.head = HOMEBREW_REPOSITORY.git_head tab.source["path"] = formula.specified_path.to_s tab.installed_as_dependency = installed_as_dependency tab.installed_on_request = installed_on_request tab.write end def audit_check_output(output) return unless output opoo output @show_summary_heading = true end def audit_installed audit_check_output(check_env_path(formula.bin)) audit_check_output(check_env_path(formula.sbin)) super end private def hold_locks? @hold_locks || false end def lock return unless (@@locked ||= []).empty? unless ignore_deps? formula.recursive_dependencies.each do |dep| @@locked << dep.to_formula end end @@locked.unshift(formula) @@locked.uniq! @@locked.each(&:lock) @hold_locks = true end def unlock return unless hold_locks? @@locked.each(&:unlock) @@locked.clear @hold_locks = false end def puts_requirement_messages return unless @requirement_messages return if @requirement_messages.empty? puts @requirement_messages end end