parent
10eb7e8bc1
commit
576608b69d
25
.github/workflows/tests_grid.yml
vendored
25
.github/workflows/tests_grid.yml
vendored
|
|
@ -1,25 +0,0 @@
|
||||||
name: "tests grid"
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
env:
|
|
||||||
FORCE_COLOR: 1
|
|
||||||
ELECTRON_SKIP_BINARY_DOWNLOAD: 1
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
test:
|
|
||||||
name: "Grid"
|
|
||||||
runs-on: ubuntu-20.04
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v3
|
|
||||||
- uses: actions/setup-node@v3
|
|
||||||
- run: npm ci
|
|
||||||
env:
|
|
||||||
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1
|
|
||||||
- run: npm run build
|
|
||||||
- run: npx playwright install-deps
|
|
||||||
- run: npx playwright install
|
|
||||||
- run: xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" -- npm run test -- --retries=0
|
|
||||||
env:
|
|
||||||
PWTEST_MODE: service-grid
|
|
||||||
99
package-lock.json
generated
99
package-lock.json
generated
|
|
@ -1424,10 +1424,6 @@
|
||||||
"resolved": "packages/playwright-ct-vue2",
|
"resolved": "packages/playwright-ct-vue2",
|
||||||
"link": true
|
"link": true
|
||||||
},
|
},
|
||||||
"node_modules/@playwright/experimental-grid": {
|
|
||||||
"resolved": "packages/playwright-grid",
|
|
||||||
"link": true
|
|
||||||
},
|
|
||||||
"node_modules/@playwright/test": {
|
"node_modules/@playwright/test": {
|
||||||
"resolved": "packages/playwright-test",
|
"resolved": "packages/playwright-test",
|
||||||
"link": true
|
"link": true
|
||||||
|
|
@ -1528,25 +1524,6 @@
|
||||||
"@types/tern": "*"
|
"@types/tern": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@types/commander": {
|
|
||||||
"version": "2.12.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/@types/commander/-/commander-2.12.2.tgz",
|
|
||||||
"integrity": "sha512-0QEFiR8ljcHp9bAbWxecjVRuAMr16ivPiGOw6KFQBVrVd0RQIcM3xKdRisH2EDWgVWujiYtHwhSkSUoAAGzH7Q==",
|
|
||||||
"deprecated": "This is a stub types definition for commander (https://github.com/tj/commander.js). commander provides its own type definitions, so you don't need @types/commander installed!",
|
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
|
||||||
"commander": "*"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@types/debug": {
|
|
||||||
"version": "4.1.8",
|
|
||||||
"resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.8.tgz",
|
|
||||||
"integrity": "sha512-/vPO1EPOs306Cvhwv7KfVfYvOJqA/S/AXjaHQiJboCZzcNDb+TIJFN9/2C9DZ//ijSKWioNyUxD792QmDJ+HKQ==",
|
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
|
||||||
"@types/ms": "*"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@types/estree": {
|
"node_modules/@types/estree": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.0.tgz",
|
||||||
|
|
@ -1567,12 +1544,6 @@
|
||||||
"integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==",
|
"integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/@types/ms": {
|
|
||||||
"version": "0.7.31",
|
|
||||||
"resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.31.tgz",
|
|
||||||
"integrity": "sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA==",
|
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"node_modules/@types/node": {
|
"node_modules/@types/node": {
|
||||||
"version": "16.18.34",
|
"version": "16.18.34",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.34.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.34.tgz",
|
||||||
|
|
@ -6199,6 +6170,7 @@
|
||||||
},
|
},
|
||||||
"node_modules/ws": {
|
"node_modules/ws": {
|
||||||
"version": "8.5.0",
|
"version": "8.5.0",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=10.0.0"
|
"node": ">=10.0.0"
|
||||||
|
|
@ -6564,6 +6536,7 @@
|
||||||
"packages/playwright-grid": {
|
"packages/playwright-grid": {
|
||||||
"name": "@playwright/experimental-grid",
|
"name": "@playwright/experimental-grid",
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
|
"extraneous": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"commander": "^11.0.0",
|
"commander": "^11.0.0",
|
||||||
|
|
@ -6583,25 +6556,6 @@
|
||||||
"node": ">=16"
|
"node": ">=16"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"packages/playwright-grid/node_modules/commander": {
|
|
||||||
"version": "11.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/commander/-/commander-11.0.0.tgz",
|
|
||||||
"integrity": "sha512-9HMlXtt/BNoYr8ooyjjNRdIilOTkVJXB+GhxMTtOKwk0R4j4lS4NpjuqmRxroBfnfTSHQIHQB7wryHhXarNjmQ==",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=16"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"packages/playwright-grid/node_modules/playwright-core": {
|
|
||||||
"version": "1.38.0-alpha-aug-10-2023",
|
|
||||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.38.0-alpha-aug-10-2023.tgz",
|
|
||||||
"integrity": "sha512-j3+Lmd9ySH2EYv45AgcDccr5v5uBVljnmCV+QYAcrhQ4hGCAoUwGTXEuvPnEgw1BCqF7QfmugeVzEi1UD8PnIQ==",
|
|
||||||
"bin": {
|
|
||||||
"playwright-core": "cli.js"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=16"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"packages/playwright-test": {
|
"packages/playwright-test": {
|
||||||
"name": "@playwright/test",
|
"name": "@playwright/test",
|
||||||
"version": "1.38.0-next",
|
"version": "1.38.0-next",
|
||||||
|
|
@ -7574,30 +7528,6 @@
|
||||||
"vue": "^2.7.14"
|
"vue": "^2.7.14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@playwright/experimental-grid": {
|
|
||||||
"version": "file:packages/playwright-grid",
|
|
||||||
"requires": {
|
|
||||||
"@types/commander": "^2.12.2",
|
|
||||||
"@types/debug": "^4.1.8",
|
|
||||||
"@types/ws": "^8.5.5",
|
|
||||||
"commander": "^11.0.0",
|
|
||||||
"debug": "^4.3.2",
|
|
||||||
"playwright-core": "1.38.0-alpha-aug-10-2023",
|
|
||||||
"ws": "^8.1.0"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"commander": {
|
|
||||||
"version": "11.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/commander/-/commander-11.0.0.tgz",
|
|
||||||
"integrity": "sha512-9HMlXtt/BNoYr8ooyjjNRdIilOTkVJXB+GhxMTtOKwk0R4j4lS4NpjuqmRxroBfnfTSHQIHQB7wryHhXarNjmQ=="
|
|
||||||
},
|
|
||||||
"playwright-core": {
|
|
||||||
"version": "1.38.0-alpha-aug-10-2023",
|
|
||||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.38.0-alpha-aug-10-2023.tgz",
|
|
||||||
"integrity": "sha512-j3+Lmd9ySH2EYv45AgcDccr5v5uBVljnmCV+QYAcrhQ4hGCAoUwGTXEuvPnEgw1BCqF7QfmugeVzEi1UD8PnIQ=="
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"@playwright/test": {
|
"@playwright/test": {
|
||||||
"version": "file:packages/playwright-test",
|
"version": "file:packages/playwright-test",
|
||||||
"requires": {
|
"requires": {
|
||||||
|
|
@ -7686,24 +7616,6 @@
|
||||||
"@types/tern": "*"
|
"@types/tern": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@types/commander": {
|
|
||||||
"version": "2.12.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/@types/commander/-/commander-2.12.2.tgz",
|
|
||||||
"integrity": "sha512-0QEFiR8ljcHp9bAbWxecjVRuAMr16ivPiGOw6KFQBVrVd0RQIcM3xKdRisH2EDWgVWujiYtHwhSkSUoAAGzH7Q==",
|
|
||||||
"dev": true,
|
|
||||||
"requires": {
|
|
||||||
"commander": "*"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"@types/debug": {
|
|
||||||
"version": "4.1.8",
|
|
||||||
"resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.8.tgz",
|
|
||||||
"integrity": "sha512-/vPO1EPOs306Cvhwv7KfVfYvOJqA/S/AXjaHQiJboCZzcNDb+TIJFN9/2C9DZ//ijSKWioNyUxD792QmDJ+HKQ==",
|
|
||||||
"dev": true,
|
|
||||||
"requires": {
|
|
||||||
"@types/ms": "*"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"@types/estree": {
|
"@types/estree": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.0.tgz",
|
||||||
|
|
@ -7723,12 +7635,6 @@
|
||||||
"integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==",
|
"integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"@types/ms": {
|
|
||||||
"version": "0.7.31",
|
|
||||||
"resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.31.tgz",
|
|
||||||
"integrity": "sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA==",
|
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"@types/node": {
|
"@types/node": {
|
||||||
"version": "16.18.34",
|
"version": "16.18.34",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.34.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.34.tgz",
|
||||||
|
|
@ -10755,6 +10661,7 @@
|
||||||
},
|
},
|
||||||
"ws": {
|
"ws": {
|
||||||
"version": "8.5.0",
|
"version": "8.5.0",
|
||||||
|
"dev": true,
|
||||||
"requires": {}
|
"requires": {}
|
||||||
},
|
},
|
||||||
"xml2js": {
|
"xml2js": {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
**/*
|
|
||||||
!cli.js
|
|
||||||
!lib/**/*.js
|
|
||||||
!README.md
|
|
||||||
!https/*
|
|
||||||
|
|
@ -1,8 +0,0 @@
|
||||||
FROM mcr.microsoft.com/playwright:v1.37.0-alpha-aug-7-2023-jammy
|
|
||||||
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
COPY package.json ./
|
|
||||||
COPY cli.js ./
|
|
||||||
COPY lib ./lib
|
|
||||||
RUN npm install
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
# wip
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
#!/usr/bin/env node
|
|
||||||
|
|
||||||
require('./lib/cli.js');
|
|
||||||
|
|
@ -1,46 +0,0 @@
|
||||||
apiVersion: apps/v1
|
|
||||||
kind: Deployment
|
|
||||||
metadata:
|
|
||||||
name: grid-deployment
|
|
||||||
spec:
|
|
||||||
replicas: 1
|
|
||||||
selector:
|
|
||||||
matchLabels:
|
|
||||||
app: grid
|
|
||||||
template:
|
|
||||||
metadata:
|
|
||||||
labels:
|
|
||||||
app: grid
|
|
||||||
spec:
|
|
||||||
containers:
|
|
||||||
- name: grid
|
|
||||||
image: playwright-grid
|
|
||||||
resources:
|
|
||||||
requests:
|
|
||||||
cpu: "2"
|
|
||||||
memory: "4Gi"
|
|
||||||
imagePullPolicy: IfNotPresent
|
|
||||||
env:
|
|
||||||
- name: DEBUG
|
|
||||||
value: "pw:grid*"
|
|
||||||
- name: PLAYWRIGHT_GRID_ACCESS_KEY
|
|
||||||
valueFrom:
|
|
||||||
secretKeyRef:
|
|
||||||
name: access-key-secret
|
|
||||||
key: access-key
|
|
||||||
command: ["node", "./cli.js"]
|
|
||||||
args: ["grid", "--port=3000"]
|
|
||||||
|
|
||||||
---
|
|
||||||
apiVersion: v1
|
|
||||||
kind: Service
|
|
||||||
metadata:
|
|
||||||
name: grid-service
|
|
||||||
spec:
|
|
||||||
selector:
|
|
||||||
app: grid
|
|
||||||
ports:
|
|
||||||
- protocol: TCP
|
|
||||||
port: 3000
|
|
||||||
targetPort: 3000
|
|
||||||
type: LoadBalancer
|
|
||||||
|
|
@ -1,32 +0,0 @@
|
||||||
apiVersion: apps/v1
|
|
||||||
kind: Deployment
|
|
||||||
metadata:
|
|
||||||
name: worker-deployment
|
|
||||||
spec:
|
|
||||||
replicas: 10 # or however many nodes you want
|
|
||||||
selector:
|
|
||||||
matchLabels:
|
|
||||||
app: worker
|
|
||||||
template:
|
|
||||||
metadata:
|
|
||||||
labels:
|
|
||||||
app: worker
|
|
||||||
spec:
|
|
||||||
containers:
|
|
||||||
- name: grid
|
|
||||||
image: playwright-grid
|
|
||||||
resources:
|
|
||||||
requests:
|
|
||||||
cpu: "2"
|
|
||||||
memory: "4Gi"
|
|
||||||
imagePullPolicy: IfNotPresent
|
|
||||||
env:
|
|
||||||
- name: DEBUG
|
|
||||||
value: "pw:grid*"
|
|
||||||
- name: PLAYWRIGHT_GRID_ACCESS_KEY
|
|
||||||
valueFrom:
|
|
||||||
secretKeyRef:
|
|
||||||
name: access-key-secret
|
|
||||||
key: access-key
|
|
||||||
command: ["node", "./cli.js"]
|
|
||||||
args: ["node", "--grid=grid-service:3000"]
|
|
||||||
|
|
@ -1,57 +0,0 @@
|
||||||
```sh
|
|
||||||
# Create resource group
|
|
||||||
az group create --name group-grid-001 --location westus3
|
|
||||||
|
|
||||||
# Create ACR
|
|
||||||
az acr create --resource-group group-grid-001 --name acrgrid001 --sku Basic
|
|
||||||
az acr login --name acrgrid001
|
|
||||||
az acr list --resource-group group-grid-001 --query "[].{acrLoginServer:loginServer}" --output table
|
|
||||||
|
|
||||||
# Create AKS
|
|
||||||
az aks create --resource-group group-grid-001 --name aks-grid-001 --node-count 4 --enable-addons monitoring --generate-ssh-keys
|
|
||||||
az aks get-credentials --resource-group group-grid-001 --name aks-grid-001
|
|
||||||
|
|
||||||
# Grant AKS access to ACR
|
|
||||||
az aks show --resource-group group-grid-001 --name aks-grid-001 --query "servicePrincipalProfile.clientId" --output tsv
|
|
||||||
# az aks show --resource-group group-grid-001 --name aks-grid-001 --query "identityProfile.kubeletidentity.clientId" -o tsv
|
|
||||||
# az acr show --name acrgrid001 --resource-group group-grid-001 --query "id" -o tsv
|
|
||||||
# az role assignment create --assignee <GUID> --role AcrPull --scope <SCOP PATH>
|
|
||||||
|
|
||||||
# Create secrets
|
|
||||||
kubectl create secret generic access-key-secret --from-literal=access-key=$PLAYWRIGHT_GRID_ACCESS_KEY
|
|
||||||
|
|
||||||
# Create TLS
|
|
||||||
# kubectl create secret tls grid-tls-secret --cert=../../tests/config/testserver/cert.pem --key=../../tests/config/testserver/key.pem
|
|
||||||
# az network public-ip create --resource-group MC_group-grid-001_aks-grid-001_westus3 --name public-ip-grid-001 --sku Standard --allocation-method static
|
|
||||||
# az network public-ip show --resource-group MC_group-grid-001_aks-grid-001_westus3 --name public-ip-grid-001 --query ipAddress --output tsv
|
|
||||||
# # use output below
|
|
||||||
# helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx
|
|
||||||
# helm install nginx-ingress ingress-nginx/ingress-nginx \
|
|
||||||
# --set controller.replicaCount=1 \
|
|
||||||
# --set controller.nodeSelector."beta\.kubernetes\.io/os"=linux \
|
|
||||||
# --set defaultBackend.nodeSelector."beta\.kubernetes\.io/os"=linux \
|
|
||||||
# --set controller.service.loadBalancerIP="20.118.130.255"
|
|
||||||
|
|
||||||
# Push Docker container
|
|
||||||
docker build -t playwright-grid:latest -f Dockerfile .
|
|
||||||
docker tag playwright-grid acrgrid001.azurecr.io/playwright-grid
|
|
||||||
docker push acrgrid001.azurecr.io/playwright-grid
|
|
||||||
|
|
||||||
# Delete deployment
|
|
||||||
kubectl delete deployment grid-deployment
|
|
||||||
kubectl delete deployment worker-deployment
|
|
||||||
kubectl delete svc grid-service
|
|
||||||
|
|
||||||
# Update deployment
|
|
||||||
kubectl apply -f deployment-grid.yaml
|
|
||||||
kubectl apply -f deployment-worker.yaml
|
|
||||||
|
|
||||||
# Debug
|
|
||||||
kubectl config
|
|
||||||
kubectl get pods -l app=grid
|
|
||||||
kubectl logs grid-6cbbfc866c-wh8dw
|
|
||||||
kubectl get pods -n ingress-basic
|
|
||||||
kubectl get svc grid-service
|
|
||||||
kubectl describe node
|
|
||||||
az aks show --resource-group group-grid-001 --name aks-grid-001 --query fqdn --output tsv
|
|
||||||
```
|
|
||||||
|
|
@ -1,31 +0,0 @@
|
||||||
```sh
|
|
||||||
minikube config set memory 65536
|
|
||||||
minikube config set cpus 12
|
|
||||||
minikube start
|
|
||||||
minikube dashboard
|
|
||||||
|
|
||||||
# Point docker to minikube
|
|
||||||
minikube -p minikube docker-env
|
|
||||||
eval $(minikube docker-env)
|
|
||||||
kubectl config use-context minikube
|
|
||||||
kubectl create secret generic access-key-secret --from-literal=access-key=$PLAYWRIGHT_GRID_ACCESS_KEY
|
|
||||||
|
|
||||||
# Push Docker container
|
|
||||||
docker build -t playwright-grid:latest -f Dockerfile .
|
|
||||||
|
|
||||||
# Delete deployment
|
|
||||||
kubectl delete deployment grid-deployment
|
|
||||||
kubectl delete deployment worker-deployment
|
|
||||||
kubectl delete svc grid-service
|
|
||||||
|
|
||||||
# Update deployment
|
|
||||||
|
|
||||||
kubectl apply -f deployment-grid.yaml
|
|
||||||
kubectl apply -f deployment-worker.yaml
|
|
||||||
|
|
||||||
# Debug
|
|
||||||
minikube ip
|
|
||||||
kubectl get svc grid-service
|
|
||||||
kubectl get pods -l app=grid
|
|
||||||
kubectl logs grid-6cbbfc866c-wh8dw
|
|
||||||
```
|
|
||||||
|
|
@ -1,29 +0,0 @@
|
||||||
-----BEGIN CERTIFICATE-----
|
|
||||||
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
|
|
||||||
-----END CERTIFICATE-----
|
|
||||||
|
|
@ -1,52 +0,0 @@
|
||||||
-----BEGIN PRIVATE KEY-----
|
|
||||||
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
|
|
||||||
ZkZVSOEp+sYBf/tmptlKr49nO+dTjQ==
|
|
||||||
-----END PRIVATE KEY-----
|
|
||||||
|
|
@ -1,19 +0,0 @@
|
||||||
# openssl req -new -x509 -days 3650 -key key.pem -out cert.pem -config san.cnf -extensions v3_req
|
|
||||||
|
|
||||||
[req]
|
|
||||||
distinguished_name = req_distinguished_name
|
|
||||||
req_extensions = v3_req
|
|
||||||
prompt = no
|
|
||||||
|
|
||||||
[req_distinguished_name]
|
|
||||||
CN = playwright-test
|
|
||||||
|
|
||||||
[v3_req]
|
|
||||||
basicConstraints = CA:FALSE
|
|
||||||
keyUsage = nonRepudiation, digitalSignature, keyEncipherment
|
|
||||||
subjectAltName = @alt_names
|
|
||||||
|
|
||||||
[alt_names]
|
|
||||||
DNS.1 = localhost
|
|
||||||
IP.1 = 127.0.0.1
|
|
||||||
IP.2 = ::1
|
|
||||||
|
|
@ -1,30 +0,0 @@
|
||||||
{
|
|
||||||
"name": "@playwright/experimental-grid",
|
|
||||||
"version": "0.0.1",
|
|
||||||
"private": true,
|
|
||||||
"description": "Playwright Grid",
|
|
||||||
"scripts": {},
|
|
||||||
"bin": {
|
|
||||||
"playwright-grid": "./cli.js"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"commander": "^11.0.0",
|
|
||||||
"debug": "^4.3.2",
|
|
||||||
"playwright-core": "1.38.0-alpha-aug-10-2023",
|
|
||||||
"ws": "^8.1.0"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@types/commander": "^2.12.2",
|
|
||||||
"@types/debug": "^4.1.8",
|
|
||||||
"@types/ws": "^8.5.5"
|
|
||||||
},
|
|
||||||
"repository": "github:Microsoft/playwright",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=16"
|
|
||||||
},
|
|
||||||
"homepage": "https://playwright.dev",
|
|
||||||
"author": {
|
|
||||||
"name": "Microsoft Corporation"
|
|
||||||
},
|
|
||||||
"license": "Apache-2.0"
|
|
||||||
}
|
|
||||||
|
|
@ -1,52 +0,0 @@
|
||||||
/**
|
|
||||||
* Copyright (c) Microsoft Corporation.
|
|
||||||
*
|
|
||||||
* 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 { program } from 'commander';
|
|
||||||
const packageJSON = require('../package.json');
|
|
||||||
|
|
||||||
program
|
|
||||||
.version('Version ' + packageJSON.version)
|
|
||||||
.name('playwright-grid');
|
|
||||||
|
|
||||||
program
|
|
||||||
.command('grid')
|
|
||||||
.option('--port <port>', 'port to listen to, 3333 by default')
|
|
||||||
.option('--access-key <key>', 'access key to the grid')
|
|
||||||
.option('--https-cert <cert>', 'path to the HTTPS certificate')
|
|
||||||
.option('--https-key <key>', 'path to the HTTPS key')
|
|
||||||
.action(async opts => {
|
|
||||||
const port = opts.port || +(process.env.PLAYWRIGHT_GRID_PORT || '3333');
|
|
||||||
const accessKey = opts.accessKey || process.env.PLAYWRIGHT_GRID_ACCESS_KEY;
|
|
||||||
const httpsCert = opts.httpsCert || process.env.PLAYWRIGHT_GRID_HTTPS_CERT;
|
|
||||||
const httpsKey = opts.httpsKey || process.env.PLAYWRIGHT_GRID_HTTPS_KEY;
|
|
||||||
const { Grid } = await import('./grid/grid.js');
|
|
||||||
const grid = await Grid.create({ port, accessKey, httpsCert, httpsKey });
|
|
||||||
grid.start();
|
|
||||||
});
|
|
||||||
|
|
||||||
program
|
|
||||||
.command('node')
|
|
||||||
.option('--grid <url>', 'grid address', 'localhost:3333')
|
|
||||||
.option('--capacity <capacity>', 'node capacity', '1')
|
|
||||||
.option('--access-key <key>', 'access key to the grid', '')
|
|
||||||
.action(async opts => {
|
|
||||||
const { Node } = await import('./node/node.js');
|
|
||||||
const accessKey = opts.accessKey || process.env.PLAYWRIGHT_GRID_ACCESS_KEY;
|
|
||||||
const node = new Node(opts.grid, +opts.capacity, accessKey);
|
|
||||||
await node.connect();
|
|
||||||
});
|
|
||||||
|
|
||||||
program.parse(process.argv);
|
|
||||||
|
|
@ -1,19 +0,0 @@
|
||||||
/**
|
|
||||||
* Copyright (c) Microsoft Corporation.
|
|
||||||
*
|
|
||||||
* 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.
|
|
||||||
*/
|
|
||||||
|
|
||||||
export type Capabilities = {
|
|
||||||
platform?: typeof process.platform;
|
|
||||||
};
|
|
||||||
|
|
@ -1,132 +0,0 @@
|
||||||
/**
|
|
||||||
* Copyright (c) Microsoft Corporation.
|
|
||||||
*
|
|
||||||
* 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 debug from 'debug';
|
|
||||||
import fs from 'fs';
|
|
||||||
import https from 'https';
|
|
||||||
import http from 'http';
|
|
||||||
import path from 'path';
|
|
||||||
import { URL } from 'url';
|
|
||||||
import { Server as WebSocketServer } from 'ws';
|
|
||||||
|
|
||||||
export type ServerRouteHandler = (request: http.IncomingMessage, response: http.ServerResponse) => boolean;
|
|
||||||
|
|
||||||
export class HttpServer {
|
|
||||||
private _log: debug.Debugger;
|
|
||||||
readonly server: https.Server | http.Server;
|
|
||||||
private _urlPrefix: string;
|
|
||||||
private _routes: { prefix?: string, exact?: string, handler: ServerRouteHandler }[] = [];
|
|
||||||
private _isSecure: boolean;
|
|
||||||
|
|
||||||
static async create(options: { httpsKey?: string, httpsCert?: string }) {
|
|
||||||
if (options.httpsKey && options.httpsCert) {
|
|
||||||
return new HttpServer({
|
|
||||||
key: await fs.promises.readFile(options.httpsKey, 'utf8'),
|
|
||||||
cert: await fs.promises.readFile(options.httpsCert, 'utf8'),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return new HttpServer();
|
|
||||||
}
|
|
||||||
|
|
||||||
private constructor(options?: { key: string, cert: string }) {
|
|
||||||
this._log = debug(`pw:grid:http`);
|
|
||||||
this._urlPrefix = '';
|
|
||||||
this._isSecure = !!options;
|
|
||||||
this.server = options ? https.createServer(options, this._onRequest.bind(this)) : http.createServer(this._onRequest.bind(this));
|
|
||||||
}
|
|
||||||
|
|
||||||
routePrefix(prefix: string, handler: ServerRouteHandler) {
|
|
||||||
this._routes.push({ prefix, handler });
|
|
||||||
}
|
|
||||||
|
|
||||||
routePath(path: string, handler: ServerRouteHandler) {
|
|
||||||
this._routes.push({ exact: path, handler });
|
|
||||||
}
|
|
||||||
|
|
||||||
createWebSocketServer() {
|
|
||||||
return new WebSocketServer({ server: this.server });
|
|
||||||
}
|
|
||||||
|
|
||||||
async start(port?: number): Promise<string> {
|
|
||||||
this._log('starting server', port);
|
|
||||||
this.server.listen(port);
|
|
||||||
await new Promise(cb => this.server!.once('listening', cb));
|
|
||||||
const address = this.server.address();
|
|
||||||
this._urlPrefix = typeof address === 'string' ? address : `${this._isSecure ? 'https' : 'http'}://127.0.0.1:${address!.port}`;
|
|
||||||
return this._urlPrefix;
|
|
||||||
}
|
|
||||||
|
|
||||||
async stop() {
|
|
||||||
await new Promise(cb => this.server!.close(cb));
|
|
||||||
}
|
|
||||||
|
|
||||||
urlPrefix() {
|
|
||||||
return this._urlPrefix;
|
|
||||||
}
|
|
||||||
|
|
||||||
serveFile(response: http.ServerResponse, absoluteFilePath: string, headers?: { [name: string]: string }): boolean {
|
|
||||||
try {
|
|
||||||
const content = fs.readFileSync(absoluteFilePath);
|
|
||||||
response.statusCode = 200;
|
|
||||||
const contentType = extensionToMime[path.extname(absoluteFilePath).substring(1)] || 'application/octet-stream';
|
|
||||||
response.setHeader('Content-Type', contentType);
|
|
||||||
response.setHeader('Content-Length', content.byteLength);
|
|
||||||
for (const [name, value] of Object.entries(headers || {}))
|
|
||||||
response.setHeader(name, value);
|
|
||||||
response.end(content);
|
|
||||||
return true;
|
|
||||||
} catch (e) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private _onRequest(request: http.IncomingMessage, response: http.ServerResponse) {
|
|
||||||
this._log('web request', request.url);
|
|
||||||
request.on('error', () => response.end());
|
|
||||||
try {
|
|
||||||
if (!request.url) {
|
|
||||||
response.end();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const url = new URL('http://localhost' + request.url);
|
|
||||||
this._log('url pathname', url.pathname);
|
|
||||||
for (const route of this._routes) {
|
|
||||||
if (route.exact && url.pathname === route.exact && route.handler(request, response))
|
|
||||||
return;
|
|
||||||
if (route.prefix && url.pathname.startsWith(route.prefix) && route.handler(request, response))
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
response.statusCode = 404;
|
|
||||||
response.end();
|
|
||||||
} catch (e) {
|
|
||||||
response.end();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const extensionToMime: { [key: string]: string } = {
|
|
||||||
'css': 'text/css',
|
|
||||||
'html': 'text/html',
|
|
||||||
'jpeg': 'image/jpeg',
|
|
||||||
'jpg': 'image/jpeg',
|
|
||||||
'js': 'application/javascript',
|
|
||||||
'png': 'image/png',
|
|
||||||
'ttf': 'font/ttf',
|
|
||||||
'svg': 'image/svg+xml',
|
|
||||||
'webp': 'image/webp',
|
|
||||||
'woff': 'font/woff',
|
|
||||||
'woff2': 'font/woff2',
|
|
||||||
};
|
|
||||||
|
|
@ -1,376 +0,0 @@
|
||||||
/**
|
|
||||||
* Copyright (c) Microsoft Corporation.
|
|
||||||
*
|
|
||||||
* 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 crypto from 'crypto';
|
|
||||||
import debug from 'debug';
|
|
||||||
import { URL } from 'url';
|
|
||||||
import WebSocket from 'ws';
|
|
||||||
import type { Server as WebSocketServer } from 'ws';
|
|
||||||
import { HttpServer } from '../common/httpServer';
|
|
||||||
import type { Capabilities } from '../common/capabilities';
|
|
||||||
import type http from 'http';
|
|
||||||
import type stream from 'stream';
|
|
||||||
|
|
||||||
class WebSocketRequest {
|
|
||||||
private _socketError: Error | undefined;
|
|
||||||
|
|
||||||
constructor(readonly wsServer: WebSocketServer, readonly request: http.IncomingMessage, readonly socket: stream.Duplex, readonly head: Buffer) {
|
|
||||||
this.socket.on('error', e => this._socketError = e);
|
|
||||||
}
|
|
||||||
|
|
||||||
upgrade(extraHeaders: string[] = []): Promise<WebSocket | null> {
|
|
||||||
if (this._socketError || this.socket.destroyed)
|
|
||||||
return Promise.resolve(null);
|
|
||||||
|
|
||||||
return new Promise<WebSocket | null>(f => {
|
|
||||||
const socketEndTimer = setTimeout(() => {
|
|
||||||
this.socket.destroy();
|
|
||||||
f(null);
|
|
||||||
}, 5000);
|
|
||||||
this.wsServer.once('headers', headers => {
|
|
||||||
for (let i = 0; i < extraHeaders.length; i += 2) {
|
|
||||||
if (extraHeaders[i].toLowerCase().startsWith('x-playwright'))
|
|
||||||
headers.push(`${extraHeaders[i]}: ${extraHeaders[i + 1]}`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
this.wsServer.handleUpgrade(this.request, this.socket, this.head, ws => {
|
|
||||||
if (ws.readyState === WebSocket.CLOSED || ws.readyState === WebSocket.CLOSING) {
|
|
||||||
f(null);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
clearTimeout(socketEndTimer);
|
|
||||||
this.wsServer.emit('connection', ws, this.request);
|
|
||||||
f(ws);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type ClientRequest = {
|
|
||||||
webSocketRequest: WebSocketRequest;
|
|
||||||
capabilities: Capabilities;
|
|
||||||
};
|
|
||||||
|
|
||||||
class Worker {
|
|
||||||
readonly workerId = 'worker@' + createGuid();
|
|
||||||
private _workerSocketRequest: WebSocketRequest | undefined;
|
|
||||||
private _workerSocket: WebSocket | undefined;
|
|
||||||
private _clientSocket: WebSocket | undefined;
|
|
||||||
private _log: debug.Debugger;
|
|
||||||
private _state: 'new' | 'available' | 'connecting' | 'connected' | 'closed' = 'new';
|
|
||||||
private _onClose: () => void;
|
|
||||||
private _retireTimer: NodeJS.Timeout;
|
|
||||||
|
|
||||||
constructor(onClose: () => void) {
|
|
||||||
this._log = debug(`pw:grid:${this.workerId}`);
|
|
||||||
this._onClose = onClose;
|
|
||||||
this._log('worker created');
|
|
||||||
|
|
||||||
// Workers have 30 seconds to be picked up.
|
|
||||||
this._retireTimer = setTimeout(() => {
|
|
||||||
this.close();
|
|
||||||
}, 30_000);
|
|
||||||
}
|
|
||||||
|
|
||||||
state(): 'new' | 'available' | 'connecting' | 'connected' | 'closed' {
|
|
||||||
return this._state;
|
|
||||||
}
|
|
||||||
|
|
||||||
workerConnected(workerSocketRequest: WebSocketRequest) {
|
|
||||||
this._log('worker available');
|
|
||||||
this._state = 'available';
|
|
||||||
this._workerSocketRequest = workerSocketRequest;
|
|
||||||
}
|
|
||||||
|
|
||||||
async connect(clientRequest: ClientRequest): Promise<'workerError' | 'clientError' | 'success'> {
|
|
||||||
this._log('connect', clientRequest.webSocketRequest.request.headers);
|
|
||||||
this._state = 'connecting';
|
|
||||||
|
|
||||||
clearTimeout(this._retireTimer);
|
|
||||||
|
|
||||||
const workerSocket = await this._workerSocketRequest!.upgrade(clientRequest.webSocketRequest.request.rawHeaders);
|
|
||||||
if (!workerSocket || workerSocket.readyState === WebSocket.CLOSED || workerSocket.readyState === WebSocket.CLOSING) {
|
|
||||||
this.close();
|
|
||||||
return 'workerError';
|
|
||||||
}
|
|
||||||
|
|
||||||
const clientSocket = await clientRequest.webSocketRequest.upgrade();
|
|
||||||
if (!clientSocket || clientSocket.readyState === WebSocket.CLOSED || clientSocket.readyState === WebSocket.CLOSING) {
|
|
||||||
this.close();
|
|
||||||
return 'clientError';
|
|
||||||
}
|
|
||||||
|
|
||||||
this._wire(workerSocket, clientSocket);
|
|
||||||
return 'success';
|
|
||||||
}
|
|
||||||
|
|
||||||
private _wire(workerSocket: WebSocket, clientSocket: WebSocket) {
|
|
||||||
this._log('connected');
|
|
||||||
|
|
||||||
this._state = 'connected';
|
|
||||||
workerSocket.on('close', () => this.close());
|
|
||||||
workerSocket.on('error', () => this.close());
|
|
||||||
clientSocket.on('close', () => this.close());
|
|
||||||
clientSocket.on('error', () => this.close());
|
|
||||||
clientSocket.on('message', data => {
|
|
||||||
this._workerSocket?.send(data);
|
|
||||||
});
|
|
||||||
workerSocket.on('message', data => {
|
|
||||||
this._clientSocket?.send(data);
|
|
||||||
});
|
|
||||||
|
|
||||||
this._workerSocket = workerSocket;
|
|
||||||
this._clientSocket = clientSocket;
|
|
||||||
}
|
|
||||||
|
|
||||||
close() {
|
|
||||||
if (this._state === 'closed')
|
|
||||||
return;
|
|
||||||
this._log('close');
|
|
||||||
this._state = 'closed';
|
|
||||||
this._workerSocket?.close();
|
|
||||||
this._clientSocket?.close();
|
|
||||||
this._workerSocket = undefined;
|
|
||||||
this._clientSocket = undefined;
|
|
||||||
this._onClose();
|
|
||||||
}
|
|
||||||
|
|
||||||
debugInfo() {
|
|
||||||
return { state: this._state };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class Node {
|
|
||||||
readonly nodeId = 'node@' + createGuid();
|
|
||||||
private _ws: WebSocket;
|
|
||||||
readonly _workers = new Map<string, Worker>();
|
|
||||||
private _log: debug.Debugger;
|
|
||||||
private _onWorkersChanged: () => void;
|
|
||||||
private _onClose: () => void;
|
|
||||||
private _capabilities: Capabilities;
|
|
||||||
private _capacity: number;
|
|
||||||
|
|
||||||
constructor(ws: WebSocket, capacity: number, capabilities: Capabilities, onWorkersChanged: () => void, onClose: () => void) {
|
|
||||||
this._capabilities = capabilities;
|
|
||||||
this._capacity = capacity;
|
|
||||||
this._log = debug(`pw:grid:${this.nodeId}`);
|
|
||||||
ws.on('close', () => this.close());
|
|
||||||
ws.on('error', () => this.close());
|
|
||||||
ws.send(JSON.stringify({ nodeId: this.nodeId }));
|
|
||||||
this._ws = ws;
|
|
||||||
this._onWorkersChanged = onWorkersChanged;
|
|
||||||
this._onClose = onClose;
|
|
||||||
}
|
|
||||||
|
|
||||||
hasWorker(workerId: string) {
|
|
||||||
return this._workers.has(workerId);
|
|
||||||
}
|
|
||||||
|
|
||||||
hasCapabilities(capabilities: Capabilities): boolean {
|
|
||||||
return !capabilities.platform || this._capabilities.platform === capabilities.platform;
|
|
||||||
}
|
|
||||||
|
|
||||||
workers() {
|
|
||||||
return [...this._workers.values()];
|
|
||||||
}
|
|
||||||
|
|
||||||
canCreateWorker() {
|
|
||||||
return this._workers.size < this._capacity;
|
|
||||||
}
|
|
||||||
|
|
||||||
createWorker() {
|
|
||||||
const worker = new Worker(() => {
|
|
||||||
this._workers.delete(worker.workerId);
|
|
||||||
this._onWorkersChanged();
|
|
||||||
});
|
|
||||||
this._workers.set(worker.workerId, worker);
|
|
||||||
this._ws.send(JSON.stringify({ workerId: worker.workerId }));
|
|
||||||
return worker;
|
|
||||||
}
|
|
||||||
|
|
||||||
workerConnected(workerId: string, webSocketRequest: WebSocketRequest) {
|
|
||||||
const worker = this._workers.get(workerId);
|
|
||||||
if (worker) {
|
|
||||||
worker.workerConnected(webSocketRequest);
|
|
||||||
this._onWorkersChanged();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
close() {
|
|
||||||
this._log('close');
|
|
||||||
this._ws?.close();
|
|
||||||
this._onClose();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class Grid {
|
|
||||||
private _server: HttpServer;
|
|
||||||
private _wsServer: WebSocketServer;
|
|
||||||
private _nodes = new Map<string, Node>();
|
|
||||||
private _log: debug.Debugger;
|
|
||||||
private _clientRequests: ClientRequest[] = [];
|
|
||||||
private _port: number;
|
|
||||||
private _accessKey: string;
|
|
||||||
|
|
||||||
static async create(options: { port: number, accessKey?: string, httpsCert?: string, httpsKey?: string }): Promise<Grid> {
|
|
||||||
const server = await HttpServer.create(options);
|
|
||||||
return new Grid(server, options);
|
|
||||||
}
|
|
||||||
|
|
||||||
private constructor(server: HttpServer, options: { port: number, accessKey?: string }) {
|
|
||||||
this._log = debug(`pw:grid:proxy`);
|
|
||||||
this._server = server;
|
|
||||||
this._port = options.port;
|
|
||||||
this._accessKey = options.accessKey || '';
|
|
||||||
|
|
||||||
this._server.routePath('/' + this._accessKey, (request, response) => {
|
|
||||||
response.statusCode = 200;
|
|
||||||
response.setHeader('Content-Type', 'text/plain');
|
|
||||||
response.end(this._state());
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
|
|
||||||
this._wsServer = new WebSocket.Server({ noServer: true });
|
|
||||||
this._wsServer.on('connection', ws => {
|
|
||||||
ws.on('error', e => this._log(e));
|
|
||||||
});
|
|
||||||
this._server.server.on('upgrade', async (request, socket, head) => {
|
|
||||||
this._log('upgrade', request.url, request.headers);
|
|
||||||
|
|
||||||
if (this._accessKey && request.headers['x-playwright-access-key'] !== this._accessKey) {
|
|
||||||
socket.destroy();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const url = new URL('http://internal' + request.url);
|
|
||||||
const params = url.searchParams;
|
|
||||||
|
|
||||||
if (url.pathname.startsWith('/registerNode')) {
|
|
||||||
const nodeRequest = new WebSocketRequest(this._wsServer, request, socket, head);
|
|
||||||
const ws = await nodeRequest.upgrade();
|
|
||||||
if (!ws)
|
|
||||||
return;
|
|
||||||
const capacity = +(params.get('capacity') || '1');
|
|
||||||
const capabilities = JSON.parse(params.get('caps')!) as Capabilities;
|
|
||||||
const node = new Node(ws, capacity, capabilities, () => {
|
|
||||||
this._makeAMatch();
|
|
||||||
}, () => {
|
|
||||||
this._nodes.delete(node.nodeId);
|
|
||||||
});
|
|
||||||
this._nodes.set(node.nodeId, node);
|
|
||||||
this._log('register node', node.nodeId);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (url.pathname.startsWith('/registerWorker')) {
|
|
||||||
const nodeId = params.get('nodeId')!;
|
|
||||||
const workerId = params.get('workerId')!;
|
|
||||||
const node = this._nodes.get(nodeId);
|
|
||||||
if (!node) {
|
|
||||||
socket.destroy();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!node.hasWorker(workerId)) {
|
|
||||||
socket.destroy();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const workerRequest = new WebSocketRequest(this._wsServer, request, socket, head);
|
|
||||||
node.workerConnected(workerId, workerRequest);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (url.pathname === '/') {
|
|
||||||
const capabilities = JSON.parse(params.get('caps') || '{}') as Capabilities;
|
|
||||||
this._addClientRequest({
|
|
||||||
webSocketRequest: new WebSocketRequest(this._wsServer, request, socket, head),
|
|
||||||
capabilities,
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private _addClientRequest(clientRequest: ClientRequest) {
|
|
||||||
this._clientRequests.push(clientRequest);
|
|
||||||
this._makeAMatch();
|
|
||||||
}
|
|
||||||
|
|
||||||
private _nodesWithCapabilities(capabilities: Capabilities | null): Node[] {
|
|
||||||
return [...this._nodes.values()].filter(node => !capabilities || node.hasCapabilities(capabilities));
|
|
||||||
}
|
|
||||||
|
|
||||||
private _workers(capabilities: Capabilities | null): Worker[] {
|
|
||||||
const result: Worker[] = [];
|
|
||||||
for (const node of this._nodesWithCapabilities(capabilities))
|
|
||||||
result.push(...node.workers());
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
private _makeAMatch() {
|
|
||||||
this._log('making a match', {
|
|
||||||
clients: this._clientRequests.length,
|
|
||||||
nodes: this._nodes.size,
|
|
||||||
workers: this._workers(null).length,
|
|
||||||
availableWorkers: this._workers(null).filter(w => w.state() === 'available').length
|
|
||||||
});
|
|
||||||
|
|
||||||
// Remove closed client requests.
|
|
||||||
this._clientRequests = this._clientRequests.filter(c => c.webSocketRequest.socket.readable);
|
|
||||||
|
|
||||||
if (!this._clientRequests.length)
|
|
||||||
return;
|
|
||||||
|
|
||||||
const capabilities = this._clientRequests[0].capabilities;
|
|
||||||
const nodes = this._nodesWithCapabilities(capabilities);
|
|
||||||
const availableWorkers = nodes.map(n => n.workers()).flat().filter(w => w.state() === 'available');
|
|
||||||
if (!availableWorkers.length) {
|
|
||||||
// Try getting another worker for given capabilities.
|
|
||||||
const node = nodes.find(w => w.canCreateWorker());
|
|
||||||
if (node)
|
|
||||||
node.createWorker();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Make a match.
|
|
||||||
const worker = availableWorkers[0];
|
|
||||||
const clientRequest = this._clientRequests.shift()!;
|
|
||||||
worker.connect(clientRequest).then(result => {
|
|
||||||
if (result === 'workerError')
|
|
||||||
this._clientRequests.unshift(clientRequest);
|
|
||||||
this._makeAMatch();
|
|
||||||
}).catch(e => this._log(e));
|
|
||||||
}
|
|
||||||
|
|
||||||
private _state(): string {
|
|
||||||
const lines = [this._nodes.size + ' Nodes(s)'];
|
|
||||||
for (const [nodeId, node] of this._nodes) {
|
|
||||||
lines.push(` node ${nodeId}`);
|
|
||||||
for (const [workerId, worker] of node._workers)
|
|
||||||
lines.push(` ${workerId} - ${JSON.stringify(worker.debugInfo())}`);
|
|
||||||
}
|
|
||||||
return lines.join('\n');
|
|
||||||
}
|
|
||||||
|
|
||||||
async start() {
|
|
||||||
const url = await this._server.start(this._port);
|
|
||||||
// eslint-disable-next-line no-console
|
|
||||||
console.log('Server is listening on: ' + url);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function createGuid(): string {
|
|
||||||
return crypto.randomBytes(16).toString('hex');
|
|
||||||
}
|
|
||||||
|
|
@ -1,104 +0,0 @@
|
||||||
/**
|
|
||||||
* Copyright (c) Microsoft Corporation.
|
|
||||||
*
|
|
||||||
* 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 WebSocket from 'ws';
|
|
||||||
import child_process from 'child_process';
|
|
||||||
import debug from 'debug';
|
|
||||||
import type { Capabilities } from '../common/capabilities';
|
|
||||||
|
|
||||||
const log = debug('pw:grid:node');
|
|
||||||
|
|
||||||
const caps: Capabilities = {
|
|
||||||
platform: process.platform,
|
|
||||||
};
|
|
||||||
|
|
||||||
export class Node {
|
|
||||||
workerSeq = 0;
|
|
||||||
|
|
||||||
constructor(readonly grid: string, readonly capacity: number, readonly accessKey?: string) {
|
|
||||||
this.accessKey = accessKey || '';
|
|
||||||
log('node created', accessKey);
|
|
||||||
}
|
|
||||||
|
|
||||||
async connect() {
|
|
||||||
const wsGrid = this.grid;
|
|
||||||
const url = wsGrid + `/registerNode?capacity=${this.capacity}&caps=${JSON.stringify(caps)}`;
|
|
||||||
|
|
||||||
for (let i = 0; i < 5; ++i) {
|
|
||||||
const ws = await this._connect(url);
|
|
||||||
if (ws) {
|
|
||||||
this._wire(ws, wsGrid);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
await new Promise(f => setTimeout(f, 5000));
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line no-restricted-properties
|
|
||||||
process.exit(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async _connect(url: string): Promise<WebSocket | null> {
|
|
||||||
return await new Promise(resolve => {
|
|
||||||
log('connecting', url);
|
|
||||||
const ws = new WebSocket(url, {
|
|
||||||
headers: {
|
|
||||||
'x-playwright-access-key': this.accessKey,
|
|
||||||
}
|
|
||||||
});
|
|
||||||
ws.on('error', error => {
|
|
||||||
log(error);
|
|
||||||
resolve(null);
|
|
||||||
});
|
|
||||||
ws.on('open', () => {
|
|
||||||
log('connected', this.grid);
|
|
||||||
resolve(ws);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private _wire(ws: WebSocket, wsGrid: string) {
|
|
||||||
ws.on('close', () => {
|
|
||||||
// eslint-disable-next-line no-restricted-properties
|
|
||||||
process.exit(0);
|
|
||||||
});
|
|
||||||
ws.on('error', () => {
|
|
||||||
// eslint-disable-next-line no-restricted-properties
|
|
||||||
process.exit(0);
|
|
||||||
});
|
|
||||||
let nodeId = '';
|
|
||||||
ws.on('message', data => {
|
|
||||||
const text = data.toString();
|
|
||||||
const message = JSON.parse(text);
|
|
||||||
if (message.nodeId) {
|
|
||||||
nodeId = message.nodeId;
|
|
||||||
log('node id', nodeId);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const workerId = message.workerId;
|
|
||||||
log('worked requested', workerId);
|
|
||||||
child_process.fork(require.resolve('./worker.js'), {
|
|
||||||
env: {
|
|
||||||
...process.env,
|
|
||||||
PLAYWRIGHT_GRID_NODE_ID: nodeId,
|
|
||||||
PLAYWRIGHT_GRID_WORKER_ID: workerId,
|
|
||||||
PLAYWRIGHT_GRID_ENDPOINT: wsGrid,
|
|
||||||
PLAYWRIGHT_GRID_ACCESS_KEY: this.accessKey,
|
|
||||||
},
|
|
||||||
detached: true
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,96 +0,0 @@
|
||||||
/**
|
|
||||||
* Copyright (c) Microsoft Corporation.
|
|
||||||
*
|
|
||||||
* 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 debug from 'debug';
|
|
||||||
import WebSocket from 'ws';
|
|
||||||
import { DispatcherConnection, RootDispatcher, PlaywrightDispatcher, createPlaywright, serverSideCallMetadata, SocksProxy } from 'playwright-core/lib/server';
|
|
||||||
import { gracefullyCloseAll } from 'playwright-core/lib/utils';
|
|
||||||
import type { Playwright } from 'playwright-core/lib/server';
|
|
||||||
|
|
||||||
const workerId = process.env.PLAYWRIGHT_GRID_WORKER_ID!;
|
|
||||||
const log = debug('pw:grid:worker@' + workerId);
|
|
||||||
|
|
||||||
class Worker {
|
|
||||||
constructor() {
|
|
||||||
log('worker created');
|
|
||||||
const dispatcherConnection = new DispatcherConnection();
|
|
||||||
let browserName: 'chromium' | 'webkit' | 'firefox';
|
|
||||||
let launchOptions: any;
|
|
||||||
let proxyPattern: string | undefined;
|
|
||||||
let socksProxy: SocksProxy | undefined;
|
|
||||||
|
|
||||||
const dispose = async () => {
|
|
||||||
dispatcherConnection.onmessage = () => {};
|
|
||||||
// eslint-disable-next-line no-restricted-properties
|
|
||||||
setTimeout(() => process.exit(0), 30000);
|
|
||||||
await Promise.all([
|
|
||||||
socksProxy?.close(),
|
|
||||||
gracefullyCloseAll(),
|
|
||||||
]).catch(() => {});
|
|
||||||
// eslint-disable-next-line no-restricted-properties
|
|
||||||
process.exit(0);
|
|
||||||
};
|
|
||||||
|
|
||||||
const ws = new WebSocket(process.env.PLAYWRIGHT_GRID_ENDPOINT + `/registerWorker?nodeId=${process.env.PLAYWRIGHT_GRID_NODE_ID}&workerId=${workerId}`, {
|
|
||||||
headers: {
|
|
||||||
'x-playwright-access-key': process.env.PLAYWRIGHT_GRID_ACCESS_KEY!,
|
|
||||||
}
|
|
||||||
});
|
|
||||||
dispatcherConnection.onmessage = message => ws.send(JSON.stringify(message));
|
|
||||||
ws.on('upgrade', response => {
|
|
||||||
const headers: Record<string, string> = {};
|
|
||||||
for (let i = 0; i < response.rawHeaders.length; i += 2)
|
|
||||||
headers[response.rawHeaders[i]] = response.rawHeaders[i + 1];
|
|
||||||
|
|
||||||
browserName = headers['x-playwright-browser'] as any || 'chromium';
|
|
||||||
launchOptions = JSON.parse(headers['x-playwright-launch-options'] || '{}');
|
|
||||||
proxyPattern = headers['x-playwright-proxy'] || '';
|
|
||||||
|
|
||||||
log({ browserName, launchOptions, proxyPattern });
|
|
||||||
});
|
|
||||||
ws.once('open', () => {
|
|
||||||
log('worker opened');
|
|
||||||
new RootDispatcher(dispatcherConnection, async (rootScope, { sdkLanguage }) => {
|
|
||||||
const playwright = createPlaywright({ sdkLanguage });
|
|
||||||
if (proxyPattern)
|
|
||||||
socksProxy = await createOwnedSocksProxy(proxyPattern, playwright);
|
|
||||||
const browser = await playwright[browserName].launch(serverSideCallMetadata(), launchOptions);
|
|
||||||
return new PlaywrightDispatcher(rootScope, playwright, socksProxy, browser);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
ws.on('message', message => dispatcherConnection.dispatch(JSON.parse(message.toString())));
|
|
||||||
ws.on('error', error => {
|
|
||||||
log('socket error');
|
|
||||||
dispose();
|
|
||||||
});
|
|
||||||
ws.on('close', async () => {
|
|
||||||
log('worker deleted');
|
|
||||||
dispose();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function createOwnedSocksProxy(proxyPattern: string, playwright: Playwright): Promise<SocksProxy | undefined> {
|
|
||||||
if (!proxyPattern)
|
|
||||||
return;
|
|
||||||
const socksProxy = new SocksProxy();
|
|
||||||
socksProxy.setPattern(proxyPattern);
|
|
||||||
playwright.options.socksProxyPort = await socksProxy.listen(0);
|
|
||||||
log(`started socks proxy on port ${playwright.options.socksProxyPort}`);
|
|
||||||
return socksProxy;
|
|
||||||
}
|
|
||||||
|
|
||||||
new Worker();
|
|
||||||
|
|
@ -17,7 +17,7 @@
|
||||||
import { start } from '../../packages/playwright-core/lib/outofprocess';
|
import { start } from '../../packages/playwright-core/lib/outofprocess';
|
||||||
import type { Playwright } from '../../packages/playwright-core/lib/client/playwright';
|
import type { Playwright } from '../../packages/playwright-core/lib/client/playwright';
|
||||||
|
|
||||||
export type TestModeName = 'default' | 'driver' | 'service' | 'service2' | 'service-grid';
|
export type TestModeName = 'default' | 'driver' | 'service' | 'service2';
|
||||||
|
|
||||||
interface TestMode {
|
interface TestMode {
|
||||||
setup(): Promise<Playwright>;
|
setup(): Promise<Playwright>;
|
||||||
|
|
|
||||||
|
|
@ -79,35 +79,6 @@ if (mode === 'service2') {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mode === 'service-grid') {
|
|
||||||
process.env.NODE_EXTRA_CA_CERTS = require.resolve('../../packages/playwright-grid/https/cert.pem');
|
|
||||||
connectOptions = {
|
|
||||||
wsEndpoint: process.env.PLAYWRIGHT_GRID_URL || 'wss://localhost:3333',
|
|
||||||
timeout: 60 * 60 * 1000,
|
|
||||||
headers: {
|
|
||||||
'x-playwright-access-key': process.env.PLAYWRIGHT_GRID_ACCESS_KEY || 'secret'
|
|
||||||
},
|
|
||||||
exposeNetwork: '<loopback>',
|
|
||||||
};
|
|
||||||
webServer = process.env.PLAYWRIGHT_GRID_URL ? [] : [
|
|
||||||
{
|
|
||||||
command: 'node ./cli.js grid --port=3333 --access-key=secret --https-cert=./https/cert.pem --https-key=./https/key.pem',
|
|
||||||
stdout: 'pipe',
|
|
||||||
url: 'https://localhost:3333/secret',
|
|
||||||
reuseExistingServer: !process.env.CI,
|
|
||||||
cwd: '../../packages/playwright-grid',
|
|
||||||
ignoreHTTPSErrors: true,
|
|
||||||
}, {
|
|
||||||
command: 'node ./cli.js node --grid=wss://localhost:3333 --access-key=secret --capacity=2',
|
|
||||||
cwd: '../../packages/playwright-grid',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
command: 'node ./cli.js node --grid=wss://localhost:3333 --access-key=secret --capacity=2',
|
|
||||||
cwd: '../../packages/playwright-grid',
|
|
||||||
}
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
const config: Config<CoverageWorkerOptions & PlaywrightWorkerOptions & PlaywrightTestOptions & TestModeWorkerOptions> = {
|
const config: Config<CoverageWorkerOptions & PlaywrightWorkerOptions & PlaywrightTestOptions & TestModeWorkerOptions> = {
|
||||||
testDir,
|
testDir,
|
||||||
outputDir,
|
outputDir,
|
||||||
|
|
|
||||||
|
|
@ -213,12 +213,6 @@ const workspace = new Workspace(ROOT_PATH, [
|
||||||
path: path.join(ROOT_PATH, 'packages', 'playwright-ct-vue2'),
|
path: path.join(ROOT_PATH, 'packages', 'playwright-ct-vue2'),
|
||||||
files: ['LICENSE'],
|
files: ['LICENSE'],
|
||||||
}),
|
}),
|
||||||
new PWPackage({
|
|
||||||
name: '@playwright/experimental-grid',
|
|
||||||
path: path.join(ROOT_PATH, 'packages', 'playwright-grid'),
|
|
||||||
files: ['LICENSE'],
|
|
||||||
noConsistent: true,
|
|
||||||
}),
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (require.main === module) {
|
if (require.main === module) {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue