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

# do not run this as non-Puavo users (such as guest user)
exit(0) if Process.uid < 10000

require 'puavo/conf'

puavoconf = Puavo::Conf.new
exit(0) unless puavoconf.get('puavo.mounts.by_user_from_bootserver.enabled') == 'true'

require 'dbus'
require 'etc'
require 'fileutils'
require 'open3'
require 'resolv'
require 'thread'

$puavo_domain = IO.read('/etc/puavo/domain').chomp
$requested_remote_home_update = false

$home_update_semaphore = Mutex.new
$stderr_semaphore = Mutex.new
$update_gtk_bookmarks_semaphore = Mutex.new

def log(msg)
  $stderr_semaphore.synchronize do
    STDERR.puts("puavo-remotemounts-helper #{ Time.now }: #{ msg }")
  end
end

class DbusHandler
  def initialize(gvfs_mounts)
    @gvfs_mounts = gvfs_mounts

    @active_connection_count = 0
    @in_sleep = false

    bus = DBus::SystemBus.instance

    loginservice = bus.service('org.freedesktop.login1')
    loginobj = loginservice.object('/org/freedesktop/login1')
    loginobj.introspect
    loginobj.default_iface = 'org.freedesktop.login1.Manager'

    # if we are going to suspend, deactivate gvfs-mounts

    loginobj.on_signal('PrepareForSleep') do |preparing_for_sleep|
      if preparing_for_sleep then
        log('received PrepareForSleep-dbus-signal, going to sleep')
      else
        log('received PrepareForSleep-dbus-signal, waking up')
      end

      @in_sleep = preparing_for_sleep
      activate_or_deactivate
    end

    # if we have no active network connections, deactivate gvfs-mounts

    nmservice = bus.service('org.freedesktop.NetworkManager')
    nmobj = nmservice.object('/org/freedesktop/NetworkManager')
    nmobj.introspect
    nmobj.default_iface = 'org.freedesktop.NetworkManager'

    @active_connection_count \
      = nmobj['org.freedesktop.NetworkManager']['ActiveConnections'].count

    nmobj.on_signal('PropertiesChanged') do |props|
      log('received PropertiesChanged-dbus-signal')
      if props['ActiveConnections'].kind_of?(Array) then
        @active_connection_count = props['ActiveConnections'].count
        log("active connection count is #{ @active_connection_count }")
        activate_or_deactivate
      end
    end

    activate_or_deactivate

    log('starting up dbus handler')

    loop = DBus::Main.new
    loop << bus
    loop.run
  end

  def activate_or_deactivate
    method = !@in_sleep && (@active_connection_count > 0) \
                ? :activate                               \
                : :deactivate

    @gvfs_mounts.each { |gvfsmount| gvfsmount.change_plan(method) }
  end
end

class MountInfo
  attr_reader :path, :remote_host, :url

  def initialize(path, remote_host, url)
    @path        = path
    @remote_host = remote_host
    @url         = url
  end
end

