Module: Lich::Common::Frontend

Defined in:
documented/common/front-end.rb

Overview

Frontend module for managing session files and process IDs This module handles the creation and management of session files and process IDs.

Class Method Summary collapse

Class Method Details

.cleanup_session_filevoid

This method returns an undefined value.

Cleans up the session file if it exists



72
73
74
75
# File 'documented/common/front-end.rb', line 72

def self.cleanup_session_file
  return if @session_file.nil?
  File.delete(@session_file) if File.exist? @session_file
end

.create_session_file(name, host, port, display_session: true) ⇒ void

This method returns an undefined value.

Creates a session file with the given parameters

Examples:

Frontend.create_session_file("MySession", "localhost", 3000)

Parameters:

  • name (String)

    The name of the session

  • host (String)

    The host address

  • port (Integer)

    The port number

  • display_session (Boolean) (defaults to: true)

    Whether to display the session descriptor



53
54
55
56
57
58
59
60
61
62
# File 'documented/common/front-end.rb', line 53

def self.create_session_file(name, host, port, display_session: true)
  return if name.nil?
  FileUtils.mkdir_p @tmp_session_dir
  @session_file = File.join(@tmp_session_dir, "%s.session" % name.downcase.capitalize)
  session_descriptor = { name: name, host: host, port: port }.to_json
  puts "writing session descriptor to %s\n%s" % [@session_file, session_descriptor] if display_session
  File.open(@session_file, "w") do |fd|
    fd << session_descriptor
  end
end

.detect_pidInteger?

Detects the frontend process ID

Returns:

  • (Integer, nil)

    The detected process ID or nil if not found



136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
# File 'documented/common/front-end.rb', line 136

def self.detect_pid
  # Return existing PID if already set
  current_pid = self.pid
  return current_pid if current_pid && current_pid > 0

  # Try to detect based on launch method
  # This is a fallback for cases where init wasn't called
  parent_pid = Process.ppid
  resolved_pid = resolve_pid(parent_pid)

  if resolved_pid && resolved_pid > 0
    self.pid = resolved_pid
    Lich.log "Frontend PID detected (fallback): #{resolved_pid}" if defined?(Lich.log)
    resolved_pid
  else
    Lich.log "Failed to detect frontend PID" if defined?(Lich.log)
    nil
  end
end

.detect_platformSymbol

Detects the current platform

Returns:

  • (Symbol)

    The platform type (:windows, :macos, :linux, or :unsupported)



188
189
190
191
192
193
194
195
# File 'documented/common/front-end.rb', line 188

def self.detect_platform
  case RUBY_PLATFORM
  when /mingw|mswin/ then :windows
  when /darwin/      then :macos
  when /linux/       then :linux
  else                    :unsupported
  end
end

.ensure_windows_modulesBoolean

Ensures that the Windows API modules are loaded

Returns:

  • (Boolean)

    True if modules are loaded, false otherwise



394
395
396
397
398
399
400
# File 'documented/common/front-end.rb', line 394

def self.ensure_windows_modules
  # Check if modules exist - they should be defined at file load time
  if RUBY_PLATFORM =~ /mingw|mswin/
    return defined?(::Win32Enum) && defined?(::WinAPI)
  end
  false
end

.init_from_parent(parent_pid) ⇒ Integer

Initializes the frontend from the parent process ID

Examples:

Frontend.init_from_parent(Process.ppid)

Parameters:

  • parent_pid (Integer)

    The parent process ID

Returns:

  • (Integer)

    The resolved frontend process ID



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
# File 'documented/common/front-end.rb', line 97

def self.init_from_parent(parent_pid)
  Lich.log "=== Frontend.init_from_parent called ==="
  Lich.log "Parent process PID: #{parent_pid}"

  # Let's see what process this actually is on Windows
  if RUBY_PLATFORM =~ /mingw|mswin/
    begin
      require 'win32ole'
      wmi = WIN32OLE.connect('winmgmts://')
      rows = wmi.ExecQuery("SELECT Name, ProcessId FROM Win32_Process WHERE ProcessId=#{parent_pid}")
      row = rows.each.first rescue nil
      if row
        Lich.log "Parent process name: #{row.Name}"
      end
    rescue => e
      Lich.log "Could not get parent process name: #{e.message}"
    end
  end

  resolved_pid = resolve_pid(parent_pid)
  Lich.log "resolve_pid(#{parent_pid}) returned: #{resolved_pid}"

  self.pid = resolved_pid
  Lich.log "Frontend PID set to: #{self.pid}"

  resolved_pid
end

.pidInteger?

Returns the current frontend process ID

Returns:

  • (Integer, nil)

    The frontend process ID or nil if not set



80
81
82
# File 'documented/common/front-end.rb', line 80

