#!/usr/bin/env perl

use strict;
use warnings;
use diagnostics;

use FindBin;
use lib "$FindBin::Bin/Modules";

use Util;
use Mars;
use TUI;
use Filters;
use Text::CSV_XS;

use feature 'say';

# TODO: Much can be extracted into utility functions

my $local_obsidian        = '/home/christoph/Notes/Obsidian/Chriphost';
my $local_obsidian_attach = "$local_obsidian/attach";

my $local_newlib          = '/home/christoph/Notes/TU/MastersThesis/07 NewLib';
my $local_root            = '/home/christoph/Notes/TU/MastersThesis/FailNix';
my $local_wamr            = "$local_root/wamr";
my $local_scripts_dir     = "$local_root/scripts";
my $local_builds_dir      = "$local_root/builds";
my $local_archive_dir     = "$local_root/injections";
my $local_charts_dir      = "$local_root/scripts/charts";
my $local_ghidra_projects = "$local_root/ghidra";
my $local_ghidra_scripts  = "$local_root/scripts/ghidra";
my $local_db_conf         = "$local_root/db.conf";

my $resultbrowser_port = '5000';
my $resultbrowser      = 'resultbrowser.py';
my $qemu_gdb_port      = '9000';

my $remote_root       = '/home/lab/smchurla/Documents/failnix';
my $remote_builds_dir = "$remote_root/builds";

my $db_host = "127.0.0.1";
my $db_port = "3306";
my $db_user = "smchurla";

