#!/usr/bin/env ruby
# encoding: UTF-8

require 'fileutils'
require 'securerandom'
require_relative '.dpkgs'
require_relative '.utils'

EMUL_PATH         = File.realdirpath(`sysctl -qn compat.linux.emul_path`).chomp
EXPECTED_DISTROS  = ['centos', 'fedora', 'rocky']
MIN_GLIBC_VERSION = '2.34'
MIN_ROCKY_RELEASE = 9

def check_requirements

  reqs = []

  reqs << [:linux, '32-bit Linux emulation support'] if `sysctl -qn kern.features.linux`  .to_i != 1
  reqs << [:linux, '64-bit Linux emulation support'] if `sysctl -qn kern.features.linux64`.to_i != 1

  mounts = Hash[`mount -p`.lines.each_with_index.map{|line, i| p = line.split(/[ \t]+/); [p[1], {type: p[2], line: i}]}]

  for fs, path in {
    'linprocfs' => EMUL_PATH + '/proc',
    'linsysfs'  => EMUL_PATH + '/sys',
    'devfs'     => EMUL_PATH + '/dev',
    'tmpfs'     => EMUL_PATH + '/dev/shm',
    'fdescfs'   => EMUL_PATH + '/dev/fd',
  }
    m = mounts[path]
    if !(m && m[:type] == fs)
      reqs << [:linux, "#{fs} mounted at #{path}"]
    end
  end

  dev = mounts[EMUL_PATH + '/dev']
  shm = mounts[EMUL_PATH + '/dev/shm']
  fd  = mounts[EMUL_PATH + '/dev/fd']
  if dev && shm && fd
    reqs << [:linux, "#{EMUL_PATH}/dev mounted before #{EMUL_PATH}/dev/shm"] if shm[:line] < dev[:line]
    reqs << [:linux, "#{EMUL_PATH}/dev mounted before #{EMUL_PATH}/dev/fd" ] if fd [:line] < dev[:line]
  end

  reqs << [:linux,   "write access to #{EMUL_PATH}/dev/shm"] if !File.writable?("#{EMUL_PATH}/dev/shm")
  reqs << [nil,      '/etc/machine-id must exist']           if !File.exist?('/etc/machine-id')
  reqs << [:uchroot, 'Unprivileged chroot must be enabled']  if `sysctl -nq security.bsd.unprivileged_chroot`.to_i != 1
  reqs << [:umount,  'Unprivileged mounts must be enabled']  if `sysctl -nq vfs.usermount`.to_i != 1

  # without nullfs.ko mount fails with an unhelpful error message: https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=274600#c5
  reqs << [:nullfs, 'nullfs.ko must be loaded'] if !system('kldstat -q -m nullfs')

  # we expect EMUL_PATH to point to the RH-style directory structure
  os = {}
  begin
    File.read("#{EMUL_PATH}/etc/os-release").each_line(chomp: true) do |line|
      k, v = line.split('=')
      os[k] = v.undump if v
    end
  rescue Errno::ENOENT
    os = nil
  end

  if os && (EXPECTED_DISTROS.include?(os['ID']) || EXPECTED_DISTROS.intersect?(os['ID_LIKE'].split(' ')))
    if os['ID'] == 'rocky'
      version    = os['VERSION_ID']
      version_ok = if version =~ /^(\d+)(:?\.\d+)?$/
        $1.to_i >= MIN_ROCKY_RELEASE
      else
        false
      end
      if !version_ok
        reqs << [:base, "binaries from Rocky Linux #{MIN_ROCKY_RELEASE} at #{EMUL_PATH}, got #{version}"]
      end
    else
      # let's check at least the glibc version
      Dir.chdir(File.join(EMUL_PATH, 'lib64')) do
        if `readelf -s libc.so.6 | grep GLIBC_#{MIN_GLIBC_VERSION}`.chomp == ''
          reqs << [:base, "Linux base with glibc >= #{MIN_GLIBC_VERSION} at #{EMUL_PATH}"]
        end
      end
    end
  else
    reqs << [:base, "binaries from Rocky Linux or compatible distro at #{EMUL_PATH}"]
  end

  reqs
end

