Class: Makit::Process

Inherits:
Object
  • Object
show all
Defined in:
lib/makit/process.rb

Overview

Cross-platform process management utilities

This class provides methods for checking, listing, and terminating processes across different operating systems (Windows, Linux, macOS). It abstracts platform-specific commands into a unified interface.

Class Method Summary collapse

Class Method Details

.background_running?(pid_file) ⇒ Boolean

Check if a background process is still running using its PID file

Parameters:

  • pid_file (String)

    Path to the PID file containing the process ID

Returns:

  • (Boolean)

    True if the process is running, false otherwise



285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
# File 'lib/makit/process.rb', line 285

def self.background_running?(pid_file)
  return false unless File.exist?(pid_file)

  begin
    pid = File.read(pid_file).strip.to_i
    return false if pid <= 0

    # Check if process is still running

    Process.getpgid(pid)
    true
  rescue Errno::ESRCH, Errno::EPERM
    false
  rescue => e
    # Log error but return false

    puts "Error checking background process: #{e.message}" if defined?(puts)
    false
  end
end

.cleanup_stale_pids(pid_files) ⇒ Array<String>

Clean up stale PID files (processes that are no longer running)

Parameters:

  • pid_files (Array<String>)

    Array of PID file paths to check

Returns:

  • (Array<String>)

    Array of cleaned up PID file paths



325
326
327
328
329
330
331
332
333
334
335
336
337
338
# File 'lib/makit/process.rb', line 325

def self.cleanup_stale_pids(pid_files)
  cleaned = []
  
  pid_files.each do |pid_file|
    next unless File.exist?(pid_file)
    
    unless background_running?(pid_file)
      File.delete(pid_file)
      cleaned << pid_file
    end
  end
  
  cleaned
end

.get_background_pid(pid_file) ⇒ Integer?

Get the PID of a background process from its PID file

Parameters:

  • pid_file (String)

    Path to the PID file containing the process ID

Returns:

  • (Integer, nil)

    The process ID, or nil if not found or invalid



308
309
310
311
312
313
314
315
316
317
318
319
# File 'lib/makit/process.rb', line 308

def self.get_background_pid(pid_file)
  return nil unless File.exist?(pid_file)

  begin
    pid = File.read(pid_file).strip.to_i
    return nil if pid <= 0
    pid
  rescue => e
    puts "Error reading PID file: #{e.message}" if defined?(puts)
    nil
  end
end

.health_check_port(port, host: "localhost", timeout: 5) ⇒ Boolean

Health check for a service running on a specific port

Parameters:

  • port (Integer)

    Port number to check

  • host (String) (defaults to: "localhost")

    Host to check (default: “localhost”)

  • timeout (Integer) (defaults to: 5)

    Timeout in seconds (default: 5)

Returns:

  • (Boolean)

    True if the service is responding, false otherwise



346
347
348
349
350
351
352
353
354
355
356
357
358
359
# File 'lib/makit/process.rb', line 346

def self.health_check_port(port, host: "localhost", timeout: 5)
  require 'net/http'
  require 'timeout'
  
  begin
    uri = URI("http://#{host}:#{port}")
    Timeout.timeout(timeout) do
      response = Net::HTTP.get_response(uri)
      response.code.to_i < 500 # Consider 4xx as "responding" but 5xx as "unhealthy"

    end
  rescue => e
    false
  end
end

.health_check_ports(ports, host: "localhost", timeout: 5) ⇒ Hash

Health check for multiple ports

Parameters:

  • ports (Array<Integer>)

    Array of port numbers to check

  • host (String) (defaults to: "localhost")

    Host to check (default: “localhost”)

  • timeout (Integer) (defaults to: 5)

    Timeout in seconds (default: 5)

Returns:

  • (Hash)

    Hash with port numbers as keys and health status as values



367
368
369
370
371
372
373
374
375
# File 'lib/makit/process.rb', line 367

def self.health_check_ports(ports, host: "localhost", timeout: 5)
  results = {}
  
  ports.each do |port|
    results[port] = health_check_port(port, host: host, timeout: timeout)
  end
  
  results
end

.is_running?(name) ⇒ Boolean

Check if a process with the given name is currently running

Uses platform-specific commands:

  • Windows: tasklist command

  • Unix: ps command with grep filtering

Parameters:

  • name (String)

    Name of the process to check (without .exe extension)

Returns:

  • (Boolean)

    True if the process is running, false otherwise



19
20
21
22
23
24
25
26
27
28
29
30
31
# File 'lib/makit/process.rb', line 19

def self.is_running?(name)
  return false if name.nil? || name.strip.empty?

  # provide a cross-platform way to check if a process is running

  results = if Makit::Environment.is_windows?
      # on windows, use the tasklist command

      `tasklist /FI "imagename eq #{name}.exe" 2>nul`
    else
      # on linux/mac, use the ps command

      `ps aux | grep "#{name}" | grep -v grep 2>/dev/null`
    end
  results.include?(name)
end

.kill(name) ⇒ Array<String>

Terminate all processes that match a given name

Uses platform-specific termination commands:

  • Windows: taskkill command

  • Unix: kill command with SIGKILL (-9)

Parameters:

  • name (String)

    Name of the process(es) to terminate

Returns:

  • (Array<String>)

    Array of killed process IDs (empty if none found/killed)



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
# File 'lib/makit/process.rb', line 41

def self.kill(name)
  return [] if name.nil? || name.strip.empty?

  killed_pids = []

  if Makit::Environment.is_windows?
    # Windows: Use tasklist to find PIDs first, then kill them

    begin
      # Find processes by name

      tasklist_output = `tasklist /FI "imagename eq #{name}.exe" /FO CSV 2>nul`

      # Parse CSV output to extract PIDs

      lines = tasklist_output.split("\n")
      if lines.length > 1 # Skip header line

        lines[1..].each do |line|
          parts = line.split(",")
          next unless parts.length >= 2

          pid = parts[1]&.gsub('"', "")&.strip
          next unless pid && !pid.empty? && pid.match?(/^\d+$/)

          begin
            `taskkill /PID #{pid} /F 2>nul`
            killed_pids << pid
          rescue StandardError => e
            # Log error but continue with other processes

            puts "Failed to kill process #{pid}: #{e.message}"
          end
        end
      end
    rescue StandardError => e
      # If tasklist fails, try direct taskkill by name

      begin
        `taskkill /IM "#{name}.exe" /F 2>nul`
        # Since we can't get PIDs from direct kill, return empty array

        # but don't raise error

      rescue StandardError => e2
        # Both methods failed, return empty array

        puts "Failed to kill processes by name #{name}: #{e2.message}"
      end
    end
  else
    # Unix (Linux/Mac): Use pgrep to find PIDs, then kill them

    begin
      # Use pgrep to find PIDs, handle both Linux and Mac

      pids = if Makit::Environment.is_mac?
          # Mac: pgrep -f might not work the same way

          `pgrep -x "#{name}" 2>/dev/null`.split("\n").reject(&:empty?)
        else
          # Linux: pgrep -f works well

          `pgrep -f "#{name}" 2>/dev/null`.split("\n").reject(&:empty?)
        end

      pids.each do |pid|
        next unless pid.match?(/^\d+$/)

        begin
          `kill -9 #{pid} 2>/dev/null`
          killed_pids << pid
        rescue StandardError => e
          puts "Failed to kill process #{pid}: #{e.message}"
        end
      end
    rescue StandardError => e
      # If pgrep fails, return empty array but don't raise error

      puts "Failed to find processes by name #{name}: #{e.message}"
    end
  end

  killed_pids
end

.listString

List all running processes on the system

Uses platform-specific commands to get a complete process listing:

  • Windows: tasklist command

  • Unix: ps aux command

Returns:

  • (String)

    Raw output from the system process listing command



120
121
122
123
124
125
126
# File 'lib/makit/process.rb', line 120

def self.list
  if Makit::Environment.is_windows?
    `tasklist 2>nul`
  else
    `ps aux 2>/dev/null`
  end
end

.list_current_userString

List all running processes for the current user

Filters the process list to show only processes owned by the current user. Uses platform-specific filtering:

  • Windows: tasklist with USERNAME filter

  • Unix: ps aux with grep filtering

Returns:

  • (String)

    Raw output from the filtered process listing command



136
137
138
139
140
141
142
# File 'lib/makit/process.rb', line 136

def self.list_current_user
  if Makit::Environment.is_windows?
    `tasklist /FI "STATUS eq running and USERNAME eq #{Makit::Environment.current_user}" 2>nul`
  else
    `ps aux | grep "#{Makit::Environment.current_user}" | grep -v grep 2>/dev/null`
  end
end

.list_current_user_processes(name) ⇒ Array<String>

List processes for the current user filtered by name

Returns an array of process lines for consistency across platforms. Filters by both current user and process name.

Parameters:

  • name (String)

    Process name to filter by

Returns:

  • (Array<String>)

    Array of process lines matching the criteria



151
152
153
154
155
156
157
158
159
160
161
# File 'lib/makit/process.rb', line 151

