Class: Mxrb::DomainDiagram::Lifecycle

Inherits:
Object
  • Object
show all
Defined in:
lib/mxrb/domain_diagram/lifecycle.rb

Overview

Background lifecycle for the ER editor. Control happens through a token-authenticated loopback endpoint rather than a potentially stale PID. rubocop:disable Metrics

Defined Under Namespace

Classes: Status

Constant Summary collapse

STATE_ROOT_ENV =
'MXRB_DIAGRAM_STATE_ROOT'
START_TIMEOUT =
8
STOP_TIMEOUT =
8

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(source, state_root: self.class.default_state_root, executable: $PROGRAM_NAME) ⇒ Lifecycle

Returns a new instance of Lifecycle.



28
29
30
31
32
# File 'lib/mxrb/domain_diagram/lifecycle.rb', line 28

def initialize(source, state_root: self.class.default_state_root, executable: $PROGRAM_NAME)
  @source = File.expand_path(source)
  @state_root = File.expand_path(state_root)
  @executable = File.expand_path(executable)
end

Instance Attribute Details

#executableObject (readonly)

Returns the value of attribute executable.



26
27
28
# File 'lib/mxrb/domain_diagram/lifecycle.rb', line 26

def executable
  @executable
end

#sourceObject (readonly)

Returns the value of attribute source.



26
27
28
# File 'lib/mxrb/domain_diagram/lifecycle.rb', line 26

def source
  @source
end

#state_rootObject (readonly)

Returns the value of attribute state_root.



26
27
28
# File 'lib/mxrb/domain_diagram/lifecycle.rb', line 26

def state_root
  @state_root
end

Class Method Details

.default_state_rootObject



34
35
36
37
38
39
# File 'lib/mxrb/domain_diagram/lifecycle.rb', line 34

def self.default_state_root
  configured = ENV[STATE_ROOT_ENV]
  return configured unless configured.to_s.empty?

  File.join(Dir.home, '.local', 'state', 'mxrb', 'diagram-er')
end

Instance Method Details

#destroy(confirm: false) ⇒ Object

Raises:

  • (ArgumentError)


98
99
100
101
102
103
104
105
106
107
108
109
# File 'lib/mxrb/domain_diagram/lifecycle.rb', line 98

def destroy(confirm: false)
  raise ArgumentError, 'destroy requires --yes' unless confirm

  state = read_state or raise ArgumentError, "no managed diagram for #{source}"
  current = status
  down if %w[running starting].include?(current.state)
  Array(state['managed_paths']).reverse_each do |path|
    remove_managed_path(path, state.fetch('output'))
  end
  FileUtils.remove_entry_secure(instance_dir) if File.exist?(instance_dir)
  Status.new('absent', source, nil, nil, nil, nil)
end

#downObject

Raises:

  • (ArgumentError)


81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
# File 'lib/mxrb/domain_diagram/lifecycle.rb', line 81

def down
  state = read_state or raise ArgumentError, "no managed diagram for #{source}"
  unless running?(state)
    raise ArgumentError, 'managed diagram is still starting; retry in a moment' if starting?(state)

    mark_stopped(state.fetch('token'))
    return status
  end

  response = request(state, Net::HTTP::Post, '/api/admin/shutdown')
  raise ArgumentError, 'managed diagram refused the shutdown request' unless response.is_a?(Net::HTTPSuccess)

  wait_until(STOP_TIMEOUT) { !running?(read_state) }
  mark_stopped(state.fetch('token'))
  status
end

#run_worker(output:, modules:, port:, token:) ⇒ Object

Internal worker entrypoint used by mxrb diagram-er __serve.



112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
# File 'lib/mxrb/domain_diagram/lifecycle.rb', line 112

def run_worker(output:, modules:, port:, token:)
  state = read_state or raise ArgumentError, 'diagram lifecycle state is missing'
  raise ArgumentError, 'diagram lifecycle token does not match' unless state['token'] == token

  server = Server.new(
    source, output:, modules:, port:, reuse: true, lifecycle_token: token
  )
  trap('INT') { Thread.new { server.shutdown } }
  trap('TERM') { Thread.new { server.shutdown } }
  server.start do
    write_state(state.merge(
                  'pid' => Process.pid, 'state' => 'running',
                  'started_at' => Time.now.utc.iso8601
                ))
  end
ensure
  mark_stopped(token) if defined?(token) && token
end

#statusObject



65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
# File 'lib/mxrb/domain_diagram/lifecycle.rb', line 65

def status
  state = read_state
  return Status.new('absent', source, nil, nil, nil, nil) unless state

  lifecycle_state = if running?(state)
                      'running'
                    elsif starting?(state)
                      'starting'
                    else
                      'stopped'
                    end
  Status.new(
    lifecycle_state, source, state['output'], url_for(state), state['pid'], state['log']
  )
end

#up(output: nil, modules: nil, port: nil, force: false) ⇒ Object



41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
# File 'lib/mxrb/domain_diagram/lifecycle.rb', line 41

def up(output: nil, modules: nil, port: nil, force: false)
  previous = read_state
  raise ArgumentError, "diagram already running at #{url_for(previous)}" if running?(previous)
  raise ArgumentError, 'managed diagram is already starting' if starting?(previous)

  settings = settings_for(previous, output:, modules:, port:)
  managed_paths = prepare_output(previous, settings, force:)
  token = SecureRandom.hex(24)
  state = settings.merge(
    'version' => 1, 'source' => source, 'token' => token, 'state' => 'starting',
    'pid' => nil, 'log' => log_path, 'managed_paths' => managed_paths,
    'created_at' => Time.now.utc.iso8601
  )
  write_state(state)
  pid = spawn_worker(state)
  Process.detach(pid)
  write_state(read_state.merge('pid' => pid))
  wait_until(START_TIMEOUT) { running?(read_state, token:) }
  status
rescue StandardError
  mark_stopped(token) if defined?(token) && token
  raise
end