hotcell-server 0.4.0 → 0.4.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b0353cf7589212280f099039e6bec97f6da765b1d0c7442fe3183de5ba9e8077
4
- data.tar.gz: 588b3f6a12b02dc00b096843d61335291d7bf62590337c2f52487df759f4ef05
3
+ metadata.gz: 390fc0f10953763de1f745cc3002fa0e548b362cbb21c82faba7dfa807b49dd5
4
+ data.tar.gz: fd6932e090acccea7964e38d76e65ea6163b1ebf6b5f401f6854fe8fa1e38034
5
5
  SHA512:
6
- metadata.gz: a15cb0b8e9d399a3a37659e7008b3c717337ea14beb6a13ad44e4490cb4faedf59aed7520b9abc265b4c762fdbd7d624a32632886470f915925e183739178496
7
- data.tar.gz: fffde7009ac88742aa9694585203de60cee1da4e637821f851eff453f67d9beaa52305049dfe1882373c396b92c933cf84bde9cf8b0fbd4850295a5ea7b3d020
6
+ metadata.gz: a34f827e6dbb2616e4e20b122ff6a8fdd78e40c19bfd8297c767c450a06ea4efaebc245d3ca3fb117caf13b70d922236e3c51f6a0a93207b5fc0a815e60e2583
7
+ data.tar.gz: '0769c12d7f26f35380dd5532edd8073c160f6a823128dcc4c88fddc697fd896be8cabec1eaa21dedf3c505a26ccda5a90cc38716a23461fa70410eb92d2ccc1e'
data/exe/hotcell CHANGED
@@ -3,9 +3,13 @@
3
3
 
4
4
  require "hot_cell/server"
5
5
 
6
+ development = ARGV.delete("--development") ? true : false
7
+ abort "usage: hotcell [--development]" unless ARGV.empty?
8
+
6
9
  HotCell.load!
7
10
 
8
11
  HotCell::Supervisor
9
- .new(directory: ENV.fetch("HOTCELL_DIR", "/run/hotcell/cell"), workspace: ENV["HOTCELL_WORKSPACE"])
12
+ .new(directory: ENV.fetch("HOTCELL_DIR", "/run/hotcell/cell"), workspace: ENV["HOTCELL_WORKSPACE"],
13
+ development: development)
10
14
  .boot
11
15
  .run
@@ -2,6 +2,6 @@
2
2
 
3
3
  module HotCell
4
4
  module Server
5
- VERSION = "0.4.0"
5
+ VERSION = "0.4.1"
6
6
  end
7
7
  end
@@ -138,9 +138,10 @@ module HotCell
138
138
 
139
139
  attr_reader :configuration, :counters, :log, :directory, :workspace
140
140
 
141
- def initialize(directory:, workspace: nil, configuration: HotCell.configuration, log: Log.new,
142
- ptrace_scope_path: PTRACE_SCOPE)
141
+ def initialize(directory:, workspace: nil, development: false, configuration: HotCell.configuration,
142
+ log: Log.new, ptrace_scope_path: PTRACE_SCOPE)
143
143
  @directory = directory
144
+ @development_tmpdir = own_tmpdir if development
144
145
  @workspace = workspace || File.join(tmpdir, "hotcell-workspace")
145
146
  @configuration = configuration
146
147
  @log = log
@@ -156,6 +157,7 @@ module HotCell
156
157
  verify_socket_paths!
157
158
  verify_limits!
158
159
  verify_ptrace_scope!
160
+ claim_tmpdir if @development_tmpdir
159
161
  verify_scratches!
160
162
  prepare_directories
161
163
  preload
@@ -164,7 +166,7 @@ module HotCell
164
166
  @control_handler = Control.new(configuration: configuration, counters: counters)
165
167
  trap_signals
166
168
 
167
- log.write "cell.boot", pid: Process.pid, directory: directory, operations: Registry.names,
169
+ log.write "cell.boot", pid: Process.pid, directory: directory, tmpdir: tmpdir, operations: Registry.names,
168
170
  configuration: configuration.to_h
169
171
  self
170
172
  end
@@ -1032,8 +1034,56 @@ module HotCell
1032
1034
  # `Dir.tmpdir` with its fallbacks removed: it answers `.` for a `/tmp` its owner cannot write, and a
1033
1035
  # worker can leave `/tmp` in that state. The sweep is what puts the mode back, so it has to see the
1034
1036
  # directory `Dir.tmpdir` will answer once it has.
1037
+ #
1038
+ # A cell told no `TMPDIR` sweeps the system's: right on the accessory, whose `/tmp` is its own mount, and
1039
+ # wrong on a developer's machine, where `/tmp` and macOS's per-user `TMPDIR` are shared with everything
1040
+ # else the developer runs. So `--development` never sweeps the directory it is given, unless an explicit
1041
+ # workspace makes it the workspace's parent. Its scratch is a directory of its own beneath it, named for
1042
+ # the socket directory so two cells one uid runs do not sweep each other's slots, and chosen once here so
1043
+ # the default workspace and the boot agree.
1035
1044
  def tmpdir