my %handlers = (
    '01. Build Experiments' => sub { do qq{$local_scripts_dir/build.pl}; },

    '02. Deploy Experiments (Mars)' =>
      sub { do qq{$local_scripts_dir/deploy.pl}; },

    '03. Archive Experiments (Downloads from Mars)' => sub {

        # Download ran experiments from mars
        my @dirs = Mars::find_remote_subdirs($remote_builds_dir);

        my @existing = Util::find_subdirs($local_archive_dir);
        my @new_dirs;
        foreach (@dirs) {
            my $dir = $_ =~ s/:/-/gr;
            unless ( grep { /$dir/ } @existing ) {
                push @new_dirs, $_;
            }
        }

        my @selected_dirs =
          TUI::select_from_list( "Select Experiments to Download from Mars",
            1, @new_dirs );
        die "No experiment selected" unless @selected_dirs;
        Mars::download_dir( "$remote_builds_dir/$_",
            "$local_archive_dir/" . $_ =~ s/:/-/gr )
          for @selected_dirs;
    },

    '04. Query Databases (Mars)' => sub {

        # Select databases
        my @db_names = Mars::db_list();
        my @selected_dbs =
          TUI::select_from_list( "Select Databases to Query", 1, @db_names );
        die "No database selected" unless @selected_dbs;

        # Select queries
        my @queries =
          map { s/\.pm//r } Util::find_files("$local_root/scripts/Queries");
        my @selected_queries =
          TUI::select_from_list( "Select Queries to Run", 1, @queries );
        die "No query selected" unless @selected_queries;

        # Select filter configs (they're combined into one)
        my @filter_choices;
        my %filter_label_name;
        my $configs = Filters::get_configs();
        foreach my $name ( sort keys %$configs ) {
            my $label = $configs->{$name}{label};
            push @filter_choices, $label;
            $filter_label_name{$label} = $name;
        }
        my @selected_filter_labels =
          TUI::select_from_list( "Select Filters to Apply (none = unfiltered)",
            1, @filter_choices );
        my @filter_configs =
          map { $filter_label_name{$_} } @selected_filter_labels;

        # Run queries on databases
        foreach my $db (@selected_dbs) {
            foreach my $query (@selected_queries) {
                Util::rewrite_file( $local_db_conf, "database=",
                    "database=$db\n" );

                my $config_label =
                  @filter_configs
                  ? " (" . join( "+", @filter_configs ) . ")"
                  : "";
                say "Running $query$config_label on $db...";
                Util::execute_query( $db =~ s/smchurla_//r,
                    $query, $local_db_conf, $local_archive_dir, 0,
                    @filter_configs );
            }
        }
    },

    '05. Import Experiments Into Ghidra' => sub {

        my @existing = Util::find_files($local_ghidra_projects);

        # Determine if an experiment was already imported
        my $project_exists = sub {
            my ($name) = @_;
            $name =~ s/:/-/g;

            return grep { /^$name.gpr$/ } @existing;
        };

        # Import archived experiments into ghidra
        my @dirs =
          grep { !$project_exists->($_) }
          Util::find_subdirs($local_archive_dir);

        my @dirs_with_notes;
        foreach my $dir (@dirs) {
            my $info = Util::read_experiment_info($dir);

            push @dirs_with_notes,
              ( defined $info && length($info) > 0 )
              ? sprintf( "%-50s (%s)", $dir, $info )
              : $dir;
        }

        my @selected_dirs =
          TUI::select_from_list( "Select Experiments to Import into Ghidra",
            1, @dirs_with_notes );

        foreach (@selected_dirs) {
            my $experiment = $_ =~ s/(.*?)\s+\(.+\)$/$1/r;

            say "Creating Ghidra project for $experiment...";
            system(
                'ghidra-analyzeHeadless',

                $local_ghidra_projects, $experiment =~ s/:/-/gr,
                '-import',     "$local_archive_dir/$experiment/system.elf",
                '-scriptPath', $local_ghidra_scripts,
                '-postScript', 'DWARFLineInfoSourceMapScript',
                '-postScript', 'DWARFLineInfoCommentScript',
                '-postScript', 'ImportMarkersAsBookmarks',
                "$local_archive_dir/$experiment/faults.csv"
            );
        }
    },

    '06. Import Experiments Into Obsidian' => sub {

        my @experiments = Util::find_subdirs($local_archive_dir);

        # Filter experiments that already have notes
        my @new_experiments;
        my @existing_notes = split "\n", qx{obsidian files};
        foreach my $experiment (@experiments) {
            push @new_experiments, $experiment
              unless ( grep { /zettel\/$experiment/ } @existing_notes );
        }

        my @selected_experiments =
          TUI::select_from_list( "Select Experiments to Import into Obsidian",
            1, @new_experiments );
        die "No experiment selected" unless @selected_experiments;

        foreach my $experiment (@selected_experiments) {

            # Create note
            system(
                'obsidian',                'create',
                "name=$experiment",        'path=zettel',
                'template=FailExperiment', 'open',
                'newtab'
            );

            # Insert results
            if ( -f "$local_archive_dir/$experiment/results.txt" ) {
                open( my $fhandle, '<',
                    "$local_archive_dir/$experiment/results.txt" )
                  or return;
                my $results = join "", <$fhandle>;
                close($fhandle);

                say "$local_archive_dir/$experiment/results.txt does not exist";

                system( 'obsidian', 'append', "file=zettel/$experiment",
                    "content=## Results\n\n```\n$results```\n" );
            }

            # Insert charts
            system(
                'obsidian',                'append',
                "file=zettel/$experiment", "content=## Charts\n\n"
            );

            my $attach_image = sub {
                my ($name) = @_;

                system( 'obsidian', 'append', "file=zettel/$experiment",
"content=![$name](file://$local_archive_dir/$experiment/$name.svg)\n"
                );
            };
            $attach_image->("single_result");
            $attach_image->("scatter");
        }
    },

    '10. Explore Experiment Results' =>
      sub { do qq{$local_scripts_dir/explore.pl}; },

    '11. Compare Experiment Results' => sub {

        my @selected_experiments = Util::select_experiment(1);

       # TODO: Fails silently if not every selected experiment has this datafile
        my $resultsdata_csv =
          Util::pick_data_file( "$local_archive_dir/$selected_experiments[0]",
            "resultsdata" );

        # Read results
        my %all_results;
        foreach my $experiment (@selected_experiments) {

            # Schema: benchmark, resulttype, faults
            my $data = Text::CSV_XS::csv(
                in      => "$local_archive_dir/$experiment/$resultsdata_csv",
                headers => 'auto'
            );

            foreach my $row (@$data) {
                $all_results{$experiment}{ $row->{benchmark} }
                  { $row->{resulttype} } = $row->{faults};
            }
        }

        my @benchs  = ( 'ip', 'mem', 'regs' );
        my @markers = (
            'OK_MARKER',         'FAIL_MARKER',
            'DETECTED_MARKER',   'TIMEOUT',
            'TRAP',              'WRITE_TEXTSEGMENT',
            'ACCESS_OUTERSPACE', 'GROUP1_MARKER'
        );

        my $heading    = sprintf( "%5s %20s ", "BENCH", "TYPE" );
        my $subheading = sprintf( "%5s %20s ", "",      "" );
        foreach my $experiment (@selected_experiments) {
            $heading .= sprintf( "%50s  ", $experiment );
            $subheading .=
              sprintf( "%50s  ", Util::read_experiment_info($experiment) );
        }

        my @entries = ( $heading, $subheading, "" );
        foreach my $benchmark (@benchs) {
            foreach my $marker (@markers) {
                my $entry = sprintf( "%5s %20s ", $benchmark, $marker );

                foreach my $experiment (@selected_experiments) {
                    if ( exists $all_results{$experiment}{$benchmark}{$marker} )
                    {
                        $entry .= sprintf(
                            "%50s  ",
                            Util::format_number_sep(
                                $all_results{$experiment}{$benchmark}{$marker}
                            )
                        );
                    }
                    else {
                        $entry .= sprintf( "%50s  ", "" );
                    }
                }

                push @entries, $entry;
            }
            push @entries, "";
        }

        TUI::select_from_list(
            "Comparing " . scalar(@selected_experiments) . " Experiments",
            0, @entries );
    },

    '11b. Compare to Baseline' => sub {

        my $baseline = Util::select_experiment(0);

        my @selected_experiments = Util::select_experiment(1);

       # TODO: Fails silently if not every selected experiment has this datafile
        my $resultsdata_csv =
          Util::pick_data_file( "$local_archive_dir/$baseline", "resultsdata" );

        my %all_results;
        foreach my $experiment ( $baseline, @selected_experiments ) {

            my $data = Text::CSV_XS::csv(
                in      => "$local_archive_dir/$experiment/$resultsdata_csv",
                headers => 'auto'
            );

            foreach my $row (@$data) {
                $all_results{$experiment}{ $row->{benchmark} }
                  { $row->{resulttype} } = $row->{faults};
            }
        }

        my @benchs  = ( 'ip', 'mem', 'regs' );
        my @markers = (
            'OK_MARKER',         'FAIL_MARKER',
            'DETECTED_MARKER',   'TIMEOUT',
            'TRAP',              'WRITE_TEXTSEGMENT',
            'ACCESS_OUTERSPACE', 'GROUP1_MARKER'
        );

        my $heading = sprintf( "%5s %20s %50s  ", "BENCH", "TYPE", $baseline );
        my $subheading = sprintf( "%5s %20s %50s  ",
            "", "", Util::read_experiment_info($baseline) );
        foreach my $experiment (@selected_experiments) {
            $heading .= sprintf( "%50s  ", $experiment );
            $subheading .=
              sprintf( "%50s  ", Util::read_experiment_info($experiment) );
        }

        my @entries = ( $heading, $subheading, "" );
        foreach my $benchmark (@benchs) {
            foreach my $marker (@markers) {
                my $entry = sprintf( "%5s %20s ", $benchmark, $marker );

                if ( exists $all_results{$baseline}{$benchmark}{$marker} ) {
                    $entry .= sprintf(
                        "%50s  ",
                        Util::format_number_sep(
                            $all_results{$baseline}{$benchmark}{$marker}
                        )
                    );
                }
                else {
                    $entry .= sprintf( "%50s  ", "" );
                }

                foreach my $experiment (@selected_experiments) {
                    if (    exists $all_results{$baseline}{$benchmark}{$marker}
                        and
                        exists $all_results{$experiment}{$benchmark}{$marker}
                        and $all_results{$baseline}{$benchmark}{$marker} != 0 )
                    {
                        my $factor =
                          $all_results{$experiment}{$benchmark}{$marker} /
                          $all_results{$baseline}{$benchmark}{$marker};
                        $entry .=
                          sprintf( "%50s  ", sprintf( "%.2fx", $factor ) );
                    }
                    else {
                        $entry .= sprintf( "%50s  ", "" );
                    }
                }

                push @entries, $entry;
            }
            push @entries, "";
        }

        TUI::select_from_list(
            "Baseline: $baseline — Comparing "
              . scalar(@selected_experiments)
              . " Experiments",
            0, @entries
        );
    },

    '12. Open Experiment in BinaryNinja' => sub {
        my @selected_experiments = Util::select_experiment(1);
        my @paths =
          map { "$local_archive_dir/$_/system.elf" } @selected_experiments;

        system( 'binaryninja', @paths );
    },

    '13. Open Experiment in Binsider' => sub {
        my $selected_experiment = Util::select_experiment(0);

        system( 'binsider',
            "$local_archive_dir/$selected_experiment/system.elf" );
    },

    '14. Open Experiment in Ghidra' => sub {

        my @projects =
          map { s/\.gpr//r } Util::find_files($local_ghidra_projects);
        my @selected_projects =
          TUI::select_from_list( "Select Project to Open in Ghidra",
            0, @projects );
        die "No project selected" unless @selected_projects;
        my $project = $selected_projects[0];
        system(
            join " ",
            (
                "_JAVA_AWT_WM_NONREPARENTING=1", "ghidra",
                "$local_ghidra_projects/$project.gpr",
            )
        );
    },

    '15. Run Objdump on Experiment' => sub {
        my $selected_experiment = Util::select_experiment(0);

        system(
"objdump $local_archive_dir/$selected_experiment/system.elf -D -M intel -S | bat --color never"
        );
    },

    '16. Run Wasm-Objdump on Experiment' => sub {
        my $selected_experiment = Util::select_experiment(0);

        system(
"wasm-objdump -d $local_archive_dir/$selected_experiment/wasm_module.wasm | bat --color never"
        );
    },

    '17. Run Radare2 on Experiment' => sub {
        my $selected_experiment = Util::select_experiment(0);

        say "Radare help:";
        say "s <address>      - Seek to address";
        say "pd <n>           - Disassemble n instructions";
        say "pdf              - Disassemble current function";
        say "pdf @ <function> - Disassemble function";
        say "pdr              - Disassemble recursively";
        say "V                - Switch view";
        say "p                - Switch print mode";
        say "P                - Switch layout";
        system(
            'radare2', '-AA',
            '-c',      '"-s dbg.os_main"',
            '-e',      'scr.color=3',
            '-e',      'scr.scrollbar=0',
            '-e',      'scr.responsive=true',
            '-e',      'scr.interactive=true',
            '-e',      'scr.utf8=true',
            '-e',      'scr.utf8.curvy=true',
            '-e',      'asm.syntax=intel',
            '-e',      'asm.lines=false',
            '-e',      'asm.xrefs=true',
            '-e',      'asm.flags=true',
            '-e',      'asm.comments=true',
            '-e',      'asm.functions=true',
            '-e',      'asm.var=true',
            '-e',      'asm.cmt.right=true',
            '-e',      'asm.dwarf=true',
            '-e',      'asm.pseudo=false',
            '-e',      'asm.describe=false',
            '-e',      'bin.relocs.apply=true',
            "$local_archive_dir/$selected_experiment/system.elf",

        );
    },

    '18. Open Database in ResultBrowser (Mars)' => sub {

        my @db_names = Mars::db_list();
        my @selected_dbs =
          TUI::select_from_list( "Select Database for ResultBrowser",
            0, @db_names );
        die "No database selected" unless @selected_dbs;
        my $selected_db = $selected_dbs[0];

        Util::rewrite_file( $local_db_conf, "database=",
            "database=$selected_db\n" );

        system( $resultbrowser, '-c', $local_db_conf, '--host=0.0.0.0',
            "--port=$resultbrowser_port" );
    },

    '19. Open Database in LazySQL (Mars)' => sub {
        my $experiment =
          Util::select_experiment(0) =~ s/T(\d\d)-(\d\d)-(\d\d)/T$1:$2:$3/r;
        my $ssh         = Mars::ssh_connect();
        my $db_password = Mars::read_db_password_file();

        system( 'lazysql', '-read-only',
"mariadb://$db_user:$db_password\@$db_host:$db_port/${db_user}_$experiment"
        );
    },

    '20. Open TablePlus (Mars)' => sub {
        system('tableplus');
    },

    '21. Run Build in GDB' => sub {
        my @builds = grep { /linux/ } Util::find_subdirs($local_builds_dir);
        my @selected_builds =
          TUI::select_from_list( "Select Build to Run in GDB", 0, @builds );
        die "No build selected" unless @selected_builds;
        my $selected_build = $selected_builds[0];

        my $build_dir    = "$local_builds_dir/$selected_build";
        my $build_name   = $selected_build =~ s/.*?_.*?_(.*?)_.*$/$1/r;
        my $is_baremetal = $selected_build =~ /linux-baremetal$/;
        my $is_wasm      = $build_name eq 'aot' || $build_name eq 'interp';

        say "$build_name";

        # Baremetal builds boot the iso in qemu and attach gdb
        my $qemu_pid;
        if ($is_baremetal) {
            say "Forking...";
            $qemu_pid = fork();
            die "fork failed: $!\n" unless defined $qemu_pid;

            if ( $qemu_pid == 0 ) {

                # child -> qemu.
                exec(
                    'qemu-system-i386',
                    '-drive',
                    "file=$build_dir/system.iso,media=cdrom",
                    '-boot',
                    'd',
                    '-m',
                    '32',
                    '-D',
                    "$local_root/qemu.log",
                    '-d',
                    "int,cpu_reset",
                    '-no-reboot',
                    '-no-shutdown',
                    '-S',
                    '-gdb',
                    "tcp::$qemu_gdb_port",
                ) or die "failed to exec qemu: $!\n";
            }
        }

        # Shared gdb args
        my @gdb_args = (
            '--tui',
            '-q',
            "$build_dir/system.elf",
            '-ex',
            'set disassembly-flavor intel',
            '-ex',
            "set substitute-path 'build-$build_name' '$build_dir'",
            '-ex',
            "set substitute-path '/build/source/core' '$local_wamr/core'",
        );

        # Specific gdb args
        if ($is_baremetal) {
            push @gdb_args,
              '-ex', "target remote localhost:$qemu_gdb_port",
              '-ex', 'break os_main';
        }
        else {
            push @gdb_args, '-ex', 'break main';
        }

        # Shared breakpoints
        my @shared_breakpoints = (
            'fail_start_trace',     'fail_stop_trace',
            'fail_marker_positive', 'fail_marker_detected',
            'fail_marker_negative',
        );

        # Breakpoints for native C
        my @c_breakpoints = ();

        # Breakpoints for Wasm
        my @wasm_breakpoints = (
            'fail_marker_group1',

            # 'os_mmap',
            # 'wamr_malloc',
            # 'wamr_realloc',
            # 'wamr_free',
        );
        my @breakpoints = (
            @shared_breakpoints, $is_wasm ? @wasm_breakpoints : @c_breakpoints,
        );
        push @gdb_args, '-ex', "break $_" for @breakpoints;

        # Autostart execution
        push @gdb_args, '-ex', ( $is_baremetal ? 'continue' : 'run' );

        system( 'gdb', @gdb_args );

        if ($is_baremetal) {
            say "Killing qemu with pid $qemu_pid...";
            kill 'TERM', $qemu_pid;
            waitpid( $qemu_pid, 0 );
        }
    },

    '30. Plot Results' => sub {

        # Generate R ggplot2 charts
        my @selected_experiments = Util::select_experiment(1);

        my @charts = map { s/\.r//r } Util::find_files($local_charts_dir);
        my @selected_charts =
          TUI::select_from_list( "Select Plots to Generate", 1, @charts );
        die "No plot selected" unless @selected_charts;

        # Need to know which chart uses which datafile
        my @faults_charts =
          grep {
/_heatmap|_scatter|_sankey|_instr_fault_correlation|_instr_fault_rate_heatmap|fault_rates_per_instruction/
          } @selected_charts;
        my @resultsdata_charts =
          grep {
/_result|_fault_count_comparison|_ratio_comparison|_fault_count_correlation/
          } @selected_charts;

        # Select if faults.csv or a filtered variant should be used
        my $faults_csv;
        if (@faults_charts) {
            $faults_csv = Util::pick_data_file(
                "$local_archive_dir/$selected_experiments[0]", "faults" );
        }

        # Select if resultsdata.csv or a filtered variant should be used
        my $resultsdata_csv;
        if (@resultsdata_charts) {
            $resultsdata_csv = Util::pick_data_file(
                "$local_archive_dir/$selected_experiments[0]",
                "resultsdata" );
        }

        # Plot single-experiment charts
        my @single_charts = grep { /single/ } @selected_charts;
        foreach my $experiment (@selected_experiments) {
            foreach my $chart (@single_charts) {
                say " - Generating plot $chart for $experiment...";
                my @r_args = (
                    'Rscript',
                    "$local_charts_dir/$chart.r",
                    "$local_archive_dir/$experiment"
                );

                # If the chart is marked in @faults_charts,
                # append the $faults_csv to the args
                push @r_args, $faults_csv
                  if defined $faults_csv
                  && grep { $_ eq $chart } @faults_charts;

                # If the chart is marked in @resultsdata_charts,
                # append the $resultsdata_csv to the args
                push @r_args, $resultsdata_csv
                  if defined $resultsdata_csv
                  && grep { $_ eq $chart } @resultsdata_charts;
                system(@r_args);
            }
        }

        my @combined_charts =
          grep { /combined/ } @selected_charts;
        my $print_experiments = join " ", @selected_experiments;
        my @path_experiments =
          map { "$local_archive_dir/$_" } @selected_experiments;
        foreach my $chart (@combined_charts) {
            say " - Generating plot $chart for ($print_experiments)...";
            my @r_args =
              ( 'Rscript', "$local_charts_dir/$chart.r", @path_experiments );

            # If the chart is marked in @faults_charts,
            # append the $faults_csv to the args
            push @r_args, $faults_csv
              if defined $faults_csv
              && grep { $_ eq $chart } @faults_charts;

            # If the chart is marked in @resultsdata_charts,
            # append the $resultsdata_csv to the args
            push @r_args, $resultsdata_csv
              if defined $resultsdata_csv
              && grep { $_ eq $chart } @resultsdata_charts;
            system(@r_args);
        }
    },

    '95. Delete Builds' => sub {

        # Delete old build files
        my @builds = Util::find_subdirs($local_builds_dir);
        my @selected_builds =
          TUI::select_from_list( "Select Builds to Delete", 1, @builds );
        die "No builds selected" unless @selected_builds;
        system( 'rm', '-rf', "$local_builds_dir/$_" ) for @selected_builds;
    },

    '96. Delete Builds (Mars)' => sub {

        # Delete ran experiments from mars
        my @builds = Mars::find_remote_subdirs($remote_builds_dir);
        my @selected_builds =
          TUI::select_from_list( "Select Builds to Delete from Mars",
            1, @builds );
        die "No experiment selected" unless @selected_builds;
        Mars::ssh_system( 'rm', '-rf', "$remote_builds_dir/$_" )
          for @selected_builds;
    },

    '97. Delete Ghidra Projects' => sub {

        # Delete ghidra projects
        my @projects =
          map { s/\.gpr//r } Util::find_files($local_ghidra_projects);
        my @selected_projects =
          TUI::select_from_list( "Select Ghidra Projects to Delete",
            1, @projects );
        die "No project selected" unless @selected_projects;
        system( 'rm', '-rf', "$local_ghidra_projects/$_.gpr" )
          for @selected_projects;
        system( 'rm', '-rf', "$local_ghidra_projects/$_.rep" )
          for @selected_projects;
    },

    '98. Delete Archived Experiments' => sub {

        # Delete archived experiments
        my @selected_experiments = Util::select_experiment(1);

        system( 'rm', '-rf', "$local_archive_dir/$_" )
          for @selected_experiments;
    },

    '99. Drop Databases (Mars)' => sub {

        # Drop databases on mars
        my @dbs = Mars::db_list();
        my @dbs_with_notes;
        foreach my $db (@dbs) {
            my $info =
              Util::read_experiment_info( $db =~ s/smchurla_//r =~ s/:/-/gr );

            push @dbs_with_notes,
              ( defined $info && length($info) > 0 )
              ? sprintf( "%-60s (%s)", $db, $info )
              : $db;
        }

        my @selected_dbs =
          TUI::select_from_list( "Select Databases to Drop from Mars",
            1, @dbs_with_notes );
        die "No database selected" unless @selected_dbs;

        @selected_dbs =
          map { s/(.*?)\s+\(.+\)$/$1/r } @selected_dbs;

        Mars::db_drop($_) for @selected_dbs;
    },
);

while (1) {
    my @submenu =
      TUI::select_from_list( "FailNix Menu", 0, sort keys %handlers );
    die "No action selected" unless @submenu;

    say @submenu;

    eval { $handlers{ $submenu[0] }(); }
}

Mars::db_disconnect();
