class DockerSync::SyncStrategy::Unison

Constants

UNISON_CONTAINER_PORT

Public Class Methods

new(sync_name, options) click to toggle source
# File lib/docker-sync/sync_strategy/unison.rb, line 18
def initialize(sync_name, options)
  @options = options
  @sync_name = sync_name
  # if a custom image is set, apply it
  @docker_image = if @options.key?('image')
                    @options['image']
                  else
                    'eugenmayer/unison:2.51.3-4.12.0-AMD64'
                  end
  begin
    Dependencies::Unison.ensure!
    Dependencies::Unox.ensure! if Environment.mac?
  rescue StandardError => e
    say_status 'error', "#{@sync_name} has been configured to sync with unison, but no unison available", :red
    say_status 'error', e.message, :red
    exit 1
  end
end

Public Instance Methods

clean() click to toggle source
# File lib/docker-sync/sync_strategy/unison.rb, line 259
def clean
  reset_container
end
expand_ignore_strings() click to toggle source
# File lib/docker-sync/sync_strategy/unison.rb, line 95
def expand_ignore_strings
  expanded_ignore_strings = []

  exclude_type = 'Name'
  exclude_type = @options['sync_excludes_type'] unless @options['sync_excludes_type'].nil?

  unless @options['sync_excludes'].nil?
    expanded_ignore_strings = @options['sync_excludes'].map do |pattern|
      ignore_string = if exclude_type == 'none'
                        # the ignore type like Name / Path are part of the pattern
                        pattern.to_s
                      else
                        "#{exclude_type} #{pattern}"
                      end
      "-ignore='#{ignore_string}'"
    end
  end
  expanded_ignore_strings
end
get_container_name() click to toggle source
# File lib/docker-sync/sync_strategy/unison.rb, line 241
def get_container_name
  @sync_name.to_s
end
get_host_port(container_name, _container_port) click to toggle source

noinspection RubyUnusedLocalVariable

# File lib/docker-sync/sync_strategy/unison.rb, line 229
def get_host_port(container_name, _container_port)
  cmd = 'docker inspect --format=\'{{(index (index .NetworkSettings.Ports "5000/tcp") 0).HostPort}}\' ' + container_name
  say_status 'command', cmd, :white if @options['verbose']
  stdout, stderr, exit_status = Open3.capture3(cmd)
  unless exit_status.success?
    say_status 'command', cmd
    say_status 'error', "Error getting mapped port, exit code #{$?.exitstatus}", :red
    say_status 'message', stderr
  end
  stdout.gsub("\n", '')
end
get_volume_name() click to toggle source
# File lib/docker-sync/sync_strategy/unison.rb, line 245
def get_volume_name
  @sync_name
end
increase_watcher_limit() click to toggle source
# File lib/docker-sync/sync_strategy/unison.rb, line 43
def increase_watcher_limit
  current_max_files_per_proc = `sysctl kern.maxfilesperproc | awk '{print $2}'`
  if current_max_files_per_proc.to_f < @options['max_inotify_watches']
    cmd = 'sudo sysctl -w kern.maxfilesperproc=' + @options['max_inotify_watches'].to_s
    say_status 'command', cmd, :white
    `#{cmd}` || raise('Unable to increase maxfilesperproc')
  else
    say_status 'command', 'Current maxfilesperproc set to ' + current_max_files_per_proc.to_s, :white
  end
  current_max_files = `sysctl kern.maxfiles | awk '{print $2}'`
  if current_max_files.to_f < @options['max_inotify_watches']
    cmd = 'sudo sysctl -w kern.maxfiles=' + @options['max_inotify_watches'].to_s
    say_status 'command', cmd, :white
    `#{cmd}` || raise('Unable to increase maxfiles')
  else
    say_status 'command', 'Current maxfiles set to ' + current_max_files.to_s, :white
  end
end
reset_container() click to toggle source
# File lib/docker-sync/sync_strategy/unison.rb, line 253
def reset_container
  stop_container
  `docker ps -a | grep #{get_container_name} && docker rm #{get_container_name}`
  `docker volume ls -q | grep #{get_volume_name} && docker volume rm #{get_volume_name}`
