V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
goreliu
V2EX  ›  PowerShell

编写 PowerShell Server 加快 PowerShell 脚本启动速度

  •  
  •   goreliu · 2019-04-29 09:40:33 +08:00 · 4573 次点击
    这是一个创建于 1814 天前的主题,其中的信息可能已经有所发展或是发生改变。

    PowerShell 功能很强大,但对 Linux Shell 用户来说易用性依然达不到满意程度(虽然几经改进后比以前强了许多)。那么就有一个办法是平时使用 WSL 中的 Shell,并通过 PowerShell 脚本实现一些功能。但问题是 PowerShell 启动时间很慢(在我这里要 130ms 以上)。那就意味着在 WSL 中调用 PowerShell 脚本会因为启动速度慢而体验极差。

    看着功能强大的 PowerShell 却没办法舒服地使用还是比较遗憾的。不过有一个折衷的办法,运行一个常驻的 PowerShell Server,然后在 WSL 下运行命令时直接连接到这个 Server,这样就省去了 PowerShell 的启动时间。

    运行效果

    % time o '$PSversionTable.PSVersion'
    5.1.18362.1
    o '$PSversionTable.PSVersion'  0.00s user 0.00s system 0% cpu 0.006 total
    
    % time o '11+22+33'
    66
    o '11+22+33'  0.00s user 0.00s system 0% cpu 0.006 total
    
    % cat test.ps1
    #!/home/goreliu/.bin/o -f
    
    $var=@{Name = "小明"; Age = "12"; sex = "男"}
    echo $var["Name"]
    % time ./test.ps1
    小明
    ./test.ps1  0.00s user 0.02s system 249% cpu 0.006 total
    
    % time bash -c ''
    bash -c ''  0.00s user 0.02s system 193% cpu 0.008 total
    
    % o
    PS> 33*99
    3267
    PS> 12mb
    12582912
    PS>
    

    重点在于运行时间,只有惊人的 6ms,要比 WSL 下直接运行 bash 脚本还快。

    其中 o 是用于连接 Server 的 Client,代码见下文。

    用 PowerShell 写的 Server

    #!/bin/powershell
    
    $endpoint = New-Object System.Net.IPEndPoint([System.Net.IPAddress]::Loopback, 12345)
    $Listener = New-Object System.Net.Sockets.TcpListener $endpoint
    $Listener.Start()
    
    $RunspacePool = [RunspaceFactory]::CreateRunspacePool(1, 10)
    $RunspacePool.Open()
    
    while ($true) {
        $client = $Listener.AcceptTcpClient()
    
        $job = [PowerShell]::Create().AddScript({
            Param($client, $buffer)
    
            $encoding = new-object System.Text.Utf8Encoding
    
            $stream = $client.GetStream()
            $cmd = ''
            $buffer = new-object byte[] 102400
    
            while ($true) {
                try {
                    $read_size = $stream.Read($buffer, 0, $buffer.Count)
                    $cmd = $encoding.GetString($buffer, 0, $read_size)
                } catch {
                    echo $_
                    break
                }
    
                if ($cmd.length -eq 0) {
                    break
                }
    
                <#
                if ($cmd -match '@gui@') {
                    $stream.Write([BitConverter]::GetBytes(0), 0, 4)
    
                    [PowerShell]::Create().AddScript($cmd).BeginInvoke()
                    break
                } elseif ($cmd -match '@guip@') {
                    $stream.Write([BitConverter]::GetBytes(0), 0, 4)
    
                    # 用进程
                    Start-Job -ScriptBlock {iex $args[0]} -ArgumentList $cmd
                    break
                }
                #>
    
                iex $cmd -OutVariable out -ErrorVariable err
    
                $all = $err
    
                if ($all.Count -eq 0) {
                    $all = $out
                }
    
                if ($all.Count -eq 0) {
                    $stream.Write([BitConverter]::GetBytes(0), 0, 4)
                } else {
                    $result = $encoding.GetBytes(($all -join "`n") + "`n")
                    $stream.Write([BitConverter]::GetBytes($result.length) + $result,`
                        0, 4 + $result.length)
                }
            }
    
            $stream.Close()
            $client.Close()
        }).AddParameter('client', $client).AddParameter('buffer', $buffer)
    
        $job.RunspacePool = $RunspacePool
    
        $job.BeginInvoke()
    }
    

    分别运行于 WSL 和 Windows 下的 Client

    #include <arpa/inet.h>
    #include <sys/types.h>
    #include <fcntl.h>
    #include <stdio.h>
    #include <string.h>
    #include <unistd.h>
    
    #define buf_size 1048576
    char buf[buf_size] = "";
    
    int main(int argc, char *argv[]) {
        // -f file: run file
        if (argc == 3 && argv[1][0] == '-' && argv[1][1] == 'f') {
            int fd = open(argv[2], O_RDONLY);
            if (fd < 0) {
                printf("Failed to open %s.\n", argv[2]);
                return 1;
            }
    
            if (read(fd, buf, buf_size) <= 1) {
                printf("Failed to read %s.\n", argv[2]);
                close(fd);
                return 1;
            }
    
            close(fd);
        } else {
            for (int i = 1; i < argc; ++i) {
                strcat(buf, argv[i]);
                strcat(buf, " ");
            }
        }
    
        struct sockaddr_in their_addr;
        their_addr.sin_family = AF_INET;
        their_addr.sin_port = htons(12345);
        their_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
        bzero(&(their_addr.sin_zero), 8);
        
        int sockfd = socket(AF_INET, SOCK_STREAM, 0);
        if (connect(sockfd, (struct sockaddr *)&their_addr, sizeof(struct sockaddr)) != 0) {
            printf("Failed to connect.\n");
            return 1;
        }
    
        int manual = 0;
        if (argc == 1) {
            readlink("/proc/self/fd/0", buf, buf_size);
            // pipe:[...]
            if (buf[0] != 'p') {
                // run as a shell
                manual = 1;
            }
        }
    
        while (1) {
            if (manual) {
                write(1, "PS> ", 4);
            }
    
            int numbytes;
    
            if (argc == 1) {
                numbytes = read(0, buf, buf_size);
                if (numbytes == 1) {
                    // 只读到一个换行符
                    continue;
                } else if (numbytes < 1) {
                    if (manual) {
                        write(1, "\n", 1);
                    }
    
                    close(sockfd);
                    return 0;
                }
            } else {
                numbytes = strlen(buf);
            }
    
            if (numbytes > 102400) {
                printf("To long: %d.\n", numbytes);
                close(sockfd);
                return 0;
            }
    
            send(sockfd, buf, numbytes, 0);
    
            int length;
            if (recv(sockfd, &length, 4, 0) != 4) {
                printf("Failed to parse length.\n");
    
                if (argc == 1) {
                    break;
                }
    
                continue;
            }
    
            while (length > 0) {
                numbytes = recv(sockfd, buf, length > buf_size ? buf_size : length, 0);
                length -= numbytes;
    
                write(1, buf, numbytes);
            }
    
            if (!manual) {
                break;
            }
        }
    
        close(sockfd);
        return 0;
    }
    

    还有一个运行在 Windows 的 Client,功能很简单,只用来运行图形界面(或者不需要结果的脚本),可以关联到 .ps1 (或者用新扩展名)文件上。可以用 tcc 或者 MinGW 编译。

    // tcc -lws2_32 wino.c
    
    #include <fcntl.h>
    #include <io.h>
    #include <stdio.h>
    #include <windows.h>
    
    #define buf_size 102400
    char buf[buf_size] = "";
    char *filename = NULL;
    
    int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) {
        filename = lpCmdLine;
    
        if (filename[0] == '\0') {
            MessageBoxA(0, "Usage:\n    o filename", "Error", MB_ICONERROR);
            return 1;
        }
    
        if (filename[0] == '"') {
            ++filename;
            filename[strlen(filename) - 1] = '\0';
        }
    
        int fd = open(filename, O_RDONLY);
        if (fd < 0) {
            sprintf(buf, "Failed to open %s", lpCmdLine);
            MessageBoxA(0, buf, "Error", MB_ICONERROR);
            return 1;
        }
    
        int numbytes = read(fd, buf, buf_size);
    
        if (numbytes <= 1) {
            sprintf(buf, "Failed to read %s", lpCmdLine);
            MessageBoxA(0, buf, "Error", MB_ICONERROR);
    
            close(fd);
            return 1;
        }
    
        WSADATA wsaData;
        SOCKET ConnectSocket = INVALID_SOCKET;
        int iResult;
    
        struct sockaddr_in server_addr;
        server_addr.sin_family = AF_INET;
        server_addr.sin_port = htons(12345);
        server_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
    
        iResult = WSAStartup(MAKEWORD(2,2), &wsaData);
        if (iResult != 0) {
            sprintf(buf, "WSAStartup failed with error: %d", iResult);
            MessageBoxA(0, buf, "Error", MB_ICONERROR);
            return 1;
        }
    
        ConnectSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
        if (ConnectSocket == INVALID_SOCKET) {
            sprintf(buf, "socket failed with error: %ld", WSAGetLastError());
            MessageBoxA(0, buf, "Error", MB_ICONERROR);
            WSACleanup();
            return 1;
        }
    
        iResult = connect(ConnectSocket, (const struct sockaddr *)&server_addr, sizeof(server_addr));
        if (iResult == SOCKET_ERROR) {
            closesocket(ConnectSocket);
            WSACleanup();
    
            MessageBoxA(0, "Unable to connect to server", "Error", MB_ICONERROR);
            return 1;
        }
    
        iResult = send(ConnectSocket, buf, numbytes, 0);
    
        if (iResult == SOCKET_ERROR) {
            sprintf(buf, "send failed with error: %d", WSAGetLastError());
            MessageBoxA(0, buf, "Error", MB_ICONERROR);
            closesocket(ConnectSocket);
            WSACleanup();
            return 1;
        }
    
        iResult = shutdown(ConnectSocket, SD_SEND);
        if (iResult == SOCKET_ERROR) {
            sprintf(buf, "shutdown failed with error: %d", WSAGetLastError());
            closesocket(ConnectSocket);
            WSACleanup();
            return 1;
        }
    
        closesocket(ConnectSocket);
        WSACleanup();
    
        return 0;
    }
    

    端口都写死在了代码里( 12345 )。

    其中 Server 是多线程的,最多 10 个线程( CreateRunspacePool(1, 10))。

    但需要注意一个事情,运行命令的输出结果和直接在 PowerShell 终端中不同:

    % o '$PSVersionTable'
    System.Collections.Hashtable
    
    % /init /mnt/c/Windows/System32/WindowsPowerShell/v1.0/powershell.exe '$PSVersionTable'
    
    Name                           Value
    ----                           -----
    PSVersion                      5.1.18362.1
    PSEdition                      Desktop
    PSCompatibleVersions           {1.0, 2.0, 3.0, 4.0...}
    BuildVersion                   10.0.18362.1
    CLRVersion                     4.0.30319.42000
    WSManStackVersion              3.0
    PSRemotingProtocolVersion      2.3
    SerializationVersion           1.1.0.1
    

    写脚本的话基本不影响使用。

    另外需要注意安全问题。

    4 条回复    2019-06-05 20:08:39 +08:00
    ps1aniuge
        1
    ps1aniuge  
       2019-06-05 19:17:52 +08:00
    楼主这什么玩意?不知所谓!!!

    楼主的 win,powershell 慢( mnt/c/Windows/System32/WindowsPowerShell/v1.0/powershell.exe )
    你折腾 linux ( wsl )干嘛??!

    1 130ms,启动 powershell 不算慢。

    2 可以买高速 ssd,装在 c 盘上。想快速尽量不能贫穷。

    3 可以经常手动运行 。net 优化,来提速。
    任务计划 -》
    \Microsoft\Windows\.NET Framework\.NET Framework NGEN v4.0.30319 64

    4 可以启动 Superfetch 服务,即内存缓存程序来提速。

    5 可以运行 linux 版( wsl 版) powershell,来提速 linux 中使用 pwsh。不过这个和 win 中的 powershell 没啥关系。
    goreliu
        2
    goreliu  
    OP
       2019-06-05 19:22:36 +08:00 via Android
    @ps1aniuge 我想我写得很清楚了。
    ps1aniuge
        3
    ps1aniuge  
       2019-06-05 19:47:56 +08:00
    你来回折腾,到底要干嘛?

    你要在你的 wsl 发行版的 linux 中,使用 powershell,不需要远程去使用,
    只需要在 wsl 发行版 linux 中,安装 powershell 6.2.1。
    装好后,在 linux 本地使用 /usr/bin/pwsh 即可。
    不需要联网 服务器 /客户机这样使用。

    就算是服务器 /客户机这样使用。用 ssh-ps-remoting 也行,也不需要自己开发。
    你这是过度开发。会写 c,会写锤子,看啥都是钉子。
    goreliu
        4
    goreliu  
    OP
       2019-06-05 20:08:39 +08:00 via Android
    @ps1aniuge 你如果想理解我在做什么,要先仔细了解我所说的场景。而你回复我的内容,我认为你是好心的,但无助于解决我所说的问题。如果你还没有理解,我也不想继续展开了。
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   我们的愿景   ·   实用小工具   ·   1681 人在线   最高记录 6543   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 27ms · UTC 16:48 · PVG 00:48 · LAX 09:48 · JFK 12:48
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.