summaryrefslogtreecommitdiff
path: root/lib/hyperstack/state.rb
blob: 0ce26875c6de1e6e660502ac241b6b593686205f (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
# frozen_string_literal: true

require 'json'

module HyperstackVM
  # Persists VM state to a JSON file with atomic writes (write-to-tmp + rename).
  # Used to track provisioned VM ID, IP, WireGuard keys, and related metadata.
  class StateStore
    def initialize(path)
      @path = path
    end

    attr_reader :path

    def load
      return nil unless File.exist?(@path)

      JSON.parse(File.read(@path))
    rescue JSON::ParserError => e
      raise Error, "Failed to parse state file #{@path}: #{e.message}"
    end

    def save(payload)
      temp_path = "#{@path}.tmp"
      File.write(temp_path, JSON.pretty_generate(payload))
      File.rename(temp_path, @path)
    end

    def delete
      File.delete(@path) if File.exist?(@path)
    end
  end

  # Thread-safe output wrapper that prepends a fixed prefix to each line.
  # Used by create-both so interleaved output from VM1 and VM2 threads is distinguishable.
  # #print buffers partial lines until a newline is received, then flushes with the prefix.
  class PrefixedOutput
    def initialize(prefix, delegate, mutex)
      @prefix   = prefix
      @delegate = delegate
      @mutex    = mutex
      @buffer   = +''
    end

    def puts(msg = '')
      @mutex.synchronize { @delegate.puts("#{@prefix}#{msg}") }
    end

    def print(msg)
      @buffer << msg.to_s
      while (idx = @buffer.index("\n"))
        line = @buffer.slice!(0, idx + 1)
        @mutex.synchronize { @delegate.print("#{@prefix}#{line}") }
      end
    end
  end
end