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

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

另外需要注意安全问题。

v2ex 地址