def self.pid
  @pid_mutex.synchronize { @frontend_pid }
end

.pid=(value) ⇒ void

This method returns an undefined value.

Sets the frontend process ID

Parameters:

  • value (Integer)

    The process ID to set



87
88
89
90
# File 'documented/common/front-end.rb', line 87

def self.pid=(value)
  value = value.to_i
  @pid_mutex.synchronize { @frontend_pid = value }
end

.refocusBoolean

Refocuses the frontend window based on the detected platform

Returns:

  • (Boolean)

    True if refocused successfully, false otherwise



158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
# File 'documented/common/front-end.rb', line 158

def self.refocus
  pid = self.pid
  return false unless pid && pid > 0

  case detect_platform
  when :windows
    refocus_windows(pid)
  when :macos
    refocus_macos(pid)
  when :linux
    refocus_linux(pid)
  else
    false
  end
end

.refocus_callbackProc

Returns a callback proc for refocusing the window

Returns:

  • (Proc)

    A proc that refocuses the window



176
177
178
179
180
181
182
183
184
# File 'documented/common/front-end.rb', line 176

def self.refocus_callback
  proc {
    if defined?(GLib) && GLib.respond_to?(:Idle)
      GLib::Idle.add(50) { self.refocus; false }
    else
      self.refocus
    end
  }
end

.refocus_linux(pid) ⇒ Boolean

Refocuses the window for a given Linux process ID

Parameters:

  • pid (Integer)

    The process ID to refocus

Returns:

  • (Boolean)

    True if refocused successfully, false otherwise



376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
# File 'documented/common/front-end.rb', line 376

def self.refocus_linux(pid)
  return false unless system('which xdotool > /dev/null 2>&1')

  _stdout, stderr, status = Open3.capture3('xdotool', 'search', '--pid', pid.to_s, 'windowactivate')

  if status.success?
    true
  else
    Lich.log "Error refocusing Linux: #{stderr}" if defined?(Lich.log)
    false
  end
rescue => e
  Lich.log "Error refocusing Linux: #{e}" if defined?(Lich.log)
  false
end

.refocus_macos(pid) ⇒ Boolean

Refocuses the window for a given macOS process ID

Parameters:

  • pid (Integer)

    The process ID to refocus

Returns:

  • (Boolean)

    True if refocused successfully, false otherwise



356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
# File 'documented/common/front-end.rb', line 356