def safe_system(*args)
  raise "Command failed: #{args.join(' ').inspect}" if !system(*args)
end

def get_linux_cmd_output(*args)
  env = {
    'PATH'            => [File.expand_path('../lxbin', __dir__), File.join(EMUL_PATH, 'bin')].join(':'),
    'LD_LIBRARY_PATH' => nil,
    'LD_PRELOAD'      => nil
  }
  with_env(env) do
    IO.popen([File.join(EMUL_PATH, 'bin/bash'), '-c', *args]){|io| io.read.chomp}
  end
end

requirements = check_requirements()
if !requirements.empty?
  perr 'Please, make sure the following requirements are satisfied:'
  for req in requirements
    perr "  * #{req[1]}"
  end

  if requirements.find{|req| req[0] == :linux}
    perr "\nRun (as root) `sysrc linux_enable=YES && service linux start` to enable Linux emulation."
  end

  if requirements.find{|req| req[0] == :uchroot}
    perr "\nRun (as root) `sysctl security.bsd.unprivileged_chroot=1`.\nAdd the setting to /etc/sysctl.conf to persist it."
  end

  if requirements.find{|req| req[0] == :umount}
    perr "\nRun (as root) `sysctl vfs.usermount=1`.\nAdd the setting to /etc/sysctl.conf to persist it."
  end

  if requirements.find{|req| req[0] == :nullfs}
    perr "\nRun (as root) `kldload nullfs` and `sysrc kld_list+=nullfs`."
  end

  if requirements.find{|req| req[0] == :base}
    perr "\nIs compat.linux.emul_path set correctly?"
  end

  exit 1
end

if `sysctl -q hw.nvidia.version` =~ /hw.nvidia.version: NVIDIA UNIX x86_64 Kernel Module\s+(\d+\.\d+\.\d+|\d+\.\d+)/
  version = $1
  libgl32 = File.join(EMUL_PATH, "usr/lib/libGLX_nvidia.so.#{version}")
  if !File.exist?(libgl32)
    perr "#{libgl32} doesn't exist. You might want to install (or update) linux-nvidia-libs."
    exit 1
  end
end

if !File.readable?(File.join(__dir__, '../lib32/steam/steamfix.so'))
  perr "Can't find steamfix.so"
  exit 1
end

if !File.exist?(STEAM_ROOT_PATH)
  perr "Steam doesn't appear to be installed for user #{ENV['USER']}."
  perr "Perhaps you forgot to run #{File.join(__dir__, 'lsu-bootstrap')}?"
  exit 1
end

STEAM_RUNTIME_ROOT_PATH = File.join(STEAM_ROOT_PATH, 'ubuntu12_32/steam-runtime')

if !File.exist?(STEAM_RUNTIME_ROOT_PATH)
  perr "Can't find steam-runtime"
  exit 1
end

# let's download chroot dependencies here, otherwise Steam might think
# steamwebhelper (which we run in said chroot) has stuck
if LSU_MESA_LIBS == 'ubuntu'
  download_debs(DPKGS, LSU_DIST_PATH)
end

if ENV['LSU_COREDUMP'] != '1'
  Process.setrlimit(:CORE, 0)
end

steam_runtime_bin_path = get_linux_cmd_output('"$0" --print-bin-path',                    File.join(STEAM_RUNTIME_ROOT_PATH, 'setup.sh'))
steam_runtime_lib_path = get_linux_cmd_output('"$0" --print-steam-runtime-library-paths', File.join(STEAM_RUNTIME_ROOT_PATH, 'run.sh'))

bin_path = [
  File.expand_path('../lxbin', __dir__),
  steam_runtime_bin_path,
  File.join(EMUL_PATH, 'bin')
].compact.join(':')

client_library_path = [
  File.expand_path('../lib32/steam',     __dir__),
  File.expand_path('../lib32/fakenm',    __dir__),
  File.expand_path('../lib32/fakepulse', __dir__),
  File.expand_path('../lib64/fakepulse', __dir__),
  File.expand_path('../lib32/fakeudev',  __dir__),
  File.expand_path('../lib64/fakeudev',  __dir__),
  File.join(EMUL_PATH, '/usr/lib64/nss'),
  File.join(STEAM_ROOT_PATH, 'ubuntu12_32'),
  File.join(STEAM_ROOT_PATH, 'ubuntu12_32/panorama'),
  steam_runtime_lib_path
].join(':')

