#!/usr/bin/python3
# vim: tabstop=4 shiftwidth=4 softtabstop=4

# Copyright 2015 Lenovo
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import base64
import optparse
import os
import subprocess
import sys
path = os.path.dirname(os.path.realpath(__file__))
path = os.path.realpath(os.path.join(path, '..', 'lib', 'python'))
if path.startswith('/opt'):
    sys.path.append(path)

import confluent.client as client
import confluent.sortutil as sortutil
import confluent.logreader as logreader
import time
import select
import signal
import socket
import re
import tty
import termios
import fcntl
import confluent.screensqueeze as sq
try:
    from PIL import Image, ImageDraw, ImageFont
except ImportError:
    Image = None

try:
    # sixel is optional, attempt to import but stub out if unavailable
    import io
    import sixel

    class DumbWriter(sixel.SixelWriter):
        def restore_position(self, output):
            return
except ImportError:
    class DumbWriter():
        def draw(self, imgfile):
            sys.stderr.write("PySixel not detected, Sixel format display not supported\n")

confettypath = os.path.join(os.path.dirname(sys.argv[0]), 'confetty')
argparser = optparse.OptionParser(
    usage="Usage: %prog [options] <noderange> [kill][-- [passthroughoptions]]",
    epilog="Command sequences are available while connected to a console, hit "
           "ctrl-'e', then release ctrl, then 'c', then '?' for a full list. "
           "For example, ctrl-'e', then 'c', then '.' will exit the current "
           "console")
argparser.add_option('-t', '--tile', action='store_true', default=False,
                     help='Tile console windows in the terminal')
argparser.add_option('-l', '--log', action='store_true', default=False,
                     help='Enter log replay mode instead of showing a live console')

argparser.add_option('-T', '--Timestamp', action='store_true', default=False,
                     help= 'Dump log in stdout with timestamps')

argparser.add_option('-s', '--screenshot', action='store_true', default=False,
                     help='Attempt to grab screenshot and render using kitty image protocol')
argparser.add_option('-i', '--interval', type='float',
                     help='Interval in seconds to redraw the screenshot. Currently only '
                          'works for one node')
argparser.add_option('-w','--windowed', action='store_true', default=False,
                     help='Open terminal windows for each node.  The '
                          'environment variable NODECONSOLE_WINDOWED_COMMAND '
                          'should be set, which should be a text string corresponding '
                          'to a command that can be used to open a windowed console,'
                          ' omitting the "nodeconsole <noderange>" part of the '
                          'command, for example, to open a set of consoles for a '
                          'range of nodes in separate xterm windows, set '
                          'NODECONSOLE_WINDOWED_COMMAND to "xterm -e".  To open a '
                          'set of consoles for a range of nodes in separate '
                          'GNOME Terminal windows with a size of 100 columns and '
                          '31 rows, set NODECONSOLE_WINDOWED_COMMAND '
                          'to "gnome-terminal --geometry 100x31 --" or in a WSL '
                          'environment, to open a set of consoles for a range of '
                          'nodes in separate Windows Terminal windows, with the '
                          'title set for each node, set NODECONSOLE_WINDOWED_COMMAND'
                          ' to "wt.exe wsl.exe -d AlmaLinux-8 '
                          '--shell-type login".  If the NODECONSOLE_WINDOWED_COMMAND '
                          'environment variable isn\'t set, xterm will be used by'
                          'default.')

(options, args) = argparser.parse_args()

oldtcattr = None
oldfl = None

def get_coords():
    sys.stdout.write('\x1b[6n') #
    sys.stdout.flush()
    gotreply = select.select([sys.stdin,], [], [], 0.250)[0]
    if gotreply:
        response = ''
        while select.select([sys.stdin,], [], [], 0.1)[0] and 'R' not in response:
            response += sys.stdin.read()
        coords = response.replace('R', '').split('[')[1].split(';')
    #sys.stdout.write('\x1b[{}:{}H'.format(*coords))

def direct_console():
    global oldtcattr
    global oldfl
    oldtcattr = termios.tcgetattr(sys.stdin.fileno())
    oldfl = fcntl.fcntl(sys.stdin.fileno(), fcntl.F_GETFL)
    tty.setraw(sys.stdin.fileno())
    fcntl.fcntl(sys.stdin.fileno(), fcntl.F_SETFL, oldfl | os.O_NONBLOCK)