def self.refocus_macos(pid)
  return false unless system('which osascript > /dev/null 2>&1')

  script = %{tell application "System Events" to set frontmost of (first process whose unix id is #{pid}) to true}
  _stdout, stderr, status = Open3.capture3('osascript', '-e', script)

  if status.success?
    true
  else
    Lich.log "Error refocusing macOS: #{stderr}" if defined?(Lich.log)
    false
  end
rescue => e
  Lich.log "Error refocusing macOS: #{e}" if defined?(Lich.log)
  false
end

.refocus_windows(pid) ⇒ Boolean

Refocuses the window for a given Windows process ID

Parameters:

  • pid (Integer)

    The process ID to refocus

Returns:

  • (Boolean)

    True if refocused successfully, false otherwise



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
# File 'documented/common/front-end.rb', line 315

def self.refocus_windows(pid)
  ensure_windows_modules

  hwnd_buf = Fiddle::Pointer.malloc(Fiddle::SIZEOF_VOIDP)

  enum_cb = Fiddle::Closure::BlockCaller.new(
    Fiddle::TYPE_INT,
    [Fiddle::TYPE_VOIDP, Fiddle::TYPE_LONG]
  ) do |hwnd, _|
    next 1 if ::WinAPI.IsWindowVisible(hwnd).zero?

    pid_tmp = [0].pack('L')
    ::WinAPI.GetWindowThreadProcessId(hwnd, pid_tmp)
    win_pid = pid_tmp.unpack1('L')

    if win_pid == pid
      hwnd_buf[0, Fiddle::SIZEOF_VOIDP] = [hwnd].pack('L!')
      0  # stop enumeration
    else
      1  # continue enumeration
    end
  end

  ::WinAPI.EnumWindows(enum_cb, 0)
  hwnd = hwnd_buf[0, Fiddle::SIZEOF_VOIDP].unpack1('L!')

  if hwnd != 0
    ::WinAPI.SetForegroundWindow(hwnd)
    true
  else
    Lich.log "Frontend window for PID #{pid} not found" if defined?(Lich.log)
    false
  end
rescue => e
  Lich.log "Error refocusing Windows: #{e}" if defined?(Lich.log)
  false
end

.resolve_linux_pid(pid) ⇒ Integer

Resolves the Linux process ID to find the correct one

Parameters:

  • pid (Integer)

    The process ID to resolve

Returns:

  • (Integer)

    The resolved process ID



287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
# File 'documented/common/front-end.rb', line 287

def self.resolve_linux_pid(pid)
  return pid unless system('which xdotool > /dev/null 2>&1')

  p = pid
  16.times do
    # Check if this process has a window
    return p if system("xdotool search --pid #{p} >/dev/null 2>&1")

    # Walk up to parent process
    begin
      status = File.read("/proc/#{p}/status")
      parent = status[/PPid:\s+(\d+)/, 1].to_i
    rescue
      parent = 0
    end
    return pid if parent.zero? || parent == p
    p = parent
  end

  pid # fallback
rescue => e
  Lich.log "Error resolving Linux PID: #{e}" if defined?(Lich.log)
  pid
end

.resolve_pid(pid) ⇒ Integer

Resolves the process ID to find the correct one based on the platform

Parameters:

  • pid (Integer)

    The process ID to resolve

Returns:

  • (Integer)

    The resolved process ID



200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
# File 'documented/common/front-end.rb', line 200

def self.resolve_pid(pid)
  pid = pid.to_i
  return pid if pid <= 0 # Return as-is if invalid

  # Use the FrontendPID resolver logic
  case detect_platform
  when :windows
    resolve_windows_pid(pid)
  when :linux
    resolve_linux_pid(pid)
  else
    # macOS/other: PID usually already owns the window
    pid
  end
end

.resolve_windows_pid(pid) ⇒ Integer

Resolves the Windows process ID to find the correct one

Parameters:

  • pid (Integer)

    The process ID to resolve

Returns:

  • (Integer)

    The resolved process ID



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
# File 'documented/common/front-end.rb', line 219

def self.resolve_windows_pid(pid)
  Lich.log "=== resolve_windows_pid starting with PID: #{pid} ==="

  ensure_windows_modules
  require 'win32ole' rescue (return pid)

  begin
    wmi = WIN32OLE.connect('winmgmts://')
    p = pid

    16.times do
      # Get process name for debugging
      rows = wmi.ExecQuery("SELECT Name FROM Win32_Process WHERE ProcessId=#{p}")
      row = rows.each.first rescue nil
      process_name = row ? row.Name : "unknown"
      Lich.log "  Process name: #{process_name}"

      # Check if this process owns any visible window
      found = false
      cb = Fiddle::Closure::BlockCaller.new(
        Fiddle::TYPE_INT,
        [Fiddle::TYPE_VOIDP, Fiddle::TYPE_LONG]
      ) do |hwnd, _|
        next 1 if ::Win32Enum.IsWindowVisible(hwnd).zero?
        buf = [0].pack('L')
        ::Win32Enum.GetWindowThreadProcessId(hwnd, buf)
        if buf.unpack1('L') == p
          found = true
          Lich.log "  Found visible window for PID #{p}"
          0  # stop enumeration
        else
          1  # continue enumeration
        end
      end
      ::Win32Enum.EnumWindows(cb, 0)

      if found
        Lich.log "  Stopping at PID #{p} (#{process_name}) - has visible window"
        return p
      end

      # Walk up to parent process
      parent = windows_parent_pid(wmi, p)

      break if parent.nil? || parent.zero? || parent == p
      p = parent
    end
  rescue => e
    Lich.log "ERROR in resolve_windows_pid: #{e}"
  end

  Lich.log "Fallback: returning original PID #{pid}"
  pid
end

.session_file_locationString?

Returns the location of the current session file

Returns:

  • (String, nil)

    The path to the session file or nil if not set



66
67
68
# File 'documented/common/front-end.rb', line 66

def self.session_file_location
  @session_file
end

.set_from_client(pid) ⇒ Integer

Sets the frontend process ID from the client

Parameters:

  • pid (Integer)

    The process ID from the client

Returns:

  • (Integer)

    The set process ID



128
129
130
131
132
# File 'documented/common/front-end.rb', line 128

def self.set_from_client(pid)
  self.pid = pid
  Lich.log "Frontend PID set from client: #{pid}" if defined?(Lich.log)
  pid
end

.windows_parent_pid(wmi, pid) ⇒ Integer

Retrieves the parent process ID for a given Windows process ID

Parameters:

  • wmi (WIN32OLE)

    The WMI connection object

  • pid (Integer)

    The process ID to check

Returns:

  • (Integer)

    The parent process ID or 0 if not found



278
279
280
281
282
# File 'documented/common/front-end.rb', line 278

def self.windows_parent_pid(wmi, pid)
  rows = wmi.ExecQuery("SELECT ParentProcessId FROM Win32_Process WHERE ProcessId=#{pid}")
  row = rows.each.first rescue nil
  row ? row.ParentProcessId.to_i : 0
end