end
run() click to toggle source
# File lib/docker-sync/sync_strategy/unison.rb, line 37
def run
  increase_watcher_limit if @options.key?('max_inotify_watches')
  start_container
  sync
end
start_container() click to toggle source
# File lib/docker-sync/sync_strategy/unison.rb, line 143
def start_container
  say_status 'ok', "Starting unison for sync #{@sync_name}", :white

  container_name = get_container_name
  volume_name = get_volume_name
  env = {}
  if @options.key?('sync_user')
    raise 'sync_user is no longer supported, since it ise no needed, use sync_userid only please'
  end

  env['UNISON_SRC'] = '-socket 5000'
  env['UNISON_DEST'] = '/app_sync'

  env['MONIT_ENABLE'] = 'false'
  env['MONIT_INTERVAL'] = ''
  env['MONIT_HIGH_CPU_CYCLES'] = ''

  env['UNISON_ARGS'] = ''
  ignore_strings = expand_ignore_strings
  env['UNISON_ARGS'] << ignore_strings.join(' ')
  env['UNISON_WATCH_ARGS'] = ''

  env['MAX_INOTIFY_WATCHES'] = @options['max_inotify_watches'] if @options.key?('max_inotify_watches')
  if @options['sync_userid'] == 'from_host'
    env['OWNER_UID'] = Process.uid
  else
    env['OWNER_UID'] = @options['sync_userid'] if @options.key?('sync_userid')
  end

  # start unison-image in unison socket mode mode
  env['HOSTSYNC_ENABLE'] = 0
  env['UNISONSOCKET_ENABLE'] = 1

  additional_docker_env = env.map { |key, value| "-e #{key}=\"#{value}\"" }.join(' ')
  running = `docker ps --filter 'status=running' --filter 'name=#{container_name}' --format "{{.Names}}" | grep '^#{container_name}$'`
  if running == ''
    say_status 'ok', "#{container_name} container not running", :white if @options['verbose']
    exists = `docker ps --filter "status=exited" --filter "name=#{container_name}" --format "{{.Names}}" | grep '^#{container_name}$'`
    if exists == ''
      run_privileged = ''
      if @options.key?('max_inotify_watches')
        run_privileged = '--privileged'
      end # TODO: replace by the minimum capabilities required
      tz_expression = '-e TZ=$(basename $(dirname `readlink /etc/localtime`))/$(basename `readlink /etc/localtime`)'
      say_status 'ok', 'Starting precopy', :white if @options['verbose']
      # we just run the precopy script and remove the container
      cmd = "docker run --rm -v \"#{volume_name}:#{@options['dest']}\" -e APP_VOLUME=#{@options['dest']} #{tz_expression} #{additional_docker_env} #{run_privileged} --name #{container_name} #{@docker_image} /usr/local/bin/precopy_appsync"
      say_status 'precopy', cmd, :white if @options['verbose']
      system(cmd) || raise('Precopy failed')

      say_status 'ok', "creating #{container_name} container", :white if @options['verbose']
      cmd = "docker run -p '#{@options['sync_host_ip']}::#{UNISON_CONTAINER_PORT}' -v #{volume_name}:#{@options['dest']} -e APP_VOLUME=#{@options['dest']} #{tz_expression} #{additional_docker_env} #{run_privileged} --name #{container_name} -d #{@docker_image}"
    else
      say_status 'ok', "starting #{container_name} container", :white if @options['verbose']
      cmd = "docker start #{container_name} && docker exec #{container_name} supervisorctl restart unison"
    end
  else
    say_status 'ok', "#{container_name} container still running, restarting unison in container", :blue
    cmd = "docker exec #{container_name} supervisorctl restart unison"
  end
  say_status 'command', cmd, :white if @options['verbose']
  `#{cmd}` || raise('Start failed')
  say_status 'ok', "starting initial sync of #{container_name}", :white if @options['verbose']
  # wait until container is started, then sync:
  sync_host_port = get_host_port(get_container_name, UNISON_CONTAINER_PORT)
  cmd = "unison -testserver #{@options['src']} \"socket://#{@options['sync_host_ip']}:#{sync_host_port}\""
  say_status 'command', cmd, :white if @options['verbose']
  attempt = 0
  max_attempt = @options['max_attempt'] || 5
  loop do
    # noinspection RubyUnusedLocalVariable
    stdout, stderr, exit_status = Open3.capture3(cmd)
    break if exit_status == 0

    attempt += 1
    if attempt > max_attempt
      raise "Failed to start unison container in time, try to increase max_attempt (currently #{max_attempt}) in your configuration. See https://github.com/EugenMayer/docker-sync/wiki/2.-Configuration for more informations"
    end

    sleep 1
  end
  sync
  say_status 'success', 'Unison server started', :green
