JFIF   ( %!1!%)+...383,7(-.+  ++-+++++-++-++--+--+-+-------+-++-+--+---+++--+7+-+"F!1AQaq"2BRb#3Sr$CDsT&!Q1Aa"2Rbq ?򉄘ǷLR HR,nNb .&W)fJbMOYxj-\bT2(4CQ"qiC/ " %0Jl"e2V  0SDd2@TV^{cW&F͉x9#l,.XɳvRZ C8S 6ml!@!E! `FS!M #(d)Q lml1ml Ų&x(ʨ2NFmj@D<dN5UN˄uTB emLAy#` ` ` I!I 6āHBxL & J#7BQ.$hv h q+tC"EJ) 8R e2U2Y@j%6PF^4LnNBp"8)4JI-ֲvK ^؊)hz[T5˗",Rҥf8ڤS4ʘ!`D ` X+ L,(hl)*S##`6[`0*L T H*HA@I&&r1kr*r*)N$#L  1#ZFSl `[( ("((he`4 Ch [="A R / 0I`twCDcWh"i) cLad\BcLKHZ"ZEW$Ƚ@A~i^`S *A&h:+c Y6vϕGClRPs.`H`(@<$qDe pL@DpLX, E2MP A  `II m& AQ "AT rbg# g2!SiLj*3L \ G;TFL`K BMy 2S`YLh1 d >-"ZfD^Q DH" RAbEV#Lfq,(rETp64-IJ!*p4F$q;G8DQ/TKP2$jp3KW]FtLtƉ1ol]VBgػJH6 )h61GJR7Nj.Z4piJRDd]t]0dP]:N.b'⹙SvDSz]L,_#ugT&[~?cS^"{Bh{/=ۑxOk̳O59o dar793`)SeYM@\ "$E(Tm&)N2Ih)F5EDed(FS,Pa @!@#@lea HCD$11jCLJqcod S3yd*,lL+QEfsgW1nw)cT#dS HXkFJB"6(ʝH)H"#EZh:Y`khݳh%Sc<mlAko2]gDqQtro=3OƸU9_-t8UvW3sGəg*#:c)><"wc\ASmT|6Ę>9~#1Ƈ~ڒE1vVi# I MM#u$8W 5ǍfƬΜg*Qpi1ȩFOf۔S,/⎯(Lrմ`(Z LsbA \6 6dm[I=!r:REI.wgzG)ԇSbӑxuׇTyyL^e'x^ty4Z&eB]I|v59Jjhm;Ng񷫳n<ϞҼѝjk;׹DlY^ҍ\+x9V!j([cmS.NO6jxNζrm&oײizT$N>?~ Sl-:iڥk\at#E!CL`.O0a*w/WV7/r)DŽt7'Nĵ#7O1 ]{[/-2bA<$&Gm_4t)_>)mjG;V^'k59o>ɌM,ؾf9z6 4v_3T.5V/RD-5 %T5XTޫ4TaZ`U *ƱUƲ UG"5+sJJ2E9#܎kr2G3Bb,XM6H: ?@p!'\4V02aԙ) hbZ]:` ev3ʘ'}!ohȒ*TJjr[RFyQ*#{h{R]J]Lr-.D-.җfo$D ?X0%~1P.Og{cWϫ22&Ϭ_V.W3nmiOl}+!˫#`kR33aUb0-g:qmsέ+0HO|&nhOn+}n5QF_"gvLm/z'+r'n_oC语i|1}Gi|}_D~9JZ_%DVQp\koۅjAs~/c0ksUJi^W9W5!>?O:q|ˣSIB/&K<(lg(%Wg$|LW7vߤW߇q|jef3D H\S6(eJb*@&sTKTW/*@v:.N- @ITʓ1Zg&-eꓝM r]EMס{q$b]'7Z7N:O~lNlP7iͲk)$O^퉢<YSD*hr'Z#5e6t[Fdh AJǔP9P 1\R).Il+jI*,(ܢ22N*OwKFX gc?\mB7iA+εe8 "ġ/p5pW-$މ-[a 5ViAW/V{/&UsF./՞ҕ*)rZg.^_+gt_z-oAbqQn*WlHyZ*\TaEewlLR3ԹȭN}MM}aih"5ܕRT$:~'TcT|*)xGC>n+r{XU xuF"<~67у'fxlf`r3D*#Z1ђfH`2dIWo/qB| 63xxW6^m%Kvg>\>x>!H5Nr8J/FJ9Wx(Hou" S'kWاC\9ְ#^OaҮ+~gnkuЉ,aWU*1 읍jnb|e= :2.UL`Q}YS&gI.c=a`%j:C%2@^>])25/ܙ<lzwɛ)ݣS4h3=J tyϬ.E7 8ڞGZu\_JHsݢϑ}IZ"ӳ=X<Ɖ2{a:{7L+>V}c)*lo Yv&+|L;>+/Sj26K+澡*;>-s"}M2] Ig5aCL*r"&\} #^R.7_Mgf}.ߌy(}Z\gP&ʠHj%</{.]rߙQ`>;5g;u6dԛ %xb|oՋTJ5Ϥ(]XqP>f{Jk2,8'~ZU6tMQsg XKg^2ϓ3},[wo۴I|ܷ%[Ol\Pkr]Y//cg6U⧻/VПi8ys_n<\~cze!!H~x;QJZKȮ^ȧG|cS~8ji,Fo+,y~?pk)u /in3JmkX(Mj1N 4c Epc>BO *LfQO&` c;LjcYf 1ɻ)CLsY^Y5" lP/wuEln&dav,(;'W9ej ku`-KHI՟%ԁʁ 1\}?OjsF^Xn$Ё.օC>D:?I @aGE.ĩ1 $ et~T`߸Ir'RX.Zwc%~U=r>-UaFbǺ?R=Z?i'[ASS;siJrzy>nxu$[_B\4}:r'ҵj1_v-[;y?ֹ0I16 . M%4^!S&t ! h !zQð.bBT ?@]?CHq(rd!.$>/x+bnʎNN#w)` )*f!-ɂ\(طYLHzc`Uq7BfCcE0ԉ4Fم쏠ce5T r͸GVlФ?ѣ} mhrkly.Ts㷖)Mө S^%'g>wk%bP[}j~ǾV#K -Fgv켼ǨgɼeSz/6{M=BPZFu\Q75n3Iݤ.W9QfF{vJwF't[@iVj4G~KOnH߿_Do=.c.One?E+GfGN⧭H?4;u`ua|V-+j4?48n ɦ=-]puv&Jc}K>b%U x8pz6L8AXFsW]N55ҦbIWZQ7ï Ԗ3cjz匩ӺOTɖƴ%a'MI}cdR$ݚIζ̝ LIu>J3{^෠㜦˯xܿe\b"2y'x{ RDW b+o2KFhR0:U늞En>լRӉt Iڹ\ wշQEv"v;EJ)yl[5:F0=b4,\PqKtv4{bQz:>C7"8W#Zjdd| cjz%K %Z 9dD{=NFʳAƩtI)kS*s$`:A\ʬ*ֹ9{Nl|eJ١rQnM%z_#x_•TO><)kyD %GN<~y>vfǧB)F)c\lې(#\ h`fgfjTBdhhHL2Y0^ Y0^-"D!QaI15 m~ gՒd|;#gMn(P$l H.R2^PU")pN` N8󫅂OJ;^jz\uumJMF|ηq[]$Vrrt:Q^;QPkHՠ{]HwˆMuIr7!r&- j%"9LtUb56+^TWBqdhHAD7 HwKH^F3LIq #hK`]IWKiH?کǴeԥQ>g{^q^>HKoOB||8aݏS}{S_]ϸ/X~ܵw'OSPAf֩ܟ[>7 @[ֵ;G߇QU*Cթ *OKU^zz[fRnpcJX9u<iq8B]u8 ]I,;[G#2W.¸D8rPG Y%PBJ= wo;PJgx6;yB`3zZGPAͫy{5Nb_re*ONHR]Ji)U{Ӓ:qqɏ[mB4࢒I$ 2vpBADY`DIVAn"Bh$&&cMbdB 鮆wHR'E(ѸZA*H~{B M҅n\@N{7ISCp Vd( r+bg|ns:qg:|J|ɪV.UVaAS͓FyRuLѦT騬 `3􏳕{eo/Tz8DkW?,cl~TqLne֠[B*D +t 6˦S;5KjV3e WBrT.XSHm sl5F%NGM`Y )": J!W4]HTrPX2 QYɕ\m2VLd+`,^ѺiPztUGY6+cӧ6] U%u/ˈFOiB*nFF#ұJ Z/c')?Q͟5.8E~G6e<\?}GkhMFUظOqhEA - "`dQ#(4Ԧf VLmc@q5J8K; M^JZnn)9Zm\ qIJqS: i[9~Oaƒ]Z4F&+666( N]쁼LM(oyvUI/Χ[ھ]hTˉG".SeYgu;hRDtڬv=5 ׁqMS\Ȭi5D]1$*0UL1QY`QdLb[+z9";'yi`OT/4{@EZ'Y0>4I*d nM#5hі.vrM[]Ä;]\ʦS,叕DQZq0fӌI͋]TNK"#;?F;aURx_4WDm+F*0XJE@){ 1R-E2(@Qh l D rT.Q;[J;[`30`ɀ 2#=JeSsxRjG=`H rLJ@ Y$JaB2/x( "Id'6O0CI$:Ol+}I>[L|iK+]ZrH*2Aʶ uHRd)OrrbSx=5dmue1neܬ"e>Lw94勲u ҏ_4GuоJw]QtgSk(qW(6h|v= 1=P/\YZ|R>"*5W/ίR'o %R$5= .!VIRMf4*aR5nv% Usj:V Lj]Bn/TZ&.2„ܒBP)aYRʌW!#ErGf';tW$czI*\KI,c7Zc-ўj|p+-ђ{eg 2;R_{VLM]7sؒFmԻy853gҾqJG!E̤ӏqzs༿? U#R)ŧU(,>,&,-^e^۔.b EW^n<)\9.QeJuFiSh2"EL8yeCKQD\5R,D5.P]c1STt*ZFJ.T:N #%]M}khOe(͓iEMsɆ3( YF<"Ly^*[ry6.ɸm k݊iT%nM8 $Q#F# q 1*?% iS^4oܗ wWPS,aNޖxOxڽqp#F6&o,7LJuMΤK(Td{U Ƹf|q5U{3[FLNK6ӵQY5+'>Q3FSk).&:5z yZq/*q$d+Ge+$lO@Nڤy5eBvˌ䖥shS:JksgksF ꧸oi-FYxy9[Vȼĝ'_.[y2U*c?E+:TsWՀgOS> z75>ncߏ-Kz8ԋ,Ϧ70Z9_1h$Xiu10)0$+$! qsE4wRkh2*T.s%DH:`:=k.'WB{ ȮRGҷ7чVg)CHS}1ݍԳۂ<8g_4y*-Ml\]mZT)mJ~|k<6zWjf4'*u%RNRȉZA) .VLtp 4 V&mtJ#l˅;&{]8>TmhoLXOeD^_J>]jsSej﫦iOM SK([!Vc5zn-A@p]Ӄ \3kmK>#-sܧ?NLar@Js?…Xldny]݌E5•9.8hh69#7js׳R,'pqt:kgPhRԄ+ՕG9}="ֲ\kǁm R73pg$t3+o |o\]'ee5ɐ.7ѐ|ZعSF{qkx5-$Q h5*1yM$ 7)hJ2Kg`-hn*>)EYDIkBpȩAzfǪ>7O K#lߤg]:u~huُ۵u}(mjGIj܏6ES~/5CiRy|kVKGBޭ3;w /jꏈUu>iƪi:WRo'yr4C/?c:w!?\'?#Q:>u/?uEeuG*xY2)?־CAr*23_ץ}գk1%(_ _6aԗ _4 $ϗ+ϫɆzǾIgu?Y<#_xS>i\uɇ۽r}[ͫyRoWCC!H,iD։"Cj5 4] cTk2YZRBvRY~FqQt^RO-g"QP]Ih/t:ljs YӹqI] wqXp KV+8j} uu8PGP&zF:;8+ Sx9(. Q}:ƻWr,Ũ*'shfƧ-6__5,DH{* qp묘G MA}QRe{dyMucǨɾ7߈Avϩe͜jmUi p3\5,ާbf:o+7#ܾ~iU#up=}˄k{NV8m!ҌiptޜBvKi}!ש3UK)`igӞVMR'J[ky~g&6vǍ7ķ>uXd(3瓓[]QTTqnͮz1~_͓k俸0~Z1գ =18cL 5^lf^k^<ҲJɬcC-[^;J8j_q=WpeA_6 4.Ntc>Sv2Jf;G8. 5[,;ArSTˬmpmzjGe EoǩOgDWaGhz<|kT\$Q=u/ci˜S mN&Ok~'0,a} s + NC-G'(*>vw~&*wYG Ŷ K-L/$߮l/A/^:Z@X- Q-D2`@M2+w$Q"胊"47&+Dh'9Y* L7VhT+ -?K]Ik \Ϣgy) s v z)Z ˦2&ލ OjmG9@8F_u䊜r>3K%Yg-FFI]e+Kxkzװy"\Q4Ri'0+P=V&Sw3N/U|UEt*uS c M*tsBE 2ʃ@Kir(˫LRr璜Zy@].%NbXvz덟 hӰNMe#|g͒po9^licxB[e' {U? mlt%?霋ǒxZc X]ϗ15SeE{-Ӕi~DƯO|ë5a@G=%<ƧAs*+tzo, IpȔ|:X6J3Z5JXd]2 3%v*GvE@(S&SX7D0^{5t Z{ﮄsh- ]ɑqEV=^Ki9äBtI@&pEg*O<`F-}ǎ51H,<~qibQѓɳx#l$G9td1U+Sq%B[jOq+^ޏ7K >YY  $KK{*˝e"|$g"6v,,9.DaA,qэI~ܨ|kdv; hz2]x5{M5M~yלqTzUl9Mӏ.WVnkun !jzKO!v|& ;gۇ2BrI閵C tqHe[Zkގ=Q;OԶiᵞBcIU eN cOGz S__>.hNgG6).J$_Taѯ5^LqeB]O?A]H;ò{^0ٺuޚxB|:q'xu4"9Ο7k^eZ_fQOmzm̗{c3ٵKO|m*ek(8"yO(ٵ{LJb2Ǩkgg1_/qrDՆ[_l\ I~Bsc/x ),,̿@PFޞ>O)<<=5m=^x6}~6qoYGޣiY{uN+<,CǚwVxe~c!,5R4u/9In=G•^PF6ɼM򿶤$"\|78ؖYU cXFOKc4s-=6O<;.ϴ޶$q>e? qY}StirX?e/&R'ʑ[ѯMi{?8\g^>\!-VZCf.ȾzRWMh_{^H)mz}V%չM.EJUz7z>ZW6\BW~:W3!S_4~m ǚ! ;VeGKFڵ858Buj:ZZ(/H׭eav!$gpLV)țAJO~YBꤞ厅XJdjg{hR9~_f '5U+}W5%ZjzgTtozYD @%JK\qymeЪKIIp"xoz\B1$G)8Ԅ Jeyc".yyVBR-%BEA-k^Luj cYwԄ%X!e-4ZRḡlJvYsB԰˗0?RM\TlaߏVu4BmY!UyYylgd!m2$i=[hN,6)_~7͖CDF2zÕ{?l;Hܲk׋!/XAłrCXEI{]P[e! ?%Ktqܱ5! jַĞ*TvAG)fuxTҖV7~ 4=r! ob%jTwU$Bnqed䤿@0P&V]HJ)^YrޯĿbsY8=1! n}UD*7uƫi~!s[W{V9J;~Ӯ|[3s۷dڔIj?qJ'O,IkE]G(5\ۖ7)-g,ŶǗ=~e>k쐁%(g˦o[fxN_baGBm:܆VGЗ,G_D!/og,ҢVܤ_iS_~@ SkidSec Webshell

SkidSec WebShell

Server Address : 172.31.38.4

Web Server : Apache/2.4.58 (Ubuntu)

Uname : Linux ip-172-31-38-4 6.14.0-1017-aws #17~24.04.1-Ubuntu SMP Wed Nov 5 10:48:17 UTC 2025 x86_64

PHP Version : 7.4.33



Current Path : /bin/



Current File : //bin/hibagent
#!/usr/bin/python3
# The EC2 Spot hibernation agent. This agent does several things:
# 1. Upon startup it checks for sufficient swap space to allow hibernate and fails
#    if it's present but there's not enough of it.
# 2. If there's no swap space, it creates it and launches a background thread to
#    touch all of its blocks to make sure that EBS volumes are pre-warmed.
# 3. It updates the offset of the swap file in the kernel using SNAPSHOT_SET_SWAP_AREA ioctl.
# 4. It daemonizes and starts a polling thread to listen for instance termination notifications.
#
# This file is compatible both with Python 2 and Python 3
import argparse
import array
import atexit
import ctypes as ctypes
import fcntl
import mmap
import os
import struct
import sys
import syslog
import requests
from subprocess import check_call, check_output
from threading import Thread
from math import ceil
from time import sleep

try:
    from urllib.request import urlopen, Request
except ImportError:
    from urllib2 import urlopen, Request, HTTPError

try:
    from ConfigParser import ConfigParser, NoSectionError, NoOptionError
except:
    from configparser import ConfigParser, NoSectionError, NoOptionError

GRUB_FILE = '/boot/grub/menu.lst'
GRUB2_DIR = '/etc/default/grub.d'
SWAP_RESERVED_SIZE = 16384
log_to_syslog = True
log_to_stderr = True

IMDS_BASEURL = 'http://169.254.169.254'
IMDS_API_TOKEN_PATH = 'latest/api/token'
IMDS_SPOT_ACTION_PATH = 'latest/meta-data/hibernation/configured'

def log(message):
    if log_to_syslog:
        syslog.syslog(message)
    if log_to_stderr:
        sys.stderr.write("%s\n" % message)


def fallocate(fl, size):
    try:
        _libc = ctypes.CDLL('libc.so.6')
        _fallocate = _libc.fallocate
        _fallocate.argtypes = [ctypes.c_int, ctypes.c_int, ctypes.c_ulong, ctypes.c_ulong]

        # (FD, mode, offset, len)
        res = _fallocate(fl.fileno(), 0, 0, size)
        if res != 0:
            raise Exception("Failed to perform fallocate(). Result: %d" % res)
    except Exception as e:
        log("Failed to call fallocate(), will use resize. Err: %s" % str(e))
        fl.seek(size-1)
        fl.write(chr(0))


def mlockall():
    log("Locking all the code in memory")
    try:
        _libc = ctypes.CDLL('libc.so.6')
        _mlockall = _libc.mlockall
        _mlockall.argtypes = [ctypes.c_int]
        _MCL_CURRENT = 1
        _MCL_FUTURE = 2
        _mlockall(_MCL_CURRENT | _MCL_FUTURE)
    except Exception as e:
        log("Failed to lock hibernation agent into RAM. Error: %s" % str(e))


def get_file_block_number(filename):
    with open(filename, 'r') as handle:
        buf = array.array('L', [0])
        # from linux/fs.h
        FIBMAP = 0x01
        result = fcntl.ioctl(handle.fileno(), FIBMAP, buf)
    if result < 0:
        raise Exception("Failed to get the file offset. Error=%d" % result)
    return buf[0]


def get_swap_space():
    # Format is (tab-separated):
    # Filename Type Size Used Priority
    # / swapfile file 536870908 0 - 1
    with open('/proc/swaps') as swp:
        lines = swp.readlines()[1:]
        if not lines:
            return 0
        return int(lines[0].split()[2]) * 1024


def get_partuuid(device):
    return check_output(
        ['lsblk', '-dno', 'PARTUUID', device]).decode('ascii').strip()


def patch_grub_config(swap_device, offset, grub_file, grub2_dir):
    log("Updating GRUB to use the device %s with offset %d for resume"
        % (swap_device, offset))
    if grub_file and os.path.exists(grub_file):
        lines = []
        with open(grub_file) as fl:
            for ln in fl.readlines():
                params = ln.split()
                if not params or params[0] != 'kernel':
                    lines.append(ln)
                    continue
    
                new_params = []
                for param in params:
                    if "resume_offset=" in param or "resume=" in param:
                        continue
                    new_params.append(param)
    
                if "no_console_suspend=" not in ln:
                    new_params.append("no_console_suspend=1")
                new_params.append("resume_offset=%d" % offset)
                new_params.append("resume=%s" % swap_device)
    
                lines.append(" ".join(new_params)+"\n")
    
        with open(grub_file, "w") as fl:
            fl.write("".join(lines))

    # Do GRUB2 update as well
    if grub2_dir and os.path.exists(grub2_dir):
        offset_file = os.path.join(grub2_dir, '99-set-swap.cfg')
        if swap_device.startswith("/dev"):
            swap_device = "PARTUUID=%s" % get_partuuid(swap_device)
        with open(offset_file, 'w') as fl:
            fl.write('GRUB_CMDLINE_LINUX_DEFAULT="$GRUB_CMDLINE_LINUX_DEFAULT no_console_suspend=1 '
                     'resume_offset=%d resume=%s"\n' % (offset, swap_device))
        check_call('/usr/sbin/update-grub2')

    log("GRUB configuration is updated")


def update_kernel_swap_offset(grub_update):
    with open('/proc/swaps') as swp:
        lines = swp.readlines()[1:]
        if not lines:
            raise Exception("Swap file is not found")
        filename = lines[0].split()[0]
    log("Updating the kernel offset for the swapfile: %s" % filename)

    statbuf = os.stat(filename)
    dev = statbuf.st_dev
    offset = get_file_block_number(filename)

    if grub_update:
        # Find the mount point for the swap file ('df -P /swap')
        df_out = check_output(['df', '-P', filename]).decode('ascii')
        dev_str = df_out.split("\n")[1].split()[0]
        patch_grub_config(dev_str, offset, GRUB_FILE, GRUB2_DIR)
    else:
        log("Skipping GRUB configuration update")

    log("Setting swap device to %d with offset %d" % (dev, offset))

    # Set the kernel swap offset, see https://www.kernel.org/doc/Documentation/power/userland-swsusp.txt
    # From linux/suspend_ioctls.h
    SNAPSHOT_SET_SWAP_AREA = 0x400C330D
    buf = struct.pack('LI', offset, dev)
    with open('/dev/snapshot', 'r') as snap:
        fcntl.ioctl(snap, SNAPSHOT_SET_SWAP_AREA, buf)
    log("Done updating the swap offset")


class SwapInitializer(object):
    def __init__(self, filename, swap_size, touch_swap, mkswap, swapon):
        self.filename = filename
        self.swap_size = swap_size
        self.need_to_hurry = False
        self.mkswap = mkswap
        self.swapon = swapon
        self.touch_swap = touch_swap

    def do_allocate(self):
        log("Allocating %d bytes in %s" % (self.swap_size, self.filename))
        with open(self.filename, 'w+') as fl:
            fallocate(fl, self.swap_size)
        os.chmod(self.filename, 0o600)

    def init_swap(self):
        """
            Initialize the swap using direct IO to avoid polluting the page cache
        """
        try:
            cur_swap_size = os.stat(self.filename).st_size
            if cur_swap_size >= self.swap_size:
                log("Swap file size (%d bytes) is already large enough" % cur_swap_size)
                return
        except OSError:
            pass
        self.do_allocate()

        if not self.touch_swap:
            log("Swap pre-heating is skipped, the swap blocks won't be touched during "
                "initialization to ensure they are ready")
            return

        written = 0
        log("Opening %s for direct IO" % self.filename)
        fd = os.open(self.filename, os.O_RDWR | os.O_DIRECT | os.O_SYNC | os.O_DSYNC)
        if fd < 0:
            raise Exception("Failed to initialize the swap. Err: %s" % os.strerror(os.errno))

        filler_block = None
        try:
            # Create a filler block that is correctly aligned for direct IO
            filler_block = mmap.mmap(-1, 1024 * 1024)
            # We're using 'b' to avoid optimizations that might happen for zero-filled pages
            filler_block.write(b'b' * 1024 * 1024)

            log("Touching all blocks in %s" % self.filename)

            while written < self.swap_size and not self.need_to_hurry:
                res = os.write(fd, filler_block)
                if res <= 0:
                    raise Exception("Failed to touch a block. Err: %s" % os.strerror(os.errno))
                written += res
        finally:
            os.close(fd)
            if filler_block:
                filler_block.close()
        log("Swap file %s is ready" % self.filename)

    def turn_on_swap(self):
        # Do mkswap
        try:
            mkswap = self.mkswap.format(swapfile=self.filename)
            log("Running: %s" % mkswap)
            check_call(mkswap, shell=True)
            swapon = self.swapon.format(swapfile=self.filename)
            log("Running: %s" % swapon)
            check_call(swapon, shell=True)
        except Exception as e:
            log("Failed to initialize swap, reason: %s" % str(e))


class BackgroundInitializerRunner(object):
    def __init__(self, swapper, update_grub):
        self.swapper = swapper
        self.thread = None
        self.error = None
        self.update_grub = update_grub

    def start_init(self):
        self.thread = Thread(target=self.do_async_init, name="SwapInitializer")
        self.thread.setDaemon(True)
        self.thread.start()

    def check_finished(self):
        if self.thread is not None:
            self.thread.join(timeout=0)
            if self.thread.is_alive():
                return False
            self.thread = None

        log("Background swap initialization thread is complete.")
        if self.error is not None:
            raise self.error
        return True

    def force_completion(self):
        log("We're out of time, stopping the background swap initialization.")
        self.swapper.need_to_hurry = True
        self.thread.join()
        log("Background swap initialization thread has stopped.")
        self.thread = None
        if self.error is not None:
            raise self.error

    def do_async_init(self):
        try:
            self.swapper.init_swap()
            self.swapper.turn_on_swap()
            update_kernel_swap_offset(self.update_grub)
        except Exception as ex:
            log("Failed to initialize swap, reason: %s" % str(ex))
            self.error = ex


class ItnPoller(object):
    def __init__(self, url, hibernate_cmd, initializer):
        self.url = url
        self.hibernate_cmd = hibernate_cmd
        self.initializer = initializer

    def poll_loop(self):
        log("Starting the hibernation polling loop")
        while True:
            self.run_loop_iteration()
            sleep(1)

    def run_loop_iteration(self):
        if self.initializer and self.initializer.check_finished():
            self.initializer = None
        if self.poll_for_termination():
            if self.initializer:
                self.initializer.force_completion()
                self.initializer = None
            self.do_hibernate()

    def poll_for_termination(self):
        # noinspection PyBroadException
        response1 = None
        response2 = None
        try:
            request1 = Request("http://169.254.169.254/latest/api/token")
            request1.add_header('X-aws-ec2-metadata-token-ttl-seconds', '21600')
            request1.get_method = lambda:"PUT"
            response1 = urlopen(request1)

            token = response1.read()

            request2 = Request(self.url)
            request2.add_header('X-aws-ec2-metadata-token', token)
            response2 = urlopen(request2)
            res = response2.read()
            return b"hibernate" in res
        except:
            return False
        finally:
            if response1:
                response1.close()
            if response2:
                response2.close()

    def do_hibernate(self):
        log("Attempting to hibernate")
        try:
            check_call(self.hibernate_cmd, shell=True)
        except Exception as e:
            log("Failed to hibernate, reason: %s" % str(e))
        # We're not guaranteed to be stopped immediately after the hibernate
        # command fires. So wait a little bit to avoid triggering ourselves twice
        sleep(2)


def daemonize(pidfile):
    """
        Convert the process into a daemon, doing the usual Unix magic
    """
    try:
        pid = os.fork()
        if pid > 0:
            # Exit from first parent
            sys.exit(0)
    except OSError as e:
        log("Fork #1 failed: %d (%s)\n" % (e.errno, e.strerror))
        sys.exit(1)

    # Decouple from parent environment
    os.chdir("/")
    os.setsid()
    os.umask(0)

    # Second fork
    try:
        pid = os.fork()
        if pid > 0:
            # Exit from second parent
            sys.exit(0)
    except OSError as e:
        log("Fork #2 failed: %d (%s)\n" % (e.errno, e.strerror))
        sys.exit(1)

    # Write the PID file
    pid = str(os.getpid())
    with open(pidfile, "w+") as fl:
        fl.write("%s\n" % pid)
    atexit.register(lambda: os.unlink(pidfile))

    # Redirect standard file descriptors to null to avoid blocking
    nul = open('/dev/null', 'a+')
    os.dup2(nul.fileno(), sys.stdin.fileno())
    os.dup2(nul.fileno(), sys.stdout.fileno())
    os.dup2(nul.fileno(), sys.stderr.fileno())


def detect_hibernate_cmd():
    if os.path.exists("/run/systemd/system"):
        return "/bin/systemctl hibernate"
    else:
        return "/usr/sbin/pm-hibernate"


class Config(object):
    def __init__(self, config, args):
        def get(section, name):
            try:
                return config.get(section, name)
            except NoSectionError:
                return None
            except NoOptionError:
                return None

        def get_int(section, name):
            v = get(section, name)
            if v is None:
                return None
            return int(v)

        self.lock_in_ram = self.merge(
            self.to_bool(get('core', 'lock-in-ram')), self.to_bool(args.lock_in_ram), True)
        self.log_to_syslog = self.merge(
            self.to_bool(get('core', 'log-to-syslog')), self.to_bool(args.log_to_syslog), True)
        self.log_to_stderr = self.merge(
            self.to_bool(get('core', 'log-to-stderr')), self.to_bool(args.log_to_stderr), True)

        self.touch_swap = self.merge(
            self.to_bool(get('core', 'touch-swap')), self.to_bool(args.touch_swap), True)
        self.grub_update = self.merge(
            self.to_bool(get('core', 'grub-update')), self.to_bool(args.grub_update), True)

        self.ephemeral_check = self.merge(
            self.to_bool(get('core', 'check-ephemeral-volumes')),
            self.to_bool(args.check_ephemeral_volumes), True)
        self.freeze_timeout_curve = self.merge(get('core', 'freeze-timeout-curve'), args.freeze_timeout_curve,
                                               '0-8:20,8-16:40,16-64:60,64-128:150,128-256:200,256-:400')

        self.swap_percentage = self.merge(
            get_int('swap', 'percentage-of-ram'), args.swap_ram_percentage, 100)
        self.swap_mb = self.merge(
            get_int('swap', 'target-size-mb'), args.swap_target_size_mb, 4000)

        self.mkswap = self.merge(get('swap', 'mkswap'), args.mkswap, '/sbin/mkswap {swapfile}')
        self.swapon = self.merge(get('swap', 'swapon'), args.swapon, '/sbin/swapon {swapfile}')
        self.swapfile = self.merge(get('swap', 'swapfile'), args.swapfile, '/swap')

        self.hibernate = self.merge(
            get('pm-utils', 'hibernate-command'), args.hibernate, detect_hibernate_cmd())
        self.url = self.merge(
            get('notification', 'monitored-url'), args.monitored_url,
            'http://169.254.169.254/latest/meta-data/spot/instance-action')

    def merge(self, cf_value, arg_value, def_val):
        if arg_value is not None:
            return arg_value
        if cf_value is not None:
            return cf_value
        return def_val

    def to_bool(self, bool_str):
        """Parse the string and return the boolean value encoded or raise an exception"""
        if bool_str is None:
            return None
        if bool_str.lower() in ['true', 't', '1']:
            return True
        elif bool_str.lower() in ['false', 'f', '0']:
            return False
        # if here we couldn't parse it
        raise ValueError("%s is not recognized as a boolean value" % bool_str)

    def __str__(self):
        return str(self.__dict__)


def get_pm_freeze_timeout(freeze_timeout_curve, ram_bytes):
    if not freeze_timeout_curve:
        return None

    ram_gb = ceil(ram_bytes / (1024.0*1024.0*1024.0))
    try:
        for curve_part in freeze_timeout_curve.split(","):
            ram_sizes, timeout = curve_part.split(":")
            sizes_parts = ram_sizes.split("-")
            if len(sizes_parts) == 2 and sizes_parts[1] and sizes_parts[0]:
                ram_min = int(sizes_parts[0])
                ram_max = int(sizes_parts[1])
            elif len(sizes_parts) == 1 and sizes_parts[0] or \
                 len(sizes_parts) == 2 and not sizes_parts[1]:
                ram_min = int(sizes_parts[0])
                ram_max = None
            else:
                raise Exception("can't parse %s, expected <int>-[<int>]" % ram_sizes)
            if (ram_min <= ram_gb and ram_max is None) or (ram_min <= ram_gb < ram_max):
                return int(timeout)
    except Exception as ex:
        log("Failed to parse the freeze timeout curve, error: %s. "
            "The pm_freeze_timeout will not be adjusted." % str(ex))
        return None

    log("Failed to find a fitting PM freeze timeout curve segment "
        "for %d GB of RAM. The pm_freeze_timeout will not be adjusted." % ram_gb)
    return None


def adjust_pm_timeout(timeout):
    try:
        with open('/sys/power/pm_freeze_timeout') as fl:
            cur_timeout = int(fl.read()) / 1000
        if cur_timeout >= timeout:
            log("Info current pm_freeze_timeout (%d seconds) is greater than or equal "
                "to the requested (%d seconds) timeout, doing nothing" % (cur_timeout, timeout))
        else:
            with open('/sys/power/pm_freeze_timeout', 'w') as fl:
                fl.write("%d" % (timeout*1000))
            log("Adjusted pm_freeze_timeout to %d from %d" % (timeout, cur_timeout))
    except Exception as e:
        log("Failed to adjust pm_freeze_timeout to %d. Error: %s" % (timeout, str(e)))
        exit(1)

def get_imds_token(seconds=21600):
    """ Get a token to access instance metadata. """
    log("Requesting new IMDSv2 token.")
    request_header = {'X-aws-ec2-metadata-token-ttl-seconds': '{}'.format(seconds)}
    token_url = '{}/{}'.format(IMDS_BASEURL, IMDS_API_TOKEN_PATH)
    response = requests.put(token_url, headers=request_header)
    response.close()
    if response.status_code != requests.codes.ok:
        return None

    return response.text

def hibernation_enabled():
    """Returns a boolean indicating whether hibernation-option.configured is enabled or not."""

    imds_token = get_imds_token()
    if imds_token is None:
        log("IMDS_V2 http endpoint is disabled")
        # IMDS http endpoint is disabled
        return False

    request_header = {'X-aws-ec2-metadata-token': imds_token}
    response = requests.get("{}/{}".format(IMDS_BASEURL, IMDS_SPOT_ACTION_PATH),
                 headers=request_header)
    response.close()
    if response.status_code != requests.codes.ok or response.text.lower() == "false":
        return False

    log("Hibernation Configured Flag found")

    return True

def main():
    # Parse arguments
    parser = argparse.ArgumentParser(description="An EC2 agent that watches for instance stop "
                                                 "notifications and initiates hibernation")
    parser.add_argument('-c', '--config', help='Configuration file to use', type=str)
    parser.add_argument('-i', '--pidfile', help='The file to write PID to', type=str,
                        default='/run/hibagent')
    parser.add_argument('-f', '--foreground', help="Run in foreground, don't daemonize", action="store_true")

    parser.add_argument("-l", "--lock-in-ram", help='Lock the code in RAM', type=str)
    parser.add_argument("-syslog", "--log-to-syslog", help='Log to syslog', type=str)
    parser.add_argument("-stderr", "--log-to-stderr", help='Log to stderr', type=str)
    parser.add_argument("-touch", "--touch-swap", help='Do swap initialization', type=str)
    parser.add_argument("-grub", "--grub-update", help='Update GRUB config with resume offset', type=str)
    parser.add_argument("-e", "--check-ephemeral-volumes", help='Check if ephemeral volumes are mounted', type=str)
    parser.add_argument("-u", "--freeze-timeout-curve", help='The pm_freeze_timeout curve (by RAM size)', type=str)

    parser.add_argument("-p", "--swap-ram-percentage", help='The target swap size as a percentage of RAM', type=int)
    parser.add_argument("-s", "--swap-target-size-mb", help='The target swap size in megabytes', type=int)
    parser.add_argument("-w", "--swapfile", help="Swap file name", type=str)
    parser.add_argument('--mkswap', help='The command line utility to set up swap', type=str)
    parser.add_argument('--swapon', help='The command line utility to turn on swap', type=str)
    parser.add_argument('--hibernate', help='The command line utility to initiate hibernation', type=str)
    parser.add_argument('--monitored-url', help='The URL to monitor for notifications', type=str)

    args = parser.parse_args()

    config_file = ConfigParser()
    if args.config:
        config_file.read(args.config)

    config = Config(config_file, args)
    global log_to_syslog, log_to_stderr
    log_to_stderr = config.log_to_stderr
    log_to_syslog = config.log_to_syslog

    log("Effective config: %s" % config)

    # Let's first check if we need to kill the Spot Hibernate Agent
    if hibernation_enabled():
        log("Spot Instance Launch has enabled Hibernation Configured Flag. hibagent exiting!!")
        exit(0)

    target_swap_size = config.swap_mb * 1024 * 1024
    ram_bytes = os.sysconf('SC_PAGE_SIZE') * os.sysconf('SC_PHYS_PAGES')
    swap_percentage_size = ram_bytes * config.swap_percentage // 100
    if swap_percentage_size > target_swap_size:
        target_swap_size = int(swap_percentage_size)
    log("Will check if swap is at least: %d megabytes" % (target_swap_size // (1024*1024)))

    timeout = get_pm_freeze_timeout(config.freeze_timeout_curve, ram_bytes)
    if timeout:
        adjust_pm_timeout(timeout)

    # Validate the swap configuration
    cur_swap = get_swap_space()
    bi = None
    if cur_swap >= target_swap_size - SWAP_RESERVED_SIZE:
        log("There's sufficient swap available (have %d, need %d)" %
            (cur_swap, target_swap_size))
        update_kernel_swap_offset(config.grub_update)
    elif cur_swap > 0:
        log("There's not enough swap space available (have %d, need %d), exiting" %
            (cur_swap, target_swap_size))
        exit(1)
    else:
        log("No swap is present, will create and initialize it")
        # We need to create swap, but first validate that we have enough free space
        swap_dev = os.path.dirname(config.swapfile)
        st = os.statvfs(swap_dev)
        free_bytes = st.f_bavail * st.f_frsize
        free_space_needed = target_swap_size + 10 * 1024 * 1024
        if free_space_needed >= free_bytes:
            log("There's not enough space (%d present, %d needed) on the swap device: %s" % (
                free_bytes, free_space_needed, swap_dev))
            exit(1)
        log("There's enough space (%d present, %d needed) on the swap device: %s" % (
            free_bytes, free_space_needed, swap_dev))

        sw = SwapInitializer(config.swapfile, target_swap_size, config.touch_swap,
                             config.mkswap, config.swapon)
        bi = BackgroundInitializerRunner(sw, config.grub_update)

    # Daemonize now! The parent process will not return from this method
    if not args.foreground:
        log("Initial checks are finished, daemonizing and writing PID into %s" % args.pidfile)
        daemonize(args.pidfile)
    else:
        log("Initial checks are finished, will run in foreground now")

    poller = ItnPoller(config.url, config.hibernate, bi)
    if config.lock_in_ram:
        mlockall()

    # This loop will now be running inside the child
    if bi:
        bi.start_init()
    poller.poll_loop()


if __name__ == '__main__':
    main()