class GvfsMount
  attr_reader :linkname

  def initialize(remote_path, username, user_id, linkname)
    @linkname    = linkname
    @remote_path = remote_path
    @user_id     = user_id
    @username    = username

    @mntinfo = nil

    @dns_lookups_to_try = 10
    @previous_mount_state = nil

    @new_plan = :activate
  end

  def gvfslog(msg)
    url = (@mntinfo && @mntinfo.url) || "[#{ @remote_path }]"
    log("#{ url } :: #{ msg }")
  end

  def change_plan(new_plan)
    @new_plan = new_plan
  end

  def apply_plan
    return unless @new_plan

    if @new_plan == :activate then
      activate()
    elsif @new_plan == :deactivate then
      deactivate()
    end

    @new_plan = nil
  end

  def activate()
    gvfslog('activating')
    @dns_lookups_to_try = 10
  end

  def deactivate()
    gvfslog('deactivating')
    unmount_gvfsmount()
    @dns_lookups_to_try = 0
    @mntinfo = nil
  end

  def check_mount_state()
    # returns 'not connected', 'ok' or 'timeout'

    # wait for 10 seconds for gio info operation to a remote url
    system('timeout', '-s', 'KILL', '10', 'gio', 'info', @mntinfo.url,
           { :out => '/dev/null', :err => '/dev/null' })

    return 'timeout' if $?.termsig == 9
    return 'ok'      if $?.exitstatus == 0

    # if gio info fails, check if mount exists
    if is_mounted() then
      return 'ok'
    end

    return 'not connected'
  end

  def is_mounted()
    output, s = Open3.capture2e('gio', 'mount', '-il')
    raise "gio mount -il returned error code #{ s.exitstatus }" \
      unless s.exitstatus == 0

    re = /^\s+default_location=#{ Regexp.quote(@mntinfo.url) }$/
    output.split("\n").each do |line|
      return true if line.match(re)
    end

    return false
  end

  def ensure_link(shouldbe)
    begin
      linksrc = "#{ ENV['HOME'] }/#{ @linkname }"

      if shouldbe; then
        return unless @mntinfo
        begin
          return if File.readlink(linksrc) == @mntinfo.path
        rescue Errno::ENOENT
        end
        gvfslog("adding link from '#{ linksrc }' to '#{ @mntinfo.path }'")
        FileUtils.rm_f(linksrc)
        FileUtils.symlink(@mntinfo.path, linksrc)
      else
        gvfslog("removing link '#{ linksrc }'") if File.symlink?(linksrc)
        FileUtils.rm_f(linksrc)
      end

      # Update bookmarks when links have changed.
      # Use a mutex so that threads do not run this at the same time.
      $update_gtk_bookmarks_semaphore.synchronize do
        gvfslog('running puavo-update-gtk-bookmarks')
        system('puavo-update-gtk-bookmarks')
      end
    rescue StandardError => e
      gvfslog("error in ensure_link(): #{ e.message }")
      raise e
    end
  end

  def check_and_handle_mount(checks_with_timeout)
    raise '@mntinfo not set where it should be' unless @mntinfo

    # If update_remote_homes() fails, we must not proceed with mounts,
    # because username might have changed, in which case home directory path
    # has changed, and server *should* update it to match what we expect.
    # Otherwise, Samba will create another home directory for user,
    # and then user will be missing files.
    update_remote_home()

    mount_state = check_mount_state()

    if mount_state != @previous_mount_state then
      gvfslog("mount state changed to '#{ mount_state }'")
      @previous_mount_state = mount_state
    end

    case mount_state
      when 'not connected'
        if system('timeout', '-k', '10', '20', 'gio', 'mount', @mntinfo.url) then
          gvfslog('mounted')
          ensure_link(true) rescue true
        else
          gvfslog('problem when mounting')
        end
        checks_with_timeout[0] = 0

      when 'ok'
        ensure_link(true) rescue true
        checks_with_timeout[0] = 0

      when 'timeout'
        checks_with_timeout[0] += 1
        if checks_with_timeout[0] >= 3 then
          gvfslog('got three timeouts in a row, unmounting')
          unmount_gvfsmount()
          checks_with_timeout[0] = 0
        end
    end
  end

  def ensure_mount
    checks_with_timeout = [0]

    while true do
      begin
        apply_plan()
        update_mountinfo_from_dns()
        if @mntinfo then
          check_and_handle_mount(checks_with_timeout)
        else
          checks_with_timeout = [0]
        end

      rescue StandardError => e
        gvfslog("got some error: #{ e }, yet continuing...")
        checks_with_timeout = [0]
      end

      sleep(5)
    end
  end

  def unmount_gvfsmount()
    ensure_link(false) rescue true

    return unless @mntinfo
    return unless is_mounted()

    gvfslog('unmounting')
    system('timeout', '-k', '5', '10', 'gio', 'mount', '-u', @mntinfo.url) \
      or gvfslog('problem in unmounting')
  end

  def update_mountinfo_from_dns()
    # If we are behind some server that provides samba mounts for us,
    # update our mount information.

    return unless @dns_lookups_to_try > 0

    srv_name = "_sambaserver._tcp.#{ $puavo_domain }"
    gvfslog("looking up '#{ srv_name }' from DNS" \
              + " (#{ @dns_lookups_to_try } more tries)")

    remote_host = nil
    begin
      Resolv::DNS.open do |dns|
        dns.getresources(srv_name, Resolv::DNS::Resource::IN::SRV).each do |srv|
          if srv.port == 139 then
            remote_host = srv.target
            break
          end
        end
      end
    rescue StandardError => e
    end

    @dns_lookups_to_try -= 1

    if remote_host.nil? then
      if @dns_lookups_to_try == 0 then
        gvfslog("not doing any more dns lookups for '#{ srv_name }'")
        # We should deactivate, because trying continuously mounting when
        # we are probably not behind the home directory server is pointless.
        change_plan(:deactivate)
      end
      return
    end

    new_mntpath = "/run/user/#{ @user_id }/gvfs/smb-share:server=" \
                    + "#{ remote_host },share=#{ @remote_path }"
    new_url = "smb://#{ remote_host }/#{ @remote_path }/"

    new_mntinfo = MountInfo.new(new_mntpath, remote_host, new_url)

    if @mntinfo && @mntinfo.url != new_mntinfo.url then
      gvfslog("remote url has changed to '#{ new_mntinfo.url }'")
      unmount_gvfsmount()
    end

    @mntinfo = new_mntinfo
    @dns_lookups_to_try = 0
  end

  def update_remote_home()
    $home_update_semaphore.synchronize do
      # enough to do this once (successfully)
      return if $requested_remote_home_update

      cmd = [ 'timeout', '-k', '1', '10',
              'nc', '-N', @mntinfo.remote_host.to_s, '907' ]

      output, status = Open3.capture2e(*cmd, :stdin_data => "#{ @username }\n")
      output_no_newlines = output.chomp.gsub(/\n/, ' / ')

      if status.exitstatus != 0 || output_no_newlines != 'OK' then
        raise("bad return status from command/server: '#{ cmd.join(' ') }'" \
                + " (#{ status.exitstatus }): #{ output_no_newlines }")
      end

      $requested_remote_home_update = true
    end
  end