def indirect_console():
    fcntl.fcntl(sys.stdin.fileno(), fcntl.F_SETFL, oldfl & ~os.O_NONBLOCK)
    termios.tcsetattr(sys.stdin.fileno(), termios.TCSANOW, oldtcattr)

def determine_tile_size(numnodes):
    # for now, smash everything to a common aspect ratio.  16:11
    # is pretty much wrong for everything, making 4:3 a bit too wide
    # and 16:9 significantly too narrow, but it is serviceable
    # An improvement could come with us owning the scaling
    # instead of delegating to Kitty, which says if we specify both,
    # we get stretching.  In theory we should be able to get aspect correct
    # from kitty by omitting, but:
    # then we don't know how much to move the cursor left after draw_image
    # Konsole won't scale at all with only partial scaling specified
    cheight, cwidth, pixwidth, pixheight = sq.get_screengeom()
    # 16:12 is to roughly account for the 'titles' of the tiles
    ratio = (pixwidth / 16) / (pixheight / 12)
    bestdeviation = None
    bestdims = []
    for i in range(1, numnodes + 1):
        number = numnodes
        while number % i != 0:
            number += 1
        columns = i
        rows = number // i
        deviation = abs(ratio -  (columns / rows))
        if bestdeviation is None:
            bestdeviation = deviation
            bestdims = [columns, rows]
        elif deviation < bestdeviation:
            bestdeviation = deviation
            bestdims = [columns, rows]
    # ok, the above algorithm can still pick things like
    # 1 2 3
    # 4
    # So we will let it pick the number of rows, and
    # then see if we can chop columns and still fit
    while (bestdims[0] - 1) * bestdims[1] >= numnodes:
        bestdims[0] = bestdims[0] - 1
    cellswide = cwidth // bestdims[0]
    cellshigh = cheight // bestdims[1]
    tilewidth = cellswide * pixwidth / cwidth
    tileheight = cellshigh * pixheight / cheight
    if tilewidth > (tileheight * 16 / 11):
        tilewidth = tileheight * 16 / 11
        cellswide = int(tilewidth // (pixwidth / cwidth))
    if tileheight > (tilewidth * 11 /16):
        tileheight = tilewidth * 11 / 16
        cellshigh = int(tileheight // (pixheight / cheight))
    bestdims = bestdims + [cellswide, cellshigh, cellshigh * bestdims[1]]
    # incur any scrolling we might get. This allows us to accurately
    # save/restore cursor or even get coordinates without scrolling fouling
    # the desired target
    sys.stdout.write('\n' * bestdims[4])
    sys.stdout.flush()
    cursor_up(bestdims[4])
    return bestdims

cursor_saved = False
def sticky_cursor():
    global cursor_saved
# get cursor restore_position
    if sys.stdin.isatty() and not cursor_saved:
        try:
            direct_console()
            sys.stdout.write('\x1b7')
            cursor_saved = True
        finally:
            indirect_console()
    elif cursor_saved:
        try:
            direct_console()
            sys.stdout.write('\x1b8')
        finally:
            indirect_console()

def cursor_up(count=1):
    sys.stdout.write(f'\x1b[{count}A')
def cursor_down(count=1):
    sys.stdout.write(f'\x1b[{count}B')
def cursor_right(count=1):
    sys.stdout.write(f'\x1b[{count}C')
def cursor_left(count=1):
    sys.stdout.write(f'\x1b[{count}D')
def cursor_save():
    sys.stdout.write('\x1b7')
def cursor_restore():
    sys.stdout.write('\x1b8')
def cursor_hide():
    sys.stdout.write('\x1b[?25l')
def cursor_show():
    sys.stdout.write('\x1b[?25h')

def get_pix_dimensions(width, height):
    cheight, cwidth, pixwidth, pixheight = sq.get_screengeom()
    imgwidth = int(pixwidth / cwidth * width)
    imgheight = int(pixheight / cheight * height)
    return imgwidth, imgheight

def draw_text(text, width, height):
    if Image:
        maxfntsize = 256
        imgwidth, imgheight = get_pix_dimensions(width, height)
        nerr = Image.new(mode='RGB', size=(imgwidth, imgheight), color='green')
        nd = ImageDraw.Draw(nerr)
        for txtpiece in text.split('\n'):
            fntsize = 8
            txtfont = ImageFont.truetype('DejaVuSans.ttf', size=fntsize)
            while nd.textlength(txtpiece, font=txtfont) < int(imgwidth * 0.90):
                fntsize += 1
                txtfont = ImageFont.truetype('DejaVuSans.ttf', size=fntsize)
            fntsize -= 1
            if fntsize < maxfntsize:
                maxfntsize = fntsize
        hmargin = int(imgwidth * 0.05)
        vmargin = int(imgheight * 0.10)
        nd.text((hmargin, vmargin), text, font=txtfont)
        nd.rectangle((0, 0, nerr.width - 1, nerr.height -1), outline='white')
        outfile = io.BytesIO()
        nerr.save(outfile, format='PNG')
        data = base64.b64encode(outfile.getbuffer())
        draw_image(data, width, height, doscale=False)
    else:
        sys.stdout.write(text)
        cursor_left(len(text))

def draw_image(data, width, height, doscale=True):
    imageformat = os.environ.get('CONFLUENT_IMAGE_PROTOCOL', 'kitty')
    if doscale and Image and width:
        bindata = base64.b64decode(data)
        binfile = io.BytesIO()
        binfile.write(bindata)
        binfile.seek(0)
        try:
            img = Image.open(binfile)
        except Exception as e:
            errstr = 'Error rendering image:\n' + str(e)
            return draw_text(errstr, width, height)
        imgwidth, imgheight = get_pix_dimensions(width, height)
        nimg = Image.new(mode='RGBA', size=(imgwidth, imgheight))
        imgwidth -= 4
        imgheight -= 4
        hscalefact = imgwidth / img.width
        vscalefact = imgheight / img.height
        if hscalefact < vscalefact:
            rzwidth = imgwidth
            rzheight = int(img.height * hscalefact)
        else:
            rzwidth = int(img.width * vscalefact)
            rzheight = imgheight
        img = img.resize((rzwidth, rzheight))
        nd = ImageDraw.Draw(nimg)
        nd.rectangle((1, 1, rzwidth + 2, rzheight + 2), outline='black')
        nd.rectangle((0, 0, rzwidth + 3, rzheight + 3), outline='white')
        nimg.paste(img, box=(2, 2))
        outfile = io.BytesIO()
        nimg.save(outfile, format='PNG')
        data = base64.b64encode(outfile.getbuffer())
    if imageformat == 'sixel':
        sixel_draw(data)
    elif imageformat == 'iterm':
        iterm_draw(data, width, height)
    else:
        kitty_draw(data, width, height)


def sixel_draw(data):
    bindata = base64.b64decode(data)
    binfile = io.BytesIO()
    binfile.write(bindata)
    binfile.seek(0)
    DumbWriter().draw(binfile)

def iterm_draw(data, width, height):
    if not height:
        height = 'auto'
    if not width:
        width = 'auto'
    bindata = base64.b64decode(data)
    datalen = len(bindata)
    sys.stdout.write(
        '\x1b]1337;File=inline=1;width={};height={};size={}:'.format(width,height,datalen))
    sys.stdout.write(data.decode('utf8'))
    sys.stdout.write('\a')
    sys.stdout.flush()


def kitty_draw(data, width, height):
    preamble = '\x1b_Ga=T,f=100'
    if height:
        preamble += f',r={height},c={width}'
    #sys.stdout.write(repr(preamble))
    #sys.stdout.write('\xb[{}D'.format(len(repr(preamble))))
    #return
    first = True
    while data:
        chunk, data = data[:4096], data[4096:]
        m = 1 if data else 0
        if first:
            sys.stdout.write('{},m={};'.format(preamble, m))
        else:
            sys.stdout.write('\x1b_Gm={};'.format(m))
        sys.stdout.write(chunk.decode('utf8'))
        sys.stdout.write('\x1b\\')
        sys.stdout.flush()

pass_through_args = []
killcon = False
try:
    noderange = args[0]
    if len(args) > 1:
        if args[1] == 'kill':
            killcon = True
        pass_through_args = args[1:]
        args = args[:1]
except IndexError:
    argparser.print_help()
    sys.exit(1)

if len(args) != 1:
    argparser.print_help()
    sys.exit(1)

if options.log:
    logname = args[0]
    if not os.path.exists(logname) and logname[0] != '/':
        logname = os.path.join('/var/log/confluent/consoles', logname)
    if not os.path.exists(logname):
        sys.stderr.write('Unable to locate {0} on local system\n'.format(logname))
        sys.exit(1)
    logreader.replay_to_console(logname)
    sys.exit(0)

if options.Timestamp:
    logname = args[0]
    if not os.path.exists(logname) and logname[0] != '/':
        logname = os.path.join('/var/log/confluent/consoles', logname)
    if not os.path.exists(logname):
        sys.stderr.write('Unable to locate {0} on local system\n'.format(logname))
        sys.exit(1)
    logreader.dump_to_console(logname)
    sys.exit(0)

def prep_node_tile(node):
    currcolcell, currrowcell = nodepositions[node]
    if currcolcell:
        cursor_right(currcolcell)
    if currrowcell:
        cursor_down(currrowcell)
    sys.stdout.write('▏' + node)
    cursor_left(len(node) + 1)
    cursor_down()

def reset_cursor(node):
    currcolcell, currrowcell = nodepositions[node]
    if currcolcell:
        cursor_left(currcolcell)
    cursor_up(currrowcell + 1)

nodepositions = {}
numrows = 0
cwidth = 0
cheight = 0
imagedatabynode = {}

def redraw():
    for node in imagedatabynode:
        imgdata = imagedatabynode[node]
        if node in nodepositions:
            prep_node_tile(node)
            cursor_save()
        else:
            if options.interval is not None:
                if node != firstnodename:
                    sys.stderr.write('Multiple nodes not supported for interval')
                    sys.exit(1)
                sticky_cursor()
            sys.stdout.write('{}: '.format(node))
        # one row is used by our own name, so cheight - 1 for that allowance
        draw_image(imgdata.encode(), cwidth, cheight - 1 if cheight else cheight)
        if node in nodepositions:
            cursor_restore()
            reset_cursor(node)
        else:
            sys.stdout.write('\n')
        sys.stdout.flush()
resized = False
def do_screenshot():
    global resized
    global numrows
    sess = client.Command()
    if options.tile:
        imageformat = os.environ.get('CONFLUENT_IMAGE_PROTOCOL', 'kitty')
        if imageformat not in ('kitty', 'iterm'):
            sys.stderr.write('Tiled screenshots only supported with kitty or iterm protocol')
            sys.exit(1)
        allnodes = []
        numnodes = 0
        for res in sess.read('/noderange/{}/nodes/'.format(args[0])):
            allnodes.append(res['item']['href'].replace('/', ''))
            numnodes += 1
        resized = False
        def do_resize(a=None, b=None):
            global resized
            if a:
                resized = True
                # on a window resize, clear the old stuff
                # ideally we'd retain the images and redraw them
                sys.stdout.write('\x1bc')
            global numrows
            global cwidth
            global cheight
            cols, rows, cwidth, cheight, numrows = determine_tile_size(numnodes)
            currcol = 1
            currcolcell = 0
            currrowcell = 0
            for node in allnodes:
                nodepositions[node] = currcolcell, currrowcell
                if currcol < cols:
                    currcol += 1
                    currcolcell += cwidth
                else:
                    currcol = 1
                    currcolcell = 0
                    currrowcell += cheight
            if a:
                redraw()
        do_resize()
        signal.signal(signal.SIGWINCH, do_resize)
    elif options.interval is not None:
        sys.stdout.write('\x1bc')
    firstnodename = None
    dorefresh = True
    vnconly = set([])
    while dorefresh:
        for res in sess.read('/noderange/{}/console/ikvm_screenshot'.format(args[0])):
            for node in res.get('databynode', {}):
                errorstr = ''
                if not firstnodename:
                    firstnodename = node
                error = res['databynode'][node].get('error')
                if error and 'vnc available' in error:
                    vnconly.add(node)
                    continue
                elif error:
                    errorstr = error
                imgdata = res['databynode'][node].get('image', {}).get('imgdata', None)
                if imgdata:
                    if len(imgdata) < 32: # We were subjected to error
                        errorstr = f'Unable to get screenshot'
                if errorstr or imgdata:
                    draw_node(node, imgdata, errorstr, firstnodename, cwidth, cheight)
        if asyncvnc:
            urlbynode = {}
            for node in vnconly:
                for res in sess.update(f'/nodes/{node}/console/ikvm', {'method': 'unix'}):
                    url = res.get('item', {}).get('href')
                    if url:
                        urlbynode[node] = url
            draw_vnc_grabs(urlbynode, cwidth, cheight)
            if resized:
                do_resize(True)
                resized = False
        elif vnconly:
            sys.stderr.write("Require asyncvnc installed to do VNC screenshotting\n")
        if options.interval is None:
            dorefresh = False
        else:
            dorefresh = True
            time.sleep(options.interval)
    sys.exit(0)

try:
    import asyncio, asyncvnc
except ImportError:
    asyncvnc = None

def draw_vnc_grabs(urlbynode, cwidth, cheight):
    asyncio.run(grab_vncs(urlbynode, cwidth, cheight))
async def grab_vncs(urlbynode, cwidth, cheight):
    tasks = []
    for node in urlbynode:
        url = urlbynode[node]
        tasks.append(asyncio.create_task(do_vnc_screenshot(node, url, cwidth, cheight)))
    await asyncio.gather(*tasks)

async def my_opener(host, port):
    # really, host is the unix
    return await asyncio.open_unix_connection(host)

async def do_vnc_screenshot(node, url, cwidth, cheight):
    async with asyncvnc.connect(url, opener=my_opener) as client:
        # Retrieve pixels as a 3D numpy array
        pixels = await client.screenshot()
        # Save as PNG using PIL/pillow
        image = Image.fromarray(pixels)
        outfile = io.BytesIO()
        image.save(outfile, format='PNG')
        imgdata = base64.b64encode(outfile.getbuffer()).decode()
        if imgdata:
            draw_node(node, imgdata, '', '', cwidth, cheight)

def draw_node(node, imgdata, errorstr, firstnodename, cwidth, cheight):
    imagedatabynode[node] = imgdata
    if node in nodepositions:
        prep_node_tile(node)
        cursor_save()
    else:
        if options.interval is not None:
            if node != firstnodename:
                sys.stderr.write('Multiple nodes not supported for interval')
                sys.exit(1)
            sticky_cursor()
        sys.stdout.write('{}: '.format(node))
    # one row is used by our own name, so cheight - 1 for that allowance
    if errorstr:
        draw_text(errorstr, cwidth, cheight -1 if cheight else cheight)
    else:
        draw_image(imgdata.encode(), cwidth, cheight - 1 if cheight else cheight)
    if node in nodepositions:
        cursor_restore()
        reset_cursor(node)
    else:
        sys.stdout.write('\n')
    sys.stdout.flush()

if options.screenshot:
    try:
        cursor_hide()
        do_screenshot()
    except KeyboardInterrupt:
        pass
    finally:
        cursor_show()
        cursor_down(numrows)
        sys.stdout.write('\n')
    sys.exit(0)

def kill(noderange):
    sess = client.Command()
    envstring=os.environ.get('NODECONSOLE_WINDOWED_COMMAND')
    if not envstring:
        envstring = 'xterm'

    nodes = []
    for res in sess.read('/noderange/{0}/nodes/'.format(noderange)):
        node = res.get('item', {}).get('href', '/').replace('/', '')
        if not node:
            sys.stderr.write(res.get('error', repr(res)) + '\n')
            sys.exit(1)
        nodes.append(node) 

    for node in nodes:    
        command = "ps auxww | grep {0} | grep console | egrep '\\b{1}\\b' | grep -v grep | awk '{{print $2}}'".format(envstring, node)
        process = subprocess.Popen(command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
        stdout, stderr = process.communicate()
        try:
            process_id = stdout.decode('utf-8').split()[0]
        except IndexError:
            sys.stderr.write(node + ": console window not found \n")
            continue
        subprocess.Popen(["kill", process_id], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
    sys.exit(0)

def handle_geometry(envlist, sizegeometry, side_pad=0, top_pad=0, first=False):    
    if '-geometry' in envlist:
        g_index = envlist.index('-geometry')
    elif '-g' in envlist:
        g_index = envlist.index('-g')
    else:
        g_index = 0
    if g_index:   
        if first:
             envlist[g_index+1] = '{0}+{1}+{2}'.format(envlist[g_index+1],side_pad, top_pad)
        else:
            envlist[g_index+1] = '{0}+{1}+{2}'.format(sizegeometry,side_pad, top_pad)
    else:
        envlist.insert(1, '-geometry')
        envlist.insert(2, '{0}+{1}+{2}'.format(sizegeometry,side_pad, top_pad))
        g_index = 1
    return envlist     

# add funcltionality to close/kill all open consoles
if killcon:
    kill(noderange)
    
#added functionality for wcons
if options.windowed:
    result=subprocess.Popen(['xwininfo', '-root'], stdout=subprocess.PIPE)
    rootinfo=result.communicate()[0]
    result.wait()
    for line in rootinfo.decode('utf-8').split('\n'):
        if 'Width' in line:
            screenwidth = int(line.split(':')[1])
        if 'Height' in line:
            screenheight = int(line.split(':')[1])

    envstring=os.environ.get('NODECONSOLE_WINDOWED_COMMAND')
    if not envstring:
        sizegeometry='100x31'
        corrected_x, corrected_y = (13,84)
        envlist = handle_geometry(['xterm'] + pass_through_args + ['-e'],sizegeometry, first=True)
        #envlist=['xterm', '-bg', 'black', '-fg', 'white', '-geometry', '{sizegeometry}+0+0'.format(sizegeometry=sizegeometry), '-e']
    else:
        envlist=os.environ.get('NODECONSOLE_WINDOWED_COMMAND').split(' ')
        if envlist[0] == 'xterm':
            if '-geometry' in envlist:
                g_index = envlist.index('-geometry')
            elif '-g' in envlist:
                g_index = envlist.index('-g')
            else:
                g_index = 0
            if g_index:    
                envlist[g_index+1] = envlist[g_index+1] + '+0+0'

            else:
                envlist.insert(1, '-geometry')
                envlist.insert(2, '100x31+0+0')
                g_index = 1
            
    nodes = []
    sess = client.Command()
    for res in sess.read('/noderange/{0}/nodes/'.format(args[0])):
        node = res.get('item', {}).get('href', '/').replace('/', '')
        if not node:
            sys.stderr.write(res.get('error', repr(res)) + '\n')
            sys.exit(1)
        nodes.append(node)

    if options.tile and not envlist[0] == 'xterm':
        sys.stderr.write('[ERROR] UNSUPPORTED OPTIONS. \nWindowed and tiled consoles are only supported when using xterm \n')
        sys.exit(1)
    firstnode=nodes[0]
    nodes.pop(0)
    with open(os.devnull, 'wb') as devnull:
        xopen=subprocess.Popen(envlist + [confettypath, '-c', '/tmp/controlpath-{0}'.format(firstnode), '-m', '5', 'start', '/nodes/{0}/console/session'.format(firstnode) ] , stdin=devnull)
        time.sleep(2)
        s=socket.socket(socket.AF_UNIX)
        winid=''
        try:
            s.connect('/tmp/controlpath-{firstnode}'.format(firstnode=firstnode))
            s.recv(64)
            s.send(b'GETWINID')
            winid=s.recv(64).decode('utf-8')

        except:
            time.sleep(2)
            # try to get id of first panel/xterm window using name
            win=subprocess.Popen(['xwininfo', '-tree', '-root'], stdout=subprocess.PIPE)
            wintr=win.communicate()[0]
            for line in wintr.decode('utf-8').split('\n'):
                if 'console: {firstnode}'.format(firstnode=firstnode) in line or 'confetty' in line:
                    win_obj = [ele for ele in line.split(' ') if ele.strip()]
                    winid = win_obj[0]
    if winid:
        firstnode_window=subprocess.Popen(['xwininfo', '-id', '{winid}'.format(winid=winid)], stdout=subprocess.PIPE)
        xinfo=firstnode_window.communicate()[0]
        xinfl = xinfo.decode('utf-8').split('\n')
        for line in xinfl:
            if 'Absolute upper-left X:' in line:
                side_pad = int(line.split(':')[1])
            elif 'Absolute upper-left Y:' in line:
                top_pad = int(line.split(':')[1])

            elif 'Width:' in line:
                window_width = int(line.split(':')[1])
            elif 'Height' in line:
                window_height = int(line.split(':')[1])
            elif '-geometry' in line:
                l = re.split(' |x|-|\\+', line)
                l_nosp = [ele for ele in l if ele.strip()]
                wmxo = int(l_nosp[1])
                wmyo = int(l_nosp[2])
                sizegeometry = str(wmxo) + 'x' + str(wmyo)
            else:
                pass

        window_width += side_pad*2
        window_height += side_pad+top_pad
        screenwidth -= wmxo
        screenheight -= wmyo
        currx = window_width
        curry = 0

    for node in sortutil.natural_sort(nodes):
        if options.tile and envlist[0] == 'xterm':
            corrected_x = currx
            corrected_y = curry
            xgeometry = '{0}+{1}+{2}'.format(sizegeometry, corrected_x, corrected_y)
            currx += window_width
            if currx + window_width >= screenwidth:
                currx=0
                curry += window_height
                if curry > screenheight:
                    curry =top_pad
            if not envstring:        
                envlist= handle_geometry(envlist, sizegeometry, corrected_x, corrected_y)
            else:
                if g_index:    
                    envlist[g_index+1] = xgeometry
        elif envlist[0] == 'xterm':
            envlist=handle_geometry(envlist, sizegeometry, side_pad, top_pad)
            side_pad+=(side_pad+1)
            top_pad+=(top_pad+30)
        else:
            pass 
        with open(os.devnull, 'wb') as devnull:
            xopen=subprocess.Popen(envlist + [confettypath, '-c', '/tmp/controlpath-{0}'.format(node), '-m', '5', 'start', '/nodes/{0}/console/session'.format(node)] , stdin=devnull)
    sys.exit(0)
#end of wcons
if options.tile:
    null = open('/dev/null', 'w')
    nodes = []
    sess = client.Command()
    for res in sess.read('/noderange/{0}/nodes/'.format(args[0])):
        node = res.get('item', {}).get('href', '/').replace('/', '')
        if not node:
            sys.stderr.write(res.get('error', repr(res)) + '\n')
            sys.exit(1)
        nodes.append(node)
    initial = True
    in_tmux = False
    pane = 0
    sessname = 'nodeconsole_{0}'.format(os.getpid())
    if os.environ.get("TMUX"):
        initial = False
        in_tmux = True
        subprocess.call(['tmux', 'rename-session', sessname])
    for node in sortutil.natural_sort(nodes):
        panename = '{0}:{1}'.format(sessname, pane)
        if initial:
            initial = False
            subprocess.call(
                ['tmux', 'new-session', '-d', '-s',
                 sessname, '-x', '800', '-y',
                 '800', '{0} -m 5 start /nodes/{1}/console/session'.format(
                    confettypath, node)])
        else:
            subprocess.call(['tmux', 'select-pane', '-t', sessname])
            subprocess.call(['tmux', 'set-option', '-t', panename, 'pane-border-status', 'top'], stderr=null)
            subprocess.call(
                    ['tmux', 'split', '-h', '-t', sessname,
                 '{0} -m 5 start /nodes/{1}/console/session'.format(
                     confettypath, node)])
            subprocess.call(['tmux', 'select-layout', '-t', sessname, 'tiled'], stdout=null)
            pane += 1
    subprocess.call(['tmux', 'select-pane', '-t', sessname])
    subprocess.call(['tmux', 'set-option', '-t', panename, 'pane-border-status', 'top'], stderr=null)
    if not in_tmux:
        os.execlp('tmux', 'tmux', 'attach', '-t', sessname)
else:
    os.execl(confettypath, confettypath, 'start',
             '/nodes/{0}/console/session'.format(args[0]))