end
stop() click to toggle source
# File lib/docker-sync/sync_strategy/unison.rb, line 263
def stop
  say_status 'ok', "Stopping sync container #{get_container_name}"
  begin
    stop_container
  rescue StandardError => e
    say_status 'error', "Stopping failed of #{get_container_name}:", :red
    puts e.message
  end
end
stop_container() click to toggle source
# File lib/docker-sync/sync_strategy/unison.rb, line 249
def stop_container
  `docker ps | grep #{get_container_name} && docker stop #{get_container_name} && docker wait #{get_container_name}`
end
sync() click to toggle source
# File lib/docker-sync/sync_strategy/unison.rb, line 73
def sync
  args = sync_options
  cmd = 'unison ' + args.join(' ')

  say_status 'command', cmd, :white if @options['verbose']

  stdout, stderr, exit_status = Open3.capture3(cmd)
  if !exit_status.success?
    say_status 'error', "Error starting sync, exit code #{$?.exitstatus}", :red
    say_status 'message', stdout
    say_status 'message', stderr
  else
    if @options['notify_terminal']
      TerminalNotifier.notify(
        "Synced #{@options['src']}", title: @sync_name
      )
    end
    say_status 'ok', "Synced #{@options['src']}", :white
    say_status 'output', stdout if @options['verbose']
  end
end
sync_options() click to toggle source
# File lib/docker-sync/sync_strategy/unison.rb, line 115
def sync_options
  args = []
  args = expand_ignore_strings + args
  args.push("'#{@options['src']}'")
  args.push('-auto')
  args.push('-batch')
  args.push(sync_prefer)
  args.push(@options['sync_args']) if @options.key?('sync_args')
  sync_host_port = get_host_port(get_container_name, UNISON_CONTAINER_PORT)
  args.push("socket://#{@options['sync_host_ip']}:#{sync_host_port}")

  if @options.key?('sync_group') || @options.key?('sync_groupid')
    raise('Unison does not support sync_group, sync_groupid - please use rsync if you need that')
  end

  args
end
sync_prefer() click to toggle source

cares about conflict resolution

# File lib/docker-sync/sync_strategy/unison.rb, line 134
def sync_prefer
  case @options.fetch('sync_prefer', 'default')
  when 'default' then "-prefer '#{@options['src']}' -copyonconflict" # thats our default, if nothing is set
  when 'src' then "-prefer '#{@options['src']}'"
  when 'dest' then "-prefer 'socket://#{@options['sync_host_ip']}:#{sync_host_port}'"
  else "-prefer '#{@options['sync_prefer']}'"
  end
end
watch() click to toggle source
# File lib/docker-sync/sync_strategy/unison.rb, line 62
def watch
  args = sync_options
  args.push('-repeat watch')
  cmd = ''
  cmd = cmd + 'ulimit -n ' + @options['max_inotify_watches'].to_s + ' && ' if @options.key?('max_inotify_watches')
  cmd = cmd + 'unison ' + args.join(' ')

  say_status 'command', cmd, :white if @options['verbose']
  fork_exec(cmd, "Sync #{@sync_name}", :blue)
end