games_library_path = [
  File.expand_path('../lib32/fakepulse', __dir__),
  File.expand_path('../lib64/fakepulse', __dir__),
  File.expand_path('../lib32/fakeudev',  __dir__),
  File.expand_path('../lib64/fakeudev',  __dir__),
  File.expand_path('../lib32',           __dir__),
  File.expand_path('../lib64',           __dir__),
  File.join(EMUL_PATH, 'usr/lib64/nss'),
  steam_runtime_lib_path
].join(':')

preload = [
  'steamfix.so',
  'libSegFault.so',
  ENV['STEAM_LD_PRELOAD']
].compact.join(':')

evdev_gamepads = []
for line in `sysctl kern.evdev.input`.lines
  evdev_gamepads << $1.to_i if line =~ /^kern.evdev.input.(\d+).phys: (hgame|ps4dshock|xb360gp)\d+$/
end

ENV['ALSOFT_DRIVERS']                       = 'oss'
ENV['LSU_COOKIE']                           = SecureRandom.hex(16)
ENV['LSU_LINUX_LD_LIBRARY_PATH']            = client_library_path
ENV['LSU_LINUX_LD_PRELOAD']                 = preload
ENV['LSU_LINUX_PATH']                       = bin_path
ENV['SDL_AUDIODRIVER']                      = 'dsp'  # SDL2
ENV['SDL_AUDIO_DRIVER']                     = 'alsa' # SDL3, see https://github.com/libsdl-org/SDL/commit/ed3fad18808714f9fab3111a45d06264ea6fb0c5
ENV['SDL_JOYSTICK_DEVICE']                  = [evdev_gamepads.map{|idx| "/dev/input/event#{idx}"}.join(':'), ENV['SDL_JOYSTICK_DEVICE']].compact.join(':')
ENV['STEAM_ENABLE_SHADER_CACHE_MANAGEMENT'] = '0' # ?
ENV['STEAM_EXTRA_COMPAT_TOOLS_PATHS']       = [File.expand_path('../tools', __dir__), ENV['STEAM_EXTRA_COMPAT_TOOLS_PATHS']].compact.join(':')
ENV['STEAM_RUNTIME']                        = STEAM_RUNTIME_ROOT_PATH
ENV['STEAM_RUNTIME_LIBRARY_PATH']           = games_library_path
ENV['STEAM_ZENITY']                         = 'zenity'
ENV['SYSTEM_LD_LIBRARY_PATH']               = games_library_path
ENV['SYSTEM_PATH']                          = bin_path

ENV['PRESSURE_VESSEL_BWRAP'] ||= File.expand_path('../lxbin/lsu-bwrap-stub', __dir__)

# clean up leftover LSU_TMPDIR_PATH mounts, if any
# (note that we deliberately keep them if there is no cookie)
safe_system(File.join(__dir__, 'lsu-umount')) if read_tmp_dir_cookie()

for signal in [:HUP, :INT, :TERM]
  Signal.trap(signal) do
    opts = STDOUT.tty? ? {} : {[:err, :out] => '/dev/null'}
    system(File.join(__dir__, 'lsu-kill'),   opts)
    system(File.join(__dir__, 'lsu-umount'), opts) if read_tmp_dir_cookie()
    exit
  end
end

loop do
  safe_system(File.join(__dir__, 'lsu-patch-steam'))
  safe_system(File.join(__dir__, 'lsu-upgrade-steam-runtime'))

  system([File.join(__dir__, 'lsu-freebsd-to-linux-env')] * 2, File.join(STEAM_ROOT_PATH, 'ubuntu12_32/steam'), *ARGV)
  status = $?.exitstatus || 1

  # even normal exit still leaves steamwebhelper processes
  system(File.join(__dir__, 'lsu-kill'))
  system(File.join(__dir__, 'lsu-umount')) if read_tmp_dir_cookie()

  if status == 42
    pwarn "Restarting Steam..."
  else
    exit(status)
  end
end