def self.list_current_user_processes(name)
  if Makit::Environment.is_windows?
    # filter the results to only include the current user

    results = `tasklist /FI "STATUS eq running and USERNAME eq #{Makit::Environment.current_user}" 2>nul`
    results.split("\n").select { |line| line.include?(Makit::Environment.current_user) }
  else
    # Unix (Linux/Mac): return as array for consistency

    results = `ps aux | grep "#{name}" | grep -v grep 2>/dev/null`
    results.split("\n").reject(&:empty?)
  end
end

.run_background(command, pid_file, working_dir: nil, environment: {}) ⇒ Integer

Run a command in the background and track it with a PID file

This method starts a process in the background and stores its PID in a file for later management. It handles cross-platform differences in process spawning.

Parameters:

  • command (String)

    The command to run in the background

  • pid_file (String)

    Path to the PID file to store the process ID

  • working_dir (String, nil) (defaults to: nil)

    Working directory for the process (optional)

  • environment (Hash, nil) (defaults to: {})

    Environment variables to set (optional)

Returns:

  • (Integer)

    The process ID of the started process

Raises:

  • (StandardError)

    If the process fails to start



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
# File 'lib/makit/process.rb', line 174

def self.run_background(command, pid_file, working_dir: nil, environment: {})
  # Ensure the command is not nil or empty

  raise ArgumentError, "Command cannot be nil or empty" if command.nil? || command.strip.empty?
  raise ArgumentError, "PID file path cannot be nil or empty" if pid_file.nil? || pid_file.strip.empty?

  # Convert command to array for Process.spawn

  cmd_array = if Makit::Environment.is_windows?
      # Windows: Use cmd /c for complex commands

      ["cmd", "/c", command]
    else
      # Unix-like: Use shell for command parsing

      ["sh", "-c", command]
    end

  # Prepare spawn options

  spawn_options = {}
  spawn_options[:chdir] = working_dir if working_dir
  spawn_options[:pgroup] = true unless Makit::Environment.is_windows?

  # Set environment variables

  env = ENV.to_h.merge(environment)
  spawn_options[:unsetenv_others] = false

  begin
    # Start the process

    pid = Process.spawn(env, *cmd_array, spawn_options)
    
    # Write PID to file

    File.write(pid_file, pid.to_s)
    
    # Give the process a moment to start

    sleep(0.5)
    
    # Verify the process is still running

    begin
      Process.getpgid(pid) if Makit::Environment.is_windows?
      Process.getpgid(pid) unless Makit::Environment.is_windows?
    rescue Errno::ESRCH
      raise StandardError, "Process failed to start or exited immediately"
    end

    pid
  rescue => e
    # Clean up PID file if process failed to start

    File.delete(pid_file) if File.exist?(pid_file)
    raise StandardError, "Failed to start background process: #{e.message}"
  end
end

.stop_background(pid_file, signal: "TERM") ⇒ Boolean

Stop a background process using its PID file

This method reads the PID from a file and terminates the process gracefully. It handles cross-platform differences in process termination.

Parameters:

  • pid_file (String)

    Path to the PID file containing the process ID

  • signal (String) (defaults to: "TERM")

    Signal to send (default: “TERM” for graceful shutdown)

Returns:

  • (Boolean)

    True if the process was stopped, false if it wasn’t running



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
# File 'lib/makit/process.rb', line 231

def self.stop_background(pid_file, signal: "TERM")
  return false unless File.exist?(pid_file)

  begin
    pid = File.read(pid_file).strip.to_i
    return false if pid <= 0

    # Check if process is still running

    begin
      if Makit::Environment.is_windows?
        # Windows: Use taskkill

        if signal == "TERM"
          system("taskkill /PID #{pid} /F >nul 2>&1")
        else
          system("taskkill /PID #{pid} /F >nul 2>&1")
        end
      else
        # Unix-like: Use kill with specified signal

        Process.kill(signal, pid)
      end
      
      # Wait a moment for the process to terminate

      sleep(0.5)
      
      # Verify the process is gone

      begin
        Process.getpgid(pid)
        # Process still running, force kill if TERM didn't work

        if signal == "TERM"
          return stop_background(pid_file, signal: "KILL")
        end
        return false
      rescue Errno::ESRCH
        # Process is gone, success

        true
      end
    rescue Errno::ESRCH, Errno::EPERM
      # Process was not running or permission denied

      false
    end
  rescue => e
    # Log error but don't raise

    puts "Error stopping background process: #{e.message}" if defined?(puts)
    false
  ensure
    # Clean up PID file

    File.delete(pid_file) if File.exist?(pid_file)
  end
end