1036
- ENV.values_at("TMPDIR", "TMP", "TEMP").compact.reject(&:empty?).first || Etc.systmpdir
1045
+ @development_tmpdir || configured_tmpdir || Etc.systmpdir
1046
+ end
1047
+
1048
+ def configured_tmpdir
1049
+ ENV.values_at("TMPDIR", "TMP", "TEMP").compact.reject(&:empty?).first
1050
+ end
1051
+
1052
+ def own_tmpdir
1053
+ parent = configured_tmpdir || Etc.systmpdir
1054
+ File.expand_path File.join(parent, "hotcell-#{directory.delete_prefix("/").tr("/", "-")}")
1055
+ end
1056
+
1057
+ # The parent is world-writable, so the name is claimed with `mkdir` and, when it exists, read back with
1058
+ # `lstat`: it has to be a plain directory this uid owns. Its mode goes back to `0700` because the sticky
1059
+ # parent stops another uid from replacing the directory, not from writing inside one a worker opened up.
1060
+ # The ancestors are checked before `mkdir` and `chmod` could act through an owned symlink;
1061
+ # `verify_scratches!` checks them again. `ENV["TMPDIR"]` is left alone: the supervisor writes no temporary
1062
+ # files, a worker sets its own per request, and a second supervisor in this process would nest its
1063
+ # scratch inside this one.
1064
+ #
1065
+ # Accepted: an entry another uid plants inside, once a worker has opened the directory up, survives the
1066
+ # sweep, which removes only what this uid owns, and a slot follows it. Opening it up needs code already
1067
+ # running as this uid, which can reach the same files directly.
1068
+ def claim_tmpdir
1069
+ claimed = tmpdir
1070
+ if (link = owned_symlink_on(File.dirname(claimed)))
1071
+ raise ConfigurationError, "#{link} is a symlink the cell's uid owns, on the path to the scratch " \
1072
+ "#{claimed}, and a boot sweeps the scratch"
1073
+ end
1074
+
1075
+ begin
1076
+ Dir.mkdir claimed, 0o700
1077
+ rescue Errno::EEXIST
1078
+ stat = File.lstat(claimed)
1079
+ unless stat.directory? && stat.uid == Process.uid
1080
+ raise ConfigurationError, "#{claimed} is not a directory this uid owns, and a boot sweeps it. Set " \
1081
+ "TMPDIR to a directory of the cell's own."
1082
+ end
1083
+ File.chmod 0o700, claimed
1084
+ end
1085
+ rescue SystemCallError => error
1086
+ raise ConfigurationError, "the scratch #{claimed} could not be claimed: #{error.message}"
1037
1087
  end
1038
1088
 
1039
1089
  def scratches
@@ -45,7 +45,8 @@ module HotCell
45
45
  end
46
46
 
47
47
  # Anything in `supervisor:` goes to Supervisor.new; everything else is the cell's own limits.
48
- def initialize(name: "test", supervisor: {}, operations: nil, **options)
48
+ # `own_tmpdir: false` boots the cell with no `TMPDIR` at all, the way a Procfile that sets none does.
49
+ def initialize(name: "test", supervisor: {}, operations: nil, own_tmpdir: true, **options)
49
50
  @name = name
50
51
  @supervisor_options = supervisor
51
52
  @operations = operations
@@ -54,8 +55,8 @@ module HotCell
54
55
  @directory = File.join(@root, name)
55
56
  # Boot empties `Dir.tmpdir` and the workspace's parent of what this uid owns. Inside the cell both are
56
57
  # this directory, so the sweep reaches neither the developer's `/tmp` nor the log and sockets beside it.
57
- @tmpdir = File.join(@root, "tmp")
58
- @workspace = File.join(@tmpdir, "workspace")
58
+ @tmpdir = File.join(@root, "tmp") if own_tmpdir
59
+ @workspace = File.join(@tmpdir, "workspace") if own_tmpdir
59
60
  @log_path = File.join(@root, "cell.log")
60
61
  end
61
62
 
@@ -78,8 +79,12 @@ module HotCell
78
79
 
79
80
  HotCell.limits(**@options) unless @options.empty?
80
81
 
81
- FileUtils.mkdir_p @tmpdir
82
- ENV["TMPDIR"] = @tmpdir
82
+ if @tmpdir
83
+ FileUtils.mkdir_p @tmpdir
84
+ ENV["TMPDIR"] = @tmpdir
85
+ else
86
+ %w[ TMPDIR TMP TEMP ].each { |key| ENV.delete key }
87
+ end
83
88
 
84
89
  supervisor = Supervisor.new(directory: directory, workspace: workspace,
85
90
  log: Log.new(File.open(log_path, "w")), **@supervisor_options)
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: hotcell-server
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.0
4
+ version: 0.4.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mike Dalessio
@@ -15,14 +15,14 @@ dependencies:
15
15
  requirements:
16
16
  - - '='
17
17
  - !ruby/object:Gem::Version
18
- version: 0.4.0
18
+ version: 0.4.1
19
19
  type: :runtime
20
20
  prerelease: false
21
21
  version_requirements: !ruby/object:Gem::Requirement
22
22
  requirements:
23
23
  - - '='
24
24
  - !ruby/object:Gem::Version
25
- version: 0.4.0
25
+ version: 0.4.1
26
26
  description: |
27
27
  Runs a HotCell container. A supervisor listens on two Unix sockets, forks a worker for each request,
28
28
  and enforces a wall clock deadline and resource limits on it. Write the work as a subclass of
@@ -62,8 +62,8 @@ licenses:
62
62
  - MIT
63
63
  metadata:
64
64
  homepage_uri: https://github.com/basecamp/hotcell
65
- source_code_uri: https://github.com/basecamp/hotcell/tree/v0.4.0/hotcell-server
66
- changelog_uri: https://github.com/basecamp/hotcell/blob/v0.4.0/CHANGELOG.md
65
+ source_code_uri: https://github.com/basecamp/hotcell/tree/v0.4.1/hotcell-server
66
+ changelog_uri: https://github.com/basecamp/hotcell/blob/v0.4.1/CHANGELOG.md
67
67
  bug_tracker_uri: https://github.com/basecamp/hotcell/issues
68
68
  rubygems_mfa_required: 'true'
69
69
  rdoc_options: []