【整活】IPython Console联动Sagemath shell

打密码无论是做题还是研究,使用sagemath都很频繁,虽然单独开一个窗口运行sage的shell不算太麻烦,但是总想整点活,就试着IPython Console跟sage的shell联动一波。

首先从启动方式开始看,研究一下双击电脑上的sagemath都运行了些什么东西才打开的窗口:

实际上,其运行了一段指令:
"C:\Users\10310\AppData\Local\SageMath 9.3\runtime\bin\mintty.exe" -t 'SageMath 9.3 Console' -i sagemath.ico /bin/bash --login -c '/opt/sagemath-9.3/sage'

主要是运行mintty.exe程序来调用bash程序,并在bash里面启用sage。-t是title参数,-i是icon参数。

理论上,pwntools(在windows上是winpwn)可以实现这种交互操作,我们可以直接在IPython Console中开启一个Process,使用interactive函数进行交互即可,但实际情况是,我使用的IDE为Spyder,其IPython Console不支持这样一个函数:sys.stdout.fileno(),这个函数直接影响了winpwn模块中process.interactive()函数与shell的交互。

也就是说,spyder里面的console对内核io支持的不是很好。起初我认为这是我的认知有限,其有特殊的方法我不知道,于是我去GitHub联系了Spyder的开发者,得到的回复如下:

哈哈 开发者没办法 无语,那我自己看下
里面使用sys.stdin.read()函数进行读入,根据impact27作者的回复,使用input是一样的。我就大胆的改成了input函数,这个问题解决后,又出现了下面的问题:

shell交互数据传输应使用Lattin1编码

发现这个问题,是因为我成功运行了sage,但是得到的输出全都是乱码,我把输出的字节码复制下来,进行了一番比对以及阅读源码,在winpwn/misc.py下找到了这样的代码:

(那里的函数encoding参数是我加上去的,原来的代码中并没有)

IPython Console使用的是utf-8编码,而正常的cmd使用的是gbk编码,都不是Lattin1编码,因此还需要一次编码转化。于是我在winpwn.py中进行了如下调整:
tube类中的interactive()函数,修改如下

def interactive(self):
        # it exited, contrl+C, timeout
        #showbanner('Interacting',is_noout=False)
        go = threading.Event()
        go.clear()
        def recv_thread():
            try:                  
                while not go.is_set():
                    buf = self.read(0x10000,0.125,interactive=True)
                    if buf:
                        out = bytes(buf,"Latin1").decode(self.encoding).rstrip('sage: ')
                        print(out, end = '')
                        #showbuf(buf, self.encoding, is_noout=False)
                        #showbanner('Interacting',is_noout=False)
                    go.wait(0.2)
            except KeyboardInterrupt:
                go.set()
                print(color('[-]: Exited','red'))
        t = threading.Thread(target = recv_thread)
        t.daemon = True
        t.start()

        try:
            while not go.is_set():
                go.wait(0.2)
                try:
                    if self.is_exit():
                        time.sleep(0.2) # wait for time to read output
                    #buf = sys.stdin.readline()
                    buf = input("sage: ") + '\n'
                    if buf == 'exit\n' or buf == 'exit()\n':
                        self.write(buf)
                        raise KeyboardInterrupt
                    #buf = input()
                    if buf:
                       # sys.stdout.write(buf)
                       self.write(buf)           # remote.write() may cause exception
                except Exception as e:
                    go.set()
                    print(color('[-]: Exited','red'))
                    break
        except KeyboardInterrupt: # control+C
            go.set()
            print(color('[-]: Exited','red'))
        while t.is_alive():
            t.join(timeout = 0.1)

下面的process类加一个encoding参数:

class process(tube):
    def __init__(self,argv,cwd=None,flags=None, encoding='gbk'):
        tube.__init__(self)
        # en/disable PIE, need: pip install pefile
        if context.pie is not None:
            fpath=""
            if not isinstance(argv,list):
                fpath=argv
            else:
                fpath=argv[0]
            if context.pie:
                PIE(fpath)
            else:
                NOPIE(fpath)
        self.Process=winProcess(argv,cwd,flags)
        self.pid=self.Process.pid
        self.encoding = encoding

然后外部再写一个封装函数:

from winpwn import process

def sageshell(encoding='utf-8'):
    sh = process(["C:/Users/10310/AppData/Local/SageMath 9.3/runtime/bin/bash.exe", "--login", "-c",
                  "/opt/sagemath-9.3/sage"], encoding=encoding)
    sh.interactive()

使用效果如下:

显然,受winpwn的影响,语法高亮、代码补全还不会做,不过现在已经阶段性进步了。
趁热打铁,我又写了一个runfile函数,只要提供了sage代码的完整路径,就能调用runfile函数在sage中运行该代码,就像spyder中按F5可以直接在console中跑现在写的代码,并且运行后自动会添加__file____dir__变量,提供当前文件的路径:

本质就是调用sageshell后通过process.send()发送指令,定义__file__变量和__dir__变量,然后调用load函数跑代码。

def run_in_console(filepath, encoding='utf-8'):
    real_path = filepath[15:]
    real_path = '/home/sage/' + real_path
    sh = process(["C:/Users/10310/AppData/Local/SageMath 9.3/runtime/bin/bash.exe", "--login", "-c",
                  "/opt/sagemath-9.3/sage"], encoding=encoding)

    sh.sendline('__file__ = "%s"' % real_path)
    sh.sendline('__dir__ = "%s"' % ('/'.join(real_path.split('/')[:-1])))
    sh.sendline('load("%s")' % real_path)
    sh.interactive()

整挺好。

发表评论

您的电子邮箱地址不会被公开。 必填项已用 * 标注