end

def get_our_gvfsmounts
  sharenames = {
    'de' => 'geteilt',
    'fi' => 'yhteiset',
    'sv' => 'delade_filer',
    'en' => 'share',
  }

  userdir_labels = {
    'de' => 'Netzwerkordner',
    'fi' => 'Verkkokansio',
    'sv' => 'Nätverksmapp',
    'en' => 'Network folder',
  }

  schooldir_labels = {
    'de' => 'Geteilte Dateien',
    'fi' => 'Yhteiset',
    'sv' => 'Delade filer',
    'en' => 'Share',
  }

  lang = (ENV['LANG'] || '')[0..1]

  sharename       = sharenames[lang]       || sharenames['en']
  userdir_label   = userdir_labels[lang]   || userdir_labels['en']
  schooldir_label = schooldir_labels[lang] || schooldir_labels['en']

  username = Etc.getpwuid.name
  user_id = Process.uid
  user_primary_group = Etc.getgrgid( Etc.getpwuid.gid ).name

  [
    GvfsMount.new(username, username, user_id, userdir_label),
    GvfsMount.new("share/#{ sharename }/#{ user_primary_group }",
                  username,
                  user_id,
                  schooldir_label)
  ]
end

# ensure mounts to remote directories come up and stay

gvfs_mounts = get_our_gvfsmounts()

gvfs_threads = gvfs_mounts.map do |gvfsmnt|
                 Thread.new do
                   log("starting a thread to handle '#{ gvfsmnt.linkname }'")
                   gvfsmnt.ensure_mount()
                 end
               end

# we want to run this as long as the gvfs_threads are running
Thread.new do
  begin
    DbusHandler.new(gvfs_mounts)
  rescue StandardError => e
    log("error when starting up dbushandler: #{ e.message }")
    exit 1
  end
end

gvfs_threads.each &:join

exit(0)
