编写 PowerShell Server 加快 PowerShell 脚本启动速度
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.0 国际许可协议 进行许可。