From 1b6ddb66f0b09ab9ad67f50cef6275e0731e17b0 Mon Sep 17 00:00:00 2001 From: Matthew Beckett Date: Wed, 2 Sep 2020 20:41:58 +0100 Subject: [PATCH 001/247] Re-commit 0f1bbc --- deployment/chart/Chart.yaml | 5 + deployment/chart/README.md | 115 ++++++++++++++ deployment/chart/templates/NOTES.txt | 25 +++ deployment/chart/templates/_helpers.tpl | 43 ++++++ deployment/chart/templates/deployment.yaml | 144 ++++++++++++++++++ deployment/chart/templates/ingress.yaml | 39 +++++ deployment/chart/templates/pvc.yaml | 29 ++++ deployment/chart/templates/secrets.yaml | 18 +++ deployment/chart/templates/service.yaml | 19 +++ .../chart/templates/serviceaccount.yaml | 11 ++ .../templates/tests/test-connection.yaml | 18 +++ deployment/chart/values.yaml | 135 ++++++++++++++++ deployment/manifests/aws/deployment.yaml | 74 +++++++++ deployment/manifests/deployment.yaml | 43 ++++++ 14 files changed, 718 insertions(+) create mode 100644 deployment/chart/Chart.yaml create mode 100644 deployment/chart/README.md create mode 100644 deployment/chart/templates/NOTES.txt create mode 100644 deployment/chart/templates/_helpers.tpl create mode 100644 deployment/chart/templates/deployment.yaml create mode 100644 deployment/chart/templates/ingress.yaml create mode 100644 deployment/chart/templates/pvc.yaml create mode 100644 deployment/chart/templates/secrets.yaml create mode 100644 deployment/chart/templates/service.yaml create mode 100644 deployment/chart/templates/serviceaccount.yaml create mode 100644 deployment/chart/templates/tests/test-connection.yaml create mode 100644 deployment/chart/values.yaml create mode 100644 deployment/manifests/aws/deployment.yaml create mode 100644 deployment/manifests/deployment.yaml diff --git a/deployment/chart/Chart.yaml b/deployment/chart/Chart.yaml new file mode 100644 index 000000000..283eea6de --- /dev/null +++ b/deployment/chart/Chart.yaml @@ -0,0 +1,5 @@ +apiVersion: v1 +appVersion: "1.0" +description: A Helm chart for code-server +name: code-server +version: 1.0.0 diff --git a/deployment/chart/README.md b/deployment/chart/README.md new file mode 100644 index 000000000..34481eaf7 --- /dev/null +++ b/deployment/chart/README.md @@ -0,0 +1,115 @@ +# code-server + +[code-server](https://github.com/cdr/code-server) code-server is VS Code running +on a remote server, accessible through the browser. + +## TL;DR; + +```console +$ git clone https://github.com/cdr/code-server.git +$ helm install deployment/chart +``` + +## Introduction + +This chart bootstraps a code-server deployment on a +[Kubernetes](http://kubernetes.io) cluster using the [Helm](https://helm.sh) +package manager. + +## Prerequisites + + - Kubernetes 1.6+ + +## Installing the Chart + +To install the chart with the release name `my-release`: + +```console +$ helm install --name my-release deployment/chart +``` + +The command deploys code-server on the Kubernetes cluster in the default +configuration. The [configuration](#configuration) section lists the parameters +that can be configured during installation. + +> **Tip**: List all releases using `helm list` + +## Uninstalling the Chart + +To uninstall/delete the `my-release` deployment: + +```console +$ helm delete my-release +``` + +The command removes all the Kubernetes components associated with the chart and +deletes the release. + +## Configuration + +The following table lists the configurable parameters of the nginx-ingress chart +and their default values. + + +The following table lists the configurable parameters of the code-server chart +and their default values. + +| Parameter | Description | Default | +| --------------------------------- | ------------------------------------------ | --------------------------------------------------------- | +| `image.registry` | Code-server image registry | `docker.io` | +| `image.repository` | Code-server Image name | `codercom/code-server` | +| `image.tag` | Code-server Image tag | `{TAG_NAME}` | +| `image.pullPolicy` | Code-server image pull policy | `IfNotPresent` | +| `nameOverride` | String to partially override code-server.fullname template with a string (will prepend the release name) | `nil` | +| `fullnameOverride` | String to fully override code-server.fullname template with a string | +| `hostnameOverride` | String to fully override code-server container hostname | +| `service.type` | Kubernetes Service type | `NodePort` | +| `service.port` | Service HTTP port | `8443` | +| `ingress.enabled` | Enable ingress controller resource | `false` | +| `ingress.hosts[0].name` | Hostname to your code-server installation | `code-server.local` | +| `ingress.hosts[0].path` | Path within the url structure | `/` | +| `ingress.hosts[0].tls` | Utilize TLS backend in ingress | `false` | +| `ingress.hosts[0].certManager` | Add annotations for cert-manager | `false` | +| `ingress.hosts[0].tlsSecret` | TLS Secret (certificates) | `code-server.local-tls-secret` | +| `ingress.hosts[0].annotations` | Annotations for this host's ingress record | `[]` | +| `ingress.secrets[0].name` | TLS Secret Name | `nil` | +| `ingress.secrets[0].certificate` | TLS Secret Certificate | `nil` | +| `ingress.secrets[0].key` | TLS Secret Key | `nil` | +| `extraArgs` | Additional code-server container arguments | `{}` | +| `extraVars` | Optional environment variables for code-server | `{}` | +| `volumePermissions.enabled` | Enable volume permissions init container | `true` | +| `volumePermissions.securityContext.runAsUser` | User ID for the init container | `0` | +| `securityContext.enabled` | Enable security context | `true` | +| `securityContext.fsGroup` | Group ID for the container | `1000` | +| `securityContext.runAsUser` | User ID for the container | `1000` | +| `resources` | CPU/Memory resource requests/limits | `{}` | +| `persistence.enabled` | Enable persistence using PVC | `true` | +| `persistence.storageClass` | PVC Storage Class for code-server volume | `nil` | +| `persistence.accessMode` | PVC Access Mode for code-server volume | `ReadWriteOnce` | +| `persistence.size` | PVC Storage Request for code-server volume | `8Gi` | +| `extraContainers` | Sidecar containers to add to the code-server pod | `{}` | +| `extraSecretMounts` | Additional code-server server secret mounts | `[]` | +| `extraVolumeMounts` | Additional code-server server volume mounts | `[]` | +| `extraConfigmapMounts` | Additional code-server server configMap volume mounts | `[]` | + +Specify each parameter using the `--set key=value[,key=value]` argument to `helm +install`. For example, + +```console +$ helm install --name my-release \ + --set persistence.enabled=false \ + deployment/chart +``` + +The above command sets the the persistence storage to false. + +Alternatively, a YAML file that specifies the values for the above parameters +can be provided while installing the chart. For example, + +```console +$ helm install --name my-release -f values.yaml deployment/chart +``` + +> **Tip**: You can use the default [values.yaml](values.yaml) + + diff --git a/deployment/chart/templates/NOTES.txt b/deployment/chart/templates/NOTES.txt new file mode 100644 index 000000000..e9af85992 --- /dev/null +++ b/deployment/chart/templates/NOTES.txt @@ -0,0 +1,25 @@ +1. Get the application URL by running these commands: +{{- if .Values.ingress.enabled }} +{{- range $host := .Values.ingress.hosts }} + {{- range .paths }} + http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ . }} + {{- end }} +{{- end }} +{{- else if contains "NodePort" .Values.service.type }} + export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "code-server.fullname" . }}) + export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}") + echo http://$NODE_IP:$NODE_PORT +{{- else if contains "LoadBalancer" .Values.service.type }} + NOTE: It may take a few minutes for the LoadBalancer IP to be available. + You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "code-server.fullname" . }}' + export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "code-server.fullname" . }} -o jsonpath='{.status.loadBalancer.ingress[0].ip}') + echo http://$SERVICE_IP:{{ .Values.service.port }} +{{- else if contains "ClusterIP" .Values.service.type }} + export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "code-server.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") + echo "Visit http://127.0.0.1:8080 to use your application" + kubectl port-forward $POD_NAME 8080:80 +{{- end }} + +Administrator credentials: + + Password : $(kubectl get secret --namespace {{ .Release.Namespace }} {{ template "code-server.fullname" . }} -o jsonpath="{.data.password}" | base64 --decode) diff --git a/deployment/chart/templates/_helpers.tpl b/deployment/chart/templates/_helpers.tpl new file mode 100644 index 000000000..811252b9e --- /dev/null +++ b/deployment/chart/templates/_helpers.tpl @@ -0,0 +1,43 @@ +{{/* vim: set filetype=mustache: */}} +{{/* +Expand the name of the chart. +*/}} +{{- define "code-server.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}} +{{- end -}} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "code-server.fullname" -}} +{{- if .Values.fullnameOverride -}} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}} +{{- else -}} +{{- $name := default .Chart.Name .Values.nameOverride -}} +{{- if contains $name .Release.Name -}} +{{- .Release.Name | trunc 63 | trimSuffix "-" -}} +{{- else -}} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}} +{{- end -}} +{{- end -}} +{{- end -}} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "code-server.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}} +{{- end -}} + +{{/* +Create the name of the service account to use +*/}} +{{- define "code-server.serviceAccountName" -}} +{{- if .Values.serviceAccount.create -}} + {{ default (include "code-server.fullname" .) .Values.serviceAccount.name }} +{{- else -}} + {{ default "default" .Values.serviceAccount.name }} +{{- end -}} +{{- end -}} diff --git a/deployment/chart/templates/deployment.yaml b/deployment/chart/templates/deployment.yaml new file mode 100644 index 000000000..63a84d61d --- /dev/null +++ b/deployment/chart/templates/deployment.yaml @@ -0,0 +1,144 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "code-server.fullname" . }} + labels: + app.kubernetes.io/name: {{ include "code-server.name" . }} + helm.sh/chart: {{ include "code-server.chart" . }} + app.kubernetes.io/instance: {{ .Release.Name }} + app.kubernetes.io/managed-by: {{ .Release.Service }} +spec: + replicas: 1 + strategy: + type: Recreate + selector: + matchLabels: + app.kubernetes.io/name: {{ include "code-server.name" . }} + app.kubernetes.io/instance: {{ .Release.Name }} + template: + metadata: + labels: + app.kubernetes.io/name: {{ include "code-server.name" . }} + app.kubernetes.io/instance: {{ .Release.Name }} + spec: + {{- if .Values.hostnameOverride }} + hostname: {{ .Values.hostnameOverride }} + {{- end }} + {{- if .Values.securityContext.enabled }} + securityContext: + fsGroup: {{ .Values.securityContext.fsGroup }} + {{- end }} + {{- if and .Values.volumePermissions.enabled .Values.persistence.enabled }} + initContainers: + - name: init-chmod-data + image: busybox:latest + imagePullPolicy: IfNotPresent + command: + - sh + - -c + - | + chown -R {{ .Values.securityContext.runAsUser }}:{{ .Values.securityContext.fsGroup }} /home/coder + securityContext: + runAsUser: {{ .Values.volumePermissions.securityContext.runAsUser }} + volumeMounts: + - name: data + mountPath: /home/coder + {{- end }} + containers: +{{- if .Values.extraContainers }} +{{ toYaml .Values.extraContainers | indent 8}} +{{- end }} + - name: {{ .Chart.Name }} + image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + {{- if .Values.securityContext.enabled }} + securityContext: + runAsUser: {{ .Values.securityContext.runAsUser }} + {{- end }} + env: + {{- if .Values.extraVars }} +{{ toYaml .Values.extraVars | indent 10 }} + {{- end }} + - name: PASSWORD + valueFrom: + secretKeyRef: + {{- if .Values.existingSecret }} + name: {{ .Values.existingSecret }} + {{- else }} + name: {{ template "code-server.fullname" . }} + {{- end }} + key: password + {{- if .Values.extraArgs }} + args: +{{ toYaml .Values.extraArgs | indent 10 }} + {{- end }} + volumeMounts: + - name: data + mountPath: /home/coder/project + subPath: project + - name: data + mountPath: /home/coder/.local/share/code-server + subPath: code-server + {{- range .Values.extraConfigmapMounts }} + - name: {{ .name }} + mountPath: {{ .mountPath }} + subPath: {{ .subPath | default "" }} + readOnly: {{ .readOnly }} + {{- end }} + {{- range .Values.extraSecretMounts }} + - name: {{ .name }} + mountPath: {{ .mountPath }} + readOnly: {{ .readOnly }} + {{- end }} + {{- range .Values.extraVolumeMounts }} + - name: {{ .name }} + mountPath: {{ .mountPath }} + subPath: {{ .subPath | default "" }} + readOnly: {{ .readOnly }} + {{- end }} + ports: + - name: http + containerPort: 8443 + protocol: TCP + livenessProbe: + httpGet: + path: / + port: http + readinessProbe: + httpGet: + path: / + port: http + resources: + {{- toYaml .Values.resources | nindent 12 }} + {{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} + serviceAccountName: {{ template "code-server.serviceAccountName" . }} + volumes: + - name: data + {{- if .Values.persistence.enabled }} + persistentVolumeClaim: + claimName: {{ .Values.persistence.existingClaim | default (include "code-server.fullname" .) }} + {{- else }} + emptyDir: {} + {{- end -}} + {{- range .Values.extraSecretMounts }} + - name: {{ .name }} + secret: + secretName: {{ .secretName }} + defaultMode: {{ .defaultMode }} + {{- end }} + {{- range .Values.extraVolumeMounts }} + - name: {{ .name }} + persistentVolumeClaim: + claimName: {{ .existingClaim }} + {{- end }} diff --git a/deployment/chart/templates/ingress.yaml b/deployment/chart/templates/ingress.yaml new file mode 100644 index 000000000..1858bb5f2 --- /dev/null +++ b/deployment/chart/templates/ingress.yaml @@ -0,0 +1,39 @@ +{{- if .Values.ingress.enabled -}} +{{- $fullName := include "code-server.fullname" . -}} +apiVersion: extensions/v1beta1 +kind: Ingress +metadata: + name: {{ $fullName }} + labels: + app.kubernetes.io/name: {{ include "code-server.name" . }} + helm.sh/chart: {{ include "code-server.chart" . }} + app.kubernetes.io/instance: {{ .Release.Name }} + app.kubernetes.io/managed-by: {{ .Release.Service }} + {{- with .Values.ingress.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: +{{- if .Values.ingress.tls }} + tls: + {{- range .Values.ingress.tls }} + - hosts: + {{- range .hosts }} + - {{ . | quote }} + {{- end }} + secretName: {{ .secretName }} + {{- end }} +{{- end }} + rules: + {{- range .Values.ingress.hosts }} + - host: {{ .host | quote }} + http: + paths: + {{- range .paths }} + - path: {{ . }} + backend: + serviceName: {{ $fullName }} + servicePort: http + {{- end }} + {{- end }} +{{- end }} diff --git a/deployment/chart/templates/pvc.yaml b/deployment/chart/templates/pvc.yaml new file mode 100644 index 000000000..f29301477 --- /dev/null +++ b/deployment/chart/templates/pvc.yaml @@ -0,0 +1,29 @@ +{{- if and .Values.persistence.enabled (not .Values.persistence.existingClaim) }} +kind: PersistentVolumeClaim +apiVersion: v1 +metadata: + name: {{ include "code-server.fullname" . }} + namespace: {{ .Release.Namespace }} +{{- with .Values.persistence.annotations }} + annotations: +{{ toYaml . | indent 4 }} +{{- end }} + labels: + app.kubernetes.io/name: {{ include "code-server.name" . }} + helm.sh/chart: {{ include "code-server.chart" . }} + app.kubernetes.io/instance: {{ .Release.Name }} + app.kubernetes.io/managed-by: {{ .Release.Service }} +spec: + accessModes: + - {{ .Values.persistence.accessMode | quote }} + resources: + requests: + storage: {{ .Values.persistence.size | quote }} +{{- if .Values.persistence.storageClass }} +{{- if (eq "-" .Values.persistence.storageClass) }} + storageClassName: "" +{{- else }} + storageClassName: "{{ .Values.persistence.storageClass }}" +{{- end }} +{{- end }} +{{- end }} diff --git a/deployment/chart/templates/secrets.yaml b/deployment/chart/templates/secrets.yaml new file mode 100644 index 000000000..6c600417a --- /dev/null +++ b/deployment/chart/templates/secrets.yaml @@ -0,0 +1,18 @@ +apiVersion: v1 +kind: Secret +metadata: + name: {{ include "code-server.fullname" . }} + annotations: + "helm.sh/hook": "pre-install" + labels: + app.kubernetes.io/name: {{ include "code-server.name" . }} + helm.sh/chart: {{ include "code-server.chart" . }} + app.kubernetes.io/instance: {{ .Release.Name }} + app.kubernetes.io/managed-by: {{ .Release.Service }} +type: Opaque +data: + {{ if .Values.password }} + password: "{{ .Values.password | b64enc }}" + {{ else }} + password: "{{ randAlphaNum 24 | b64enc }}" + {{ end }} diff --git a/deployment/chart/templates/service.yaml b/deployment/chart/templates/service.yaml new file mode 100644 index 000000000..038b6cd0d --- /dev/null +++ b/deployment/chart/templates/service.yaml @@ -0,0 +1,19 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ include "code-server.fullname" . }} + labels: + app.kubernetes.io/name: {{ include "code-server.name" . }} + helm.sh/chart: {{ include "code-server.chart" . }} + app.kubernetes.io/instance: {{ .Release.Name }} + app.kubernetes.io/managed-by: {{ .Release.Service }} +spec: + type: {{ .Values.service.type }} + ports: + - port: {{ .Values.service.port }} + targetPort: http + protocol: TCP + name: http + selector: + app.kubernetes.io/name: {{ include "code-server.name" . }} + app.kubernetes.io/instance: {{ .Release.Name }} diff --git a/deployment/chart/templates/serviceaccount.yaml b/deployment/chart/templates/serviceaccount.yaml new file mode 100644 index 000000000..df9e1e375 --- /dev/null +++ b/deployment/chart/templates/serviceaccount.yaml @@ -0,0 +1,11 @@ +{{- if or .Values.serviceAccount.create -}} +apiVersion: v1 +kind: ServiceAccount +metadata: + labels: + app.kubernetes.io/name: {{ include "code-server.name" . }} + helm.sh/chart: {{ include "code-server.chart" . }} + app.kubernetes.io/instance: {{ .Release.Name }} + app.kubernetes.io/managed-by: {{ .Release.Service }} + name: {{ template "code-server.serviceAccountName" . }} +{{- end -}} diff --git a/deployment/chart/templates/tests/test-connection.yaml b/deployment/chart/templates/tests/test-connection.yaml new file mode 100644 index 000000000..2e67f56ec --- /dev/null +++ b/deployment/chart/templates/tests/test-connection.yaml @@ -0,0 +1,18 @@ +apiVersion: v1 +kind: Pod +metadata: + name: "{{ include "code-server.fullname" . }}-test-connection" + labels: + app.kubernetes.io/name: {{ include "code-server.name" . }} + helm.sh/chart: {{ include "code-server.chart" . }} + app.kubernetes.io/instance: {{ .Release.Name }} + app.kubernetes.io/managed-by: {{ .Release.Service }} + annotations: + "helm.sh/hook": test-success +spec: + containers: + - name: wget + image: busybox + command: ['wget'] + args: ['{{ include "code-server.fullname" . }}:{{ .Values.service.port }}'] + restartPolicy: Never diff --git a/deployment/chart/values.yaml b/deployment/chart/values.yaml new file mode 100644 index 000000000..102ef620f --- /dev/null +++ b/deployment/chart/values.yaml @@ -0,0 +1,135 @@ +# Default values for code-server. +# This is a YAML-formatted file. +# Declare variables to be passed into your templates. + +image: + repository: codercom/code-server + tag: 1.1156-vsc1.33.1 + pullPolicy: Always + +nameOverride: "" +fullnameOverride: "" +hostnameOverride: "" + +service: + type: NodePort + port: 8443 + +ingress: + enabled: false + #annotations: + # kubernetes.io/ingress.class: nginx + # kubernetes.io/tls-acme: "true" + #hosts: + # - host: code-server.example.loc + # paths: + # - / + + #tls: + # - secretName: code-server + # hosts: + # - code-server.example.loc + +# Optional additional arguments +extraArgs: [] +# - --allow-http +# - --no-auth + +# Optional additional environment variables +extraVars: [] +# - name: DISABLE_TELEMETRY +# value: true + +## +## Init containers parameters: +## volumePermissions: Change the owner of the persist volume mountpoint to RunAsUser:fsGroup +## +volumePermissions: + enabled: true + securityContext: + runAsUser: 0 + +## Pod Security Context +## ref: https://kubernetes.io/docs/tasks/configure-pod-container/security-context/ +## +securityContext: + enabled: true + fsGroup: 1000 + runAsUser: 1000 + +resources: {} + # We usually recommend not to specify default resources and to leave this as a conscious + # choice for the user. This also increases chances charts run on environments with little + # resources, such as Minikube. If you do want to specify resources, uncomment the following + # lines, adjust them as necessary, and remove the curly braces after 'resources:'. + # limits: + # cpu: 100m + # memory: 128Mi + # requests: + # cpu: 100m + # memory: 1000Mi + +nodeSelector: {} + +tolerations: [] + +affinity: {} + +## Persist data to a persistent volume +persistence: + enabled: true + ## code-server data Persistent Volume Storage Class + ## If defined, storageClassName: + ## If set to "-", storageClassName: "", which disables dynamic provisioning + ## If undefined (the default) or set to null, no storageClassName spec is + ## set, choosing the default provisioner. (gp2 on AWS, standard on + ## GKE, AWS & OpenStack) + ## + # storageClass: "-" + accessMode: ReadWriteOnce + size: 1Gi + annotations: {} + +serviceAccount: + create: true + name: + +## Enable an Specify container in extraContainers. +## This is meant to allow adding code-server dependencies, like docker-dind. +extraContainers: | +#- name: docker-dind +# image: docker:19.03-dind +# imagePullPolicy: IfNotPresent +# resources: +# requests: +# cpu: 250m +# memory: 256M +# securityContext: +# privileged: true +# procMount: Default +# env: +# - name: DOCKER_TLS_CERTDIR +# value: "" +# - name: DOCKER_DRIVER +# value: "overlay2" + +## Additional code-server secret mounts +extraSecretMounts: [] + # - name: secret-files + # mountPath: /etc/secrets + # secretName: code-server-secret-files + # readOnly: true + +## Additional code-server volume mounts +extraVolumeMounts: [] + # - name: extra-volume + # mountPath: /mnt/volume + # readOnly: true + # existingClaim: volume-claim + +extraConfigmapMounts: [] + # - name: certs-configmap + # mountPath: /etc/code-server/ssl/ + # subPath: certificates.crt # (optional) + # configMap: certs-configmap + # readOnly: true diff --git a/deployment/manifests/aws/deployment.yaml b/deployment/manifests/aws/deployment.yaml new file mode 100644 index 000000000..6f265634d --- /dev/null +++ b/deployment/manifests/aws/deployment.yaml @@ -0,0 +1,74 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: code-server +--- +apiVersion: v1 +kind: Service +metadata: + name: code-server + namespace: code-server +spec: + ports: + - port: 8443 + name: https + protocol: TCP + selector: + app: code-server + type: ClusterIP +--- +kind: StorageClass +apiVersion: storage.k8s.io/v1 +metadata: + name: gp2 + annotations: + storageclass.kubernetes.io/is-default-class: "true" +provisioner: kubernetes.io/aws-ebs +parameters: + type: gp2 + fsType: ext4 +--- +kind: PersistentVolumeClaim +apiVersion: v1 +metadata: + name: code-store + namespace: code-server +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 60Gi +--- +apiVersion: extensions/v1beta1 +kind: Deployment +metadata: + labels: + app: code-server + name: code-server + namespace: code-server +spec: + selector: + matchLabels: + app: code-server + replicas: 1 + template: + metadata: + labels: + app: code-server + spec: + containers: + - image: codercom/code-server + imagePullPolicy: Always + name: code-servery + ports: + - containerPort: 8443 + name: https + volumeMounts: + - name: code-server-storage + mountPath: /go/src + volumes: + - name: code-server-storage + persistentVolumeClaim: + claimName: code-store + diff --git a/deployment/manifests/deployment.yaml b/deployment/manifests/deployment.yaml new file mode 100644 index 000000000..a032d5234 --- /dev/null +++ b/deployment/manifests/deployment.yaml @@ -0,0 +1,43 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: code-server +--- +apiVersion: v1 +kind: Service +metadata: + name: code-server + namespace: code-server +spec: + ports: + - port: 8443 + name: https + protocol: TCP + selector: + app: code-server + type: ClusterIP +--- +apiVersion: extensions/v1beta1 +kind: Deployment +metadata: + labels: + app: code-server + name: code-server + namespace: code-server +spec: + selector: + matchLabels: + app: code-server + replicas: 1 + template: + metadata: + labels: + app: code-server + spec: + containers: + - image: codercom/code-server + imagePullPolicy: Always + name: code-server + ports: + - containerPort: 8443 + name: https From 85d5858b1df861fd1d8cb0a4379bab55ebe25adf Mon Sep 17 00:00:00 2001 From: Matthew Beckett Date: Wed, 2 Sep 2020 20:43:57 +0100 Subject: [PATCH 002/247] Remove Kubernetes native manifests --- deployment/manifests/aws/deployment.yaml | 74 ------------------------ deployment/manifests/deployment.yaml | 43 -------------- 2 files changed, 117 deletions(-) delete mode 100644 deployment/manifests/aws/deployment.yaml delete mode 100644 deployment/manifests/deployment.yaml diff --git a/deployment/manifests/aws/deployment.yaml b/deployment/manifests/aws/deployment.yaml deleted file mode 100644 index 6f265634d..000000000 --- a/deployment/manifests/aws/deployment.yaml +++ /dev/null @@ -1,74 +0,0 @@ -apiVersion: v1 -kind: Namespace -metadata: - name: code-server ---- -apiVersion: v1 -kind: Service -metadata: - name: code-server - namespace: code-server -spec: - ports: - - port: 8443 - name: https - protocol: TCP - selector: - app: code-server - type: ClusterIP ---- -kind: StorageClass -apiVersion: storage.k8s.io/v1 -metadata: - name: gp2 - annotations: - storageclass.kubernetes.io/is-default-class: "true" -provisioner: kubernetes.io/aws-ebs -parameters: - type: gp2 - fsType: ext4 ---- -kind: PersistentVolumeClaim -apiVersion: v1 -metadata: - name: code-store - namespace: code-server -spec: - accessModes: - - ReadWriteOnce - resources: - requests: - storage: 60Gi ---- -apiVersion: extensions/v1beta1 -kind: Deployment -metadata: - labels: - app: code-server - name: code-server - namespace: code-server -spec: - selector: - matchLabels: - app: code-server - replicas: 1 - template: - metadata: - labels: - app: code-server - spec: - containers: - - image: codercom/code-server - imagePullPolicy: Always - name: code-servery - ports: - - containerPort: 8443 - name: https - volumeMounts: - - name: code-server-storage - mountPath: /go/src - volumes: - - name: code-server-storage - persistentVolumeClaim: - claimName: code-store - diff --git a/deployment/manifests/deployment.yaml b/deployment/manifests/deployment.yaml deleted file mode 100644 index a032d5234..000000000 --- a/deployment/manifests/deployment.yaml +++ /dev/null @@ -1,43 +0,0 @@ -apiVersion: v1 -kind: Namespace -metadata: - name: code-server ---- -apiVersion: v1 -kind: Service -metadata: - name: code-server - namespace: code-server -spec: - ports: - - port: 8443 - name: https - protocol: TCP - selector: - app: code-server - type: ClusterIP ---- -apiVersion: extensions/v1beta1 -kind: Deployment -metadata: - labels: - app: code-server - name: code-server - namespace: code-server -spec: - selector: - matchLabels: - app: code-server - replicas: 1 - template: - metadata: - labels: - app: code-server - spec: - containers: - - image: codercom/code-server - imagePullPolicy: Always - name: code-server - ports: - - containerPort: 8443 - name: https From d33df756629f137d77c4c10f161c3b398cc2b561 Mon Sep 17 00:00:00 2001 From: Matthew Beckett Date: Wed, 2 Sep 2020 21:00:12 +0100 Subject: [PATCH 003/247] Add updated Helm chart --- charts/code-server/Chart.yaml | 23 +++++++++++++++++ .../chart => charts/code-server}/README.md | 0 .../code-server}/templates/NOTES.txt | 0 .../code-server}/templates/_helpers.tpl | 0 .../code-server}/templates/deployment.yaml | 0 .../code-server}/templates/ingress.yaml | 0 .../code-server}/templates/pvc.yaml | 0 .../code-server}/templates/secrets.yaml | 0 .../code-server}/templates/service.yaml | 0 .../templates/serviceaccount.yaml | 0 .../templates/tests/test-connection.yaml | 0 .../chart => charts/code-server}/values.yaml | 25 +++++++++++++++++++ deployment/chart/Chart.yaml | 5 ---- 13 files changed, 48 insertions(+), 5 deletions(-) create mode 100644 charts/code-server/Chart.yaml rename {deployment/chart => charts/code-server}/README.md (100%) rename {deployment/chart => charts/code-server}/templates/NOTES.txt (100%) rename {deployment/chart => charts/code-server}/templates/_helpers.tpl (100%) rename {deployment/chart => charts/code-server}/templates/deployment.yaml (100%) rename {deployment/chart => charts/code-server}/templates/ingress.yaml (100%) rename {deployment/chart => charts/code-server}/templates/pvc.yaml (100%) rename {deployment/chart => charts/code-server}/templates/secrets.yaml (100%) rename {deployment/chart => charts/code-server}/templates/service.yaml (100%) rename {deployment/chart => charts/code-server}/templates/serviceaccount.yaml (100%) rename {deployment/chart => charts/code-server}/templates/tests/test-connection.yaml (100%) rename {deployment/chart => charts/code-server}/values.yaml (85%) delete mode 100644 deployment/chart/Chart.yaml diff --git a/charts/code-server/Chart.yaml b/charts/code-server/Chart.yaml new file mode 100644 index 000000000..ba9281ab8 --- /dev/null +++ b/charts/code-server/Chart.yaml @@ -0,0 +1,23 @@ +apiVersion: v2 +name: code-server +description: A Helm chart for cdr/code-server + +# A chart can be either an 'application' or a 'library' chart. +# +# Application charts are a collection of templates that can be packaged into versioned archives +# to be deployed. +# +# Library charts provide useful utilities or functions for the chart developer. They're included as +# a dependency of application charts to inject those utilities and functions into the rendering +# pipeline. Library charts do not define any templates and therefore cannot be deployed. +type: application + +# This is the chart version. This version number should be incremented each time you make changes +# to the chart and its templates, including the app version. +# Versions are expected to follow Semantic Versioning (https://semver.org/) +version: 1.0.0 + +# This is the version number of the application being deployed. This version number should be +# incremented each time you make changes to the application. Versions are not expected to +# follow Semantic Versioning. They should reflect the version the application is using. +appVersion: 3.5.0 diff --git a/deployment/chart/README.md b/charts/code-server/README.md similarity index 100% rename from deployment/chart/README.md rename to charts/code-server/README.md diff --git a/deployment/chart/templates/NOTES.txt b/charts/code-server/templates/NOTES.txt similarity index 100% rename from deployment/chart/templates/NOTES.txt rename to charts/code-server/templates/NOTES.txt diff --git a/deployment/chart/templates/_helpers.tpl b/charts/code-server/templates/_helpers.tpl similarity index 100% rename from deployment/chart/templates/_helpers.tpl rename to charts/code-server/templates/_helpers.tpl diff --git a/deployment/chart/templates/deployment.yaml b/charts/code-server/templates/deployment.yaml similarity index 100% rename from deployment/chart/templates/deployment.yaml rename to charts/code-server/templates/deployment.yaml diff --git a/deployment/chart/templates/ingress.yaml b/charts/code-server/templates/ingress.yaml similarity index 100% rename from deployment/chart/templates/ingress.yaml rename to charts/code-server/templates/ingress.yaml diff --git a/deployment/chart/templates/pvc.yaml b/charts/code-server/templates/pvc.yaml similarity index 100% rename from deployment/chart/templates/pvc.yaml rename to charts/code-server/templates/pvc.yaml diff --git a/deployment/chart/templates/secrets.yaml b/charts/code-server/templates/secrets.yaml similarity index 100% rename from deployment/chart/templates/secrets.yaml rename to charts/code-server/templates/secrets.yaml diff --git a/deployment/chart/templates/service.yaml b/charts/code-server/templates/service.yaml similarity index 100% rename from deployment/chart/templates/service.yaml rename to charts/code-server/templates/service.yaml diff --git a/deployment/chart/templates/serviceaccount.yaml b/charts/code-server/templates/serviceaccount.yaml similarity index 100% rename from deployment/chart/templates/serviceaccount.yaml rename to charts/code-server/templates/serviceaccount.yaml diff --git a/deployment/chart/templates/tests/test-connection.yaml b/charts/code-server/templates/tests/test-connection.yaml similarity index 100% rename from deployment/chart/templates/tests/test-connection.yaml rename to charts/code-server/templates/tests/test-connection.yaml diff --git a/deployment/chart/values.yaml b/charts/code-server/values.yaml similarity index 85% rename from deployment/chart/values.yaml rename to charts/code-server/values.yaml index 102ef620f..ebbdbc167 100644 --- a/deployment/chart/values.yaml +++ b/charts/code-server/values.yaml @@ -2,15 +2,40 @@ # This is a YAML-formatted file. # Declare variables to be passed into your templates. +replicaCount: 1 + image: repository: codercom/code-server tag: 1.1156-vsc1.33.1 pullPolicy: Always +imagePullSecrets: [] nameOverride: "" fullnameOverride: "" hostnameOverride: "" +serviceAccount: + # Specifies whether a service account should be created + create: true + # Annotations to add to the service account + annotations: {} + # The name of the service account to use. + # If not set and create is true, a name is generated using the fullname template + name: "" + +podAnnotations: {} + +podSecurityContext: {} + # fsGroup: 2000 + +securityContext: {} + # capabilities: + # drop: + # - ALL + # readOnlyRootFilesystem: true + # runAsNonRoot: true + # runAsUser: 1000 + service: type: NodePort port: 8443 diff --git a/deployment/chart/Chart.yaml b/deployment/chart/Chart.yaml deleted file mode 100644 index 283eea6de..000000000 --- a/deployment/chart/Chart.yaml +++ /dev/null @@ -1,5 +0,0 @@ -apiVersion: v1 -appVersion: "1.0" -description: A Helm chart for code-server -name: code-server -version: 1.0.0 From 34f8c77a0309013742ac5b90ef6eb61d64791fb1 Mon Sep 17 00:00:00 2001 From: Matthew Beckett Date: Wed, 2 Sep 2020 21:05:57 +0100 Subject: [PATCH 004/247] Change service default to ClusterIP and add helmignore --- charts/code-server/.helmignore | 23 +++++++++++++++++++++++ charts/code-server/values.yaml | 2 +- 2 files changed, 24 insertions(+), 1 deletion(-) create mode 100644 charts/code-server/.helmignore diff --git a/charts/code-server/.helmignore b/charts/code-server/.helmignore new file mode 100644 index 000000000..0e8a0eb36 --- /dev/null +++ b/charts/code-server/.helmignore @@ -0,0 +1,23 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*.orig +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ diff --git a/charts/code-server/values.yaml b/charts/code-server/values.yaml index ebbdbc167..408741115 100644 --- a/charts/code-server/values.yaml +++ b/charts/code-server/values.yaml @@ -37,7 +37,7 @@ securityContext: {} # runAsUser: 1000 service: - type: NodePort + type: ClusterIP port: 8443 ingress: From 341cb342b25a316a2dcc68ecd98c4d9f7cac7cdb Mon Sep 17 00:00:00 2001 From: Matthew Beckett Date: Wed, 2 Sep 2020 21:53:52 +0100 Subject: [PATCH 005/247] Patch helm chart to stable with v3.5.0 --- charts/code-server/templates/_helpers.tpl | 20 +++++++++++++ charts/code-server/templates/deployment.yaml | 2 +- charts/code-server/templates/ingress.yaml | 30 +++++++++++--------- charts/code-server/values.yaml | 2 +- 4 files changed, 38 insertions(+), 16 deletions(-) diff --git a/charts/code-server/templates/_helpers.tpl b/charts/code-server/templates/_helpers.tpl index 811252b9e..bb36e8c21 100644 --- a/charts/code-server/templates/_helpers.tpl +++ b/charts/code-server/templates/_helpers.tpl @@ -31,6 +31,26 @@ Create chart name and version as used by the chart label. {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}} {{- end -}} +{{/* +Common labels +*/}} +{{- define "code-server.labels" -}} +helm.sh/chart: {{ include "code-server.chart" . }} +{{ include "code-server.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "code-server.selectorLabels" -}} +app.kubernetes.io/name: {{ include "code-server.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + {{/* Create the name of the service account to use */}} diff --git a/charts/code-server/templates/deployment.yaml b/charts/code-server/templates/deployment.yaml index 63a84d61d..52c1ee954 100644 --- a/charts/code-server/templates/deployment.yaml +++ b/charts/code-server/templates/deployment.yaml @@ -98,7 +98,7 @@ spec: {{- end }} ports: - name: http - containerPort: 8443 + containerPort: 8080 protocol: TCP livenessProbe: httpGet: diff --git a/charts/code-server/templates/ingress.yaml b/charts/code-server/templates/ingress.yaml index 1858bb5f2..07a3abd0b 100644 --- a/charts/code-server/templates/ingress.yaml +++ b/charts/code-server/templates/ingress.yaml @@ -1,39 +1,41 @@ {{- if .Values.ingress.enabled -}} {{- $fullName := include "code-server.fullname" . -}} +{{- $svcPort := .Values.service.port -}} +{{- if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}} +apiVersion: networking.k8s.io/v1beta1 +{{- else -}} apiVersion: extensions/v1beta1 +{{- end }} kind: Ingress metadata: name: {{ $fullName }} labels: - app.kubernetes.io/name: {{ include "code-server.name" . }} - helm.sh/chart: {{ include "code-server.chart" . }} - app.kubernetes.io/instance: {{ .Release.Name }} - app.kubernetes.io/managed-by: {{ .Release.Service }} + {{- include "code-server.labels" . | nindent 4 }} {{- with .Values.ingress.annotations }} annotations: {{- toYaml . | nindent 4 }} {{- end }} spec: -{{- if .Values.ingress.tls }} + {{- if .Values.ingress.tls }} tls: - {{- range .Values.ingress.tls }} + {{- range .Values.ingress.tls }} - hosts: - {{- range .hosts }} + {{- range .hosts }} - {{ . | quote }} - {{- end }} + {{- end }} secretName: {{ .secretName }} + {{- end }} {{- end }} -{{- end }} rules: - {{- range .Values.ingress.hosts }} + {{- range .Values.ingress.hosts }} - host: {{ .host | quote }} http: paths: - {{- range .paths }} + {{- range .paths }} - path: {{ . }} backend: serviceName: {{ $fullName }} - servicePort: http - {{- end }} + servicePort: {{ $svcPort }} + {{- end }} + {{- end }} {{- end }} -{{- end }} diff --git a/charts/code-server/values.yaml b/charts/code-server/values.yaml index 408741115..72dc5501e 100644 --- a/charts/code-server/values.yaml +++ b/charts/code-server/values.yaml @@ -6,7 +6,7 @@ replicaCount: 1 image: repository: codercom/code-server - tag: 1.1156-vsc1.33.1 + tag: '3.5.0' pullPolicy: Always imagePullSecrets: [] From 559d05bb7b5e1654e99dc7fb004be97fe8297758 Mon Sep 17 00:00:00 2001 From: Matthew Beckett Date: Wed, 2 Sep 2020 22:02:37 +0100 Subject: [PATCH 006/247] Update readme and service port --- charts/code-server/README.md | 77 ++++++++++++++++++---------------- charts/code-server/values.yaml | 2 +- 2 files changed, 41 insertions(+), 38 deletions(-) diff --git a/charts/code-server/README.md b/charts/code-server/README.md index 34481eaf7..0044909c0 100644 --- a/charts/code-server/README.md +++ b/charts/code-server/README.md @@ -1,5 +1,7 @@ # code-server +![Version: 1.0.0](https://img.shields.io/badge/Version-1.0.0-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: 3.5.0](https://img.shields.io/badge/AppVersion-3.5.0-informational?style=flat-square) + [code-server](https://github.com/cdr/code-server) code-server is VS Code running on a remote server, accessible through the browser. @@ -54,43 +56,44 @@ and their default values. The following table lists the configurable parameters of the code-server chart and their default values. -| Parameter | Description | Default | -| --------------------------------- | ------------------------------------------ | --------------------------------------------------------- | -| `image.registry` | Code-server image registry | `docker.io` | -| `image.repository` | Code-server Image name | `codercom/code-server` | -| `image.tag` | Code-server Image tag | `{TAG_NAME}` | -| `image.pullPolicy` | Code-server image pull policy | `IfNotPresent` | -| `nameOverride` | String to partially override code-server.fullname template with a string (will prepend the release name) | `nil` | -| `fullnameOverride` | String to fully override code-server.fullname template with a string | -| `hostnameOverride` | String to fully override code-server container hostname | -| `service.type` | Kubernetes Service type | `NodePort` | -| `service.port` | Service HTTP port | `8443` | -| `ingress.enabled` | Enable ingress controller resource | `false` | -| `ingress.hosts[0].name` | Hostname to your code-server installation | `code-server.local` | -| `ingress.hosts[0].path` | Path within the url structure | `/` | -| `ingress.hosts[0].tls` | Utilize TLS backend in ingress | `false` | -| `ingress.hosts[0].certManager` | Add annotations for cert-manager | `false` | -| `ingress.hosts[0].tlsSecret` | TLS Secret (certificates) | `code-server.local-tls-secret` | -| `ingress.hosts[0].annotations` | Annotations for this host's ingress record | `[]` | -| `ingress.secrets[0].name` | TLS Secret Name | `nil` | -| `ingress.secrets[0].certificate` | TLS Secret Certificate | `nil` | -| `ingress.secrets[0].key` | TLS Secret Key | `nil` | -| `extraArgs` | Additional code-server container arguments | `{}` | -| `extraVars` | Optional environment variables for code-server | `{}` | -| `volumePermissions.enabled` | Enable volume permissions init container | `true` | -| `volumePermissions.securityContext.runAsUser` | User ID for the init container | `0` | -| `securityContext.enabled` | Enable security context | `true` | -| `securityContext.fsGroup` | Group ID for the container | `1000` | -| `securityContext.runAsUser` | User ID for the container | `1000` | -| `resources` | CPU/Memory resource requests/limits | `{}` | -| `persistence.enabled` | Enable persistence using PVC | `true` | -| `persistence.storageClass` | PVC Storage Class for code-server volume | `nil` | -| `persistence.accessMode` | PVC Access Mode for code-server volume | `ReadWriteOnce` | -| `persistence.size` | PVC Storage Request for code-server volume | `8Gi` | -| `extraContainers` | Sidecar containers to add to the code-server pod | `{}` | -| `extraSecretMounts` | Additional code-server server secret mounts | `[]` | -| `extraVolumeMounts` | Additional code-server server volume mounts | `[]` | -| `extraConfigmapMounts` | Additional code-server server configMap volume mounts | `[]` | +## Values + +| Key | Type | Default | Description | +|-----|------|---------|-------------| +| affinity | object | `{}` | | +| extraArgs | list | `[]` | | +| extraConfigmapMounts | list | `[]` | | +| extraContainers | string | `""` | | +| extraSecretMounts | list | `[]` | | +| extraVars | list | `[]` | | +| extraVolumeMounts | list | `[]` | | +| fullnameOverride | string | `""` | | +| hostnameOverride | string | `""` | | +| image.pullPolicy | string | `"Always"` | | +| image.repository | string | `"codercom/code-server"` | | +| image.tag | string | `"3.5.0"` | | +| imagePullSecrets | list | `[]` | | +| ingress.enabled | bool | `false` | | +| nameOverride | string | `""` | | +| nodeSelector | object | `{}` | | +| persistence.accessMode | string | `"ReadWriteOnce"` | | +| persistence.annotations | object | `{}` | | +| persistence.enabled | bool | `true` | | +| persistence.size | string | `"1Gi"` | | +| podAnnotations | object | `{}` | | +| podSecurityContext | object | `{}` | | +| replicaCount | int | `1` | | +| resources | object | `{}` | | +| securityContext.enabled | bool | `true` | | +| securityContext.fsGroup | int | `1000` | | +| securityContext.runAsUser | int | `1000` | | +| service.port | int | `8443` | | +| service.type | string | `"ClusterIP"` | | +| serviceAccount.create | bool | `true` | | +| serviceAccount.name | string | `nil` | | +| tolerations | list | `[]` | | +| volumePermissions.enabled | bool | `true` | | +| volumePermissions.securityContext.runAsUser | int | `0` | | Specify each parameter using the `--set key=value[,key=value]` argument to `helm install`. For example, diff --git a/charts/code-server/values.yaml b/charts/code-server/values.yaml index 72dc5501e..57a01d873 100644 --- a/charts/code-server/values.yaml +++ b/charts/code-server/values.yaml @@ -38,7 +38,7 @@ securityContext: {} service: type: ClusterIP - port: 8443 + port: 8080 ingress: enabled: false From 8fe7986d0d42d917fa739d090235f50dc466b16a Mon Sep 17 00:00:00 2001 From: Matthew Beckett Date: Wed, 2 Sep 2020 22:19:56 +0100 Subject: [PATCH 007/247] Add kubeval workflow --- .github/workflows/helm_validation.yaml | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 .github/workflows/helm_validation.yaml diff --git a/.github/workflows/helm_validation.yaml b/.github/workflows/helm_validation.yaml new file mode 100644 index 000000000..02d991247 --- /dev/null +++ b/.github/workflows/helm_validation.yaml @@ -0,0 +1,10 @@ +name: Helm Validation + +on: [pull_request, issues] + +jobs: + validate: + runs-on: ubuntu-latest + steps: + - name: Kubeval Helm Chart + uses: junior/kubeval-helm-chart-action@0.2.0-alpha.0 From 70b73d7cb9709f0213907651402af015619dc36a Mon Sep 17 00:00:00 2001 From: Matthew Beckett Date: Wed, 2 Sep 2020 22:24:24 +0100 Subject: [PATCH 008/247] Add kubernetes version environment variable --- .github/workflows/helm_validation.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/helm_validation.yaml b/.github/workflows/helm_validation.yaml index 02d991247..39581f4d9 100644 --- a/.github/workflows/helm_validation.yaml +++ b/.github/workflows/helm_validation.yaml @@ -8,3 +8,5 @@ jobs: steps: - name: Kubeval Helm Chart uses: junior/kubeval-helm-chart-action@0.2.0-alpha.0 + env: + INPUT_KUBERNETES_VERSION: 1.19 From 96a78c98d15548405736b595c7524f1e151ec5eb Mon Sep 17 00:00:00 2001 From: Matthew Beckett Date: Wed, 2 Sep 2020 22:26:06 +0100 Subject: [PATCH 009/247] Add checkout of repo --- .github/workflows/helm_validation.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/helm_validation.yaml b/.github/workflows/helm_validation.yaml index 39581f4d9..9ba82ec08 100644 --- a/.github/workflows/helm_validation.yaml +++ b/.github/workflows/helm_validation.yaml @@ -6,6 +6,8 @@ jobs: validate: runs-on: ubuntu-latest steps: + - name: Checkout Repo + uses: actions/checkout@v2 - name: Kubeval Helm Chart uses: junior/kubeval-helm-chart-action@0.2.0-alpha.0 env: From 8b5deac92b339b621b5a6b6219180003d1c38196 Mon Sep 17 00:00:00 2001 From: Asher Date: Wed, 30 Sep 2020 11:56:49 -0500 Subject: [PATCH 010/247] Fix 80 getting dropped from bind-addr --- src/node/cli.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/node/cli.ts b/src/node/cli.ts index e5d069551..d3afe203d 100644 --- a/src/node/cli.ts +++ b/src/node/cli.ts @@ -401,7 +401,10 @@ export async function readConfigFile(configPath?: string): Promise { function parseBindAddr(bindAddr: string): [string, number] { const u = new URL(`http://${bindAddr}`) - return [u.hostname, parseInt(u.port, 10)] + // With the http scheme 80 will be dropped so assume it's 80 if missing. This + // means --bind-addr without a port will default to 80 as well and not + // the code-server default. + return [u.hostname, u.port ? parseInt(u.port, 10) : 80] } interface Addr { From 11eaf0b470c922693851c8a613e2cc2eb2fa6503 Mon Sep 17 00:00:00 2001 From: Asher Date: Wed, 30 Sep 2020 12:02:31 -0500 Subject: [PATCH 011/247] Fix being unable to use [::] for the host Fixes #1582. --- src/node/http.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/node/http.ts b/src/node/http.ts index a8abb94b0..297dda0cc 100644 --- a/src/node/http.ts +++ b/src/node/http.ts @@ -584,8 +584,11 @@ export class HttpServer { const onListen = (): void => resolve(this.address()) if (this.options.socket) { this.server.listen(this.options.socket, onListen) + } else if (this.options.host) { + // [] is the correct format when using :: but Node errors with them. + this.server.listen(this.options.port, this.options.host.replace(/^\[|\]$/g, ""), onListen) } else { - this.server.listen(this.options.port, this.options.host, onListen) + this.server.listen(this.options.port, onListen) } }) } From e64b186527a7a0a2585fe5cd3aefb5de5f1b90dc Mon Sep 17 00:00:00 2001 From: Asher Date: Wed, 30 Sep 2020 15:22:54 -0500 Subject: [PATCH 012/247] Add variables to better customize plugin directories --- src/node/plugin.ts | 53 ++++++++++++++++++++++++++++++++++------------ 1 file changed, 39 insertions(+), 14 deletions(-) diff --git a/src/node/plugin.ts b/src/node/plugin.ts index 54f7f2b76..d1d71518e 100644 --- a/src/node/plugin.ts +++ b/src/node/plugin.ts @@ -4,11 +4,15 @@ import * as path from "path" import * as util from "util" import { Args } from "./cli" import { HttpServer } from "./http" +import { paths } from "./util" /* eslint-disable @typescript-eslint/no-var-requires */ export type Activate = (httpServer: HttpServer, args: Args) => void +/** + * Plugins must implement this interface. + */ export interface Plugin { activate: Activate } @@ -23,6 +27,9 @@ require("module")._load = function (request: string, parent: object, isMain: boo return originalLoad.apply(this, [request.replace(/^code-server/, path.resolve(__dirname, "../..")), parent, isMain]) } +/** + * Load a plugin and run its activation function. + */ const loadPlugin = async (pluginPath: string, httpServer: HttpServer, args: Args): Promise => { try { const plugin: Plugin = require(pluginPath) @@ -37,24 +44,42 @@ const loadPlugin = async (pluginPath: string, httpServer: HttpServer, args: Args } } -const _loadPlugins = async (httpServer: HttpServer, args: Args): Promise => { - const pluginPath = path.resolve(__dirname, "../../plugins") - const files = await util.promisify(fs.readdir)(pluginPath, { - withFileTypes: true, - }) - await Promise.all(files.map((file) => loadPlugin(path.join(pluginPath, file.name), httpServer, args))) -} - -export const loadPlugins = async (httpServer: HttpServer, args: Args): Promise => { +/** + * Load all plugins in the specified directory. + */ +const _loadPlugins = async (pluginDir: string, httpServer: HttpServer, args: Args): Promise => { try { - await _loadPlugins(httpServer, args) + const files = await util.promisify(fs.readdir)(pluginDir, { + withFileTypes: true, + }) + await Promise.all(files.map((file) => loadPlugin(path.join(pluginDir, file.name), httpServer, args))) } catch (error) { if (error.code !== "ENOENT") { logger.warn(error.message) } } - - if (process.env.PLUGIN_DIR) { - await loadPlugin(process.env.PLUGIN_DIR, httpServer, args) - } +} + +/** + * Load all plugins from the `plugins` directory and the directory specified by + * `PLUGIN_DIR`. + + * Also load any individual plugins found in `PLUGIN_DIRS` (colon-separated). + * This allows you to test and develop plugins without having to move or symlink + * them into one directory. + */ +export const loadPlugins = async (httpServer: HttpServer, args: Args): Promise => { + await Promise.all([ + // Built-in plugins. + _loadPlugins(path.resolve(__dirname, "../../plugins"), httpServer, args), + // User-added plugins. + _loadPlugins( + path.resolve(process.env.PLUGIN_DIR || path.join(paths.data, "code-server-extensions")), + httpServer, + args, + ), + // For development so you don't have to use symlinks. + process.env.PLUGIN_DIRS && + (await Promise.all(process.env.PLUGIN_DIRS.split(":").map((dir) => loadPlugin(dir, httpServer, args)))), + ]) } From 7a982555a837f8eab064c89414c24b13ef33d1b3 Mon Sep 17 00:00:00 2001 From: Asher Date: Wed, 30 Sep 2020 15:43:43 -0500 Subject: [PATCH 013/247] Add version to plugin load log --- src/node/plugin.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/node/plugin.ts b/src/node/plugin.ts index d1d71518e..299862191 100644 --- a/src/node/plugin.ts +++ b/src/node/plugin.ts @@ -34,7 +34,12 @@ const loadPlugin = async (pluginPath: string, httpServer: HttpServer, args: Args try { const plugin: Plugin = require(pluginPath) plugin.activate(httpServer, args) - logger.debug("Loaded plugin", field("name", path.basename(pluginPath))) + + logger.debug( + "Loaded plugin", + field("name", path.basename(pluginPath)), + field("version", require(path.join(pluginPath, "package.json")).version || "n/a"), + ) } catch (error) { if (error.code !== "MODULE_NOT_FOUND") { logger.warn(error.message) From b415b7524f6d3d1ed30553e0fb2b3e075dd3f475 Mon Sep 17 00:00:00 2001 From: Ben Potter Date: Tue, 6 Oct 2020 17:29:53 -0400 Subject: [PATCH 014/247] Add social badges (#2142) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f395968ce..f126580b8 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# code-server +# code-server · [!["GitHub Discussions"](https://img.shields.io/badge/%20GitHub-%20Discussions-gray.svg?longCache=true&logo=github&colorB=purple)](https://github.com/cdr/code-server/discussions) [!["Join us on Slack"](https://img.shields.io/badge/join-us%20on%20slack-gray.svg?longCache=true&logo=slack&colorB=brightgreen)](https://cdr.co/join-community) [![Twitter Follow](https://img.shields.io/twitter/follow/CoderHQ?label=%40CoderHQ&style=social)](https://twitter.com/coderhq) Run [VS Code](https://github.com/Microsoft/vscode) on any machine anywhere and access it in the browser. From ddda280df4ed90519a675b85e2854dc7d6c6d8e6 Mon Sep 17 00:00:00 2001 From: Asher Date: Wed, 7 Oct 2020 12:13:12 -0500 Subject: [PATCH 015/247] Rename plugin vars and make both colon-separated Only one was colon separated but now they both are. --- src/node/plugin.ts | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/src/node/plugin.ts b/src/node/plugin.ts index 299862191..5a8d777cd 100644 --- a/src/node/plugin.ts +++ b/src/node/plugin.ts @@ -66,25 +66,23 @@ const _loadPlugins = async (pluginDir: string, httpServer: HttpServer, args: Arg } /** - * Load all plugins from the `plugins` directory and the directory specified by - * `PLUGIN_DIR`. - - * Also load any individual plugins found in `PLUGIN_DIRS` (colon-separated). - * This allows you to test and develop plugins without having to move or symlink - * them into one directory. + * Load all plugins from the `plugins` directory, directories specified by + * `CS_PLUGIN_PATH` (colon-separated), and individual plugins specified by + * `CS_PLUGIN` (also colon-separated). */ export const loadPlugins = async (httpServer: HttpServer, args: Args): Promise => { + const pluginPath = process.env.CS_PLUGIN_PATH || `${path.join(paths.data, "plugins")}:/etc/code-server/plugins` + const plugin = process.env.CS_PLUGIN || "" await Promise.all([ // Built-in plugins. _loadPlugins(path.resolve(__dirname, "../../plugins"), httpServer, args), // User-added plugins. - _loadPlugins( - path.resolve(process.env.PLUGIN_DIR || path.join(paths.data, "code-server-extensions")), - httpServer, - args, - ), - // For development so you don't have to use symlinks. - process.env.PLUGIN_DIRS && - (await Promise.all(process.env.PLUGIN_DIRS.split(":").map((dir) => loadPlugin(dir, httpServer, args)))), + ...pluginPath.split(":").map((dir) => _loadPlugins(path.resolve(dir), httpServer, args)), + // Individual plugins so you don't have to symlink or move them into a + // directory specifically for plugins. This lets you load plugins that are + // on the same level as other directories that are not plugins (if you tried + // to use CS_PLUGIN_PATH code-server would try to load those other + // directories as plugins). Intended for development. + ...plugin.split(":").map((dir) => loadPlugin(path.resolve(dir), httpServer, args)), ]) } From b3811a67e0deda7d2e0279fbf402eb561ad9ddcb Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Wed, 7 Oct 2020 13:24:31 -0400 Subject: [PATCH 016/247] Add $KEEP_MODULES argument to build-release.sh (#2167) --- ci/build/build-release.sh | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/ci/build/build-release.sh b/ci/build/build-release.sh index 74d991ac9..8d8d1c903 100755 --- a/ci/build/build-release.sh +++ b/ci/build/build-release.sh @@ -6,6 +6,10 @@ set -euo pipefail # MINIFY controls whether minified vscode is bundled. MINIFY="${MINIFY-true}" +# KEEP_MODULES controls whether the script cleans all node_modules requiring a yarn install +# to run first. +KEEP_MODULES="${KEEP_MODULES-0}" + main() { cd "$(dirname "${0}")/../.." source ./ci/lib.sh @@ -52,6 +56,11 @@ EOF ) > "$RELEASE_PATH/package.json" rsync yarn.lock "$RELEASE_PATH" rsync ci/build/npm-postinstall.sh "$RELEASE_PATH/postinstall.sh" + + + if [ "$KEEP_MODULES" = 1 ]; then + rsync node_modules/ "$RELEASE_PATH/node_modules" + fi } bundle_vscode() { @@ -60,7 +69,11 @@ bundle_vscode() { rsync "$VSCODE_SRC_PATH/out-vscode${MINIFY+-min}/" "$VSCODE_OUT_PATH/out" rsync "$VSCODE_SRC_PATH/.build/extensions/" "$VSCODE_OUT_PATH/extensions" - rm -Rf "$VSCODE_OUT_PATH/extensions/node_modules" + if [ "$KEEP_MODULES" = 0 ]; then + rm -Rf "$VSCODE_OUT_PATH/extensions/node_modules" + else + rsync "$VSCODE_SRC_PATH/node_modules/" "$VSCODE_OUT_PATH/node_modules" + fi rsync "$VSCODE_SRC_PATH/extensions/package.json" "$VSCODE_OUT_PATH/extensions" rsync "$VSCODE_SRC_PATH/extensions/yarn.lock" "$VSCODE_OUT_PATH/extensions" rsync "$VSCODE_SRC_PATH/extensions/postinstall.js" "$VSCODE_OUT_PATH/extensions" From c2ac126a501622947d8c644a6a2bb1e8c751a017 Mon Sep 17 00:00:00 2001 From: Asher Date: Wed, 7 Oct 2020 12:25:23 -0500 Subject: [PATCH 017/247] Log all plugin errors as errors --- src/node/plugin.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/node/plugin.ts b/src/node/plugin.ts index 5a8d777cd..2860763ad 100644 --- a/src/node/plugin.ts +++ b/src/node/plugin.ts @@ -41,11 +41,7 @@ const loadPlugin = async (pluginPath: string, httpServer: HttpServer, args: Args field("version", require(path.join(pluginPath, "package.json")).version || "n/a"), ) } catch (error) { - if (error.code !== "MODULE_NOT_FOUND") { - logger.warn(error.message) - } else { - logger.error(error.message) - } + logger.error(error.message) } } From 402f5ebd77df43d68fd53fcf09a4e5510b83be77 Mon Sep 17 00:00:00 2001 From: Asher Date: Wed, 7 Oct 2020 12:37:37 -0500 Subject: [PATCH 018/247] Update VS code to 1.49.3 (#2081) --- ci/dev/vscode.patch | 140 ++++++++++++++++++++++---------------------- lib/vscode | 2 +- 2 files changed, 71 insertions(+), 71 deletions(-) diff --git a/ci/dev/vscode.patch b/ci/dev/vscode.patch index 5fe34a20f..1632dcd42 100644 --- a/ci/dev/vscode.patch +++ b/ci/dev/vscode.patch @@ -12,12 +12,12 @@ index 0fe46b6eadc4ccc819fbf342ee1071bb657792b3..e545e004cef31fa5f40ba8df6a2317ea coverage/ diff --git a/.yarnrc b/.yarnrc deleted file mode 100644 -index 135e10442a7e5184cf8c47615322bb7d622855d9..0000000000000000000000000000000000000000 +index 3c6eccfb102f2084d16395d70d65f05a91b6d47b..0000000000000000000000000000000000000000 --- a/.yarnrc +++ /dev/null @@ -1,3 +0,0 @@ -disturl "https://atom.io/download/electron" --target "7.3.2" +-target "9.2.1" -runtime "electron" diff --git a/build/gulpfile.reh.js b/build/gulpfile.reh.js index f2ea1bd37010b1eb8a43ce9beaae4a88810f6e2d..3f660f9981921ec465d2b8809a1a5ea5663f4c1f 100644 @@ -144,10 +144,10 @@ index cb88d37adefd4882f61a2711fdd7f72b89e1a6e3..6b3253af0a3a0aa4d75456379ef1c00f const cp = require('child_process'); diff --git a/coder.js b/coder.js new file mode 100644 -index 0000000000000000000000000000000000000000..9cb693af63b86b4a6b35c442e6ea501a1076d18a +index 0000000000000000000000000000000000000000..df5b42cba463b6c0043aebbc835f852f1284aa36 --- /dev/null +++ b/coder.js -@@ -0,0 +1,63 @@ +@@ -0,0 +1,64 @@ +// This must be ran from VS Code's root. +const gulp = require("gulp"); +const path = require("path"); @@ -163,6 +163,7 @@ index 0000000000000000000000000000000000000000..9cb693af63b86b4a6b35c442e6ea501a + buildfile.base, + buildfile.workbenchWeb, + buildfile.workerExtensionHost, ++ buildfile.workerNotebook, + buildfile.keyboardMaps, + buildfile.entrypoint("vs/platform/files/node/watcher/unix/watcherApp", ["vs/css", "vs/nls"]), + buildfile.entrypoint("vs/platform/files/node/watcher/nsfw/watcherApp", ["vs/css", "vs/nls"]), @@ -226,10 +227,10 @@ index da4fa3e9d0443d679dfbab1000b434af2ae01afd..50f3e1144f8057883dea8b91ec2f7073 function processLib() { diff --git a/package.json b/package.json -index 226f51a1ec55fc31ae90e175bc1ab62d74eb6294..5c4e5af5f69c0fd994f4b54cb7a9dfb20f868bfa 100644 +index 9b5ee0f876303283eb766fd2bb3ed818c50b1d3e..30ef9fa81b1cd844138388d794d4d6d9db5c7fba 100644 --- a/package.json +++ b/package.json -@@ -45,7 +45,11 @@ +@@ -46,7 +46,11 @@ "watch-web": "gulp watch-web --max_old_space_size=4095", "eslint": "eslint -c .eslintrc.json --rulesdir ./build/lib/eslint --ext .ts --ext .js ./src/vs ./extensions" }, @@ -241,15 +242,15 @@ index 226f51a1ec55fc31ae90e175bc1ab62d74eb6294..5c4e5af5f69c0fd994f4b54cb7a9dfb2 "applicationinsights": "1.0.8", "chokidar": "3.2.3", "graceful-fs": "4.2.3", -@@ -59,6 +63,7 @@ - "native-keymap": "2.1.2", +@@ -60,6 +64,7 @@ + "native-keymap": "2.2.0", "native-watchdog": "1.3.0", "node-pty": "0.10.0-beta8", + "rimraf": "^2.2.8", "semver-umd": "^5.5.7", "spdlog": "^0.11.1", "sudo-prompt": "9.1.1", -@@ -159,7 +164,6 @@ +@@ -160,7 +165,6 @@ "pump": "^1.0.1", "queue": "3.0.6", "rcedit": "^1.1.0", @@ -257,7 +258,7 @@ index 226f51a1ec55fc31ae90e175bc1ab62d74eb6294..5c4e5af5f69c0fd994f4b54cb7a9dfb2 "sinon": "^1.17.2", "source-map": "^0.4.4", "style-loader": "^1.0.0", -@@ -190,5 +194,8 @@ +@@ -192,5 +196,8 @@ "windows-foreground-love": "0.2.0", "windows-mutex": "0.3.0", "windows-process-tree": "0.2.4" @@ -267,7 +268,7 @@ index 226f51a1ec55fc31ae90e175bc1ab62d74eb6294..5c4e5af5f69c0fd994f4b54cb7a9dfb2 } } diff --git a/product.json b/product.json -index 2b884d18f301b86bf29e3a8f1343cfb651f8727c..518b935b837dd21251089bdd317d6c3c03756d7b 100644 +index b9349015e3475bff07104ca2fa859954a37f962a..4c32260abc42efe17ee7d717e4dcebf182044e8c 100644 --- a/product.json +++ b/product.json @@ -20,7 +20,7 @@ @@ -281,18 +282,18 @@ index 2b884d18f301b86bf29e3a8f1343cfb651f8727c..518b935b837dd21251089bdd317d6c3c "ms-vscode.vscode-js-profile-flame", diff --git a/remote/.yarnrc b/remote/.yarnrc deleted file mode 100644 -index 1e16cde724c7703d2836b3641de48c99f7f47e68..0000000000000000000000000000000000000000 +index c1a32ce532afa501fb19bdbcf6bcb0ec151ecd99..0000000000000000000000000000000000000000 --- a/remote/.yarnrc +++ /dev/null @@ -1,3 +0,0 @@ -disturl "http://nodejs.org/dist" --target "12.4.0" +-target "12.14.1" -runtime "node" diff --git a/src/vs/base/common/network.ts b/src/vs/base/common/network.ts -index 1286c5117a4cae9d6075ed36f32f6414897d705b..e60dd11d03992800853e76d4d68b8ff211da7627 100644 +index 4b6aebc16466dff58a9dfab4a680d230fa1f71a5..dd72e179ec0fa9a0b3e16e497225cb6da6218af3 100644 --- a/src/vs/base/common/network.ts +++ b/src/vs/base/common/network.ts -@@ -111,16 +111,17 @@ class RemoteAuthoritiesImpl { +@@ -113,16 +113,17 @@ class RemoteAuthoritiesImpl { if (host && host.indexOf(':') !== -1) { host = `[${host}]`; } @@ -432,19 +433,18 @@ index 2c64061da7b01aef0bfe3cec851da232ca9461c8..c0ef8faedd406c38bf9c55bbbdbbb060 // Do nothing. If we can't read the file we have no // language pack config. diff --git a/src/vs/code/browser/workbench/workbench.ts b/src/vs/code/browser/workbench/workbench.ts -index c629f7fffa1faad78e5d9907fd38aec289db0428..c266e1fb06ffc44f1ec4230ac59fd094a53b257b 100644 +index ad5272b22320a361cec0eed40d57629b06147c01..c9280b14472507ebb9a277f554485f08b090cb69 100644 --- a/src/vs/code/browser/workbench/workbench.ts +++ b/src/vs/code/browser/workbench/workbench.ts -@@ -13,6 +13,8 @@ import { isFolderToOpen, isWorkspaceToOpen } from 'vs/platform/windows/common/wi - import { isEqual } from 'vs/base/common/resources'; +@@ -16,6 +16,7 @@ import { isEqual } from 'vs/base/common/resources'; import { isStandalone } from 'vs/base/browser/browser'; import { localize } from 'vs/nls'; -+import { Schemas } from 'vs/base/common/network'; + import { Schemas } from 'vs/base/common/network'; +import { encodePath } from 'vs/server/node/util'; interface ICredential { service: string; -@@ -243,12 +245,18 @@ class WorkspaceProvider implements IWorkspaceProvider { +@@ -253,12 +254,18 @@ class WorkspaceProvider implements IWorkspaceProvider { // Folder else if (isFolderToOpen(workspace)) { @@ -465,7 +465,7 @@ index c629f7fffa1faad78e5d9907fd38aec289db0428..c266e1fb06ffc44f1ec4230ac59fd094 } // Append payload if any -@@ -285,7 +293,22 @@ class WorkspaceProvider implements IWorkspaceProvider { +@@ -348,7 +355,22 @@ class WindowIndicator implements IWindowIndicator { throw new Error('Missing web configuration element'); } @@ -489,7 +489,7 @@ index c629f7fffa1faad78e5d9907fd38aec289db0428..c266e1fb06ffc44f1ec4230ac59fd094 // Revive static extension locations if (Array.isArray(config.staticExtensions)) { -@@ -297,40 +320,7 @@ class WorkspaceProvider implements IWorkspaceProvider { +@@ -360,40 +382,7 @@ class WindowIndicator implements IWindowIndicator { // Find workspace to open and payload let foundWorkspace = false; let workspace: IWorkspace; @@ -532,7 +532,7 @@ index c629f7fffa1faad78e5d9907fd38aec289db0428..c266e1fb06ffc44f1ec4230ac59fd094 // If no workspace is provided through the URL, check for config attribute from server if (!foundWorkspace) { diff --git a/src/vs/platform/environment/node/argv.ts b/src/vs/platform/environment/node/argv.ts -index 2379b626c81321afe18267c69c6903efbfa354f4..28f8971cf398b048c7d8f56df2e9dc4e32aadb6d 100644 +index 92dd2bcf87dba5e5f07f2707a91b1a364ab1b05f..047522bd1a2c1edfda05c3739838fecbd70db6c5 100644 --- a/src/vs/platform/environment/node/argv.ts +++ b/src/vs/platform/environment/node/argv.ts @@ -8,6 +8,8 @@ import { localize } from 'vs/nls'; @@ -544,7 +544,7 @@ index 2379b626c81321afe18267c69c6903efbfa354f4..28f8971cf398b048c7d8f56df2e9dc4e _: string[]; 'folder-uri'?: string[]; // undefined or array of 1 or more 'file-uri'?: string[]; // undefined or array of 1 or more -@@ -141,6 +143,8 @@ export const OPTIONS: OptionDescriptions> = { +@@ -142,6 +144,8 @@ export const OPTIONS: OptionDescriptions> = { 'extensions-dir': { type: 'string', deprecates: 'extensionHomePath', cat: 'e', args: 'dir', description: localize('extensionHomePath', "Set the root path for extensions.") }, 'extensions-download-dir': { type: 'string' }, 'builtin-extensions-dir': { type: 'string' }, @@ -553,13 +553,13 @@ index 2379b626c81321afe18267c69c6903efbfa354f4..28f8971cf398b048c7d8f56df2e9dc4e 'list-extensions': { type: 'boolean', cat: 'e', description: localize('listExtensions', "List the installed extensions.") }, 'show-versions': { type: 'boolean', cat: 'e', description: localize('showVersions', "Show versions of installed extensions, when using --list-extension.") }, 'category': { type: 'string', cat: 'e', description: localize('category', "Filters installed extensions by provided category, when using --list-extension.") }, -@@ -403,4 +407,3 @@ export function buildHelpMessage(productName: string, executableName: string, ve +@@ -405,4 +409,3 @@ export function buildHelpMessage(productName: string, executableName: string, ve export function buildVersionMessage(version: string | undefined, commit: string | undefined): string { return `${version || localize('unknownVersion', "Unknown version")}\n${commit || localize('unknownCommit', "Unknown commit")}\n${process.arch}`; } - diff --git a/src/vs/platform/environment/node/environmentService.ts b/src/vs/platform/environment/node/environmentService.ts -index 5c0dc4ad4ae79a172bed4bc3d6440cdf6dd22386..38b8c7573a872d587c5f3f6c5e0521d2bd918daa 100644 +index 45d5ec2cc02707d91f19a66d408ae46a1201a9e8..4ed498c63ceb55d15bd104a92b701ead3dfa81f2 100644 --- a/src/vs/platform/environment/node/environmentService.ts +++ b/src/vs/platform/environment/node/environmentService.ts @@ -38,6 +38,8 @@ export interface INativeEnvironmentService extends IEnvironmentService { @@ -633,7 +633,7 @@ index 575b2aafc3802cd6f5f943930e30de9f2c2690de..873181f967856759e3dc001e5bbe06e2 + } } diff --git a/src/vs/platform/product/common/product.ts b/src/vs/platform/product/common/product.ts -index 3370a608b4b54c238a6ea69a92cdfcd4817cab57..37b3592d39d8ee3aed5455d599612485ea621323 100644 +index bb33203d1727b1c076efac9113afc3b2580cdbd9..c53cea338cdaa0f0ac15542c129e1572b3f13b80 100644 --- a/src/vs/platform/product/common/product.ts +++ b/src/vs/platform/product/common/product.ts @@ -30,6 +30,12 @@ if (isWeb) { @@ -650,7 +650,7 @@ index 3370a608b4b54c238a6ea69a92cdfcd4817cab57..37b3592d39d8ee3aed5455d599612485 // Node: AMD loader diff --git a/src/vs/platform/product/common/productService.ts b/src/vs/platform/product/common/productService.ts -index 040c869d94ceb278350c1d752f55712feedda379..bf16defcf7bc4229dedbbe9eae8a965e996c69d9 100644 +index d1cb00a6d63621a4873a6a5e815220d084ceac2a..1a69d6f63a7406d364aa3e2b32fb75309f212e98 100644 --- a/src/vs/platform/product/common/productService.ts +++ b/src/vs/platform/product/common/productService.ts @@ -30,6 +30,8 @@ export type ConfigurationSyncStore = { @@ -684,10 +684,10 @@ index 3715cbb8e6ee41c3d9b5090918d243b723ae2d00..c65de8ad37e727d66da97a8f8b170cbc - - diff --git a/src/vs/platform/remote/common/remoteAgentConnection.ts b/src/vs/platform/remote/common/remoteAgentConnection.ts -index 2185bb5228c3f9603f307237e7f146fe386708d8..35463ca6520a7da2308d01a51ef2d3e544f10a10 100644 +index 18d3d04fd20335975293e37b3b641120dd92da20..4e49f9d63623da6c84624144765f76ec127ea526 100644 --- a/src/vs/platform/remote/common/remoteAgentConnection.ts +++ b/src/vs/platform/remote/common/remoteAgentConnection.ts -@@ -89,7 +89,7 @@ async function connectToRemoteExtensionHostAgent(options: ISimpleConnectionOptio +@@ -92,7 +92,7 @@ async function connectToRemoteExtensionHostAgent(options: ISimpleConnectionOptio options.socketFactory.connect( options.host, options.port, @@ -697,10 +697,10 @@ index 2185bb5228c3f9603f307237e7f146fe386708d8..35463ca6520a7da2308d01a51ef2d3e5 if (err || !socket) { options.logService.error(`${logPrefix} socketFactory.connect() failed. Error:`); diff --git a/src/vs/platform/storage/browser/storageService.ts b/src/vs/platform/storage/browser/storageService.ts -index 59b1baf9120cb0ccf1ebc425ed708224b5513d41..cf9805554b91176ac2521963a7775711e287e4cc 100644 +index ab3fd347b69f8a3d9b96e706cd87c911b8ffed6b..9d351037b577f9f1edfd18ae9b3c48a211f4467f 100644 --- a/src/vs/platform/storage/browser/storageService.ts +++ b/src/vs/platform/storage/browser/storageService.ts -@@ -116,8 +116,8 @@ export class BrowserStorageService extends Disposable implements IStorageService +@@ -122,8 +122,8 @@ export class BrowserStorageService extends Disposable implements IStorageService return this.getStorage(scope).getNumber(key, fallbackValue); } @@ -712,10 +712,10 @@ index 59b1baf9120cb0ccf1ebc425ed708224b5513d41..cf9805554b91176ac2521963a7775711 remove(key: string, scope: StorageScope): void { diff --git a/src/vs/platform/storage/common/storage.ts b/src/vs/platform/storage/common/storage.ts -index 1623957cb18eedbf968cca6231d226b587f51935..d366438d54d36a86bd416aeb9f802ad9015f601c 100644 +index 6611f1dae42055f69a55c1c154d9475f11cd4d0a..d598d4909d5ff6d1614e4a038b1865e1f9a4e963 100644 --- a/src/vs/platform/storage/common/storage.ts +++ b/src/vs/platform/storage/common/storage.ts -@@ -83,7 +83,7 @@ export interface IStorageService { +@@ -85,7 +85,7 @@ export interface IStorageService { * The scope argument allows to define the scope of the storage * operation to either the current workspace only or all workspaces. */ @@ -725,10 +725,10 @@ index 1623957cb18eedbf968cca6231d226b587f51935..d366438d54d36a86bd416aeb9f802ad9 /** * Delete an element stored under the provided key from storage. diff --git a/src/vs/platform/storage/node/storageService.ts b/src/vs/platform/storage/node/storageService.ts -index 75514fe5a4fabdc885556311954ab016c593bac3..62d97c60488856dfde0bb64fea85032b2e49bb94 100644 +index ac657056aa68549f0053cfb1ec68835ba4ce20f9..143f9b5681eb867c5e5c5437946ab785eb34e4b4 100644 --- a/src/vs/platform/storage/node/storageService.ts +++ b/src/vs/platform/storage/node/storageService.ts -@@ -204,8 +204,8 @@ export class NativeStorageService extends Disposable implements IStorageService +@@ -202,8 +202,8 @@ export class NativeStorageService extends Disposable implements IStorageService return this.getStorage(scope).getNumber(key, fallbackValue); } @@ -2843,7 +2843,7 @@ index 0000000000000000000000000000000000000000..fa47e993b46802f1a26457649e9e8bc4 + return path.split("/").map((p) => encodeURIComponent(p)).join("/"); +}; diff --git a/src/vs/workbench/api/browser/extensionHost.contribution.ts b/src/vs/workbench/api/browser/extensionHost.contribution.ts -index 3d77009b908f61690a56dc589360627f6f5a3924..11deb1b99ac9d3baa4aa583d711a5e020b4379ec 100644 +index bfabf0008910c87146df53a2e10fe63bae517a86..32b3b1cf84c8d280fd7f03d541b867691d51c2fb 100644 --- a/src/vs/workbench/api/browser/extensionHost.contribution.ts +++ b/src/vs/workbench/api/browser/extensionHost.contribution.ts @@ -60,6 +60,7 @@ import './mainThreadComments'; @@ -2873,7 +2873,7 @@ index 7bc3904963bed2925f3640b6bd929347159dd3cf..c6db2368ae9eaca61889efcf3c49763c return Promise.reject(err); } diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts -index 97793666ad8abf7d052ba96a88565042b21ebcec..13cd137db1e9435ef66ade3220774b1ddb608d91 100644 +index 3595cd3e38136222044a13050b15105bbe539068..989caefff7c4b8203c03cec8fa451f5e70ea8964 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -68,6 +68,7 @@ import { IURITransformerService } from 'vs/workbench/api/common/extHostUriTransf @@ -2884,7 +2884,7 @@ index 97793666ad8abf7d052ba96a88565042b21ebcec..13cd137db1e9435ef66ade3220774b1d import { ExtHostTheming } from 'vs/workbench/api/common/extHostTheming'; import { IExtHostTunnelService } from 'vs/workbench/api/common/extHostTunnelService'; import { IExtHostApiDeprecationService } from 'vs/workbench/api/common/extHostApiDeprecationService'; -@@ -97,6 +98,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I +@@ -100,6 +101,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I const extHostStorage = accessor.get(IExtHostStorage); const extensionStoragePaths = accessor.get(IExtensionStoragePaths); const extHostLogService = accessor.get(ILogService); @@ -2892,7 +2892,7 @@ index 97793666ad8abf7d052ba96a88565042b21ebcec..13cd137db1e9435ef66ade3220774b1d const extHostTunnelService = accessor.get(IExtHostTunnelService); const extHostApiDeprecation = accessor.get(IExtHostApiDeprecationService); const extHostWindow = accessor.get(IExtHostWindow); -@@ -107,6 +109,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I +@@ -110,6 +112,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I rpcProtocol.set(ExtHostContext.ExtHostConfiguration, extHostConfiguration); rpcProtocol.set(ExtHostContext.ExtHostExtensionService, extensionService); rpcProtocol.set(ExtHostContext.ExtHostStorage, extHostStorage); @@ -2901,10 +2901,10 @@ index 97793666ad8abf7d052ba96a88565042b21ebcec..13cd137db1e9435ef66ade3220774b1d rpcProtocol.set(ExtHostContext.ExtHostWindow, extHostWindow); diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts -index eb5d8ea84551030a9d72918813fca7adb49a5cd8..da9eb521ca4c660de1bb23df52c18c0efe6f5979 100644 +index 4b7946662950f18179a5b6e3552abd39e68ca80e..ca1352d311a94b42e18d0d9e4859b18ec2bb271d 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts -@@ -769,6 +769,16 @@ export interface MainThreadLabelServiceShape extends IDisposable { +@@ -795,6 +795,16 @@ export interface MainThreadLabelServiceShape extends IDisposable { $unregisterResourceLabelFormatter(handle: number): void; } @@ -2921,7 +2921,7 @@ index eb5d8ea84551030a9d72918813fca7adb49a5cd8..da9eb521ca4c660de1bb23df52c18c0e export interface MainThreadSearchShape extends IDisposable { $registerFileSearchProvider(handle: number, scheme: string): void; $registerTextSearchProvider(handle: number, scheme: string): void; -@@ -1707,6 +1717,7 @@ export const MainContext = { +@@ -1765,6 +1775,7 @@ export const MainContext = { MainThreadWindow: createMainId('MainThreadWindow'), MainThreadLabelService: createMainId('MainThreadLabelService'), MainThreadNotebook: createMainId('MainThreadNotebook'), @@ -2929,7 +2929,7 @@ index eb5d8ea84551030a9d72918813fca7adb49a5cd8..da9eb521ca4c660de1bb23df52c18c0e MainThreadTheming: createMainId('MainThreadTheming'), MainThreadTunnelService: createMainId('MainThreadTunnelService'), MainThreadTimeline: createMainId('MainThreadTimeline') -@@ -1745,6 +1756,7 @@ export const ExtHostContext = { +@@ -1806,6 +1817,7 @@ export const ExtHostContext = { ExtHostOutputService: createMainId('ExtHostOutputService'), ExtHosLabelService: createMainId('ExtHostLabelService'), ExtHostNotebook: createMainId('ExtHostNotebook'), @@ -2938,10 +2938,10 @@ index eb5d8ea84551030a9d72918813fca7adb49a5cd8..da9eb521ca4c660de1bb23df52c18c0e ExtHostTunnelService: createMainId('ExtHostTunnelService'), ExtHostAuthentication: createMainId('ExtHostAuthentication'), diff --git a/src/vs/workbench/api/common/extHostExtensionService.ts b/src/vs/workbench/api/common/extHostExtensionService.ts -index 34639e18b6fb567feaf19cf20bd312b6b578723f..9c22fe6f090f3cfceea5c3f41695a1ab1d797a19 100644 +index 0bb5188614bcbf98b85c9208edc2b173f70b1670..38ff3e2e05645be8df619ed2b47fa2984b918741 100644 --- a/src/vs/workbench/api/common/extHostExtensionService.ts +++ b/src/vs/workbench/api/common/extHostExtensionService.ts -@@ -32,6 +32,7 @@ import { IExtHostInitDataService } from 'vs/workbench/api/common/extHostInitData +@@ -31,6 +31,7 @@ import { IExtHostInitDataService } from 'vs/workbench/api/common/extHostInitData import { IExtensionStoragePaths } from 'vs/workbench/api/common/extHostStoragePaths'; import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; @@ -2982,7 +2982,7 @@ index 34639e18b6fb567feaf19cf20bd312b6b578723f..9c22fe6f090f3cfceea5c3f41695a1ab this._loadExtensionContext(extensionDescription) ]).then(values => { return AbstractExtHostExtensionService._callActivate(this._logService, extensionDescription.identifier, values[0], values[1], activationTimesBuilder); -@@ -754,7 +758,7 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme +@@ -746,7 +750,7 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme protected abstract _beforeAlmostReadyToRunExtensions(): Promise; protected abstract _getEntryPoint(extensionDescription: IExtensionDescription): string | undefined; @@ -3027,18 +3027,18 @@ index 3843fdec386edc09a1d361b63de892a04e0070ed..8aac4df527857e964798362a69f5591b registerSingleton(ILogService, ExtHostLogService); +registerSingleton(IExtHostNodeProxy, ExtHostNodeProxy); diff --git a/src/vs/workbench/api/worker/extHostExtensionService.ts b/src/vs/workbench/api/worker/extHostExtensionService.ts -index c71ab1c7da462da8f4a12146d45e6cde7f06ad81..572b07ff2516154f49ab9e02bfcab2b4d8b3009f 100644 +index a6a149083719d7479268e24eb5339f6cbf93e655..360888dc7dff9437f6c85f7a2043ad9e7c4daf21 100644 --- a/src/vs/workbench/api/worker/extHostExtensionService.ts +++ b/src/vs/workbench/api/worker/extHostExtensionService.ts -@@ -9,6 +9,7 @@ import { AbstractExtHostExtensionService } from 'vs/workbench/api/common/extHost - import { URI } from 'vs/base/common/uri'; +@@ -10,6 +10,7 @@ import { URI } from 'vs/base/common/uri'; import { RequireInterceptor } from 'vs/workbench/api/common/extHostRequireInterceptor'; import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; + import { ExtensionRuntime } from 'vs/workbench/api/common/extHostTypes'; +import { loadCommonJSModule } from 'vs/server/browser/worker'; class WorkerRequireInterceptor extends RequireInterceptor { -@@ -42,10 +43,15 @@ export class ExtHostExtensionService extends AbstractExtHostExtensionService { +@@ -44,10 +45,15 @@ export class ExtHostExtensionService extends AbstractExtHostExtensionService { } protected _getEntryPoint(extensionDescription: IExtensionDescription): string | undefined { @@ -3072,7 +3072,7 @@ index ced2d815834e40a1543e80516472799075980733..dfcae73e8a042307600c67f163aa00ba .monaco-workbench .activitybar > .content > .home-bar > .home-bar-icon-badge { diff --git a/src/vs/workbench/browser/web.main.ts b/src/vs/workbench/browser/web.main.ts -index 0462617196b39111cb22e5abbf4b096406496bf8..11434d27af9ce3262c57918e0f5a44d4eebcf8ac 100644 +index 511d7376a2bfebde59b4c67fed54c39e9dd534c9..c7c45f8e4e4ffe56a8782f58af75c6a7835142cf 100644 --- a/src/vs/workbench/browser/web.main.ts +++ b/src/vs/workbench/browser/web.main.ts @@ -45,6 +45,7 @@ import { FileLogService } from 'vs/platform/log/common/fileLogService'; @@ -3083,7 +3083,7 @@ index 0462617196b39111cb22e5abbf4b096406496bf8..11434d27af9ce3262c57918e0f5a44d4 import { coalesce } from 'vs/base/common/arrays'; import { InMemoryFileSystemProvider } from 'vs/platform/files/common/inMemoryFilesystemProvider'; import { WebResourceIdentityService, IResourceIdentityService } from 'vs/platform/resource/common/resourceIdentityService'; -@@ -84,6 +85,8 @@ class BrowserMain extends Disposable { +@@ -87,6 +88,8 @@ class BrowserMain extends Disposable { // Startup const instantiationService = workbench.startup(); @@ -3115,7 +3115,7 @@ index 18ea0bfedb4492327429a38237b05915b29f6dd0..d59a17c17f4fffa23d786ce36b4ff624 this._langIdKey.set(value ? this._modeService.getModeIdByFilepathOrFirstLine(value) : null); this._extensionKey.set(value ? extname(value) : null); diff --git a/src/vs/workbench/contrib/scm/browser/media/scm.css b/src/vs/workbench/contrib/scm/browser/media/scm.css -index 9947f240bf20b42069bd3d50a96d7a783615f54b..bdba0a2fc64a2e6c2cd2644bcc6afc0d131501a7 100644 +index b1838de8f21c60141d01cc424a5e000a32f1c828..0a480032e0cc8d5219cd240f8807aa317718659d 100644 --- a/src/vs/workbench/contrib/scm/browser/media/scm.css +++ b/src/vs/workbench/contrib/scm/browser/media/scm.css @@ -138,9 +138,11 @@ @@ -3134,7 +3134,7 @@ index 9947f240bf20b42069bd3d50a96d7a783615f54b..bdba0a2fc64a2e6c2cd2644bcc6afc0d .scm-view .monaco-list .monaco-list-row .resource-group > .actions, .scm-view .monaco-list .monaco-list-row .resource > .name > .monaco-icon-label > .actions { diff --git a/src/vs/workbench/services/dialogs/browser/dialogService.ts b/src/vs/workbench/services/dialogs/browser/dialogService.ts -index 6e3182a696dd3443e68ad9e92d029b6ec2d01677..7df85da165a3ba157629c6b9e92f08dd18c7511a 100644 +index 1360c248eb7ff937c92d08bbf30d2b76ea606dc0..adccf8b88d62381c3ec484df40c6d63142ec9ef5 100644 --- a/src/vs/workbench/services/dialogs/browser/dialogService.ts +++ b/src/vs/workbench/services/dialogs/browser/dialogService.ts @@ -124,11 +124,12 @@ export class DialogService implements IDialogService { @@ -3153,10 +3153,10 @@ index 6e3182a696dd3443e68ad9e92d029b6ec2d01677..7df85da165a3ba157629c6b9e92f08dd }; diff --git a/src/vs/workbench/services/environment/browser/environmentService.ts b/src/vs/workbench/services/environment/browser/environmentService.ts -index ba2701ec54d1a70eaf66afacce585fe906644319..4d4aaa6958b636480178470570e856e62ab922ee 100644 +index 819607be0c13fed28eb7fbe6d4a62c0b860b1aa9..b046943311b713a579cc3a94983ea1b7fca7b9b1 100644 --- a/src/vs/workbench/services/environment/browser/environmentService.ts +++ b/src/vs/workbench/services/environment/browser/environmentService.ts -@@ -121,8 +121,18 @@ export class BrowserWorkbenchEnvironmentService implements IWorkbenchEnvironment +@@ -116,8 +116,18 @@ export class BrowserWorkbenchEnvironmentService implements IWorkbenchEnvironment @memoize get logFile(): URI { return joinPath(this.options.logsPath, 'window.log'); } @@ -3176,7 +3176,7 @@ index ba2701ec54d1a70eaf66afacce585fe906644319..4d4aaa6958b636480178470570e856e6 @memoize get settingsResource(): URI { return joinPath(this.userRoamingDataHome, 'settings.json'); } -@@ -284,7 +294,12 @@ export class BrowserWorkbenchEnvironmentService implements IWorkbenchEnvironment +@@ -279,7 +289,12 @@ export class BrowserWorkbenchEnvironmentService implements IWorkbenchEnvironment extensionHostDebugEnvironment.params.port = parseInt(value); break; case 'enableProposedApi': @@ -3191,10 +3191,10 @@ index ba2701ec54d1a70eaf66afacce585fe906644319..4d4aaa6958b636480178470570e856e6 } } diff --git a/src/vs/workbench/services/extensionManagement/common/extensionEnablementService.ts b/src/vs/workbench/services/extensionManagement/common/extensionEnablementService.ts -index c28b14774005509f58dddd2dec25547bac85e09f..6090200d9c3671fc1239880dbd060a01a84db1fb 100644 +index 32f3dc52c1ff645df6471a03542d6ec3eb73a277..c2f4497d2eba13a771b2665ad58f12ecdfa7606a 100644 --- a/src/vs/workbench/services/extensionManagement/common/extensionEnablementService.ts +++ b/src/vs/workbench/services/extensionManagement/common/extensionEnablementService.ts -@@ -163,7 +163,7 @@ export class ExtensionEnablementService extends Disposable implements IWorkbench +@@ -205,7 +205,7 @@ export class ExtensionEnablementService extends Disposable implements IWorkbench } } } @@ -3204,10 +3204,10 @@ index c28b14774005509f58dddd2dec25547bac85e09f..6090200d9c3671fc1239880dbd060a01 return false; } diff --git a/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts b/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts -index 33eb56db3c25a0dc028b0d54dfa102e5584441cf..de70af33529e40a56969d8f241c82906cda72e1e 100644 +index a982b3ecc58c5a2f3a92be7b8cca3a1cacbb7d47..97f9bfcf0e679be683b1b09cd569149e7962f5ad 100644 --- a/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts +++ b/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts -@@ -202,8 +202,11 @@ export class ExtensionManagementService extends Disposable implements IExtension +@@ -211,8 +211,11 @@ export class ExtensionManagementService extends Disposable implements IExtension } // Install Language pack on all servers @@ -3220,7 +3220,7 @@ index 33eb56db3c25a0dc028b0d54dfa102e5584441cf..de70af33529e40a56969d8f241c82906 } // 1. Install on preferred location -@@ -236,6 +239,11 @@ export class ExtensionManagementService extends Disposable implements IExtension +@@ -245,6 +248,11 @@ export class ExtensionManagementService extends Disposable implements IExtension return this.extensionManagementServerService.webExtensionManagementServer.extensionManagementService.installFromGallery(gallery); } @@ -3233,10 +3233,10 @@ index 33eb56db3c25a0dc028b0d54dfa102e5584441cf..de70af33529e40a56969d8f241c82906 const error = new Error(localize('cannot be installed', "Cannot install '{0}' because this extension has defined that it cannot run on the remote server.", gallery.displayName || gallery.name)); error.name = INSTALL_ERROR_NOT_SUPPORTED; diff --git a/src/vs/workbench/services/extensions/browser/extensionService.ts b/src/vs/workbench/services/extensions/browser/extensionService.ts -index d0710e77fa28aacf5b4dfe85efbf67a6a9ae78ab..ceb27174aee3c78ca5a086f05a6b1d3188888034 100644 +index 9e979d28691d0b0b26fde5e46b606731e31f3da5..dd31879c7dd899c73c4a1371996912f4513bfd0d 100644 --- a/src/vs/workbench/services/extensions/browser/extensionService.ts +++ b/src/vs/workbench/services/extensions/browser/extensionService.ts -@@ -116,8 +116,10 @@ export class ExtensionService extends AbstractExtensionService implements IExten +@@ -125,8 +125,10 @@ export class ExtensionService extends AbstractExtensionService implements IExten this._remoteAgentService.getEnvironment(), this._remoteAgentService.scanExtensions() ]); @@ -3362,7 +3362,7 @@ index 44999bd842eae12b752b2e7e8c4904272b111dc1..601b1c5408835c743fe07e34da4d4534 } diff --git a/src/vs/workbench/workbench.web.main.ts b/src/vs/workbench/workbench.web.main.ts -index 0669178db4cf5efe28ffd2a8fe3301de47bcc545..28fafeb2de2efea5c6412853044ce84775f1e038 100644 +index f02bbbf874b5b18ac8d077ad56a8a4a57e77a4a6..86271940724aaf28e4eda93e59920820a7d93987 100644 --- a/src/vs/workbench/workbench.web.main.ts +++ b/src/vs/workbench/workbench.web.main.ts @@ -35,7 +35,8 @@ import 'vs/workbench/services/textfile/browser/browserTextFileService'; @@ -3376,7 +3376,7 @@ index 0669178db4cf5efe28ffd2a8fe3301de47bcc545..28fafeb2de2efea5c6412853044ce847 import 'vs/workbench/services/credentials/browser/credentialsService'; import 'vs/workbench/services/url/browser/urlService'; diff --git a/yarn.lock b/yarn.lock -index b2fbf543af319fcc3973248b4ed4981db1ca213a..f10dddd6594bed959e2caa69911f70a6ae8e0554 100644 +index 140ed883c1a92ebcd7a284b98ca71261fa9cb631..b363d7de5000fd370bb4221f48e193382648a185 100644 --- a/yarn.lock +++ b/yarn.lock @@ -140,6 +140,23 @@ @@ -3403,7 +3403,7 @@ index b2fbf543af319fcc3973248b4ed4981db1ca213a..f10dddd6594bed959e2caa69911f70a6 "@electron/get@^1.0.1": version "1.7.2" resolved "https://registry.yarnpkg.com/@electron/get/-/get-1.7.2.tgz#286436a9fb56ff1a1fcdf0e80131fd65f4d1e0fd" -@@ -5421,6 +5438,13 @@ jsprim@^1.2.2: +@@ -5375,6 +5392,13 @@ jsprim@^1.2.2: json-schema "0.2.3" verror "1.10.0" @@ -3417,7 +3417,7 @@ index b2fbf543af319fcc3973248b4ed4981db1ca213a..f10dddd6594bed959e2caa69911f70a6 just-debounce@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/just-debounce/-/just-debounce-1.0.0.tgz#87fccfaeffc0b68cd19d55f6722943f929ea35ea" -@@ -6008,26 +6032,11 @@ minimatch@0.3: +@@ -5955,26 +5979,11 @@ minimatch@0.3: dependencies: brace-expansion "^1.1.7" @@ -3445,7 +3445,7 @@ index b2fbf543af319fcc3973248b4ed4981db1ca213a..f10dddd6594bed959e2caa69911f70a6 minipass@^2.2.1, minipass@^2.3.3: version "2.3.3" resolved "https://registry.yarnpkg.com/minipass/-/minipass-2.3.3.tgz#a7dcc8b7b833f5d368759cce544dccb55f50f233" -@@ -6797,6 +6806,11 @@ p-try@^2.0.0: +@@ -6716,6 +6725,11 @@ p-try@^2.0.0: resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.0.0.tgz#85080bb87c64688fa47996fe8f7dfbe8211760b1" integrity sha512-hMp0onDKIajHfIkdRk3P4CdCmErkYAxxDtP3Wx/4nZ3aGlau2VKh3mZpcuFkH27WQkL/3WBCPOktzA9ZOAnMQQ== diff --git a/lib/vscode b/lib/vscode index a0479759d..2af051012 160000 --- a/lib/vscode +++ b/lib/vscode @@ -1 +1 @@ -Subproject commit a0479759d6e9ea56afa657e454193f72aef85bd0 +Subproject commit 2af051012b66169dde0c4dfae3f5ef48f787ff69 From a44b4455f5f633bfaa66882d1e71777105c38134 Mon Sep 17 00:00:00 2001 From: Asher Date: Wed, 7 Oct 2020 12:54:40 -0500 Subject: [PATCH 019/247] Read plugin name from package.json --- src/node/plugin.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/node/plugin.ts b/src/node/plugin.ts index 2860763ad..bd3765b6a 100644 --- a/src/node/plugin.ts +++ b/src/node/plugin.ts @@ -35,10 +35,12 @@ const loadPlugin = async (pluginPath: string, httpServer: HttpServer, args: Args const plugin: Plugin = require(pluginPath) plugin.activate(httpServer, args) + const packageJson = require(path.join(pluginPath, "package.json")) logger.debug( "Loaded plugin", - field("name", path.basename(pluginPath)), - field("version", require(path.join(pluginPath, "package.json")).version || "n/a"), + field("name", packageJson.name || path.basename(pluginPath)), + field("path", pluginPath), + field("version", packageJson.version || "n/a"), ) } catch (error) { logger.error(error.message) From 579bb94a6c702b331aa333d2e0f3e8ef598939dc Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Tue, 8 Sep 2020 19:39:17 -0400 Subject: [PATCH 020/247] Add coder cloud expose command --- .gitignore | 1 + package.json | 2 ++ src/node/cli.ts | 5 +++++ src/node/coder-cloud.ts | 30 ++++++++++++++++++++++++++++++ src/node/entry.ts | 15 +++++++++++++++ yarn.lock | 16 +++++++++++++++- 6 files changed, 68 insertions(+), 1 deletion(-) create mode 100644 src/node/coder-cloud.ts diff --git a/.gitignore b/.gitignore index 616f9b01b..0b810b296 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,4 @@ release-images/ node_modules node-* /plugins +/lib/coder-cloud-agent diff --git a/package.json b/package.json index 4d75331e9..bf5977c8c 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,7 @@ "@types/pem": "^1.9.5", "@types/safe-compare": "^1.1.0", "@types/semver": "^7.1.0", + "@types/split2": "^2.1.6", "@types/tar-fs": "^2.0.0", "@types/tar-stream": "^2.1.0", "@types/ws": "^7.2.6", @@ -76,6 +77,7 @@ "safe-buffer": "^5.1.1", "safe-compare": "^1.1.4", "semver": "^7.1.3", + "split2": "^3.2.2", "tar": "^6.0.1", "tar-fs": "^2.0.0", "ws": "^7.2.0", diff --git a/src/node/cli.ts b/src/node/cli.ts index d3afe203d..b8272aa51 100644 --- a/src/node/cli.ts +++ b/src/node/cli.ts @@ -47,6 +47,8 @@ export interface Args extends VsArgs { readonly _: string[] readonly "reuse-window"?: boolean readonly "new-window"?: boolean + + readonly "expose"?: OptionalString } interface Option { @@ -155,6 +157,9 @@ const options: Options> = { locale: { type: "string" }, log: { type: LogLevel }, verbose: { type: "boolean", short: "vvv", description: "Enable verbose logging." }, + + "expose": { type: OptionalString, description: "Expose via Coder Cloud with the passed name. You'll get a URL" + + "like https://myname.coder-cloud.com at which you can easily access your code-server instance. Authorization is done via GitHub." }, } export const optionDescriptions = (): string[] => { diff --git a/src/node/coder-cloud.ts b/src/node/coder-cloud.ts new file mode 100644 index 000000000..082e2d823 --- /dev/null +++ b/src/node/coder-cloud.ts @@ -0,0 +1,30 @@ +import { spawn } from "child_process" +import path from "path" +import { logger } from "@coder/logger" +import split2 from "split2" + +export async function coderCloudExpose(serverName: string): Promise { + const coderCloudAgent = path.resolve(__dirname, "../../lib/coder-cloud-agent") + const agent = spawn(coderCloudAgent, ["link", serverName], { + stdio: ["inherit", "inherit", "pipe"], + }) + + agent.stderr.pipe(split2()).on("data", line => { + line = line.replace(/^[0-9-]+ [0-9:]+ [^ ]+\t/, "") + logger.info(line) + }) + + return new Promise((res, rej) => { + agent.on("error", rej) + + agent.on("close", code => { + if (code !== 0) { + rej({ + message: `coder cloud agent exited with ${code}`, + }) + return + } + res() + }) + }) +} diff --git a/src/node/entry.ts b/src/node/entry.ts index a416ae993..860d8de7a 100644 --- a/src/node/entry.ts +++ b/src/node/entry.ts @@ -16,6 +16,7 @@ import { AuthType, HttpServer, HttpServerOptions } from "./http" import { loadPlugins } from "./plugin" import { generateCertificate, hash, humanPath, open } from "./util" import { ipcMain, wrap } from "./wrapper" +import { coderCloudExpose } from "./coder-cloud" process.on("uncaughtException", (error) => { logger.error(`Uncaught exception: ${error.message}`) @@ -188,6 +189,20 @@ async function entry(): Promise { process.exit(1) }) vscode.on("exit", (code) => process.exit(code || 0)) + } else if (args["expose"]) { + logger.debug("exposing code-server via the coder-cloud agent") + + if (!args["expose"].value) { + logger.error("You must pass a name to expose with coder cloud. See --help") + process.exit(1) + } + + try { + await coderCloudExpose(args["expose"].value) + } catch (err) { + logger.error(err.message) + process.exit(1) + } } else if (process.env.VSCODE_IPC_HOOK_CLI) { const pipeArgs: OpenCommandPipeArgs = { type: "open", diff --git a/yarn.lock b/yarn.lock index 68221a85d..6f388626b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1107,6 +1107,13 @@ resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.3.3.tgz#3ad6ed949e7487e7bda6f886b4a2434a2c3d7b1a" integrity sha512-jQxClWFzv9IXdLdhSaTf16XI3NYe6zrEbckSpb5xhKfPbWgIyAY0AFyWWWfaiDcBuj3UHmMkCIwSRqpKMTZL2Q== +"@types/split2@^2.1.6": + version "2.1.6" + resolved "https://registry.yarnpkg.com/@types/split2/-/split2-2.1.6.tgz#b095c9e064853824b22c67993d99b066777402b1" + integrity sha512-ddaFSOMuy2Rp97l6q/LEteQygvTQJuEZ+SRhxFKR0uXGsdbFDqX/QF2xoGcOqLQ8XV91v01SnAv2vpgihNgW/Q== + dependencies: + "@types/node" "*" + "@types/tar-fs@^2.0.0": version "2.0.0" resolved "https://registry.yarnpkg.com/@types/tar-fs/-/tar-fs-2.0.0.tgz#db94cb4ea1cccecafe3d1a53812807efb4bbdbc1" @@ -5996,7 +6003,7 @@ readable-stream@^2.0.2, readable-stream@^2.2.2, readable-stream@^2.3.3, readable string_decoder "~1.1.1" util-deprecate "~1.0.1" -readable-stream@^3.1.1, readable-stream@^3.4.0, readable-stream@^3.6.0: +readable-stream@^3.0.0, readable-stream@^3.1.1, readable-stream@^3.4.0, readable-stream@^3.6.0: version "3.6.0" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198" integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA== @@ -6621,6 +6628,13 @@ split-string@^3.0.1, split-string@^3.0.2: dependencies: extend-shallow "^3.0.0" +split2@^3.2.2: + version "3.2.2" + resolved "https://registry.yarnpkg.com/split2/-/split2-3.2.2.tgz#bf2cf2a37d838312c249c89206fd7a17dd12365f" + integrity sha512-9NThjpgZnifTkJpzTZ7Eue85S49QwpNhZTq6GRJwObb6jnLFNGB7Qm73V5HewTROPyxD0C29xqmaI68bQtV+hg== + dependencies: + readable-stream "^3.0.0" + sprintf-js@~1.0.2: version "1.0.3" resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" From c7c62daa67ef926c57c2f38271d5b9f2018e07a7 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Tue, 8 Sep 2020 19:53:14 -0400 Subject: [PATCH 021/247] Remove unused code in optionDescriptions --- src/node/cli.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/node/cli.ts b/src/node/cli.ts index b8272aa51..8830531fc 100644 --- a/src/node/cli.ts +++ b/src/node/cli.ts @@ -175,7 +175,7 @@ export const optionDescriptions = (): string[] => { ([k, v]) => `${" ".repeat(widths.short - (v.short ? v.short.length : 0))}${v.short ? `-${v.short}` : " "} --${k}${" ".repeat( widths.long - k.length, - )} ${v.description}${typeof v.type === "object" ? ` [${Object.values(v.type).join(", ")}]` : ""}`, + )} ${v.description}`, ) } From 916e24e1098d21893e4df6b0ec5a1d3723d71987 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Tue, 8 Sep 2020 20:30:31 -0400 Subject: [PATCH 022/247] Add support for multiline descriptions --- src/node/cli.ts | 28 ++++++++++++++++++++-------- 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/src/node/cli.ts b/src/node/cli.ts index 8830531fc..318b85935 100644 --- a/src/node/cli.ts +++ b/src/node/cli.ts @@ -131,8 +131,8 @@ const options: Options> = { force: { type: "boolean", description: "Avoid prompts when installing VS Code extensions." }, "install-extension": { type: "string[]", - description: - "Install or update a VS Code extension by id or vsix. The identifier of an extension is `${publisher}.${name}`. To install a specific version provide `@${version}`. For example: 'vscode.csharp@1.2.3'.", + description: "Install or update a VS Code extension by id or vsix. The identifier of an extension is `${publisher}.${name}`.\n" + + "To install a specific version provide `@${version}`. For example: 'vscode.csharp@1.2.3'.", }, "enable-proposed-api": { type: "string[]", @@ -158,8 +158,14 @@ const options: Options> = { log: { type: LogLevel }, verbose: { type: "boolean", short: "vvv", description: "Enable verbose logging." }, - "expose": { type: OptionalString, description: "Expose via Coder Cloud with the passed name. You'll get a URL" + - "like https://myname.coder-cloud.com at which you can easily access your code-server instance. Authorization is done via GitHub." }, + "expose": { + type: OptionalString, + description: ` + Securely expose code-server via Coder Cloud with the passed name. You'll get a URL like + https://myname.coder-cloud.com at which you can easily access your code-server instance. + Authorization is done via GitHub. Only the first code-server spawned with the current + configuration will be accessible.` + }, } export const optionDescriptions = (): string[] => { @@ -172,10 +178,16 @@ export const optionDescriptions = (): string[] => { { short: 0, long: 0 }, ) return entries.map( - ([k, v]) => - `${" ".repeat(widths.short - (v.short ? v.short.length : 0))}${v.short ? `-${v.short}` : " "} --${k}${" ".repeat( - widths.long - k.length, - )} ${v.description}`, + ([k, v]) => { + let help = `${" ".repeat(widths.short - (v.short ? v.short.length : 0))}${v.short ? `-${v.short}` : " "} --${k} ` + return help + v.description?.trim().split(/\n/).map((line, i) => { + line = line.trim() + if (i == 0) { + return " ".repeat(widths.long - k.length) + line + } + return " ".repeat(widths.long + widths.short + 6) + line + }).join("\n") + }, ) } From 55a7e8b56fa9d5d89d5a7df9509d38fb421d292a Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Wed, 9 Sep 2020 00:03:01 -0400 Subject: [PATCH 023/247] Implement automatic cloud proxying --- package.json | 3 +- src/node/coder-cloud.ts | 129 +++++++++++++++++++++++++++++++++++++++- src/node/entry.ts | 4 +- yarn.lock | 5 ++ 4 files changed, 138 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index bf5977c8c..5bf72f641 100644 --- a/package.json +++ b/package.json @@ -82,7 +82,8 @@ "tar-fs": "^2.0.0", "ws": "^7.2.0", "xdg-basedir": "^4.0.0", - "yarn": "^1.22.4" + "yarn": "^1.22.4", + "delay": "^4.4.0" }, "bin": { "code-server": "out/node/entry.js" diff --git a/src/node/coder-cloud.ts b/src/node/coder-cloud.ts index 082e2d823..167b80c3f 100644 --- a/src/node/coder-cloud.ts +++ b/src/node/coder-cloud.ts @@ -2,9 +2,14 @@ import { spawn } from "child_process" import path from "path" import { logger } from "@coder/logger" import split2 from "split2" +import delay from "delay" +import fs from "fs" +import { promisify } from "util" +import xdgBasedir from "xdg-basedir" + +const coderCloudAgent = path.resolve(__dirname, "../../lib/coder-cloud-agent") export async function coderCloudExpose(serverName: string): Promise { - const coderCloudAgent = path.resolve(__dirname, "../../lib/coder-cloud-agent") const agent = spawn(coderCloudAgent, ["link", serverName], { stdio: ["inherit", "inherit", "pipe"], }) @@ -28,3 +33,125 @@ export async function coderCloudExpose(serverName: string): Promise { }) }) } + +export function coderCloudProxy(addr: string) { + // addr needs to be in host:port format. + // So we trim the protocol. + addr = addr.replace(/^https?:\/\//, "") + + if (!xdgBasedir.config) { + return + } + + const sessionTokenPath = path.join(xdgBasedir.config, "coder-cloud", "session") + + const _proxy = async () => { + await waitForPath(sessionTokenPath) + + logger.info("exposing coder-server with coder-cloud") + + const agent = spawn(coderCloudAgent, ["proxy", "--code-server-addr", addr], { + stdio: ["inherit", "inherit", "pipe"], + }) + + agent.stderr.pipe(split2()).on("data", line => { + line = line.replace(/^[0-9-]+ [0-9:]+ [^ ]+\t/, "") + logger.info(line) + }) + + return new Promise((res, rej) => { + agent.on("error", rej) + + agent.on("close", code => { + if (code !== 0) { + rej({ + message: `coder cloud agent exited with ${code}`, + }) + return + } + res() + }) + }) + } + + const proxy = async () => { + try { + await _proxy() + } catch(err) { + logger.error(err.message) + } + setTimeout(proxy, 3000) + } + proxy() +} + +/** + * waitForPath efficiently implements waiting for the existence of a path. + * + * We intentionally do not use fs.watchFile as it is very slow from testing. + * I believe it polls instead of watching. + * + * The way this works is for each level of the path it will check if it exists + * and if not, it will wait for it. e.g. if the path is /home/nhooyr/.config/coder-cloud/session + * then first it will check if /home exists, then /home/nhooyr and so on. + * + * The wait works by first creating a watch promise for the p segment. + * We call fs.watch on the dirname of the p segment. When the dirname has a change, + * we check if the p segment exists and if it does, we resolve the watch promise. + * On any error or the watcher being closed, we reject the watch promise. + * + * Once that promise is setup, we check if the p segment exists with fs.exists + * and if it does, we close the watcher and return. + * + * Now we race the watch promise and a 2000ms delay promise. Once the race + * is complete, we close the watcher. + * + * If the watch promise was the one to resolve, we return. + * Otherwise we setup the watch promise again and retry. + * + * This combination of polling and watching is very reliable and efficient. + */ +async function waitForPath(p: string): Promise { + const segs = p.split(path.sep) + for (let i = 0; i < segs.length; i++) { + const s = path.join("/", ...segs.slice(0, i + 1)) + // We need to wait for each segment to exist. + await _waitForPath(s) + } +} + +async function _waitForPath(p: string): Promise { + const watchDir = path.dirname(p) + + logger.debug(`waiting for ${p}`) + + for (;;) { + const w = fs.watch(watchDir) + const watchPromise = new Promise((res, rej) => { + w.on("change", async () => { + if (await promisify(fs.exists)(p)) { + res() + } + }) + w.on("close", () => rej(new Error("watcher closed"))) + w.on("error", rej) + }) + + // We want to ignore any errors from this promise being rejected if the file + // already exists below. + watchPromise.catch(() => {}) + + if (await promisify(fs.exists)(p)) { + // The path exists! + w.close() + return + } + + // Now we wait for either the watch promise to resolve/reject or 2000ms. + const s = await Promise.race([watchPromise.then(() => "exists"), delay(2000)]) + w.close() + if (s === "exists") { + return + } + } +} diff --git a/src/node/entry.ts b/src/node/entry.ts index 860d8de7a..539b3bccd 100644 --- a/src/node/entry.ts +++ b/src/node/entry.ts @@ -12,11 +12,11 @@ import { StaticHttpProvider } from "./app/static" import { UpdateHttpProvider } from "./app/update" import { VscodeHttpProvider } from "./app/vscode" import { Args, bindAddrFromAllSources, optionDescriptions, parse, readConfigFile, setDefaults } from "./cli" +import { coderCloudExpose, coderCloudProxy } from "./coder-cloud" import { AuthType, HttpServer, HttpServerOptions } from "./http" import { loadPlugins } from "./plugin" import { generateCertificate, hash, humanPath, open } from "./util" import { ipcMain, wrap } from "./wrapper" -import { coderCloudExpose } from "./coder-cloud" process.on("uncaughtException", (error) => { logger.error(`Uncaught exception: ${error.message}`) @@ -123,6 +123,8 @@ const main = async (args: Args, cliArgs: Args, configArgs: Args): Promise httpServer.proxyDomains.forEach((domain) => logger.info(` - *.${domain}`)) } + coderCloudProxy(serverAddress!) + if (serverAddress && !options.socket && args.open) { // The web socket doesn't seem to work if browsing with 0.0.0.0. const openAddress = serverAddress.replace(/:\/\/0.0.0.0/, "://localhost") diff --git a/yarn.lock b/yarn.lock index 6f388626b..8e13b6f35 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2525,6 +2525,11 @@ define-property@^2.0.2: is-descriptor "^1.0.2" isobject "^3.0.1" +delay@^4.4.0: + version "4.4.0" + resolved "https://registry.yarnpkg.com/delay/-/delay-4.4.0.tgz#71abc745f3ce043fe7f450491236541edec4ad0c" + integrity sha512-txgOrJu3OdtOfTiEOT2e76dJVfG/1dz2NZ4F0Pyt4UGZJryssMRp5vdM5wQoLwSOBNdrJv3F9PAhp/heqd7vrA== + delayed-stream@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" From 0aa98279d679c97ded60e71a94267b4d2284f0cd Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Wed, 9 Sep 2020 00:06:28 -0400 Subject: [PATCH 024/247] Fixes for CI --- src/node/cli.ts | 40 +++++++++++++++++++++++----------------- src/node/coder-cloud.ts | 16 ++++++++-------- 2 files changed, 31 insertions(+), 25 deletions(-) diff --git a/src/node/cli.ts b/src/node/cli.ts index 318b85935..c4d0d9dd4 100644 --- a/src/node/cli.ts +++ b/src/node/cli.ts @@ -48,7 +48,7 @@ export interface Args extends VsArgs { readonly "reuse-window"?: boolean readonly "new-window"?: boolean - readonly "expose"?: OptionalString + readonly expose?: OptionalString } interface Option { @@ -131,8 +131,9 @@ const options: Options> = { force: { type: "boolean", description: "Avoid prompts when installing VS Code extensions." }, "install-extension": { type: "string[]", - description: "Install or update a VS Code extension by id or vsix. The identifier of an extension is `${publisher}.${name}`.\n" + - "To install a specific version provide `@${version}`. For example: 'vscode.csharp@1.2.3'.", + description: + "Install or update a VS Code extension by id or vsix. The identifier of an extension is `${publisher}.${name}`.\n" + + "To install a specific version provide `@${version}`. For example: 'vscode.csharp@1.2.3'.", }, "enable-proposed-api": { type: "string[]", @@ -158,13 +159,13 @@ const options: Options> = { log: { type: LogLevel }, verbose: { type: "boolean", short: "vvv", description: "Enable verbose logging." }, - "expose": { + expose: { type: OptionalString, description: ` Securely expose code-server via Coder Cloud with the passed name. You'll get a URL like https://myname.coder-cloud.com at which you can easily access your code-server instance. Authorization is done via GitHub. Only the first code-server spawned with the current - configuration will be accessible.` + configuration will be accessible.`, }, } @@ -177,18 +178,23 @@ export const optionDescriptions = (): string[] => { }), { short: 0, long: 0 }, ) - return entries.map( - ([k, v]) => { - let help = `${" ".repeat(widths.short - (v.short ? v.short.length : 0))}${v.short ? `-${v.short}` : " "} --${k} ` - return help + v.description?.trim().split(/\n/).map((line, i) => { - line = line.trim() - if (i == 0) { - return " ".repeat(widths.long - k.length) + line - } - return " ".repeat(widths.long + widths.short + 6) + line - }).join("\n") - }, - ) + return entries.map(([k, v]) => { + const help = `${" ".repeat(widths.short - (v.short ? v.short.length : 0))}${v.short ? `-${v.short}` : " "} --${k} ` + return ( + help + + v.description + ?.trim() + .split(/\n/) + .map((line, i) => { + line = line.trim() + if (i === 0) { + return " ".repeat(widths.long - k.length) + line + } + return " ".repeat(widths.long + widths.short + 6) + line + }) + .join("\n") + ) + }) } export const parse = ( diff --git a/src/node/coder-cloud.ts b/src/node/coder-cloud.ts index 167b80c3f..b621b08cd 100644 --- a/src/node/coder-cloud.ts +++ b/src/node/coder-cloud.ts @@ -1,9 +1,9 @@ -import { spawn } from "child_process" -import path from "path" import { logger } from "@coder/logger" -import split2 from "split2" +import { spawn } from "child_process" import delay from "delay" import fs from "fs" +import path from "path" +import split2 from "split2" import { promisify } from "util" import xdgBasedir from "xdg-basedir" @@ -14,7 +14,7 @@ export async function coderCloudExpose(serverName: string): Promise { stdio: ["inherit", "inherit", "pipe"], }) - agent.stderr.pipe(split2()).on("data", line => { + agent.stderr.pipe(split2()).on("data", (line) => { line = line.replace(/^[0-9-]+ [0-9:]+ [^ ]+\t/, "") logger.info(line) }) @@ -22,7 +22,7 @@ export async function coderCloudExpose(serverName: string): Promise { return new Promise((res, rej) => { agent.on("error", rej) - agent.on("close", code => { + agent.on("close", (code) => { if (code !== 0) { rej({ message: `coder cloud agent exited with ${code}`, @@ -54,7 +54,7 @@ export function coderCloudProxy(addr: string) { stdio: ["inherit", "inherit", "pipe"], }) - agent.stderr.pipe(split2()).on("data", line => { + agent.stderr.pipe(split2()).on("data", (line) => { line = line.replace(/^[0-9-]+ [0-9:]+ [^ ]+\t/, "") logger.info(line) }) @@ -62,7 +62,7 @@ export function coderCloudProxy(addr: string) { return new Promise((res, rej) => { agent.on("error", rej) - agent.on("close", code => { + agent.on("close", (code) => { if (code !== 0) { rej({ message: `coder cloud agent exited with ${code}`, @@ -77,7 +77,7 @@ export function coderCloudProxy(addr: string) { const proxy = async () => { try { await _proxy() - } catch(err) { + } catch (err) { logger.error(err.message) } setTimeout(proxy, 3000) From eacca7d6929cc1a40f8ad7053a09db118d9733a3 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Wed, 9 Sep 2020 00:07:04 -0400 Subject: [PATCH 025/247] Unrelated fixes for CI --- ci/release-image/entrypoint.sh | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/ci/release-image/entrypoint.sh b/ci/release-image/entrypoint.sh index ee58d07a6..7842a3564 100755 --- a/ci/release-image/entrypoint.sh +++ b/ci/release-image/entrypoint.sh @@ -2,7 +2,8 @@ set -eu # This isn't set by default. -export USER="$(whoami)" +USER="$(whoami)" +export USER if [ "${DOCKER_USER-}" != "$USER" ]; then echo "$DOCKER_USER ALL=(ALL) NOPASSWD:ALL" | sudo tee -a /etc/sudoers.d/nopasswd > /dev/null @@ -11,7 +12,7 @@ if [ "${DOCKER_USER-}" != "$USER" ]; then sudo usermod --login "$DOCKER_USER" coder sudo groupmod -n "$DOCKER_USER" coder - export USER="$(whoami)" + USER="$DOCKER_USER" sudo sed -i "/coder/d" /etc/sudoers.d/nopasswd sudo sed -i "s/coder/$DOCKER_USER/g" /etc/fixuid/config.yml From b22f3cb72f4e95dd0b432dc1b0f98295969c3d12 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Tue, 15 Sep 2020 10:15:51 -0400 Subject: [PATCH 026/247] Add $HOME to ./ci/dev/image/run.sh --- .gitignore | 1 + ci/build/clean.sh | 3 ++- ci/dev/image/run.sh | 2 ++ 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 0b810b296..4929c46fb 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,4 @@ node_modules node-* /plugins /lib/coder-cloud-agent +.home diff --git a/ci/build/clean.sh b/ci/build/clean.sh index 0e0425a4b..7a83a2845 100755 --- a/ci/build/clean.sh +++ b/ci/build/clean.sh @@ -14,7 +14,8 @@ main() { release-images \ dist \ .cache \ - node-* + node-* \ + .home pushd lib/vscode git clean -xffd diff --git a/ci/dev/image/run.sh b/ci/dev/image/run.sh index 70ab67e1d..0f17d4c27 100755 --- a/ci/dev/image/run.sh +++ b/ci/dev/image/run.sh @@ -4,11 +4,13 @@ set -euo pipefail main() { cd "$(dirname "$0")/../../.." source ./ci/lib.sh + mkdir -p .home docker run \ -it \ --rm \ -v "$PWD:/src" \ + -e HOME="/src/.home" \ -w /src \ -p 127.0.0.1:8080:8080 \ -u "$(id -u):$(id -g)" \ From 607444c695aefe0a29503ee0434024a90109b8f7 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Tue, 15 Sep 2020 10:41:47 -0400 Subject: [PATCH 027/247] Switch off debian:8 to debian:10 for the typescript build image We only want to use an old version for glibc which the centos:7 image takes care of. The old version of git used in debian:8 was causing problems with the uid/gid passthrough with no user in passwd. --- .github/workflows/ci.yaml | 10 +++++----- .github/workflows/publish.yaml | 4 ++-- ci/dev/image/run.sh | 3 ++- ci/dev/lint.sh | 5 +---- ci/images/centos7/Dockerfile | 2 +- ci/images/{debian8 => debian10}/Dockerfile | 22 ++++++---------------- doc/CONTRIBUTING.md | 4 ++-- 7 files changed, 19 insertions(+), 31 deletions(-) rename ci/images/{debian8 => debian10}/Dockerfile (57%) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index dcf917841..a265c98ef 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -8,7 +8,7 @@ jobs: steps: - uses: actions/checkout@v1 - name: Run ./ci/steps/fmt.sh - uses: ./ci/images/debian8 + uses: ./ci/images/debian10 with: args: ./ci/steps/fmt.sh @@ -17,7 +17,7 @@ jobs: steps: - uses: actions/checkout@v1 - name: Run ./ci/steps/lint.sh - uses: ./ci/images/debian8 + uses: ./ci/images/debian10 with: args: ./ci/steps/lint.sh @@ -26,7 +26,7 @@ jobs: steps: - uses: actions/checkout@v1 - name: Run ./ci/steps/test.sh - uses: ./ci/images/debian8 + uses: ./ci/images/debian10 with: args: ./ci/steps/test.sh @@ -35,7 +35,7 @@ jobs: steps: - uses: actions/checkout@v1 - name: Run ./ci/steps/release.sh - uses: ./ci/images/debian8 + uses: ./ci/images/debian10 with: args: ./ci/steps/release.sh - name: Upload npm package artifact @@ -116,7 +116,7 @@ jobs: name: release-packages path: ./release-packages - name: Run ./ci/steps/build-docker-image.sh - uses: ./ci/images/debian8 + uses: ./ci/images/debian10 with: args: ./ci/steps/build-docker-image.sh - name: Upload release image diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml index c2fe429b9..74540651f 100644 --- a/.github/workflows/publish.yaml +++ b/.github/workflows/publish.yaml @@ -10,7 +10,7 @@ jobs: steps: - uses: actions/checkout@v1 - name: Run ./ci/steps/publish-npm.sh - uses: ./ci/images/debian8 + uses: ./ci/images/debian10 with: args: ./ci/steps/publish-npm.sh env: @@ -22,7 +22,7 @@ jobs: steps: - uses: actions/checkout@v1 - name: Run ./ci/steps/push-docker-manifest.sh - uses: ./ci/images/debian8 + uses: ./ci/images/debian10 with: args: ./ci/steps/push-docker-manifest.sh env: diff --git a/ci/dev/image/run.sh b/ci/dev/image/run.sh index 0f17d4c27..08391581b 100755 --- a/ci/dev/image/run.sh +++ b/ci/dev/image/run.sh @@ -11,11 +11,12 @@ main() { --rm \ -v "$PWD:/src" \ -e HOME="/src/.home" \ + -e USER="coder" \ -w /src \ -p 127.0.0.1:8080:8080 \ -u "$(id -u):$(id -g)" \ -e CI \ - "$(docker_build ./ci/images/debian8)" \ + "$(docker_build ./ci/images/"${IMAGE-debian10}")" \ "$@" } diff --git a/ci/dev/lint.sh b/ci/dev/lint.sh index 219c3793b..5f7c549bc 100755 --- a/ci/dev/lint.sh +++ b/ci/dev/lint.sh @@ -7,10 +7,7 @@ main() { eslint --max-warnings=0 --fix $(git ls-files "*.ts" "*.tsx" "*.js") stylelint $(git ls-files "*.css") tsc --noEmit - # See comment in ./ci/image/debian8 - if [[ ! ${CI-} ]]; then - shellcheck -e SC2046,SC2164,SC2154,SC1091,SC1090,SC2002 $(git ls-files "*.sh") - fi + shellcheck -e SC2046,SC2164,SC2154,SC1091,SC1090,SC2002 $(git ls-files "*.sh") } main "$@" diff --git a/ci/images/centos7/Dockerfile b/ci/images/centos7/Dockerfile index 92c212024..2c0c71ecd 100644 --- a/ci/images/centos7/Dockerfile +++ b/ci/images/centos7/Dockerfile @@ -15,7 +15,7 @@ RUN npm config set python python2 RUN yum install -y epel-release && yum install -y jq RUN yum install -y rsync -# Copied from ../debian8/Dockerfile +# Copied from ../debian10/Dockerfile # Install Go dependencies RUN ARCH="$(uname -m | sed 's/x86_64/amd64/; s/aarch64/arm64/')" && \ curl -fsSL "https://dl.google.com/go/go1.14.3.linux-$ARCH.tar.gz" | tar -C /usr/local -xz diff --git a/ci/images/debian8/Dockerfile b/ci/images/debian10/Dockerfile similarity index 57% rename from ci/images/debian8/Dockerfile rename to ci/images/debian10/Dockerfile index 4c62a398b..a13a25a03 100644 --- a/ci/images/debian8/Dockerfile +++ b/ci/images/debian10/Dockerfile @@ -1,4 +1,4 @@ -FROM debian:8 +FROM debian:10 RUN apt-get update @@ -24,23 +24,13 @@ RUN apt-get install -y build-essential \ RUN apt-get install -y gettext-base # Misc build dependencies. -RUN apt-get install -y git rsync unzip - -# We need latest jq from debian buster for date support. -RUN ARCH="$(dpkg --print-architecture)" && \ - curl -fsSOL http://http.us.debian.org/debian/pool/main/libo/libonig/libonig5_6.9.1-1_$ARCH.deb && \ - dpkg -i libonig*.deb && \ - curl -fsSOL http://http.us.debian.org/debian/pool/main/j/jq/libjq1_1.5+dfsg-2+b1_$ARCH.deb && \ - dpkg -i libjq*.deb && \ - curl -fsSOL http://http.us.debian.org/debian/pool/main/j/jq/jq_1.5+dfsg-2+b1_$ARCH.deb && \ - dpkg -i jq*.deb && rm *.deb +RUN apt-get install -y git rsync unzip jq # Installs shellcheck. -# Unfortunately coredumps on debian:8 so disabled for now. -#RUN curl -fsSL https://github.com/koalaman/shellcheck/releases/download/v0.7.1/shellcheck-v0.7.1.linux.$(uname -m).tar.xz | \ -# tar -xJ && \ -# mv shellcheck*/shellcheck /usr/local/bin && \ -# rm -R shellcheck* +RUN curl -fsSL https://github.com/koalaman/shellcheck/releases/download/v0.7.1/shellcheck-v0.7.1.linux.$(uname -m).tar.xz | \ + tar -xJ && \ + mv shellcheck*/shellcheck /usr/local/bin && \ + rm -R shellcheck* # Install Go dependencies RUN ARCH="$(uname -m | sed 's/x86_64/amd64/; s/aarch64/arm64/')" && \ diff --git a/doc/CONTRIBUTING.md b/doc/CONTRIBUTING.md index 80348848d..a15e25d20 100644 --- a/doc/CONTRIBUTING.md +++ b/doc/CONTRIBUTING.md @@ -32,7 +32,7 @@ Differences: - We require a minimum of node v12 but later versions should work. - We use [nfpm](https://github.com/goreleaser/nfpm) to build `.deb` and `.rpm` packages. - We use [jq](https://stedolan.github.io/jq/) to build code-server releases. -- The [CI container](../ci/images/debian8/Dockerfile) is a useful reference for all our dependencies. +- The [CI container](../ci/images/debian10/Dockerfile) is a useful reference for all our dependencies. ## Development Workflow @@ -76,7 +76,7 @@ node . Build release packages (make sure you run `./ci/steps/release.sh` first): ``` -./ci/dev/image/run.sh ./ci/steps/release-packages.sh +IMAGE=centos7 ./ci/dev/image/run.sh ./ci/steps/release-packages.sh # The standalone release is in ./release-standalone # .deb, .rpm and the standalone archive are in ./release-packages ``` From 22c4a7e10f8cfd4a7a6f752952221f80f37bc5f1 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Mon, 28 Sep 2020 15:43:26 -0400 Subject: [PATCH 028/247] Make linking and starting code-server to the cloud a single command --- src/node/cli.ts | 6 +++--- src/node/coder-cloud.ts | 2 +- src/node/entry.ts | 41 ++++++++++++++++++++++++++--------------- 3 files changed, 30 insertions(+), 19 deletions(-) diff --git a/src/node/cli.ts b/src/node/cli.ts index c4d0d9dd4..854686685 100644 --- a/src/node/cli.ts +++ b/src/node/cli.ts @@ -48,7 +48,7 @@ export interface Args extends VsArgs { readonly "reuse-window"?: boolean readonly "new-window"?: boolean - readonly expose?: OptionalString + readonly "coder-link"?: OptionalString } interface Option { @@ -159,10 +159,10 @@ const options: Options> = { log: { type: LogLevel }, verbose: { type: "boolean", short: "vvv", description: "Enable verbose logging." }, - expose: { + "coder-link": { type: OptionalString, description: ` - Securely expose code-server via Coder Cloud with the passed name. You'll get a URL like + Securely link code-server via Coder Cloud with the passed name. You'll get a URL like https://myname.coder-cloud.com at which you can easily access your code-server instance. Authorization is done via GitHub. Only the first code-server spawned with the current configuration will be accessible.`, diff --git a/src/node/coder-cloud.ts b/src/node/coder-cloud.ts index b621b08cd..c8782812b 100644 --- a/src/node/coder-cloud.ts +++ b/src/node/coder-cloud.ts @@ -9,7 +9,7 @@ import xdgBasedir from "xdg-basedir" const coderCloudAgent = path.resolve(__dirname, "../../lib/coder-cloud-agent") -export async function coderCloudExpose(serverName: string): Promise { +export async function coderCloudLink(serverName: string): Promise { const agent = spawn(coderCloudAgent, ["link", serverName], { stdio: ["inherit", "inherit", "pipe"], }) diff --git a/src/node/entry.ts b/src/node/entry.ts index 539b3bccd..42abbc2de 100644 --- a/src/node/entry.ts +++ b/src/node/entry.ts @@ -12,7 +12,7 @@ import { StaticHttpProvider } from "./app/static" import { UpdateHttpProvider } from "./app/update" import { VscodeHttpProvider } from "./app/vscode" import { Args, bindAddrFromAllSources, optionDescriptions, parse, readConfigFile, setDefaults } from "./cli" -import { coderCloudExpose, coderCloudProxy } from "./coder-cloud" +import { coderCloudLink, coderCloudProxy } from "./coder-cloud" import { AuthType, HttpServer, HttpServerOptions } from "./http" import { loadPlugins } from "./plugin" import { generateCertificate, hash, humanPath, open } from "./util" @@ -36,6 +36,15 @@ const version = pkg.version || "development" const commit = pkg.commit || "development" const main = async (args: Args, cliArgs: Args, configArgs: Args): Promise => { + if (args["coder-link"]) { + // If we're being exposed to the cloud, we listen on a random address. + args = { + ...args, + host: "localhost", + port: 0, + } + } + if (!args.auth) { args = { ...args, @@ -131,6 +140,22 @@ const main = async (args: Args, cliArgs: Args, configArgs: Args): Promise await open(openAddress).catch(console.error) logger.info(`Opened ${openAddress}`) } + + if (args["coder-link"]) { + if (!args["coder-link"].value) { + logger.error("You must pass a name to link with coder cloud. See --help") + process.exit(1) + } + + logger.info(`linking code-server to the cloud with name ${args["coder-link"].value}`) + + try { + await coderCloudLink(args["coder-link"].value) + } catch (err) { + logger.error(err.message) + process.exit(1) + } + } } async function entry(): Promise { @@ -191,20 +216,6 @@ async function entry(): Promise { process.exit(1) }) vscode.on("exit", (code) => process.exit(code || 0)) - } else if (args["expose"]) { - logger.debug("exposing code-server via the coder-cloud agent") - - if (!args["expose"].value) { - logger.error("You must pass a name to expose with coder cloud. See --help") - process.exit(1) - } - - try { - await coderCloudExpose(args["expose"].value) - } catch (err) { - logger.error(err.message) - process.exit(1) - } } else if (process.env.VSCODE_IPC_HOOK_CLI) { const pipeArgs: OpenCommandPipeArgs = { type: "open", From 9035bfa871669a5ff8ab9e3ab4ae485d2d57c26f Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Tue, 6 Oct 2020 14:19:04 -0400 Subject: [PATCH 029/247] Add coder cloud agent binary to build process --- ci/build/build-code-server.sh | 6 ++++++ ci/build/build-release.sh | 1 + ci/build/clean.sh | 3 ++- ci/dev/image/run.sh | 1 + 4 files changed, 10 insertions(+), 1 deletion(-) diff --git a/ci/build/build-code-server.sh b/ci/build/build-code-server.sh index df0852804..0aff035af 100755 --- a/ci/build/build-code-server.sh +++ b/ci/build/build-code-server.sh @@ -18,6 +18,12 @@ main() { chmod +x out/node/entry.js fi + if ! [ -f ./lib/coder-cloud-agent ]; then + OS="$(uname | tr '[:upper:]' '[:lower:]')" + curl -fsSL "https://storage.googleapis.com/coder-cloud-releases/agent/latest/$OS/cloud-agent" -o ./lib/coder-cloud-agent + chmod +x ./lib/coder-cloud-agent + fi + parcel build \ --public-url "." \ --out-dir dist \ diff --git a/ci/build/build-release.sh b/ci/build/build-release.sh index 8d8d1c903..88d3fe613 100755 --- a/ci/build/build-release.sh +++ b/ci/build/build-release.sh @@ -25,6 +25,7 @@ main() { rsync README.md "$RELEASE_PATH" rsync LICENSE.txt "$RELEASE_PATH" rsync ./lib/vscode/ThirdPartyNotices.txt "$RELEASE_PATH" + rsync ./lib/coder-cloud-agent "$RELEASE_PATH/lib" # code-server exports types which can be imported and used by plugins. Those # types import ipc.d.ts but it isn't included in the final vscode build so diff --git a/ci/build/clean.sh b/ci/build/clean.sh index 7a83a2845..52d123c9a 100755 --- a/ci/build/clean.sh +++ b/ci/build/clean.sh @@ -15,7 +15,8 @@ main() { dist \ .cache \ node-* \ - .home + .home \ + lib/coder-cloud-agent pushd lib/vscode git clean -xffd diff --git a/ci/dev/image/run.sh b/ci/dev/image/run.sh index 08391581b..0557f1b2a 100755 --- a/ci/dev/image/run.sh +++ b/ci/dev/image/run.sh @@ -12,6 +12,7 @@ main() { -v "$PWD:/src" \ -e HOME="/src/.home" \ -e USER="coder" \ + -e GITHUB_TOKEN \ -w /src \ -p 127.0.0.1:8080:8080 \ -u "$(id -u):$(id -g)" \ From c308ae0eddb71bbeea3da025c1fdb676e4485ac3 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Tue, 6 Oct 2020 14:21:08 -0400 Subject: [PATCH 030/247] Ignore dirty lib/vscode --- .gitmodules | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitmodules b/.gitmodules index 9854a1b1d..f2cdafc7a 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,4 @@ [submodule "lib/vscode"] path = lib/vscode url = https://github.com/microsoft/vscode + ignore = dirty From fae07e14fbd2ed4101d25c8c5bf715efcd041253 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Tue, 6 Oct 2020 14:36:55 -0400 Subject: [PATCH 031/247] Fix Go inside dev image --- ci/images/centos7/Dockerfile | 9 +++++++-- ci/images/debian10/Dockerfile | 9 +++++++-- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/ci/images/centos7/Dockerfile b/ci/images/centos7/Dockerfile index 2c0c71ecd..52a101778 100644 --- a/ci/images/centos7/Dockerfile +++ b/ci/images/centos7/Dockerfile @@ -16,10 +16,15 @@ RUN yum install -y epel-release && yum install -y jq RUN yum install -y rsync # Copied from ../debian10/Dockerfile -# Install Go dependencies +# Install Go. RUN ARCH="$(uname -m | sed 's/x86_64/amd64/; s/aarch64/arm64/')" && \ curl -fsSL "https://dl.google.com/go/go1.14.3.linux-$ARCH.tar.gz" | tar -C /usr/local -xz -ENV PATH=/usr/local/go/bin:/root/go/bin:$PATH +ENV GOPATH=/gopath +# Ensures running this image as another user works. +RUN mkdir -p $GOPATH && chmod -R 777 $GOPATH +ENV PATH=/usr/local/go/bin:$GOPATH/bin:$PATH + +# Install Go dependencies ENV GO111MODULE=on RUN go get mvdan.cc/sh/v3/cmd/shfmt RUN go get github.com/goreleaser/nfpm/cmd/nfpm diff --git a/ci/images/debian10/Dockerfile b/ci/images/debian10/Dockerfile index a13a25a03..108348b65 100644 --- a/ci/images/debian10/Dockerfile +++ b/ci/images/debian10/Dockerfile @@ -32,10 +32,15 @@ RUN curl -fsSL https://github.com/koalaman/shellcheck/releases/download/v0.7.1/s mv shellcheck*/shellcheck /usr/local/bin && \ rm -R shellcheck* -# Install Go dependencies +# Install Go. RUN ARCH="$(uname -m | sed 's/x86_64/amd64/; s/aarch64/arm64/')" && \ curl -fsSL "https://dl.google.com/go/go1.14.3.linux-$ARCH.tar.gz" | tar -C /usr/local -xz -ENV PATH=/usr/local/go/bin:/root/go/bin:$PATH +ENV GOPATH=/gopath +# Ensures running this image as another user works. +RUN mkdir -p $GOPATH && chmod -R 777 $GOPATH +ENV PATH=/usr/local/go/bin:$GOPATH/bin:$PATH + +# Install Go dependencies ENV GO111MODULE=on RUN go get mvdan.cc/sh/v3/cmd/shfmt RUN go get github.com/goreleaser/nfpm/cmd/nfpm From dd996d8f6051741baff7ef1a37cfd8e21e2bd61b Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Tue, 6 Oct 2020 14:45:27 -0400 Subject: [PATCH 032/247] v3.6.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 5bf72f641..878eebf18 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "code-server", "license": "MIT", - "version": "3.5.0", + "version": "3.6.0", "description": "Run VS Code on a remote server.", "homepage": "https://github.com/cdr/code-server", "bugs": { From 6e8248cf0cf16383af7f6d732862a5d021649ea4 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Tue, 6 Oct 2020 17:19:36 -0400 Subject: [PATCH 033/247] Fix zip release creation --- ci/build/build-packages.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ci/build/build-packages.sh b/ci/build/build-packages.sh index 058a54781..c3ad6577a 100755 --- a/ci/build/build-packages.sh +++ b/ci/build/build-packages.sh @@ -33,7 +33,7 @@ release_archive() { elif [[ $OS == "darwin" && $ARCH == "x86_64" ]]; then # Just exists to make autoupdating from 3.2.0 work again. mv ./release-standalone "./$release_name" - zip -r "release-packages/$release_name.zip" "./$release_name" + zip -yr "release-packages/$release_name.zip" "./$release_name" mv "./$release_name" ./release-standalone return else From c3c24fe4d2935ae553ddd5a6be192f449b3f4512 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Tue, 6 Oct 2020 21:05:32 -0400 Subject: [PATCH 034/247] Fixes for @ammarb --- src/node/cli.ts | 8 ++++---- src/node/coder-cloud.ts | 2 +- src/node/entry.ts | 22 +++++++++------------- 3 files changed, 14 insertions(+), 18 deletions(-) diff --git a/src/node/cli.ts b/src/node/cli.ts index 854686685..61e5d9d78 100644 --- a/src/node/cli.ts +++ b/src/node/cli.ts @@ -48,7 +48,7 @@ export interface Args extends VsArgs { readonly "reuse-window"?: boolean readonly "new-window"?: boolean - readonly "coder-link"?: OptionalString + readonly "coder-bind"?: string } interface Option { @@ -159,10 +159,10 @@ const options: Options> = { log: { type: LogLevel }, verbose: { type: "boolean", short: "vvv", description: "Enable verbose logging." }, - "coder-link": { - type: OptionalString, + "coder-bind": { + type: "string", description: ` - Securely link code-server via Coder Cloud with the passed name. You'll get a URL like + Securely bind code-server via Coder Cloud with the passed name. You'll get a URL like https://myname.coder-cloud.com at which you can easily access your code-server instance. Authorization is done via GitHub. Only the first code-server spawned with the current configuration will be accessible.`, diff --git a/src/node/coder-cloud.ts b/src/node/coder-cloud.ts index c8782812b..5f7b7c0a1 100644 --- a/src/node/coder-cloud.ts +++ b/src/node/coder-cloud.ts @@ -9,7 +9,7 @@ import xdgBasedir from "xdg-basedir" const coderCloudAgent = path.resolve(__dirname, "../../lib/coder-cloud-agent") -export async function coderCloudLink(serverName: string): Promise { +export async function coderCloudBind(serverName: string): Promise { const agent = spawn(coderCloudAgent, ["link", serverName], { stdio: ["inherit", "inherit", "pipe"], }) diff --git a/src/node/entry.ts b/src/node/entry.ts index 42abbc2de..1be886e03 100644 --- a/src/node/entry.ts +++ b/src/node/entry.ts @@ -12,7 +12,7 @@ import { StaticHttpProvider } from "./app/static" import { UpdateHttpProvider } from "./app/update" import { VscodeHttpProvider } from "./app/vscode" import { Args, bindAddrFromAllSources, optionDescriptions, parse, readConfigFile, setDefaults } from "./cli" -import { coderCloudLink, coderCloudProxy } from "./coder-cloud" +import { coderCloudBind, coderCloudProxy } from "./coder-cloud" import { AuthType, HttpServer, HttpServerOptions } from "./http" import { loadPlugins } from "./plugin" import { generateCertificate, hash, humanPath, open } from "./util" @@ -36,13 +36,15 @@ const version = pkg.version || "development" const commit = pkg.commit || "development" const main = async (args: Args, cliArgs: Args, configArgs: Args): Promise => { - if (args["coder-link"]) { - // If we're being exposed to the cloud, we listen on a random address. + if (args["coder-bind"]) { + // If we're being exposed to the cloud, we listen on a random address and disable auth. args = { ...args, host: "localhost", port: 0, + auth: AuthType.None, } + logger.info("coder-bind: disabling auth and listening on random localhost port") } if (!args.auth) { @@ -132,8 +134,6 @@ const main = async (args: Args, cliArgs: Args, configArgs: Args): Promise httpServer.proxyDomains.forEach((domain) => logger.info(` - *.${domain}`)) } - coderCloudProxy(serverAddress!) - if (serverAddress && !options.socket && args.open) { // The web socket doesn't seem to work if browsing with 0.0.0.0. const openAddress = serverAddress.replace(/:\/\/0.0.0.0/, "://localhost") @@ -141,16 +141,12 @@ const main = async (args: Args, cliArgs: Args, configArgs: Args): Promise logger.info(`Opened ${openAddress}`) } - if (args["coder-link"]) { - if (!args["coder-link"].value) { - logger.error("You must pass a name to link with coder cloud. See --help") - process.exit(1) - } - - logger.info(`linking code-server to the cloud with name ${args["coder-link"].value}`) + if (args["coder-bind"]) { + logger.info(`linking code-server to the cloud with name ${args["coder-bind"]}`) try { - await coderCloudLink(args["coder-link"].value) + await coderCloudBind(args["coder-bind"]) + coderCloudProxy(serverAddress!) } catch (err) { logger.error(err.message) process.exit(1) From 1c16814a89e8c057591101763e4d39b54bc6d8f8 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Tue, 6 Oct 2020 21:10:46 -0400 Subject: [PATCH 035/247] Update coder-bind docs --- src/node/cli.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/node/cli.ts b/src/node/cli.ts index 61e5d9d78..d7ae7fc23 100644 --- a/src/node/cli.ts +++ b/src/node/cli.ts @@ -164,8 +164,8 @@ const options: Options> = { description: ` Securely bind code-server via Coder Cloud with the passed name. You'll get a URL like https://myname.coder-cloud.com at which you can easily access your code-server instance. - Authorization is done via GitHub. Only the first code-server spawned with the current - configuration will be accessible.`, + Authorization is done via GitHub. + `, }, } From 4b3c089630aa0ad495e3177df824f0bb0c7d60fa Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Wed, 7 Oct 2020 02:03:27 -0400 Subject: [PATCH 036/247] Remove dead code --- package.json | 3 +- src/node/coder-cloud.ts | 85 ----------------------------------------- src/node/entry.ts | 4 +- 3 files changed, 3 insertions(+), 89 deletions(-) diff --git a/package.json b/package.json index 878eebf18..cc3edd30a 100644 --- a/package.json +++ b/package.json @@ -82,8 +82,7 @@ "tar-fs": "^2.0.0", "ws": "^7.2.0", "xdg-basedir": "^4.0.0", - "yarn": "^1.22.4", - "delay": "^4.4.0" + "yarn": "^1.22.4" }, "bin": { "code-server": "out/node/entry.js" diff --git a/src/node/coder-cloud.ts b/src/node/coder-cloud.ts index 5f7b7c0a1..a3c3c5906 100644 --- a/src/node/coder-cloud.ts +++ b/src/node/coder-cloud.ts @@ -1,11 +1,7 @@ import { logger } from "@coder/logger" import { spawn } from "child_process" -import delay from "delay" -import fs from "fs" import path from "path" import split2 from "split2" -import { promisify } from "util" -import xdgBasedir from "xdg-basedir" const coderCloudAgent = path.resolve(__dirname, "../../lib/coder-cloud-agent") @@ -39,17 +35,7 @@ export function coderCloudProxy(addr: string) { // So we trim the protocol. addr = addr.replace(/^https?:\/\//, "") - if (!xdgBasedir.config) { - return - } - - const sessionTokenPath = path.join(xdgBasedir.config, "coder-cloud", "session") - const _proxy = async () => { - await waitForPath(sessionTokenPath) - - logger.info("exposing coder-server with coder-cloud") - const agent = spawn(coderCloudAgent, ["proxy", "--code-server-addr", addr], { stdio: ["inherit", "inherit", "pipe"], }) @@ -84,74 +70,3 @@ export function coderCloudProxy(addr: string) { } proxy() } - -/** - * waitForPath efficiently implements waiting for the existence of a path. - * - * We intentionally do not use fs.watchFile as it is very slow from testing. - * I believe it polls instead of watching. - * - * The way this works is for each level of the path it will check if it exists - * and if not, it will wait for it. e.g. if the path is /home/nhooyr/.config/coder-cloud/session - * then first it will check if /home exists, then /home/nhooyr and so on. - * - * The wait works by first creating a watch promise for the p segment. - * We call fs.watch on the dirname of the p segment. When the dirname has a change, - * we check if the p segment exists and if it does, we resolve the watch promise. - * On any error or the watcher being closed, we reject the watch promise. - * - * Once that promise is setup, we check if the p segment exists with fs.exists - * and if it does, we close the watcher and return. - * - * Now we race the watch promise and a 2000ms delay promise. Once the race - * is complete, we close the watcher. - * - * If the watch promise was the one to resolve, we return. - * Otherwise we setup the watch promise again and retry. - * - * This combination of polling and watching is very reliable and efficient. - */ -async function waitForPath(p: string): Promise { - const segs = p.split(path.sep) - for (let i = 0; i < segs.length; i++) { - const s = path.join("/", ...segs.slice(0, i + 1)) - // We need to wait for each segment to exist. - await _waitForPath(s) - } -} - -async function _waitForPath(p: string): Promise { - const watchDir = path.dirname(p) - - logger.debug(`waiting for ${p}`) - - for (;;) { - const w = fs.watch(watchDir) - const watchPromise = new Promise((res, rej) => { - w.on("change", async () => { - if (await promisify(fs.exists)(p)) { - res() - } - }) - w.on("close", () => rej(new Error("watcher closed"))) - w.on("error", rej) - }) - - // We want to ignore any errors from this promise being rejected if the file - // already exists below. - watchPromise.catch(() => {}) - - if (await promisify(fs.exists)(p)) { - // The path exists! - w.close() - return - } - - // Now we wait for either the watch promise to resolve/reject or 2000ms. - const s = await Promise.race([watchPromise.then(() => "exists"), delay(2000)]) - w.close() - if (s === "exists") { - return - } - } -} diff --git a/src/node/entry.ts b/src/node/entry.ts index 1be886e03..901c732f7 100644 --- a/src/node/entry.ts +++ b/src/node/entry.ts @@ -142,14 +142,14 @@ const main = async (args: Args, cliArgs: Args, configArgs: Args): Promise } if (args["coder-bind"]) { - logger.info(`linking code-server to the cloud with name ${args["coder-bind"]}`) try { + logger.info(`binding code-server to the cloud with name ${args["coder-bind"]}`) await coderCloudBind(args["coder-bind"]) coderCloudProxy(serverAddress!) } catch (err) { logger.error(err.message) - process.exit(1) + ipcMain().exit(1) } } } From c4f1c053bf51536cc3884d6a61b256c457e17dd4 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Wed, 7 Oct 2020 03:52:37 -0400 Subject: [PATCH 037/247] Show valid values for --auth in --help See https://github.com/nhooyr/code-server/pull/1/files#r485847134 --- src/node/cli.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/node/cli.ts b/src/node/cli.ts index d7ae7fc23..deedf8309 100644 --- a/src/node/cli.ts +++ b/src/node/cli.ts @@ -192,7 +192,8 @@ export const optionDescriptions = (): string[] => { } return " ".repeat(widths.long + widths.short + 6) + line }) - .join("\n") + .join("\n") + + (typeof v.type === "object" ? ` [${Object.values(v.type).join(", ")}]` : "") ) }) } From bfe731f4f30782528a054f7d1e103ef9e95b9b2b Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Wed, 7 Oct 2020 12:16:27 -0400 Subject: [PATCH 038/247] Ensure socket is undefined with --coder-bind --- src/node/entry.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/node/entry.ts b/src/node/entry.ts index 901c732f7..16118a1ef 100644 --- a/src/node/entry.ts +++ b/src/node/entry.ts @@ -38,11 +38,12 @@ const commit = pkg.commit || "development" const main = async (args: Args, cliArgs: Args, configArgs: Args): Promise => { if (args["coder-bind"]) { // If we're being exposed to the cloud, we listen on a random address and disable auth. - args = { - ...args, + cliArgs = { + ...cliArgs, host: "localhost", port: 0, auth: AuthType.None, + socket: undefined, } logger.info("coder-bind: disabling auth and listening on random localhost port") } From 7cc16ceb3aee37b8fb9a3afc17c21b1cb6f7a2e4 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Wed, 7 Oct 2020 13:26:57 -0400 Subject: [PATCH 039/247] Document KEEP_MODULES --- ci/dev/image/run.sh | 2 ++ doc/CONTRIBUTING.md | 7 +++++++ 2 files changed, 9 insertions(+) diff --git a/ci/dev/image/run.sh b/ci/dev/image/run.sh index 0557f1b2a..3d5e15dd6 100755 --- a/ci/dev/image/run.sh +++ b/ci/dev/image/run.sh @@ -13,6 +13,8 @@ main() { -e HOME="/src/.home" \ -e USER="coder" \ -e GITHUB_TOKEN \ + -e KEEP_MODULES \ + -e MINIFY \ -w /src \ -p 127.0.0.1:8080:8080 \ -u "$(id -u):$(id -g)" \ diff --git a/doc/CONTRIBUTING.md b/doc/CONTRIBUTING.md index a15e25d20..62c20f915 100644 --- a/doc/CONTRIBUTING.md +++ b/doc/CONTRIBUTING.md @@ -99,6 +99,13 @@ yarn test:standalone-release yarn package ``` +For a faster release build you can also run: + +``` +KEEP_MODULES=1 ./ci/steps/release.sh +node ./release +``` + ## Structure The `code-server` script serves an HTTP API to login and start a remote VS Code process. From df3089f3ad4b8231407e365819ef88aac42fcd76 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Wed, 7 Oct 2020 15:54:41 -0400 Subject: [PATCH 040/247] coder-cloud: Use consolidated bind command --- src/node/cli.ts | 4 ++-- src/node/coder-cloud.ts | 44 +++++------------------------------------ src/node/entry.ts | 7 ++----- 3 files changed, 9 insertions(+), 46 deletions(-) diff --git a/src/node/cli.ts b/src/node/cli.ts index deedf8309..ed188f281 100644 --- a/src/node/cli.ts +++ b/src/node/cli.ts @@ -48,7 +48,7 @@ export interface Args extends VsArgs { readonly "reuse-window"?: boolean readonly "new-window"?: boolean - readonly "coder-bind"?: string + readonly "coder-bind"?: OptionalString } interface Option { @@ -160,7 +160,7 @@ const options: Options> = { verbose: { type: "boolean", short: "vvv", description: "Enable verbose logging." }, "coder-bind": { - type: "string", + type: OptionalString, description: ` Securely bind code-server via Coder Cloud with the passed name. You'll get a URL like https://myname.coder-cloud.com at which you can easily access your code-server instance. diff --git a/src/node/coder-cloud.ts b/src/node/coder-cloud.ts index a3c3c5906..b57cf36df 100644 --- a/src/node/coder-cloud.ts +++ b/src/node/coder-cloud.ts @@ -5,8 +5,8 @@ import split2 from "split2" const coderCloudAgent = path.resolve(__dirname, "../../lib/coder-cloud-agent") -export async function coderCloudBind(serverName: string): Promise { - const agent = spawn(coderCloudAgent, ["link", serverName], { +function runAgent(...args: string[]): Promise { + const agent = spawn(coderCloudAgent, args, { stdio: ["inherit", "inherit", "pipe"], }) @@ -30,43 +30,9 @@ export async function coderCloudBind(serverName: string): Promise { }) } -export function coderCloudProxy(addr: string) { +export function coderCloudBind(csAddr: string, serverName = ""): Promise { // addr needs to be in host:port format. // So we trim the protocol. - addr = addr.replace(/^https?:\/\//, "") - - const _proxy = async () => { - const agent = spawn(coderCloudAgent, ["proxy", "--code-server-addr", addr], { - stdio: ["inherit", "inherit", "pipe"], - }) - - agent.stderr.pipe(split2()).on("data", (line) => { - line = line.replace(/^[0-9-]+ [0-9:]+ [^ ]+\t/, "") - logger.info(line) - }) - - return new Promise((res, rej) => { - agent.on("error", rej) - - agent.on("close", (code) => { - if (code !== 0) { - rej({ - message: `coder cloud agent exited with ${code}`, - }) - return - } - res() - }) - }) - } - - const proxy = async () => { - try { - await _proxy() - } catch (err) { - logger.error(err.message) - } - setTimeout(proxy, 3000) - } - proxy() + csAddr = csAddr.replace(/^https?:\/\//, "") + return runAgent("bind", `--code-server-addr=${csAddr}`, serverName) } diff --git a/src/node/entry.ts b/src/node/entry.ts index 16118a1ef..a39db7efb 100644 --- a/src/node/entry.ts +++ b/src/node/entry.ts @@ -12,7 +12,7 @@ import { StaticHttpProvider } from "./app/static" import { UpdateHttpProvider } from "./app/update" import { VscodeHttpProvider } from "./app/vscode" import { Args, bindAddrFromAllSources, optionDescriptions, parse, readConfigFile, setDefaults } from "./cli" -import { coderCloudBind, coderCloudProxy } from "./coder-cloud" +import { coderCloudBind } from "./coder-cloud" import { AuthType, HttpServer, HttpServerOptions } from "./http" import { loadPlugins } from "./plugin" import { generateCertificate, hash, humanPath, open } from "./util" @@ -143,11 +143,8 @@ const main = async (args: Args, cliArgs: Args, configArgs: Args): Promise } if (args["coder-bind"]) { - try { - logger.info(`binding code-server to the cloud with name ${args["coder-bind"]}`) - await coderCloudBind(args["coder-bind"]) - coderCloudProxy(serverAddress!) + await coderCloudBind(serverAddress!, args["coder-bind"].value) } catch (err) { logger.error(err.message) ipcMain().exit(1) From ebbcb8d6a7e2fc0acbe02c2534e11b46650a6a81 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Wed, 7 Oct 2020 16:09:40 -0400 Subject: [PATCH 041/247] Update yarn.lock --- yarn.lock | 5 ----- 1 file changed, 5 deletions(-) diff --git a/yarn.lock b/yarn.lock index 8e13b6f35..6f388626b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2525,11 +2525,6 @@ define-property@^2.0.2: is-descriptor "^1.0.2" isobject "^3.0.1" -delay@^4.4.0: - version "4.4.0" - resolved "https://registry.yarnpkg.com/delay/-/delay-4.4.0.tgz#71abc745f3ce043fe7f450491236541edec4ad0c" - integrity sha512-txgOrJu3OdtOfTiEOT2e76dJVfG/1dz2NZ4F0Pyt4UGZJryssMRp5vdM5wQoLwSOBNdrJv3F9PAhp/heqd7vrA== - delayed-stream@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" From 85b0804be5174940851ebdccc7d58b3eba581a32 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Wed, 7 Oct 2020 16:21:10 -0400 Subject: [PATCH 042/247] Remove cliArgs from main No purpose when all the args are in the args parameter. We only need configArgs for bindAddrFromAllSources. --- ci/build/build-release.sh | 1 - src/node/entry.ts | 16 ++++++++-------- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/ci/build/build-release.sh b/ci/build/build-release.sh index 88d3fe613..eedda98e2 100755 --- a/ci/build/build-release.sh +++ b/ci/build/build-release.sh @@ -58,7 +58,6 @@ EOF rsync yarn.lock "$RELEASE_PATH" rsync ci/build/npm-postinstall.sh "$RELEASE_PATH/postinstall.sh" - if [ "$KEEP_MODULES" = 1 ]; then rsync node_modules/ "$RELEASE_PATH/node_modules" fi diff --git a/src/node/entry.ts b/src/node/entry.ts index a39db7efb..664134197 100644 --- a/src/node/entry.ts +++ b/src/node/entry.ts @@ -35,11 +35,11 @@ try { const version = pkg.version || "development" const commit = pkg.commit || "development" -const main = async (args: Args, cliArgs: Args, configArgs: Args): Promise => { +const main = async (args: Args, configArgs: Args): Promise => { if (args["coder-bind"]) { // If we're being exposed to the cloud, we listen on a random address and disable auth. - cliArgs = { - ...cliArgs, + args = { + ...args, host: "localhost", port: 0, auth: AuthType.None, @@ -64,7 +64,7 @@ const main = async (args: Args, cliArgs: Args, configArgs: Args): Promise if (args.auth === AuthType.Password && !password) { throw new Error("Please pass in a password via the config file or $PASSWORD") } - const [host, port] = bindAddrFromAllSources(cliArgs, configArgs) + const [host, port] = bindAddrFromAllSources(args, configArgs) // Spawn the main HTTP server. const options: HttpServerOptions = { @@ -153,21 +153,21 @@ const main = async (args: Args, cliArgs: Args, configArgs: Args): Promise } async function entry(): Promise { - const tryParse = async (): Promise<[Args, Args, Args]> => { + const tryParse = async (): Promise<[Args, Args]> => { try { const cliArgs = parse(process.argv.slice(2)) const configArgs = await readConfigFile(cliArgs.config) // This prioritizes the flags set in args over the ones in the config file. let args = Object.assign(configArgs, cliArgs) args = await setDefaults(args) - return [args, cliArgs, configArgs] + return [args, configArgs] } catch (error) { console.error(error.message) process.exit(1) } } - const [args, cliArgs, configArgs] = await tryParse() + const [args, configArgs] = await tryParse() if (args.help) { console.log("code-server", version, commit) console.log("") @@ -262,7 +262,7 @@ async function entry(): Promise { vscode.write(JSON.stringify(pipeArgs)) vscode.end() } else { - wrap(() => main(args, cliArgs, configArgs)) + wrap(() => main(args, configArgs)) } } From 3e28ab85a072e8df7cecd295fbd3e01b4d08d40d Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Wed, 7 Oct 2020 17:16:20 -0400 Subject: [PATCH 043/247] Add debug log for options passed to the agent --- src/node/coder-cloud.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/node/coder-cloud.ts b/src/node/coder-cloud.ts index b57cf36df..3b36a2f57 100644 --- a/src/node/coder-cloud.ts +++ b/src/node/coder-cloud.ts @@ -6,6 +6,8 @@ import split2 from "split2" const coderCloudAgent = path.resolve(__dirname, "../../lib/coder-cloud-agent") function runAgent(...args: string[]): Promise { + logger.debug(`running agent with ${args}`) + const agent = spawn(coderCloudAgent, args, { stdio: ["inherit", "inherit", "pipe"], }) From febf4ead9631a252695c25690f61c416b49c60ec Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Wed, 7 Oct 2020 17:28:13 -0400 Subject: [PATCH 044/247] Fix the clean script :facepalm: --- ci/build/clean.sh | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/ci/build/clean.sh b/ci/build/clean.sh index 52d123c9a..b80632278 100755 --- a/ci/build/clean.sh +++ b/ci/build/clean.sh @@ -5,18 +5,7 @@ main() { cd "$(dirname "${0}")/../.." source ./ci/lib.sh - rm -rf \ - out \ - release \ - release-standalone \ - release-packages \ - release-gcp \ - release-images \ - dist \ - .cache \ - node-* \ - .home \ - lib/coder-cloud-agent + git clean -Xffd pushd lib/vscode git clean -xffd From 8063c79e4436a1fc4ab65c186d1ff9fd60fe3ea9 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Thu, 8 Oct 2020 16:55:13 -0400 Subject: [PATCH 045/247] Patch VS Code to avoid deleting extension dependencies (#2170) Closes #1961 --- ci/dev/vscode.patch | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/ci/dev/vscode.patch b/ci/dev/vscode.patch index 1632dcd42..b3a7289df 100644 --- a/ci/dev/vscode.patch +++ b/ci/dev/vscode.patch @@ -31,6 +31,32 @@ index f2ea1bd37010b1eb8a43ce9beaae4a88810f6e2d..3f660f9981921ec465d2b8809a1a5ea5 const yarnrc = fs.readFileSync(path.join(REPO_ROOT, 'remote', '.yarnrc'), 'utf8'); const target = /^target "(.*)"$/m.exec(yarnrc)[1]; return target; +diff --git a/build/lib/extensions.js b/build/lib/extensions.js +index 9cc40c4e1befd38886dc5880581d6f462a38dd3a..34e1fc89a8ac1c273a5cb41f19a088a8ec759d24 100644 +--- a/build/lib/extensions.js ++++ b/build/lib/extensions.js +@@ -66,7 +66,7 @@ function fromLocal(extensionPath, forWeb) { + if (isWebPacked) { + input = updateExtensionPackageJSON(input, (data) => { + delete data.scripts; +- delete data.dependencies; ++ // https://github.com/cdr/code-server/pull/2041#issuecomment-685910322 + delete data.devDependencies; + if (data.main) { + data.main = data.main.replace('/out/', /dist/); +diff --git a/build/lib/extensions.ts b/build/lib/extensions.ts +index 7e529f17cb84d28d84de4ff64fa9fb8fc48135a9..462d699dc485369c74a4d9fdfefa48ba6124ac3a 100644 +--- a/build/lib/extensions.ts ++++ b/build/lib/extensions.ts +@@ -70,7 +70,7 @@ function fromLocal(extensionPath: string, forWeb: boolean): Stream { + if (isWebPacked) { + input = updateExtensionPackageJSON(input, (data: any) => { + delete data.scripts; +- delete data.dependencies; ++ // https://github.com/cdr/code-server/pull/2041#issuecomment-685910322 + delete data.devDependencies; + if (data.main) { + data.main = data.main.replace('/out/', /dist/); diff --git a/build/lib/node.js b/build/lib/node.js index 403ae3d9657f823019542e739fc39292db20e4fe..738ee8cee0e79aa239af10e1abefc9e836b8ce33 100644 --- a/build/lib/node.js From 9f963c7e66771bebae6e4cea634c597788d6c503 Mon Sep 17 00:00:00 2001 From: Asher Date: Thu, 8 Oct 2020 16:15:05 -0500 Subject: [PATCH 046/247] Update Node to 12.18.4 (#2175) --- ci/images/centos7/Dockerfile | 2 +- ci/steps/release-packages.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ci/images/centos7/Dockerfile b/ci/images/centos7/Dockerfile index 92c212024..038e6dc73 100644 --- a/ci/images/centos7/Dockerfile +++ b/ci/images/centos7/Dockerfile @@ -1,6 +1,6 @@ FROM centos:7 -ARG NODE_VERSION=v12.18.3 +ARG NODE_VERSION=v12.18.4 RUN ARCH="$(uname -m | sed 's/86_64/64/; s/aarch64/arm64/')" && \ curl -fsSL "https://nodejs.org/dist/$NODE_VERSION/node-$NODE_VERSION-linux-$ARCH.tar.xz" | tar -C /usr/local -xJ && \ mv "/usr/local/node-$NODE_VERSION-linux-$ARCH" "/usr/local/node-$NODE_VERSION" diff --git a/ci/steps/release-packages.sh b/ci/steps/release-packages.sh index cc6cd2a06..ba8d61d5c 100755 --- a/ci/steps/release-packages.sh +++ b/ci/steps/release-packages.sh @@ -4,7 +4,7 @@ set -euo pipefail main() { cd "$(dirname "$0")/../.." - NODE_VERSION=v12.18.3 + NODE_VERSION=v12.18.4 NODE_OS="$(uname | tr '[:upper:]' '[:lower:]')" NODE_ARCH="$(uname -m | sed 's/86_64/64/; s/aarch64/arm64/')" curl -L "https://nodejs.org/dist/$NODE_VERSION/node-$NODE_VERSION-$NODE_OS-$NODE_ARCH.tar.gz" | tar -xz From c86d7398abdace6a593853e9accdbf66aab04ec2 Mon Sep 17 00:00:00 2001 From: Asher Date: Thu, 8 Oct 2020 16:18:00 -0500 Subject: [PATCH 047/247] Use system data directory for plugins --- src/node/plugin.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/node/plugin.ts b/src/node/plugin.ts index bd3765b6a..e17a9909c 100644 --- a/src/node/plugin.ts +++ b/src/node/plugin.ts @@ -69,7 +69,7 @@ const _loadPlugins = async (pluginDir: string, httpServer: HttpServer, args: Arg * `CS_PLUGIN` (also colon-separated). */ export const loadPlugins = async (httpServer: HttpServer, args: Args): Promise => { - const pluginPath = process.env.CS_PLUGIN_PATH || `${path.join(paths.data, "plugins")}:/etc/code-server/plugins` + const pluginPath = process.env.CS_PLUGIN_PATH || `${path.join(paths.data, "plugins")}:/usr/share/code-server/plugins` const plugin = process.env.CS_PLUGIN || "" await Promise.all([ // Built-in plugins. From f5489cd3a0c119ddf4ee01eaa02b5051fc042413 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Fri, 9 Oct 2020 07:38:38 -0400 Subject: [PATCH 048/247] Hide -coder-bind for now --- src/node/cli.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/node/cli.ts b/src/node/cli.ts index ed188f281..80c1e7402 100644 --- a/src/node/cli.ts +++ b/src/node/cli.ts @@ -65,6 +65,11 @@ interface Option { * Description of the option. Leave blank to hide the option. */ description?: string + + /** + * Whether to print this option in --help output + */ + hidden?: boolean } type OptionType = T extends boolean @@ -166,6 +171,7 @@ const options: Options> = { https://myname.coder-cloud.com at which you can easily access your code-server instance. Authorization is done via GitHub. `, + hidden: true, }, } @@ -178,7 +184,7 @@ export const optionDescriptions = (): string[] => { }), { short: 0, long: 0 }, ) - return entries.map(([k, v]) => { + return entries.filter(([_, v]) => !v.hidden).map(([k, v]) => { const help = `${" ".repeat(widths.short - (v.short ? v.short.length : 0))}${v.short ? `-${v.short}` : " "} --${k} ` return ( help + From 9ff37977a8169fcde8a770d1f08b4961841d2b77 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Fri, 9 Oct 2020 07:38:58 -0400 Subject: [PATCH 049/247] Make --coder-bind disable HTTPS --- src/node/entry.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/node/entry.ts b/src/node/entry.ts index 664134197..d05d5d552 100644 --- a/src/node/entry.ts +++ b/src/node/entry.ts @@ -44,6 +44,7 @@ const main = async (args: Args, configArgs: Args): Promise => { port: 0, auth: AuthType.None, socket: undefined, + cert: undefined, } logger.info("coder-bind: disabling auth and listening on random localhost port") } From a5b6d080bd425280f0c3d2a9c139f835c17f817d Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Fri, 9 Oct 2020 07:45:20 -0400 Subject: [PATCH 050/247] Add CS_BETA and note --coder-bind is in beta --- src/node/cli.ts | 52 ++++++++++++++++++++++++----------------- src/node/coder-cloud.ts | 2 ++ 2 files changed, 33 insertions(+), 21 deletions(-) diff --git a/src/node/cli.ts b/src/node/cli.ts index 80c1e7402..5e9e71534 100644 --- a/src/node/cli.ts +++ b/src/node/cli.ts @@ -67,9 +67,9 @@ interface Option { description?: string /** - * Whether to print this option in --help output + * If marked as beta, the option is not printed unless $CS_BETA is set. */ - hidden?: boolean + beta?: boolean } type OptionType = T extends boolean @@ -170,8 +170,10 @@ const options: Options> = { Securely bind code-server via Coder Cloud with the passed name. You'll get a URL like https://myname.coder-cloud.com at which you can easily access your code-server instance. Authorization is done via GitHub. + This is presently beta and requires being accepted for testing. + See https://github.com/cdr/code-server/discussions/2137 `, - hidden: true, + beta: true, }, } @@ -184,24 +186,32 @@ export const optionDescriptions = (): string[] => { }), { short: 0, long: 0 }, ) - return entries.filter(([_, v]) => !v.hidden).map(([k, v]) => { - const help = `${" ".repeat(widths.short - (v.short ? v.short.length : 0))}${v.short ? `-${v.short}` : " "} --${k} ` - return ( - help + - v.description - ?.trim() - .split(/\n/) - .map((line, i) => { - line = line.trim() - if (i === 0) { - return " ".repeat(widths.long - k.length) + line - } - return " ".repeat(widths.long + widths.short + 6) + line - }) - .join("\n") + - (typeof v.type === "object" ? ` [${Object.values(v.type).join(", ")}]` : "") - ) - }) + return entries + .filter(([, v]) => { + // If CS_BETA is set, we show beta options but if not, then we do not want + // to show beta options. + return process.env.CS_BETA || !v.beta + }) + .map(([k, v]) => { + const help = `${" ".repeat(widths.short - (v.short ? v.short.length : 0))}${ + v.short ? `-${v.short}` : " " + } --${k} ` + return ( + help + + v.description + ?.trim() + .split(/\n/) + .map((line, i) => { + line = line.trim() + if (i === 0) { + return " ".repeat(widths.long - k.length) + line + } + return " ".repeat(widths.long + widths.short + 6) + line + }) + .join("\n") + + (typeof v.type === "object" ? ` [${Object.values(v.type).join(", ")}]` : "") + ) + }) } export const parse = ( diff --git a/src/node/coder-cloud.ts b/src/node/coder-cloud.ts index 3b36a2f57..f8038cbe9 100644 --- a/src/node/coder-cloud.ts +++ b/src/node/coder-cloud.ts @@ -33,6 +33,8 @@ function runAgent(...args: string[]): Promise { } export function coderCloudBind(csAddr: string, serverName = ""): Promise { + logger.info("Remember --coder-bind is a beta feature and requires being accepted for testing") + logger.info("See https://github.com/cdr/code-server/discussions/2137") // addr needs to be in host:port format. // So we trim the protocol. csAddr = csAddr.replace(/^https?:\/\//, "") From 9002f118c3358a228bd561353cb764a2bfaff7b5 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Fri, 9 Oct 2020 07:50:58 -0400 Subject: [PATCH 051/247] Remove the extra releases for autoupdating purposes --- ci/build/build-packages.sh | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/ci/build/build-packages.sh b/ci/build/build-packages.sh index c3ad6577a..a5ef794e5 100755 --- a/ci/build/build-packages.sh +++ b/ci/build/build-packages.sh @@ -11,15 +11,6 @@ main() { mkdir -p release-packages release_archive - # Will stop the auto update issues and allow people to upgrade their scripts - # for the new release structure. - if [[ $ARCH == "amd64" ]]; then - if [[ $OS == "linux" ]]; then - ARCH=x86_64 release_archive - elif [[ $OS == "macos" ]]; then - OS=darwin ARCH=x86_64 release_archive - fi - fi if [[ $OS == "linux" ]]; then release_nfpm @@ -30,12 +21,6 @@ release_archive() { local release_name="code-server-$VERSION-$OS-$ARCH" if [[ $OS == "linux" ]]; then tar -czf "release-packages/$release_name.tar.gz" --transform "s/^\.\/release-standalone/$release_name/" ./release-standalone - elif [[ $OS == "darwin" && $ARCH == "x86_64" ]]; then - # Just exists to make autoupdating from 3.2.0 work again. - mv ./release-standalone "./$release_name" - zip -yr "release-packages/$release_name.zip" "./$release_name" - mv "./$release_name" ./release-standalone - return else tar -czf "release-packages/$release_name.tar.gz" -s "/^release-standalone/$release_name/" release-standalone fi From 2d1de749f46d3f961420d754d050d05339f3dc8f Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Fri, 9 Oct 2020 12:34:52 -0400 Subject: [PATCH 052/247] Unlink socket before using (#2181) See https://stackoverflow.com/a/34881585/4283659 Closes #1538 --- .eslintrc.yaml | 1 + src/node/http.ts | 9 ++++++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/.eslintrc.yaml b/.eslintrc.yaml index 306dd2c22..92657d629 100644 --- a/.eslintrc.yaml +++ b/.eslintrc.yaml @@ -30,6 +30,7 @@ rules: eqeqeq: error import/order: [error, { alphabetize: { order: "asc" }, groups: [["builtin", "external", "internal"], "parent", "sibling"] }] + no-async-promise-executor: off settings: # Does not work with CommonJS unfortunately. diff --git a/src/node/http.ts b/src/node/http.ts index 297dda0cc..c616c8837 100644 --- a/src/node/http.ts +++ b/src/node/http.ts @@ -578,11 +578,18 @@ export class HttpServer { */ public listen(): Promise { if (!this.listenPromise) { - this.listenPromise = new Promise((resolve, reject) => { + this.listenPromise = new Promise(async (resolve, reject) => { this.server.on("error", reject) this.server.on("upgrade", this.onUpgrade) const onListen = (): void => resolve(this.address()) if (this.options.socket) { + try { + await fs.unlink(this.options.socket) + } catch (err) { + if (err.code !== "ENOENT") { + logger.warn(err.message) + } + } this.server.listen(this.options.socket, onListen) } else if (this.options.host) { // [] is the correct format when using :: but Node errors with them. From d67bd3f60479c4f425a7a04875e517d45a76e86f Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Fri, 9 Oct 2020 12:57:20 -0400 Subject: [PATCH 053/247] cloud: Rename --coder-bind to --link --- src/node/cli.ts | 4 ++-- src/node/coder-cloud.ts | 2 +- src/node/entry.ts | 8 ++++---- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/node/cli.ts b/src/node/cli.ts index 5e9e71534..b723417d1 100644 --- a/src/node/cli.ts +++ b/src/node/cli.ts @@ -48,7 +48,7 @@ export interface Args extends VsArgs { readonly "reuse-window"?: boolean readonly "new-window"?: boolean - readonly "coder-bind"?: OptionalString + readonly link?: OptionalString } interface Option { @@ -164,7 +164,7 @@ const options: Options> = { log: { type: LogLevel }, verbose: { type: "boolean", short: "vvv", description: "Enable verbose logging." }, - "coder-bind": { + link: { type: OptionalString, description: ` Securely bind code-server via Coder Cloud with the passed name. You'll get a URL like diff --git a/src/node/coder-cloud.ts b/src/node/coder-cloud.ts index f8038cbe9..570d9cc68 100644 --- a/src/node/coder-cloud.ts +++ b/src/node/coder-cloud.ts @@ -33,7 +33,7 @@ function runAgent(...args: string[]): Promise { } export function coderCloudBind(csAddr: string, serverName = ""): Promise { - logger.info("Remember --coder-bind is a beta feature and requires being accepted for testing") + logger.info("Remember --link is a beta feature and requires being accepted for testing") logger.info("See https://github.com/cdr/code-server/discussions/2137") // addr needs to be in host:port format. // So we trim the protocol. diff --git a/src/node/entry.ts b/src/node/entry.ts index d05d5d552..5869ae461 100644 --- a/src/node/entry.ts +++ b/src/node/entry.ts @@ -36,7 +36,7 @@ const version = pkg.version || "development" const commit = pkg.commit || "development" const main = async (args: Args, configArgs: Args): Promise => { - if (args["coder-bind"]) { + if (args.link) { // If we're being exposed to the cloud, we listen on a random address and disable auth. args = { ...args, @@ -46,7 +46,7 @@ const main = async (args: Args, configArgs: Args): Promise => { socket: undefined, cert: undefined, } - logger.info("coder-bind: disabling auth and listening on random localhost port") + logger.info("link: disabling auth and listening on random localhost port for cloud agent") } if (!args.auth) { @@ -143,9 +143,9 @@ const main = async (args: Args, configArgs: Args): Promise => { logger.info(`Opened ${openAddress}`) } - if (args["coder-bind"]) { + if (args.link) { try { - await coderCloudBind(serverAddress!, args["coder-bind"].value) + await coderCloudBind(serverAddress!, args.link.value) } catch (err) { logger.error(err.message) ipcMain().exit(1) From fcfb03382acb9ac307c04a3647b076038d2ba06d Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Fri, 9 Oct 2020 12:57:48 -0400 Subject: [PATCH 054/247] cloud: Add mention of cloud repo --- src/node/coder-cloud.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/node/coder-cloud.ts b/src/node/coder-cloud.ts index 570d9cc68..1241bc90b 100644 --- a/src/node/coder-cloud.ts +++ b/src/node/coder-cloud.ts @@ -3,6 +3,7 @@ import { spawn } from "child_process" import path from "path" import split2 from "split2" +// https://github.com/cdr/coder-cloud const coderCloudAgent = path.resolve(__dirname, "../../lib/coder-cloud-agent") function runAgent(...args: string[]): Promise { From 64a6a460c8d961df1538b02910a4c0b04b1158b1 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Fri, 9 Oct 2020 15:00:49 -0400 Subject: [PATCH 055/247] Adjust npm package postinstall to install extension dependencies (#2180) Closes #1961 --- ci/build/npm-postinstall.sh | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/ci/build/npm-postinstall.sh b/ci/build/npm-postinstall.sh index 127d6408a..74b904783 100755 --- a/ci/build/npm-postinstall.sh +++ b/ci/build/npm-postinstall.sh @@ -36,6 +36,13 @@ vscode_yarn() { yarn --production --frozen-lockfile cd extensions yarn --production --frozen-lockfile + for ext in */; do + ext="${ext%/}" + echo "extensions/$ext: installing dependencies" + cd "$ext" + yarn --production --frozen-lockfile + cd "$OLDPWD" + done } main "$@" From 811cf3364af86eecbf4578a1eab6ed53e5135c5b Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Fri, 9 Oct 2020 15:33:58 -0400 Subject: [PATCH 056/247] install.sh: Allow installing directly onto a remote host (#2183) Updates #1729 To fully close that issue see the various TODOs. --- install.sh | 52 ++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 48 insertions(+), 4 deletions(-) diff --git a/install.sh b/install.sh index 0b768def3..08e3a51e8 100755 --- a/install.sh +++ b/install.sh @@ -17,21 +17,28 @@ usage() { Installs code-server for Linux, macOS and FreeBSD. It tries to use the system package manager if possible. After successful installation it explains how to start using code-server. + +Pass in user@host to install code-server on user@host over ssh. +The remote host must have internet access. ${not_curl_usage-} Usage: - $arg0 [--dry-run] [--version X.X.X] [--method detect] [--prefix ~/.local] + $arg0 [--dry-run] [--version X.X.X] [--method detect] \ + [--prefix ~/.local] [user@host] --dry-run Echo the commands for the install process without running them. + --version X.X.X Install a specific version instead of the latest. + --method [detect | standalone] Choose the installation method. Defaults to detect. - detect detects the system package manager and tries to use it. Full reference on the process is further below. - standalone installs a standalone release archive into ~/.local Add ~/.local/bin to your \$PATH to use it. + --prefix Sets the prefix used by standalone release archives. Defaults to ~/.local The release is unarchived into ~/.local/lib/code-server-X.X.X @@ -100,9 +107,18 @@ main() { METHOD \ STANDALONE_INSTALL_PREFIX \ VERSION \ - OPTIONAL + OPTIONAL \ + ALL_FLAGS \ + SSH_ARGS + ALL_FLAGS="" while [ "$#" -gt 0 ]; do + case "$1" in + -*) + ALL_FLAGS="${ALL_FLAGS} $1" + ;; + esac + case "$1" in --dry-run) DRY_RUN=1 @@ -132,16 +148,33 @@ main() { usage exit 0 ;; - *) + --) + shift + # We remove the -- added above. + ALL_FLAGS="${ALL_FLAGS% --}" + SSH_ARGS="$*" + break + ;; + -*) echoerr "Unknown flag $1" echoerr "Run with --help to see usage." exit 1 ;; + *) + SSH_ARGS="$*" + break + ;; esac shift done + if [ "${SSH_ARGS-}" ]; then + echoh "Installing remotely with ssh $SSH_ARGS" + curl -fsSL https://code-server.dev/install.sh | prefix "$SSH_ARGS" ssh "$SSH_ARGS" sh -s -- "$ALL_FLAGS" + return + fi + VERSION="${VERSION-$(echo_latest_version)}" METHOD="${METHOD-detect}" if [ "$METHOD" != detect ] && [ "$METHOD" != standalone ]; then @@ -446,7 +479,7 @@ arch() { } command_exists() { - command -v "$@" > /dev/null 2>&1 + command -v "$@" > /dev/null } sh_c() { @@ -500,4 +533,15 @@ humanpath() { sed "s# $HOME# ~#g; s#\"$HOME#\"\$HOME#g" } +# We need to make sure we exit with a non zero exit if the command fails. +# /bin/sh does not support -o pipefail unfortunately. +prefix() { + PREFIX="$1" + shift + fifo="$(mktemp -d)/fifo" + mkfifo "$fifo" + sed -e "s#^#$PREFIX: #" "$fifo" & + "$@" > "$fifo" 2>&1 +} + main "$@" From 6bdaada689dc9e78c461e4158310d99faa54347f Mon Sep 17 00:00:00 2001 From: Asher Date: Mon, 14 Sep 2020 15:56:08 -0500 Subject: [PATCH 057/247] Move uncaught exception handler to wrapper Feels more appropriate there to me. --- src/node/entry.ts | 7 ------- src/node/wrapper.ts | 8 ++++++++ 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/src/node/entry.ts b/src/node/entry.ts index 5869ae461..dfd78cda7 100644 --- a/src/node/entry.ts +++ b/src/node/entry.ts @@ -18,13 +18,6 @@ import { loadPlugins } from "./plugin" import { generateCertificate, hash, humanPath, open } from "./util" import { ipcMain, wrap } from "./wrapper" -process.on("uncaughtException", (error) => { - logger.error(`Uncaught exception: ${error.message}`) - if (typeof error.stack !== "undefined") { - logger.error(error.stack) - } -}) - let pkg: { version?: string; commit?: string } = {} try { pkg = require("../../package.json") diff --git a/src/node/wrapper.ts b/src/node/wrapper.ts index ba459efd1..2f1eb037f 100644 --- a/src/node/wrapper.ts +++ b/src/node/wrapper.ts @@ -254,6 +254,14 @@ if (!process.stdout.isTTY) { process.stdout.on("error", () => ipcMain().exit()) } +// Don't let uncaught exceptions crash the process. +process.on("uncaughtException", (error) => { + logger.error(`Uncaught exception: ${error.message}`) + if (typeof error.stack !== "undefined") { + logger.error(error.stack) + } +}) + export const wrap = (fn: () => Promise): void => { if (ipcMain().parentPid) { ipcMain() From 0a8e71c6474ed59f3979c68005288f75c29bcd10 Mon Sep 17 00:00:00 2001 From: Asher Date: Mon, 14 Sep 2020 15:57:58 -0500 Subject: [PATCH 058/247] Refactor wrapper - Immediately create ipcMain so it doesn't have to be a function which I think feels cleaner. - Move exit handling to a separate function to compensate (otherwise the VS Code CLI for example won't be able to exit on its own). - New isChild prop that is clearer than checking for parentPid (IMO). - Skip all the checks that aren't necessary for the child process (like --help, --version, etc). - Since we check if we're the child in entry go ahead and move the wrap code into entry as well since that's basically what it does. - Use a single catch at the end of the entry. - Split out the VS Code CLI and existing instance code into separate functions. --- src/node/cli.ts | 20 ++++ src/node/entry.ts | 231 ++++++++++++++++++++++++++------------------ src/node/wrapper.ts | 102 +++++++++---------- 3 files changed, 199 insertions(+), 154 deletions(-) diff --git a/src/node/cli.ts b/src/node/cli.ts index b723417d1..9683945d4 100644 --- a/src/node/cli.ts +++ b/src/node/cli.ts @@ -496,3 +496,23 @@ async function copyOldMacOSDataDir(): Promise { await fs.copy(oldDataDir, paths.data) } } + +export const shouldRunVsCodeCli = (args: Args): boolean => { + return !!args["list-extensions"] || !!args["install-extension"] || !!args["uninstall-extension"] +} + +/** + * Determine if it looks like the user is trying to open a file or folder in an + * existing instance. The arguments here should be the arguments the user + * explicitly passed on the command line, not defaults or the configuration. + */ +export const shouldOpenInExistingInstance = async (args: Args): Promise => { + // Always use the existing instance if we're running from VS Code's terminal. + if (process.env.VSCODE_IPC_HOOK_CLI) { + return process.env.VSCODE_IPC_HOOK_CLI + } + + // TODO: implement + + return undefined +} diff --git a/src/node/entry.ts b/src/node/entry.ts index dfd78cda7..0d16c250d 100644 --- a/src/node/entry.ts +++ b/src/node/entry.ts @@ -11,12 +11,21 @@ import { ProxyHttpProvider } from "./app/proxy" import { StaticHttpProvider } from "./app/static" import { UpdateHttpProvider } from "./app/update" import { VscodeHttpProvider } from "./app/vscode" -import { Args, bindAddrFromAllSources, optionDescriptions, parse, readConfigFile, setDefaults } from "./cli" +import { + Args, + bindAddrFromAllSources, + optionDescriptions, + parse, + readConfigFile, + setDefaults, + shouldOpenInExistingInstance, + shouldRunVsCodeCli, +} from "./cli" import { coderCloudBind } from "./coder-cloud" import { AuthType, HttpServer, HttpServerOptions } from "./http" import { loadPlugins } from "./plugin" import { generateCertificate, hash, humanPath, open } from "./util" -import { ipcMain, wrap } from "./wrapper" +import { ipcMain, WrapperProcess } from "./wrapper" let pkg: { version?: string; commit?: string } = {} try { @@ -28,6 +37,86 @@ try { const version = pkg.version || "development" const commit = pkg.commit || "development" +export const runVsCodeCli = (args: Args): void => { + logger.debug("forking vs code cli...") + const vscode = cp.fork(path.resolve(__dirname, "../../lib/vscode/out/vs/server/fork"), [], { + env: { + ...process.env, + CODE_SERVER_PARENT_PID: process.pid.toString(), + }, + }) + vscode.once("message", (message: any) => { + logger.debug("got message from VS Code", field("message", message)) + if (message.type !== "ready") { + logger.error("Unexpected response waiting for ready response", field("type", message.type)) + process.exit(1) + } + const send: CliMessage = { type: "cli", args } + vscode.send(send) + }) + vscode.once("error", (error) => { + logger.error("Got error from VS Code", field("error", error)) + process.exit(1) + }) + vscode.on("exit", (code) => process.exit(code || 0)) +} + +export const openInExistingInstance = async (args: Args, socketPath: string): Promise => { + const pipeArgs: OpenCommandPipeArgs & { fileURIs: string[] } = { + type: "open", + folderURIs: [], + fileURIs: [], + forceReuseWindow: args["reuse-window"], + forceNewWindow: args["new-window"], + } + + const isDir = async (path: string): Promise => { + try { + const st = await fs.stat(path) + return st.isDirectory() + } catch (error) { + return false + } + } + + for (let i = 0; i < args._.length; i++) { + const fp = path.resolve(args._[i]) + if (await isDir(fp)) { + pipeArgs.folderURIs.push(fp) + } else { + pipeArgs.fileURIs.push(fp) + } + } + + if (pipeArgs.forceNewWindow && pipeArgs.fileURIs.length > 0) { + logger.error("--new-window can only be used with folder paths") + process.exit(1) + } + + if (pipeArgs.folderURIs.length === 0 && pipeArgs.fileURIs.length === 0) { + logger.error("Please specify at least one file or folder") + process.exit(1) + } + + const vscode = http.request( + { + path: "/", + method: "POST", + socketPath, + }, + (response) => { + response.on("data", (message) => { + logger.debug("got message from VS Code", field("message", message.toString())) + }) + }, + ) + vscode.on("error", (error: unknown) => { + logger.error("got error from VS Code", field("error", error)) + }) + vscode.write(JSON.stringify(pipeArgs)) + vscode.end() +} + const main = async (args: Args, configArgs: Args): Promise => { if (args.link) { // If we're being exposed to the cloud, we listen on a random address and disable auth. @@ -92,7 +181,7 @@ const main = async (args: Args, configArgs: Args): Promise => { await loadPlugins(httpServer, args) - ipcMain().onDispose(() => { + ipcMain.onDispose(() => { httpServer.dispose().then((errors) => { errors.forEach((error) => logger.error(error.message)) }) @@ -132,7 +221,9 @@ const main = async (args: Args, configArgs: Args): Promise => { if (serverAddress && !options.socket && args.open) { // The web socket doesn't seem to work if browsing with 0.0.0.0. const openAddress = serverAddress.replace(/:\/\/0.0.0.0/, "://localhost") - await open(openAddress).catch(console.error) + await open(openAddress).catch((error: Error) => { + logger.error("Failed to open", field("address", openAddress), field("error", error)) + }) logger.info(`Opened ${openAddress}`) } @@ -141,27 +232,32 @@ const main = async (args: Args, configArgs: Args): Promise => { await coderCloudBind(serverAddress!, args.link.value) } catch (err) { logger.error(err.message) - ipcMain().exit(1) + ipcMain.exit(1) } } } async function entry(): Promise { - const tryParse = async (): Promise<[Args, Args]> => { - try { - const cliArgs = parse(process.argv.slice(2)) - const configArgs = await readConfigFile(cliArgs.config) - // This prioritizes the flags set in args over the ones in the config file. - let args = Object.assign(configArgs, cliArgs) - args = await setDefaults(args) - return [args, configArgs] - } catch (error) { - console.error(error.message) - process.exit(1) - } + const tryParse = async (): Promise<[Args, Args, Args]> => { + const cliArgs = parse(process.argv.slice(2)) + const configArgs = await readConfigFile(cliArgs.config) + // This prioritizes the flags set in args over the ones in the config file. + let args = Object.assign(configArgs, cliArgs) + args = await setDefaults(args) + return [args, cliArgs, configArgs] + } + + const [args, cliArgs, configArgs] = await tryParse() + + // There's no need to check flags like --help or to spawn in an existing + // instance for the child process because these would have already happened in + // the parent and the child wouldn't have been spawned. + if (ipcMain.isChild) { + await ipcMain.handshake() + ipcMain.preventExit() + return main(args, configArgs) } - const [args, configArgs] = await tryParse() if (args.help) { console.log("code-server", version, commit) console.log("") @@ -171,7 +267,10 @@ async function entry(): Promise { optionDescriptions().forEach((description) => { console.log("", description) }) - } else if (args.version) { + return + } + + if (args.version) { if (args.json) { console.log({ codeServer: version, @@ -181,83 +280,23 @@ async function entry(): Promise { } else { console.log(version, commit) } - process.exit(0) - } else if (args["list-extensions"] || args["install-extension"] || args["uninstall-extension"]) { - logger.debug("forking vs code cli...") - const vscode = cp.fork(path.resolve(__dirname, "../../lib/vscode/out/vs/server/fork"), [], { - env: { - ...process.env, - CODE_SERVER_PARENT_PID: process.pid.toString(), - }, - }) - vscode.once("message", (message: any) => { - logger.debug("Got message from VS Code", field("message", message)) - if (message.type !== "ready") { - logger.error("Unexpected response waiting for ready response") - process.exit(1) - } - const send: CliMessage = { type: "cli", args } - vscode.send(send) - }) - vscode.once("error", (error) => { - logger.error(error.message) - process.exit(1) - }) - vscode.on("exit", (code) => process.exit(code || 0)) - } else if (process.env.VSCODE_IPC_HOOK_CLI) { - const pipeArgs: OpenCommandPipeArgs = { - type: "open", - folderURIs: [], - forceReuseWindow: args["reuse-window"], - forceNewWindow: args["new-window"], - } - const isDir = async (path: string): Promise => { - try { - const st = await fs.stat(path) - return st.isDirectory() - } catch (error) { - return false - } - } - for (let i = 0; i < args._.length; i++) { - const fp = path.resolve(args._[i]) - if (await isDir(fp)) { - pipeArgs.folderURIs.push(fp) - } else { - if (!pipeArgs.fileURIs) { - pipeArgs.fileURIs = [] - } - pipeArgs.fileURIs.push(fp) - } - } - if (pipeArgs.forceNewWindow && pipeArgs.fileURIs && pipeArgs.fileURIs.length > 0) { - logger.error("new-window can only be used with folder paths") - process.exit(1) - } - if (pipeArgs.folderURIs.length === 0 && (!pipeArgs.fileURIs || pipeArgs.fileURIs.length === 0)) { - logger.error("Please specify at least one file or folder argument") - process.exit(1) - } - const vscode = http.request( - { - path: "/", - method: "POST", - socketPath: process.env["VSCODE_IPC_HOOK_CLI"], - }, - (res) => { - res.on("data", (message) => { - logger.debug("Got message from VS Code", field("message", message.toString())) - }) - }, - ) - vscode.on("error", (err) => { - logger.debug("Got error from VS Code", field("error", err)) - }) - vscode.write(JSON.stringify(pipeArgs)) - vscode.end() - } else { - wrap(() => main(args, configArgs)) + return } + + if (shouldRunVsCodeCli(args)) { + return runVsCodeCli(args) + } + + const socketPath = await shouldOpenInExistingInstance(cliArgs) + if (socketPath) { + return openInExistingInstance(args, socketPath) + } + + const wrapper = new WrapperProcess(require("../../package.json").version) + return wrapper.start() } -entry() +entry().catch((error) => { + logger.error(error.message) + ipcMain.exit(error) +}) diff --git a/src/node/wrapper.ts b/src/node/wrapper.ts index 2f1eb037f..ea29eb28a 100644 --- a/src/node/wrapper.ts +++ b/src/node/wrapper.ts @@ -32,19 +32,13 @@ export class IpcMain { public readonly onMessage = this._onMessage.event private readonly _onDispose = new Emitter() public readonly onDispose = this._onDispose.event - public readonly processExit: (code?: number) => never + public readonly processExit: (code?: number) => never = process.exit - public constructor(public readonly parentPid?: number) { + public constructor(private readonly parentPid?: number) { process.on("SIGINT", () => this._onDispose.emit("SIGINT")) process.on("SIGTERM", () => this._onDispose.emit("SIGTERM")) process.on("exit", () => this._onDispose.emit(undefined)) - // Ensure we control when the process exits. - this.processExit = process.exit - process.exit = function (code?: number) { - logger.warn(`process.exit() was prevented: ${code || "unknown code"}.`) - } as (code?: number) => never - this.onDispose((signal) => { // Remove listeners to avoid possibly triggering disposal again. process.removeAllListeners() @@ -71,6 +65,19 @@ export class IpcMain { } } + /** + * Ensure we control when the process exits. + */ + public preventExit(): void { + process.exit = function (code?: number) { + logger.warn(`process.exit() was prevented: ${code || "unknown code"}.`) + } as (code?: number) => never + } + + public get isChild(): boolean { + return typeof this.parentPid !== "undefined" + } + public exit(error?: number | ProcessError): never { if (error && typeof error !== "number") { this.processExit(typeof error.code === "number" ? error.code : 1) @@ -127,17 +134,12 @@ export class IpcMain { } } -let _ipcMain: IpcMain -export const ipcMain = (): IpcMain => { - if (!_ipcMain) { - _ipcMain = new IpcMain( - typeof process.env.CODE_SERVER_PARENT_PID !== "undefined" - ? parseInt(process.env.CODE_SERVER_PARENT_PID) - : undefined, - ) - } - return _ipcMain -} +/** + * Channel for communication between the child and parent processes. + */ +export const ipcMain = new IpcMain( + typeof process.env.CODE_SERVER_PARENT_PID !== "undefined" ? parseInt(process.env.CODE_SERVER_PARENT_PID) : undefined, +) export interface WrapperOptions { maxMemory?: number @@ -162,14 +164,11 @@ export class WrapperProcess { this.logStdoutStream = rfs.createStream(path.join(paths.data, "coder-logs", "code-server-stdout.log"), opts) this.logStderrStream = rfs.createStream(path.join(paths.data, "coder-logs", "code-server-stderr.log"), opts) - ipcMain().onDispose(() => { - if (this.process) { - this.process.removeAllListeners() - this.process.kill() - } + ipcMain.onDispose(() => { + this.disposeChild() }) - ipcMain().onMessage((message) => { + ipcMain.onMessage((message) => { switch (message.type) { case "relaunch": logger.info(`Relaunching: ${this.currentVersion} -> ${message.version}`) @@ -181,28 +180,35 @@ export class WrapperProcess { break } }) - - process.on("SIGUSR1", async () => { - logger.info("Received SIGUSR1; hotswapping") - this.relaunch() - }) } - private async relaunch(): Promise { + private disposeChild(): void { this.started = undefined if (this.process) { this.process.removeAllListeners() this.process.kill() } + } + + private async relaunch(): Promise { + this.disposeChild() try { await this.start() } catch (error) { logger.error(error.message) - ipcMain().exit(typeof error.code === "number" ? error.code : 1) + ipcMain.exit(typeof error.code === "number" ? error.code : 1) } } public start(): Promise { + // If we have a process then we've already bound this. + if (!this.process) { + process.on("SIGUSR1", async () => { + logger.info("Received SIGUSR1; hotswapping") + this.relaunch() + }) + } + if (!this.started) { this.started = this.spawn().then((child) => { // Log both to stdout and to the log directory. @@ -215,14 +221,12 @@ export class WrapperProcess { child.stderr.pipe(process.stderr) } logger.debug(`spawned inner process ${child.pid}`) - ipcMain() - .handshake(child) - .then(() => { - child.once("exit", (code) => { - logger.debug(`inner process ${child.pid} exited unexpectedly`) - ipcMain().exit(code || 0) - }) + ipcMain.handshake(child).then(() => { + child.once("exit", (code) => { + logger.debug(`inner process ${child.pid} exited unexpectedly`) + ipcMain.exit(code || 0) }) + }) this.process = child }) } @@ -251,7 +255,7 @@ export class WrapperProcess { // It's possible that the pipe has closed (for example if you run code-server // --version | head -1). Assume that means we're done. if (!process.stdout.isTTY) { - process.stdout.on("error", () => ipcMain().exit()) + process.stdout.on("error", () => ipcMain.exit()) } // Don't let uncaught exceptions crash the process. @@ -261,21 +265,3 @@ process.on("uncaughtException", (error) => { logger.error(error.stack) } }) - -export const wrap = (fn: () => Promise): void => { - if (ipcMain().parentPid) { - ipcMain() - .handshake() - .then(() => fn()) - .catch((error: ProcessError): void => { - logger.error(error.message) - ipcMain().exit(error) - }) - } else { - const wrapper = new WrapperProcess(require("../../package.json").version) - wrapper.start().catch((error) => { - logger.error(error.message) - ipcMain().exit(error) - }) - } -} From bb1bf88439738eeacca246703fcf05f4a3182037 Mon Sep 17 00:00:00 2001 From: Asher Date: Mon, 14 Sep 2020 17:22:24 -0500 Subject: [PATCH 059/247] Fix wrapper.start not actually waiting for anything --- src/node/wrapper.ts | 47 +++++++++++++++++++++++++-------------------- 1 file changed, 26 insertions(+), 21 deletions(-) diff --git a/src/node/wrapper.ts b/src/node/wrapper.ts index ea29eb28a..cce841901 100644 --- a/src/node/wrapper.ts +++ b/src/node/wrapper.ts @@ -208,32 +208,37 @@ export class WrapperProcess { this.relaunch() }) } - if (!this.started) { - this.started = this.spawn().then((child) => { - // Log both to stdout and to the log directory. - if (child.stdout) { - child.stdout.pipe(this.logStdoutStream) - child.stdout.pipe(process.stdout) - } - if (child.stderr) { - child.stderr.pipe(this.logStderrStream) - child.stderr.pipe(process.stderr) - } - logger.debug(`spawned inner process ${child.pid}`) - ipcMain.handshake(child).then(() => { - child.once("exit", (code) => { - logger.debug(`inner process ${child.pid} exited unexpectedly`) - ipcMain.exit(code || 0) - }) - }) - this.process = child - }) + this.started = this._start() } return this.started } - private async spawn(): Promise { + private async _start(): Promise { + const child = this.spawn() + this.process = child + + // Log both to stdout and to the log directory. + if (child.stdout) { + child.stdout.pipe(this.logStdoutStream) + child.stdout.pipe(process.stdout) + } + if (child.stderr) { + child.stderr.pipe(this.logStderrStream) + child.stderr.pipe(process.stderr) + } + + logger.debug(`spawned inner process ${child.pid}`) + + await ipcMain.handshake(child) + + child.once("exit", (code) => { + logger.debug(`inner process ${child.pid} exited unexpectedly`) + ipcMain.exit(code || 0) + }) + } + + private spawn(): cp.ChildProcess { // Flags to pass along to the Node binary. let nodeOptions = `${process.env.NODE_OPTIONS || ""} ${(this.options && this.options.nodeOptions) || ""}` if (!/max_old_space_size=(\d+)/g.exec(nodeOptions)) { From 19022967024ac690cc5e0e7ea11ade0f05792318 Mon Sep 17 00:00:00 2001 From: Asher Date: Tue, 15 Sep 2020 12:47:33 -0500 Subject: [PATCH 060/247] Remove references to --open-in flag --- src/node/cli.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/node/cli.ts b/src/node/cli.ts index 9683945d4..3d4d0659c 100644 --- a/src/node/cli.ts +++ b/src/node/cli.ts @@ -152,12 +152,12 @@ const options: Options> = { "new-window": { type: "boolean", short: "n", - description: "Force to open a new window. (use with open-in)", + description: "Force to open a new window.", }, "reuse-window": { type: "boolean", short: "r", - description: "Force to open a file or folder in an already opened window. (use with open-in)", + description: "Force to open a file or folder in an already opened window.", }, locale: { type: "string" }, From 021c084e4315f6fbc40ca0b9feed1cf0930d43d7 Mon Sep 17 00:00:00 2001 From: Asher Date: Tue, 15 Sep 2020 13:43:07 -0500 Subject: [PATCH 061/247] Move log level defaults into setDefaults This will allow cliArgs to be only the actual arguments the user passed which will be used for some logic around opening in existing instances. --- src/node/cli.ts | 30 ++++++++++----------- test/cli.test.ts | 69 ++++++++++++++++++++++++++++++++---------------- 2 files changed, 61 insertions(+), 38 deletions(-) diff --git a/src/node/cli.ts b/src/node/cli.ts index 3d4d0659c..a52796a14 100644 --- a/src/node/cli.ts +++ b/src/node/cli.ts @@ -327,6 +327,21 @@ export const parse = ( logger.debug("parsed command line", field("args", args)) + return args +} + +export async function setDefaults(args: Args): Promise { + args = { ...args } + + if (!args["user-data-dir"]) { + await copyOldMacOSDataDir() + args["user-data-dir"] = paths.data + } + + if (!args["extensions-dir"]) { + args["extensions-dir"] = path.join(args["user-data-dir"], "extensions") + } + // --verbose takes priority over --log and --log takes priority over the // environment variable. if (args.verbose) { @@ -369,21 +384,6 @@ export const parse = ( return args } -export async function setDefaults(args: Args): Promise { - args = { ...args } - - if (!args["user-data-dir"]) { - await copyOldMacOSDataDir() - args["user-data-dir"] = paths.data - } - - if (!args["extensions-dir"]) { - args["extensions-dir"] = path.join(args["user-data-dir"], "extensions") - } - - return args -} - async function defaultConfigFile(): Promise { return `bind-addr: 127.0.0.1:8080 auth: password diff --git a/test/cli.test.ts b/test/cli.test.ts index f4f6c8849..fe78659d8 100644 --- a/test/cli.test.ts +++ b/test/cli.test.ts @@ -1,20 +1,23 @@ -import { logger, Level } from "@coder/logger" +import { Level, logger } from "@coder/logger" import * as assert from "assert" import * as path from "path" -import { parse } from "../src/node/cli" +import { parse, setDefaults } from "../src/node/cli" +import { paths } from "../src/node/util" describe("cli", () => { beforeEach(() => { delete process.env.LOG_LEVEL }) - // The parser will always fill these out. + // The parser should not set any defaults so the caller can determine what + // values the user actually set. These are set after calling `setDefaults`. const defaults = { - _: [], + "extensions-dir": path.join(paths.data, "extensions"), + "user-data-dir": paths.data, } it("should set defaults", () => { - assert.deepEqual(parse([]), defaults) + assert.deepEqual(parse([]), { _: [] }) }) it("should parse all available options", () => { @@ -69,7 +72,7 @@ describe("cli", () => { help: true, host: "0.0.0.0", json: true, - log: "trace", + log: "error", open: true, port: 8081, socket: path.resolve("mumble"), @@ -83,19 +86,20 @@ describe("cli", () => { it("should work with short options", () => { assert.deepEqual(parse(["-vvv", "-v"]), { - ...defaults, - log: "trace", + _: [], verbose: true, version: true, }) - assert.equal(process.env.LOG_LEVEL, "trace") - assert.equal(logger.level, Level.Trace) }) - it("should use log level env var", () => { + it("should use log level env var", async () => { + const args = parse([]) + assert.deepEqual(args, { _: [] }) + process.env.LOG_LEVEL = "debug" - assert.deepEqual(parse([]), { + assert.deepEqual(await setDefaults(args), { ...defaults, + _: [], log: "debug", verbose: false, }) @@ -103,8 +107,9 @@ describe("cli", () => { assert.equal(logger.level, Level.Debug) process.env.LOG_LEVEL = "trace" - assert.deepEqual(parse([]), { + assert.deepEqual(await setDefaults(args), { ...defaults, + _: [], log: "trace", verbose: true, }) @@ -113,9 +118,16 @@ describe("cli", () => { }) it("should prefer --log to env var and --verbose to --log", async () => { + let args = parse(["--log", "info"]) + assert.deepEqual(args, { + _: [], + log: "info", + }) + process.env.LOG_LEVEL = "debug" - assert.deepEqual(parse(["--log", "info"]), { + assert.deepEqual(await setDefaults(args), { ...defaults, + _: [], log: "info", verbose: false, }) @@ -123,17 +135,26 @@ describe("cli", () => { assert.equal(logger.level, Level.Info) process.env.LOG_LEVEL = "trace" - assert.deepEqual(parse(["--log", "info"]), { + assert.deepEqual(await setDefaults(args), { ...defaults, + _: [], log: "info", verbose: false, }) assert.equal(process.env.LOG_LEVEL, "info") assert.equal(logger.level, Level.Info) + args = parse(["--log", "info", "--verbose"]) + assert.deepEqual(args, { + _: [], + log: "info", + verbose: true, + }) + process.env.LOG_LEVEL = "warn" - assert.deepEqual(parse(["--log", "info", "--verbose"]), { + assert.deepEqual(await setDefaults(args), { ...defaults, + _: [], log: "trace", verbose: true, }) @@ -141,9 +162,12 @@ describe("cli", () => { assert.equal(logger.level, Level.Trace) }) - it("should ignore invalid log level env var", () => { + it("should ignore invalid log level env var", async () => { process.env.LOG_LEVEL = "bogus" - assert.deepEqual(parse([]), defaults) + assert.deepEqual(await setDefaults(parse([])), { + _: [], + ...defaults, + }) }) it("should error if value isn't provided", () => { @@ -166,7 +190,7 @@ describe("cli", () => { it("should not error if the value is optional", () => { assert.deepEqual(parse(["--cert"]), { - ...defaults, + _: [], cert: { value: undefined, }, @@ -177,7 +201,7 @@ describe("cli", () => { assert.throws(() => parse(["--socket", "--socket-path-value"]), /--socket requires a value/) // If you actually had a path like this you would do this instead: assert.deepEqual(parse(["--socket", "./--socket-path-value"]), { - ...defaults, + _: [], socket: path.resolve("--socket-path-value"), }) assert.throws(() => parse(["--cert", "--socket-path-value"]), /Unknown option --socket-path-value/) @@ -185,7 +209,6 @@ describe("cli", () => { it("should allow positional arguments before options", () => { assert.deepEqual(parse(["foo", "test", "--auth", "none"]), { - ...defaults, _: ["foo", "test"], auth: "none", }) @@ -193,11 +216,11 @@ describe("cli", () => { it("should support repeatable flags", () => { assert.deepEqual(parse(["--proxy-domain", "*.coder.com"]), { - ...defaults, + _: [], "proxy-domain": ["*.coder.com"], }) assert.deepEqual(parse(["--proxy-domain", "*.coder.com", "--proxy-domain", "test.com"]), { - ...defaults, + _: [], "proxy-domain": ["*.coder.com", "test.com"], }) }) From fe19391c036bf234e84b2e30f321de73f3b689e2 Mon Sep 17 00:00:00 2001 From: Asher Date: Tue, 15 Sep 2020 16:51:43 -0500 Subject: [PATCH 062/247] Read most recent socket path from file --- ci/dev/vscode.patch | 25 +++++++++++++ src/node/cli.ts | 32 +++++++++++++++- src/node/socket.ts | 13 +------ src/node/util.ts | 15 ++++++++ test/cli.test.ts | 89 +++++++++++++++++++++++++++++++++++++++++++-- 5 files changed, 156 insertions(+), 18 deletions(-) diff --git a/ci/dev/vscode.patch b/ci/dev/vscode.patch index b3a7289df..f15c7d7a7 100644 --- a/ci/dev/vscode.patch +++ b/ci/dev/vscode.patch @@ -3035,6 +3035,31 @@ index b3c89e51cfc25a53293a352a2a8ad50d5f26d595..e21abe4e13bc25a5b72f556bbfb61085 registerSingleton(IExtHostTerminalService, ExtHostTerminalService); registerSingleton(IExtHostTunnelService, ExtHostTunnelService); +registerSingleton(IExtHostNodeProxy, class extends NotImplementedProxy(String(IExtHostNodeProxy)) { whenReady = Promise.resolve(); }); +diff --git a/src/vs/workbench/api/node/extHostCLIServer.ts b/src/vs/workbench/api/node/extHostCLIServer.ts +index 7cae126cc0f804273850933468690e0f9f10a5b8..08c2aa5cdae3f3d06bb08b7055dc7e7def260132 100644 +--- a/src/vs/workbench/api/node/extHostCLIServer.ts ++++ b/src/vs/workbench/api/node/extHostCLIServer.ts +@@ -11,6 +11,8 @@ import { IWindowOpenable, IOpenWindowOptions } from 'vs/platform/windows/common/ + import { URI } from 'vs/base/common/uri'; + import { hasWorkspaceFileExtension } from 'vs/platform/workspaces/common/workspaces'; + import { ILogService } from 'vs/platform/log/common/log'; ++import { join } from 'vs/base/common/path'; ++import { tmpdir } from 'os'; + + export interface OpenCommandPipeArgs { + type: 'open'; +@@ -54,6 +56,11 @@ export class CLIServer { + private async setup(): Promise { + this._ipcHandlePath = generateRandomPipeName(); + ++ // NOTE@coder: Write this out so we can get the most recent path. ++ fs.promises.writeFile(join(tmpdir(), "vscode-ipc"), this._ipcHandlePath).catch((error) => { ++ this.logService.error(error); ++ }); ++ + try { + this._server.listen(this.ipcHandlePath); + this._server.on('error', err => this.logService.error(err)); diff --git a/src/vs/workbench/api/worker/extHost.worker.services.ts b/src/vs/workbench/api/worker/extHost.worker.services.ts index 3843fdec386edc09a1d361b63de892a04e0070ed..8aac4df527857e964798362a69f5591bef07c165 100644 --- a/src/vs/workbench/api/worker/extHost.worker.services.ts diff --git a/src/node/cli.ts b/src/node/cli.ts index a52796a14..028ea0d45 100644 --- a/src/node/cli.ts +++ b/src/node/cli.ts @@ -5,7 +5,7 @@ import * as os from "os" import * as path from "path" import { Args as VsArgs } from "../../lib/vscode/src/vs/server/ipc" import { AuthType } from "./http" -import { generatePassword, humanPath, paths } from "./util" +import { canConnect, generatePassword, humanPath, paths } from "./util" export class Optional { public constructor(public readonly value?: T) {} @@ -512,7 +512,35 @@ export const shouldOpenInExistingInstance = async (args: Args): Promise => { + try { + return await fs.readFile(path.join(os.tmpdir(), "vscode-ipc"), "utf8") + } catch (error) { + if (error.code !== "ENOENT") { + throw error + } + } + return undefined + } + + // If these flags are set then assume the user is trying to open in an + // existing instance since these flags have no effect otherwise. + const openInFlagCount = ["reuse-window", "new-window"].reduce((prev, cur) => { + return args[cur as keyof Args] ? prev + 1 : prev + }, 0) + if (openInFlagCount > 0) { + return readSocketPath() + } + + // It's possible the user is trying to spawn another instance of code-server. + // Check if any unrelated flags are set (add one for `_` which always exists), + // that a file or directory was passed, and that the socket is active. + if (Object.keys(args).length === openInFlagCount + 1 && args._.length > 0) { + const socketPath = await readSocketPath() + if (socketPath && (await canConnect(socketPath))) { + return socketPath + } + } return undefined } diff --git a/src/node/socket.ts b/src/node/socket.ts index e5fe66778..ada024831 100644 --- a/src/node/socket.ts +++ b/src/node/socket.ts @@ -4,7 +4,7 @@ import * as path from "path" import * as tls from "tls" import { Emitter } from "../common/emitter" import { generateUuid } from "../common/util" -import { tmpdir } from "./util" +import { canConnect, tmpdir } from "./util" /** * Provides a way to proxy a TLS socket. Can be used when you need to pass a @@ -89,17 +89,6 @@ export class SocketProxyProvider { } public async findFreeSocketPath(basePath: string, maxTries = 100): Promise { - const canConnect = (path: string): Promise => { - return new Promise((resolve) => { - const socket = net.connect(path) - socket.once("error", () => resolve(false)) - socket.once("connect", () => { - socket.destroy() - resolve(true) - }) - }) - } - let i = 0 let path = basePath while ((await canConnect(path)) && i < maxTries) { diff --git a/src/node/util.ts b/src/node/util.ts index c0f37f74b..75122fe76 100644 --- a/src/node/util.ts +++ b/src/node/util.ts @@ -2,6 +2,7 @@ import * as cp from "child_process" import * as crypto from "crypto" import envPaths from "env-paths" import * as fs from "fs-extra" +import * as net from "net" import * as os from "os" import * as path from "path" import * as util from "util" @@ -246,3 +247,17 @@ export function pathToFsPath(path: string, keepDriveLetterCasing = false): strin } return value } + +/** + * Return a promise that resolves with whether the socket path is active. + */ +export function canConnect(path: string): Promise { + return new Promise((resolve) => { + const socket = net.connect(path) + socket.once("error", () => resolve(false)) + socket.once("connect", () => { + socket.destroy() + resolve(true) + }) + }) +} diff --git a/test/cli.test.ts b/test/cli.test.ts index fe78659d8..ae5256142 100644 --- a/test/cli.test.ts +++ b/test/cli.test.ts @@ -1,16 +1,24 @@ import { Level, logger } from "@coder/logger" import * as assert from "assert" +import * as fs from "fs-extra" +import * as net from "net" +import * as os from "os" import * as path from "path" -import { parse, setDefaults } from "../src/node/cli" -import { paths } from "../src/node/util" +import { Args, parse, setDefaults, shouldOpenInExistingInstance } from "../src/node/cli" +import { paths, tmpdir } from "../src/node/util" -describe("cli", () => { +type Mutable = { + -readonly [P in keyof T]: T[P] +} + +describe("parser", () => { beforeEach(() => { delete process.env.LOG_LEVEL }) // The parser should not set any defaults so the caller can determine what - // values the user actually set. These are set after calling `setDefaults`. + // values the user actually set. These are only set after explicitly calling + // `setDefaults`. const defaults = { "extensions-dir": path.join(paths.data, "extensions"), "user-data-dir": paths.data, @@ -225,3 +233,76 @@ describe("cli", () => { }) }) }) + +describe("cli", () => { + let args: Mutable = { _: [] } + const testDir = path.join(tmpdir, "tests/cli") + const vscodeIpcPath = path.join(os.tmpdir(), "vscode-ipc") + + before(async () => { + await fs.remove(testDir) + await fs.mkdirp(testDir) + }) + + beforeEach(async () => { + delete process.env.VSCODE_IPC_HOOK_CLI + args = { _: [] } + await fs.remove(vscodeIpcPath) + }) + + it("should use existing if inside code-server", async () => { + process.env.VSCODE_IPC_HOOK_CLI = "test" + assert.strictEqual(await shouldOpenInExistingInstance(args), "test") + + args.port = 8081 + args._.push("./file") + assert.strictEqual(await shouldOpenInExistingInstance(args), "test") + }) + + it("should use existing if --reuse-window is set", async () => { + args["reuse-window"] = true + assert.strictEqual(await shouldOpenInExistingInstance(args), undefined) + + await fs.writeFile(vscodeIpcPath, "test") + assert.strictEqual(await shouldOpenInExistingInstance(args), "test") + + args.port = 8081 + assert.strictEqual(await shouldOpenInExistingInstance(args), "test") + }) + + it("should use existing if --new-window is set", async () => { + args["new-window"] = true + assert.strictEqual(await shouldOpenInExistingInstance(args), undefined) + + await fs.writeFile(vscodeIpcPath, "test") + assert.strictEqual(await shouldOpenInExistingInstance(args), "test") + + args.port = 8081 + assert.strictEqual(await shouldOpenInExistingInstance(args), "test") + }) + + it("should use existing if no unrelated flags are set, has positional, and socket is active", async () => { + assert.strictEqual(await shouldOpenInExistingInstance(args), undefined) + + args._.push("./file") + assert.strictEqual(await shouldOpenInExistingInstance(args), undefined) + + const socketPath = path.join(testDir, "socket") + await fs.writeFile(vscodeIpcPath, socketPath) + assert.strictEqual(await shouldOpenInExistingInstance(args), undefined) + + await new Promise((resolve) => { + const server = net.createServer(() => { + // Close after getting the first connection. + server.close() + }) + server.once("listening", () => resolve(server)) + server.listen(socketPath) + }) + + assert.strictEqual(await shouldOpenInExistingInstance(args), socketPath) + + args.port = 8081 + assert.strictEqual(await shouldOpenInExistingInstance(args), undefined) + }) +}) From e0769dc13a83653a10125eda19aec6b33c6f8601 Mon Sep 17 00:00:00 2001 From: Asher Date: Tue, 15 Sep 2020 17:29:53 -0500 Subject: [PATCH 063/247] Move config file info log Otherwise it outputs when trying to open a file in an existing instance externally. Externally there isn't an environment variable to branch on to skip this line so instead output it with the other info lines in the child process. --- src/node/cli.ts | 4 ---- src/node/entry.ts | 2 ++ 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/node/cli.ts b/src/node/cli.ts index 028ea0d45..92bf39cb8 100644 --- a/src/node/cli.ts +++ b/src/node/cli.ts @@ -410,10 +410,6 @@ export async function readConfigFile(configPath?: string): Promise { logger.info(`Wrote default config file to ${humanPath(configPath)}`) } - if (!process.env.CODE_SERVER_PARENT_PID && !process.env.VSCODE_IPC_HOOK_CLI) { - logger.info(`Using config file ${humanPath(configPath)}`) - } - const configFile = await fs.readFile(configPath) const config = yaml.safeLoad(configFile.toString(), { filename: configPath, diff --git a/src/node/entry.ts b/src/node/entry.ts index 0d16c250d..cec2a13cf 100644 --- a/src/node/entry.ts +++ b/src/node/entry.ts @@ -188,6 +188,8 @@ const main = async (args: Args, configArgs: Args): Promise => { }) logger.info(`code-server ${version} ${commit}`) + logger.info(`Using config file ${humanPath(args.config)}`) + const serverAddress = await httpServer.listen() logger.info(`HTTP server listening on ${serverAddress}`) From 466a04f8741ccada3f5e0e2c2a38c60ed57ee4a6 Mon Sep 17 00:00:00 2001 From: Asher Date: Wed, 7 Oct 2020 13:03:39 -0500 Subject: [PATCH 064/247] Remove pointless use of openInFlagCount It'll always be zero here. --- src/node/cli.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/node/cli.ts b/src/node/cli.ts index 92bf39cb8..1403d8920 100644 --- a/src/node/cli.ts +++ b/src/node/cli.ts @@ -529,9 +529,10 @@ export const shouldOpenInExistingInstance = async (args: Args): Promise 0) { + // Check if any unrelated flags are set (check against one because `_` always + // exists), that a file or directory was passed, and that the socket is + // active. + if (Object.keys(args).length === 1 && args._.length > 0) { const socketPath = await readSocketPath() if (socketPath && (await canConnect(socketPath))) { return socketPath From 26c735b4342674efbfd4003a17f99f3ff4c6ac3a Mon Sep 17 00:00:00 2001 From: Asher Date: Fri, 9 Oct 2020 17:05:21 -0500 Subject: [PATCH 065/247] Remove tryParse Now that the exception handling happens further up there doesn't seem to be an advantage in having this in a separate method. --- src/node/entry.ts | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/src/node/entry.ts b/src/node/entry.ts index cec2a13cf..96db046e2 100644 --- a/src/node/entry.ts +++ b/src/node/entry.ts @@ -240,16 +240,11 @@ const main = async (args: Args, configArgs: Args): Promise => { } async function entry(): Promise { - const tryParse = async (): Promise<[Args, Args, Args]> => { - const cliArgs = parse(process.argv.slice(2)) - const configArgs = await readConfigFile(cliArgs.config) - // This prioritizes the flags set in args over the ones in the config file. - let args = Object.assign(configArgs, cliArgs) - args = await setDefaults(args) - return [args, cliArgs, configArgs] - } - - const [args, cliArgs, configArgs] = await tryParse() + const cliArgs = parse(process.argv.slice(2)) + const configArgs = await readConfigFile(cliArgs.config) + // This prioritizes the flags set in args over the ones in the config file. + let args = Object.assign(configArgs, cliArgs) + args = await setDefaults(args) // There's no need to check flags like --help or to spawn in an existing // instance for the child process because these would have already happened in From d7e31126253afa0fd4b6b913318a57e8506e6aab Mon Sep 17 00:00:00 2001 From: Asher Date: Fri, 9 Oct 2020 18:01:43 -0500 Subject: [PATCH 066/247] Update standalone test --- ci/build/test-standalone-release.sh | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/ci/build/test-standalone-release.sh b/ci/build/test-standalone-release.sh index 92b58e8c0..0344ea39f 100755 --- a/ci/build/test-standalone-release.sh +++ b/ci/build/test-standalone-release.sh @@ -15,8 +15,7 @@ main() { ./release-standalone/bin/code-server --extensions-dir "$EXTENSIONS_DIR" --install-extension ms-python.python local installed_extensions installed_extensions="$(./release-standalone/bin/code-server --extensions-dir "$EXTENSIONS_DIR" --list-extensions 2>&1)" - if [[ $installed_extensions != *"info Using config file ~/.config/code-server/config.yaml -ms-python.python" ]]; then + if [[ $installed_extensions != "ms-python.python" ]]; then echo "Unexpected output from listing extensions:" echo "$installed_extensions" exit 1 From c6ba12942c584d3e4cefea0f9c84ed3092dde81c Mon Sep 17 00:00:00 2001 From: Asher Date: Sun, 11 Oct 2020 01:14:43 -0500 Subject: [PATCH 067/247] Filter blank plugin directories (#2187) I neglected to realize that "".split(":") is an array with "" in it. --- src/node/plugin.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/node/plugin.ts b/src/node/plugin.ts index e17a9909c..7469f317d 100644 --- a/src/node/plugin.ts +++ b/src/node/plugin.ts @@ -75,12 +75,18 @@ export const loadPlugins = async (httpServer: HttpServer, args: Args): Promise _loadPlugins(path.resolve(dir), httpServer, args)), + ...pluginPath + .split(":") + .filter((p) => !!p) + .map((dir) => _loadPlugins(path.resolve(dir), httpServer, args)), // Individual plugins so you don't have to symlink or move them into a // directory specifically for plugins. This lets you load plugins that are // on the same level as other directories that are not plugins (if you tried // to use CS_PLUGIN_PATH code-server would try to load those other // directories as plugins). Intended for development. - ...plugin.split(":").map((dir) => loadPlugin(path.resolve(dir), httpServer, args)), + ...plugin + .split(":") + .filter((p) => !!p) + .map((dir) => loadPlugin(path.resolve(dir), httpServer, args)), ]) } From d7ba9ae63345b2ea96f40641055d91b811bc2d17 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Mon, 12 Oct 2020 01:18:55 -0400 Subject: [PATCH 068/247] v3.6.0 --- doc/install.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/doc/install.md b/doc/install.md index 8d7e98ba8..b53a60675 100644 --- a/doc/install.md +++ b/doc/install.md @@ -79,8 +79,8 @@ commands presented in the rest of this document. ## Debian, Ubuntu ```bash -curl -fOL https://github.com/cdr/code-server/releases/download/v3.5.0/code-server_3.5.0_amd64.deb -sudo dpkg -i code-server_3.5.0_amd64.deb +curl -fOL https://github.com/cdr/code-server/releases/download/v3.6.0/code-server_3.6.0_amd64.deb +sudo dpkg -i code-server_3.6.0_amd64.deb sudo systemctl enable --now code-server@$USER # Now visit http://127.0.0.1:8080. Your password is in ~/.config/code-server/config.yaml ``` @@ -88,8 +88,8 @@ sudo systemctl enable --now code-server@$USER ## Fedora, CentOS, RHEL, SUSE ```bash -curl -fOL https://github.com/cdr/code-server/releases/download/v3.5.0/code-server-3.5.0-amd64.rpm -sudo rpm -i code-server-3.5.0-amd64.rpm +curl -fOL https://github.com/cdr/code-server/releases/download/v3.6.0/code-server-3.6.0-amd64.rpm +sudo rpm -i code-server-3.6.0-amd64.rpm sudo systemctl enable --now code-server@$USER # Now visit http://127.0.0.1:8080. Your password is in ~/.config/code-server/config.yaml ``` @@ -158,10 +158,10 @@ Here is an example script for installing and using a standalone `code-server` re ```bash mkdir -p ~/.local/lib ~/.local/bin -curl -fL https://github.com/cdr/code-server/releases/download/v3.5.0/code-server-3.5.0-linux-amd64.tar.gz \ +curl -fL https://github.com/cdr/code-server/releases/download/v3.6.0/code-server-3.6.0-linux-amd64.tar.gz \ | tar -C ~/.local/lib -xz -mv ~/.local/lib/code-server-3.5.0-linux-amd64 ~/.local/lib/code-server-3.5.0 -ln -s ~/.local/lib/code-server-3.5.0/bin/code-server ~/.local/bin/code-server +mv ~/.local/lib/code-server-3.6.0-linux-amd64 ~/.local/lib/code-server-3.6.0 +ln -s ~/.local/lib/code-server-3.6.0/bin/code-server ~/.local/bin/code-server PATH="~/.local/bin:$PATH" code-server # Now visit http://127.0.0.1:8080. Your password is in ~/.config/code-server/config.yaml From a4a03c14922ccaec2a9ff8d1b7b2af8522a4214d Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Mon, 12 Oct 2020 02:09:48 -0400 Subject: [PATCH 069/247] Fix CI --- ci/build/build-release.sh | 2 +- ci/build/npm-postinstall.sh | 4 ++++ ci/build/release-github-assets.sh | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/ci/build/build-release.sh b/ci/build/build-release.sh index eedda98e2..8f71dc339 100755 --- a/ci/build/build-release.sh +++ b/ci/build/build-release.sh @@ -25,7 +25,6 @@ main() { rsync README.md "$RELEASE_PATH" rsync LICENSE.txt "$RELEASE_PATH" rsync ./lib/vscode/ThirdPartyNotices.txt "$RELEASE_PATH" - rsync ./lib/coder-cloud-agent "$RELEASE_PATH/lib" # code-server exports types which can be imported and used by plugins. Those # types import ipc.d.ts but it isn't included in the final vscode build so @@ -60,6 +59,7 @@ EOF if [ "$KEEP_MODULES" = 1 ]; then rsync node_modules/ "$RELEASE_PATH/node_modules" + rsync ./lib/coder-cloud-agent "$RELEASE_PATH/lib" fi } diff --git a/ci/build/npm-postinstall.sh b/ci/build/npm-postinstall.sh index 74b904783..bd7922d5c 100755 --- a/ci/build/npm-postinstall.sh +++ b/ci/build/npm-postinstall.sh @@ -24,6 +24,10 @@ main() { ;; esac + OS="$(uname | tr '[:upper:]' '[:lower:]')" + curl -fsSL "https://storage.googleapis.com/coder-cloud-releases/agent/latest/$OS/cloud-agent" -o ./lib/coder-cloud-agent + chmod +x ./lib/coder-cloud-agent + if ! vscode_yarn; then echo "You may not have the required dependencies to build the native modules." echo "Please see https://github.com/cdr/code-server/blob/master/doc/npm.md" diff --git a/ci/build/release-github-assets.sh b/ci/build/release-github-assets.sh index f2d9ff8c3..7fba67703 100755 --- a/ci/build/release-github-assets.sh +++ b/ci/build/release-github-assets.sh @@ -11,7 +11,7 @@ main() { source ./ci/lib.sh download_artifact release-packages ./release-packages - local assets=(./release-packages/code-server*"$VERSION"*{.tar.gz,.zip,.deb,.rpm}) + local assets=(./release-packages/code-server*"$VERSION"*{.tar.gz,.deb,.rpm}) for i in "${!assets[@]}"; do assets[$i]="--attach=${assets[$i]}" done From ea105a9290579040e1ab396c7c0d0d2d825aad3a Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Mon, 12 Oct 2020 04:26:23 -0400 Subject: [PATCH 070/247] Fix release image entrypoint.sh --- ci/release-image/entrypoint.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ci/release-image/entrypoint.sh b/ci/release-image/entrypoint.sh index 7842a3564..b4343e7ed 100755 --- a/ci/release-image/entrypoint.sh +++ b/ci/release-image/entrypoint.sh @@ -5,7 +5,7 @@ set -eu USER="$(whoami)" export USER -if [ "${DOCKER_USER-}" != "$USER" ]; then +if [ "${DOCKER_USER-}" ] && [ "$DOCKER_USER" != "$USER" ]; then echo "$DOCKER_USER ALL=(ALL) NOPASSWD:ALL" | sudo tee -a /etc/sudoers.d/nopasswd > /dev/null # Unfortunately we cannot change $HOME as we cannot move any bind mounts # nor can we bind mount $HOME into a new home as that requires a privileged container. From 40b1efa142babc7df0ecbf1024c2b51efe7d5005 Mon Sep 17 00:00:00 2001 From: Hossam Hammady Date: Mon, 12 Oct 2020 10:45:26 +0000 Subject: [PATCH 071/247] Add echo command in NOTES to get password --- charts/code-server/templates/NOTES.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/charts/code-server/templates/NOTES.txt b/charts/code-server/templates/NOTES.txt index e9af85992..17c25f646 100644 --- a/charts/code-server/templates/NOTES.txt +++ b/charts/code-server/templates/NOTES.txt @@ -22,4 +22,4 @@ Administrator credentials: - Password : $(kubectl get secret --namespace {{ .Release.Namespace }} {{ template "code-server.fullname" . }} -o jsonpath="{.data.password}" | base64 --decode) + Password: echo $(kubectl get secret --namespace {{ .Release.Namespace }} {{ template "code-server.fullname" . }} -o jsonpath="{.data.password}" | base64 --decode) From d323f4f75bea04339043a28f4b31761641099721 Mon Sep 17 00:00:00 2001 From: Hossam Hammady Date: Mon, 12 Oct 2020 10:46:52 +0000 Subject: [PATCH 072/247] Add missing existingClaim in values.yaml comments --- charts/code-server/values.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/charts/code-server/values.yaml b/charts/code-server/values.yaml index 57a01d873..182116546 100644 --- a/charts/code-server/values.yaml +++ b/charts/code-server/values.yaml @@ -114,6 +114,7 @@ persistence: accessMode: ReadWriteOnce size: 1Gi annotations: {} + # existingClaim: "" serviceAccount: create: true From fd241d555bacdbc49f64537edd1e8c15880e375d Mon Sep 17 00:00:00 2001 From: Hossam Hammady Date: Mon, 12 Oct 2020 10:49:21 +0000 Subject: [PATCH 073/247] Fix indentation for extra mounts --- charts/code-server/templates/deployment.yaml | 22 ++++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/charts/code-server/templates/deployment.yaml b/charts/code-server/templates/deployment.yaml index 52c1ee954..60caa3307 100644 --- a/charts/code-server/templates/deployment.yaml +++ b/charts/code-server/templates/deployment.yaml @@ -80,21 +80,21 @@ spec: mountPath: /home/coder/.local/share/code-server subPath: code-server {{- range .Values.extraConfigmapMounts }} - - name: {{ .name }} - mountPath: {{ .mountPath }} - subPath: {{ .subPath | default "" }} - readOnly: {{ .readOnly }} + - name: {{ .name }} + mountPath: {{ .mountPath }} + subPath: {{ .subPath | default "" }} + readOnly: {{ .readOnly }} {{- end }} {{- range .Values.extraSecretMounts }} - - name: {{ .name }} - mountPath: {{ .mountPath }} - readOnly: {{ .readOnly }} + - name: {{ .name }} + mountPath: {{ .mountPath }} + readOnly: {{ .readOnly }} {{- end }} {{- range .Values.extraVolumeMounts }} - - name: {{ .name }} - mountPath: {{ .mountPath }} - subPath: {{ .subPath | default "" }} - readOnly: {{ .readOnly }} + - name: {{ .name }} + mountPath: {{ .mountPath }} + subPath: {{ .subPath | default "" }} + readOnly: {{ .readOnly }} {{- end }} ports: - name: http From b4fd47b5af431e2d2f6daea4e89ed841e72c498f Mon Sep 17 00:00:00 2001 From: Hossam Hammady Date: Mon, 12 Oct 2020 11:59:53 +0000 Subject: [PATCH 074/247] Add support for hostPath volumes --- charts/code-server/templates/deployment.yaml | 26 ++++++++++++++------ charts/code-server/templates/pvc.yaml | 2 +- charts/code-server/values.yaml | 2 ++ 3 files changed, 22 insertions(+), 8 deletions(-) diff --git a/charts/code-server/templates/deployment.yaml b/charts/code-server/templates/deployment.yaml index 60caa3307..e1036291c 100644 --- a/charts/code-server/templates/deployment.yaml +++ b/charts/code-server/templates/deployment.yaml @@ -126,19 +126,31 @@ spec: volumes: - name: data {{- if .Values.persistence.enabled }} + {{- if not .Values.persistence.hostPath }} persistentVolumeClaim: claimName: {{ .Values.persistence.existingClaim | default (include "code-server.fullname" .) }} + {{- else }} + hostPath: + path: {{ .Values.persistence.hostPath }} + type: Directory + {{- end -}} {{- else }} emptyDir: {} {{- end -}} {{- range .Values.extraSecretMounts }} - - name: {{ .name }} - secret: - secretName: {{ .secretName }} - defaultMode: {{ .defaultMode }} + - name: {{ .name }} + secret: + secretName: {{ .secretName }} + defaultMode: {{ .defaultMode }} {{- end }} {{- range .Values.extraVolumeMounts }} - - name: {{ .name }} - persistentVolumeClaim: - claimName: {{ .existingClaim }} + - name: {{ .name }} + {{- if .existingClaim }} + persistentVolumeClaim: + claimName: {{ .existingClaim }} + {{- else }} + hostPath: + path: {{ .hostPath }} + type: Directory + {{- end }} {{- end }} diff --git a/charts/code-server/templates/pvc.yaml b/charts/code-server/templates/pvc.yaml index f29301477..2f1c87405 100644 --- a/charts/code-server/templates/pvc.yaml +++ b/charts/code-server/templates/pvc.yaml @@ -1,4 +1,4 @@ -{{- if and .Values.persistence.enabled (not .Values.persistence.existingClaim) }} +{{- if and (and .Values.persistence.enabled (not .Values.persistence.existingClaim)) (not .Values.persistence.hostPath) }} kind: PersistentVolumeClaim apiVersion: v1 metadata: diff --git a/charts/code-server/values.yaml b/charts/code-server/values.yaml index 182116546..eff180351 100644 --- a/charts/code-server/values.yaml +++ b/charts/code-server/values.yaml @@ -115,6 +115,7 @@ persistence: size: 1Gi annotations: {} # existingClaim: "" + # hostPath: /data serviceAccount: create: true @@ -152,6 +153,7 @@ extraVolumeMounts: [] # mountPath: /mnt/volume # readOnly: true # existingClaim: volume-claim + # hostPath: "" extraConfigmapMounts: [] # - name: certs-configmap From 83465a2f4f8475b3c7ff2b3665066ed1fcc9f414 Mon Sep 17 00:00:00 2001 From: Hossam Hammady Date: Mon, 12 Oct 2020 12:50:43 +0000 Subject: [PATCH 075/247] Simplify data volume mount --- charts/code-server/templates/deployment.yaml | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/charts/code-server/templates/deployment.yaml b/charts/code-server/templates/deployment.yaml index e1036291c..9364a4706 100644 --- a/charts/code-server/templates/deployment.yaml +++ b/charts/code-server/templates/deployment.yaml @@ -74,11 +74,7 @@ spec: {{- end }} volumeMounts: - name: data - mountPath: /home/coder/project - subPath: project - - name: data - mountPath: /home/coder/.local/share/code-server - subPath: code-server + mountPath: /home/coder {{- range .Values.extraConfigmapMounts }} - name: {{ .name }} mountPath: {{ .mountPath }} From ec564091f183987333806bab432bc49062561b4a Mon Sep 17 00:00:00 2001 From: Asher Date: Mon, 12 Oct 2020 17:26:50 -0500 Subject: [PATCH 076/247] Fix agent copy during release If there isn't a lib dir yet it'll copy as lib instead of getting put inside the directory. --- ci/build/build-release.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/ci/build/build-release.sh b/ci/build/build-release.sh index 8f71dc339..95579eb82 100755 --- a/ci/build/build-release.sh +++ b/ci/build/build-release.sh @@ -59,6 +59,7 @@ EOF if [ "$KEEP_MODULES" = 1 ]; then rsync node_modules/ "$RELEASE_PATH/node_modules" + mkdir -p "$RELEASE_PATH/lib" rsync ./lib/coder-cloud-agent "$RELEASE_PATH/lib" fi } From 36b3183b75e2cdd5248d2f586c8bd1c5ba033832 Mon Sep 17 00:00:00 2001 From: Ammar Bandukwala Date: Tue, 13 Oct 2020 23:25:09 +0000 Subject: [PATCH 077/247] Add Coder Cloud alpha sign up link --- README.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/README.md b/README.md index f126580b8..433413bb9 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,15 @@ The install script will print out how to run and start using code-server. Docs on the install script, manual installation and docker image are at [./doc/install.md](./doc/install.md). +### Alpha Program 🐣 +We're working on a cloud platform to make deploying and managing code-server easier. If you don't want to worry about + +* TLS +* Authentication +* Port Forwarding + +consider [joining our alpha program](https://codercom.typeform.com/to/U4IKyv0W). + ## FAQ See [./doc/FAQ.md](./doc/FAQ.md). From e3699cf258d3c25785239aacd8c51432b608e97f Mon Sep 17 00:00:00 2001 From: Asher Date: Mon, 12 Oct 2020 15:34:54 -0500 Subject: [PATCH 078/247] Update VS Code to 1.50.0 - The .js build files are no longer committed so they're gone. - ParsedArgs and EnvironmentService are now NativeParsedArgs and NativeEnvironmentService. - Interface for environment service was moved. - getPathFromAmdModule was deprecated. --- ci/dev/vscode.patch | 296 +++++++++++++++++++++----------------------- lib/vscode | 2 +- 2 files changed, 140 insertions(+), 158 deletions(-) diff --git a/ci/dev/vscode.patch b/ci/dev/vscode.patch index f15c7d7a7..65d1c9d4e 100644 --- a/ci/dev/vscode.patch +++ b/ci/dev/vscode.patch @@ -1,5 +1,5 @@ diff --git a/.gitignore b/.gitignore -index 0fe46b6eadc4ccc819fbf342ee1071bb657792b3..e545e004cef31fa5f40ba8df6a2317ea5b69ddb5 100644 +index b7f5b58c8ede171be547c56b61ce76f79a3accc3..856fbd8c67460fe099d7fbee1475e906b500f053 100644 --- a/.gitignore +++ b/.gitignore @@ -25,7 +25,6 @@ out-vscode-reh-web-pkg/ @@ -20,32 +20,19 @@ index 3c6eccfb102f2084d16395d70d65f05a91b6d47b..00000000000000000000000000000000 -target "9.2.1" -runtime "electron" diff --git a/build/gulpfile.reh.js b/build/gulpfile.reh.js -index f2ea1bd37010b1eb8a43ce9beaae4a88810f6e2d..3f660f9981921ec465d2b8809a1a5ea5663f4c1f 100644 +index 5f367d1f0777d2cb46ad47e376337900733981b5..ba74af1d61a00ce42020418126e62879397f57bf 100644 --- a/build/gulpfile.reh.js +++ b/build/gulpfile.reh.js -@@ -52,6 +52,7 @@ gulp.task('vscode-reh-web-linux-x64-min', noop); - gulp.task('vscode-reh-web-linux-alpine-min', noop); +@@ -44,6 +44,7 @@ BUILD_TARGETS.forEach(({ platform, arch }) => { + }); function getNodeVersion() { + return process.versions.node; const yarnrc = fs.readFileSync(path.join(REPO_ROOT, 'remote', '.yarnrc'), 'utf8'); const target = /^target "(.*)"$/m.exec(yarnrc)[1]; return target; -diff --git a/build/lib/extensions.js b/build/lib/extensions.js -index 9cc40c4e1befd38886dc5880581d6f462a38dd3a..34e1fc89a8ac1c273a5cb41f19a088a8ec759d24 100644 ---- a/build/lib/extensions.js -+++ b/build/lib/extensions.js -@@ -66,7 +66,7 @@ function fromLocal(extensionPath, forWeb) { - if (isWebPacked) { - input = updateExtensionPackageJSON(input, (data) => { - delete data.scripts; -- delete data.dependencies; -+ // https://github.com/cdr/code-server/pull/2041#issuecomment-685910322 - delete data.devDependencies; - if (data.main) { - data.main = data.main.replace('/out/', /dist/); diff --git a/build/lib/extensions.ts b/build/lib/extensions.ts -index 7e529f17cb84d28d84de4ff64fa9fb8fc48135a9..462d699dc485369c74a4d9fdfefa48ba6124ac3a 100644 +index dac71c814798ecfac99750be856078e043d239bf..6edd7ea56baef7cd9f87a9020df32d3b8519b615 100644 --- a/build/lib/extensions.ts +++ b/build/lib/extensions.ts @@ -70,7 +70,7 @@ function fromLocal(extensionPath: string, forWeb: boolean): Stream { @@ -57,23 +44,6 @@ index 7e529f17cb84d28d84de4ff64fa9fb8fc48135a9..462d699dc485369c74a4d9fdfefa48ba delete data.devDependencies; if (data.main) { data.main = data.main.replace('/out/', /dist/); -diff --git a/build/lib/node.js b/build/lib/node.js -index 403ae3d9657f823019542e739fc39292db20e4fe..738ee8cee0e79aa239af10e1abefc9e836b8ce33 100644 ---- a/build/lib/node.js -+++ b/build/lib/node.js -@@ -5,11 +5,8 @@ - *--------------------------------------------------------------------------------------------*/ - Object.defineProperty(exports, "__esModule", { value: true }); - const path = require("path"); --const fs = require("fs"); - const root = path.dirname(path.dirname(__dirname)); --const yarnrcPath = path.join(root, 'remote', '.yarnrc'); --const yarnrc = fs.readFileSync(yarnrcPath, 'utf8'); --const version = /^target\s+"([^"]+)"$/m.exec(yarnrc)[1]; -+const version = process.versions.node; - const node = process.platform === 'win32' ? 'node.exe' : 'node'; - const nodePath = path.join(root, '.build', 'node', `v${version}`, `${process.platform}-${process.arch}`, node); - console.log(nodePath); diff --git a/build/lib/node.ts b/build/lib/node.ts index 64397034461b1661f82007c141cbf4c039a3b722..c53dccf4dc0a99122ed96cf10c2eb632bb25059e 100644 --- a/build/lib/node.ts @@ -95,18 +65,6 @@ index 64397034461b1661f82007c141cbf4c039a3b722..c53dccf4dc0a99122ed96cf10c2eb632 -console.log(nodePath); \ No newline at end of file +console.log(nodePath); -diff --git a/build/lib/util.js b/build/lib/util.js -index e552a036f89bd581644459fd5c27fe4ae1379f62..169e8614b9f6a2bd68446144ab7e1ce5c6d49b64 100644 ---- a/build/lib/util.js -+++ b/build/lib/util.js -@@ -257,6 +257,7 @@ function streamToPromise(stream) { - } - exports.streamToPromise = streamToPromise; - function getElectronVersion() { -+ return process.versions.node; - const yarnrc = fs.readFileSync(path.join(root, '.yarnrc'), 'utf8'); - const target = /^target "(.*)"$/m.exec(yarnrc)[1]; - return target; diff --git a/build/lib/util.ts b/build/lib/util.ts index 035c7e95ea3006bb3dabd68bbf54db80de4aaaf2..4ff8dcfe6b21a0ec8064ebc7bb05506b8f1faa91 100644 --- a/build/lib/util.ts @@ -253,7 +211,7 @@ index da4fa3e9d0443d679dfbab1000b434af2ae01afd..50f3e1144f8057883dea8b91ec2f7073 function processLib() { diff --git a/package.json b/package.json -index 9b5ee0f876303283eb766fd2bb3ed818c50b1d3e..30ef9fa81b1cd844138388d794d4d6d9db5c7fba 100644 +index 770b44b0c1ff53d903b7680ede27715376df00f2..b27ab71647a3e7c4b6076ba4fdb8fde20fa73bb0 100644 --- a/package.json +++ b/package.json @@ -46,7 +46,11 @@ @@ -266,12 +224,12 @@ index 9b5ee0f876303283eb766fd2bb3ed818c50b1d3e..30ef9fa81b1cd844138388d794d4d6d9 + "@coder/node-browser": "^1.0.8", + "@coder/requirefs": "^1.1.5", "applicationinsights": "1.0.8", - "chokidar": "3.2.3", + "chokidar": "3.4.2", "graceful-fs": "4.2.3", @@ -60,6 +64,7 @@ "native-keymap": "2.2.0", "native-watchdog": "1.3.0", - "node-pty": "0.10.0-beta8", + "node-pty": "0.10.0-beta17", + "rimraf": "^2.2.8", "semver-umd": "^5.5.7", "spdlog": "^0.11.1", @@ -294,14 +252,14 @@ index 9b5ee0f876303283eb766fd2bb3ed818c50b1d3e..30ef9fa81b1cd844138388d794d4d6d9 } } diff --git a/product.json b/product.json -index b9349015e3475bff07104ca2fa859954a37f962a..4c32260abc42efe17ee7d717e4dcebf182044e8c 100644 +index ecfb44dd74e09fc2ff1e902bea9396c2046fb9e6..026f78c698df50cfd1c2debb9823aacdcb4f7c71 100644 --- a/product.json +++ b/product.json @@ -20,7 +20,7 @@ "darwinBundleIdentifier": "com.visualstudio.code.oss", "linuxIconName": "com.visualstudio.code.oss", "licenseFileName": "LICENSE.txt", -- "reportIssueUrl": "https://github.com/Microsoft/vscode/issues/new", +- "reportIssueUrl": "https://github.com/microsoft/vscode/issues/new", + "reportIssueUrl": "https://github.com/cdr/code-server/issues/new", "urlProtocol": "code-oss", "extensionAllowedProposedApi": [ @@ -316,7 +274,7 @@ index c1a32ce532afa501fb19bdbcf6bcb0ec151ecd99..00000000000000000000000000000000 -target "12.14.1" -runtime "node" diff --git a/src/vs/base/common/network.ts b/src/vs/base/common/network.ts -index 4b6aebc16466dff58a9dfab4a680d230fa1f71a5..dd72e179ec0fa9a0b3e16e497225cb6da6218af3 100644 +index f475b10e5e81d5c2511d8d36ca5fa30a54bc415a..e9a30b2cd2a7848241d9a430c28faccb51efdb9b 100644 --- a/src/vs/base/common/network.ts +++ b/src/vs/base/common/network.ts @@ -113,16 +113,17 @@ class RemoteAuthoritiesImpl { @@ -341,13 +299,14 @@ index 4b6aebc16466dff58a9dfab4a680d230fa1f71a5..dd72e179ec0fa9a0b3e16e497225cb6d }); } diff --git a/src/vs/base/common/platform.ts b/src/vs/base/common/platform.ts -index 0bbc5d6ef911b1e98d26ad796873a9b6b7fb04ec..61f139b9c557b9c46e5a9640ab0e37a6fb7692ee 100644 +index 3361d83be5b7c3d08bdbfbe6947942a4695882c6..69ead8484e042bbad7075659f8e47f074bc217e4 100644 --- a/src/vs/base/common/platform.ts +++ b/src/vs/base/common/platform.ts -@@ -59,6 +59,17 @@ if (typeof navigator === 'object' && !isElectronRenderer) { +@@ -71,6 +71,18 @@ if (typeof navigator === 'object' && !isElectronRenderer) { _isWeb = true; _locale = navigator.language; _language = _locale; ++ + // NOTE@coder: Make languages work. + const el = typeof document !== 'undefined' && document.getElementById('vscode-remote-nls-configuration'); + const rawNlsConfig = el && el.getAttribute('data-settings'); @@ -359,9 +318,9 @@ index 0bbc5d6ef911b1e98d26ad796873a9b6b7fb04ec..61f139b9c557b9c46e5a9640ab0e37a6 + _language = nlsConfig.availableLanguages['*'] || LANGUAGE_DEFAULT; + } catch (error) { /* Oh well. */ } + } - } else if (typeof process === 'object') { - _isWindows = (process.platform === 'win32'); - _isMacintosh = (process.platform === 'darwin'); + } + + // Native environment diff --git a/src/vs/base/common/processes.ts b/src/vs/base/common/processes.ts index c52f7b3774f399d3fa161682316b20d807072806..08a87fa970f159f84691c5068cf5e38f0926015c 100644 --- a/src/vs/base/common/processes.ts @@ -459,18 +418,18 @@ index 2c64061da7b01aef0bfe3cec851da232ca9461c8..c0ef8faedd406c38bf9c55bbbdbbb060 // Do nothing. If we can't read the file we have no // language pack config. diff --git a/src/vs/code/browser/workbench/workbench.ts b/src/vs/code/browser/workbench/workbench.ts -index ad5272b22320a361cec0eed40d57629b06147c01..c9280b14472507ebb9a277f554485f08b090cb69 100644 +index 0ef8b9dc81419b53b27cf111fb206d72ba56bada..75d2ab3276049115829a38b8b7afee44bb748c2a 100644 --- a/src/vs/code/browser/workbench/workbench.ts +++ b/src/vs/code/browser/workbench/workbench.ts -@@ -16,6 +16,7 @@ import { isEqual } from 'vs/base/common/resources'; - import { isStandalone } from 'vs/base/browser/browser'; +@@ -17,6 +17,7 @@ import { isStandalone } from 'vs/base/browser/browser'; import { localize } from 'vs/nls'; import { Schemas } from 'vs/base/common/network'; + import product from 'vs/platform/product/common/product'; +import { encodePath } from 'vs/server/node/util'; - interface ICredential { - service: string; -@@ -253,12 +254,18 @@ class WorkspaceProvider implements IWorkspaceProvider { + function doCreateUri(path: string, queryValues: Map): URI { + let query: string | undefined = undefined; +@@ -309,12 +310,18 @@ class WorkspaceProvider implements IWorkspaceProvider { // Folder else if (isFolderToOpen(workspace)) { @@ -491,7 +450,7 @@ index ad5272b22320a361cec0eed40d57629b06147c01..c9280b14472507ebb9a277f554485f08 } // Append payload if any -@@ -348,7 +355,22 @@ class WindowIndicator implements IWindowIndicator { +@@ -404,7 +411,22 @@ class WindowIndicator implements IWindowIndicator { throw new Error('Missing web configuration element'); } @@ -515,7 +474,7 @@ index ad5272b22320a361cec0eed40d57629b06147c01..c9280b14472507ebb9a277f554485f08 // Revive static extension locations if (Array.isArray(config.staticExtensions)) { -@@ -360,40 +382,7 @@ class WindowIndicator implements IWindowIndicator { +@@ -416,40 +438,7 @@ class WindowIndicator implements IWindowIndicator { // Find workspace to open and payload let foundWorkspace = false; let workspace: IWorkspace; @@ -557,20 +516,37 @@ index ad5272b22320a361cec0eed40d57629b06147c01..c9280b14472507ebb9a277f554485f08 // If no workspace is provided through the URL, check for config attribute from server if (!foundWorkspace) { -diff --git a/src/vs/platform/environment/node/argv.ts b/src/vs/platform/environment/node/argv.ts -index 92dd2bcf87dba5e5f07f2707a91b1a364ab1b05f..047522bd1a2c1edfda05c3739838fecbd70db6c5 100644 ---- a/src/vs/platform/environment/node/argv.ts -+++ b/src/vs/platform/environment/node/argv.ts -@@ -8,6 +8,8 @@ import { localize } from 'vs/nls'; - import { isWindows } from 'vs/base/common/platform'; - - export interface ParsedArgs { +diff --git a/src/vs/platform/environment/common/argv.ts b/src/vs/platform/environment/common/argv.ts +index 2ac99a2120ec461703c2ff85da8b5379125c5d96..77450df65524b0daed61309614941195f5294c1d 100644 +--- a/src/vs/platform/environment/common/argv.ts ++++ b/src/vs/platform/environment/common/argv.ts +@@ -7,6 +7,8 @@ + * A list of command line arguments we support natively. + */ + export interface NativeParsedArgs { + 'extra-extensions-dir'?: string[]; + 'extra-builtin-extensions-dir'?: string[]; _: string[]; 'folder-uri'?: string[]; // undefined or array of 1 or more 'file-uri'?: string[]; // undefined or array of 1 or more -@@ -142,6 +144,8 @@ export const OPTIONS: OptionDescriptions> = { +diff --git a/src/vs/platform/environment/common/environment.ts b/src/vs/platform/environment/common/environment.ts +index eebf675762d57ddd7072b3e4182a9864c94ec81c..5c18cd4420c5f85724148bb7df14c62dfe0b34bf 100644 +--- a/src/vs/platform/environment/common/environment.ts ++++ b/src/vs/platform/environment/common/environment.ts +@@ -111,6 +111,8 @@ export interface INativeEnvironmentService extends IEnvironmentService { + extensionsPath?: string; + extensionsDownloadPath: string; + builtinExtensionsPath: string; ++ extraExtensionPaths: string[] ++ extraBuiltinExtensionPaths: string[] + + // --- Smoke test support + driverHandle?: string; +diff --git a/src/vs/platform/environment/node/argv.ts b/src/vs/platform/environment/node/argv.ts +index 662547909e57ba556c10177a29beac775cc86c80..eeaba21664ed5d797d1f08f0bd77ee075354ffe5 100644 +--- a/src/vs/platform/environment/node/argv.ts ++++ b/src/vs/platform/environment/node/argv.ts +@@ -54,6 +54,8 @@ export const OPTIONS: OptionDescriptions> = { 'extensions-dir': { type: 'string', deprecates: 'extensionHomePath', cat: 'e', args: 'dir', description: localize('extensionHomePath', "Set the root path for extensions.") }, 'extensions-download-dir': { type: 'string' }, 'builtin-extensions-dir': { type: 'string' }, @@ -579,25 +555,16 @@ index 92dd2bcf87dba5e5f07f2707a91b1a364ab1b05f..047522bd1a2c1edfda05c3739838fecb 'list-extensions': { type: 'boolean', cat: 'e', description: localize('listExtensions', "List the installed extensions.") }, 'show-versions': { type: 'boolean', cat: 'e', description: localize('showVersions', "Show versions of installed extensions, when using --list-extension.") }, 'category': { type: 'string', cat: 'e', description: localize('category', "Filters installed extensions by provided category, when using --list-extension.") }, -@@ -405,4 +409,3 @@ export function buildHelpMessage(productName: string, executableName: string, ve +@@ -316,4 +318,3 @@ export function buildHelpMessage(productName: string, executableName: string, ve export function buildVersionMessage(version: string | undefined, commit: string | undefined): string { return `${version || localize('unknownVersion', "Unknown version")}\n${commit || localize('unknownCommit', "Unknown commit")}\n${process.arch}`; } - diff --git a/src/vs/platform/environment/node/environmentService.ts b/src/vs/platform/environment/node/environmentService.ts -index 45d5ec2cc02707d91f19a66d408ae46a1201a9e8..4ed498c63ceb55d15bd104a92b701ead3dfa81f2 100644 +index 16057c43a0807fb894a94d7d658e7d24c9db173a..d8c40c94dce474f441acbbf114e53694e3b101c5 100644 --- a/src/vs/platform/environment/node/environmentService.ts +++ b/src/vs/platform/environment/node/environmentService.ts -@@ -38,6 +38,8 @@ export interface INativeEnvironmentService extends IEnvironmentService { - extensionsPath?: string; - extensionsDownloadPath: string; - builtinExtensionsPath: string; -+ extraExtensionPaths: string[]; -+ extraBuiltinExtensionPaths: string[]; - - driverHandle?: string; - driverVerbose: boolean; -@@ -180,6 +182,13 @@ export class EnvironmentService implements INativeEnvironmentService { +@@ -145,6 +145,13 @@ export class NativeEnvironmentService implements INativeEnvironmentService { return resources.joinPath(this.userHome, product.dataFolderName, 'extensions').fsPath; } @@ -612,10 +579,10 @@ index 45d5ec2cc02707d91f19a66d408ae46a1201a9e8..4ed498c63ceb55d15bd104a92b701ead get extensionDevelopmentLocationURI(): URI[] | undefined { const s = this._args.extensionDevelopmentPath; diff --git a/src/vs/platform/extensionManagement/node/extensionsScanner.ts b/src/vs/platform/extensionManagement/node/extensionsScanner.ts -index 575b2aafc3802cd6f5f943930e30de9f2c2690de..873181f967856759e3dc001e5bbe06e2f9eb676a 100644 +index e7342348d46cbcafa1b301ca1373ce01c057e70f..408867d2d3b503b46363c5a853e047d396bb7e0a 100644 --- a/src/vs/platform/extensionManagement/node/extensionsScanner.ts +++ b/src/vs/platform/extensionManagement/node/extensionsScanner.ts -@@ -85,7 +85,7 @@ export class ExtensionsScanner extends Disposable { +@@ -90,7 +90,7 @@ export class ExtensionsScanner extends Disposable { } async scanAllUserExtensions(): Promise { @@ -624,7 +591,7 @@ index 575b2aafc3802cd6f5f943930e30de9f2c2690de..873181f967856759e3dc001e5bbe06e2 } async extractUserExtension(identifierWithVersion: ExtensionIdentifierWithVersion, zipPath: string, token: CancellationToken): Promise { -@@ -211,7 +211,13 @@ export class ExtensionsScanner extends Disposable { +@@ -235,7 +235,13 @@ export class ExtensionsScanner extends Disposable { private async scanExtensionsInDir(dir: string, type: ExtensionType): Promise { const limiter = new Limiter(10); @@ -639,7 +606,7 @@ index 575b2aafc3802cd6f5f943930e30de9f2c2690de..873181f967856759e3dc001e5bbe06e2 const extensions = await Promise.all(extensionsFolders.map(extensionFolder => limiter.queue(() => this.scanExtension(extensionFolder, dir, type)))); return extensions.filter(e => e && e.identifier); } -@@ -241,7 +247,7 @@ export class ExtensionsScanner extends Disposable { +@@ -265,7 +271,7 @@ export class ExtensionsScanner extends Disposable { } private async scanDefaultSystemExtensions(): Promise { @@ -648,7 +615,7 @@ index 575b2aafc3802cd6f5f943930e30de9f2c2690de..873181f967856759e3dc001e5bbe06e2 this.logService.trace('Scanned system extensions:', result.length); return result; } -@@ -345,4 +351,9 @@ export class ExtensionsScanner extends Disposable { +@@ -369,4 +375,9 @@ export class ExtensionsScanner extends Disposable { } }); } @@ -659,10 +626,10 @@ index 575b2aafc3802cd6f5f943930e30de9f2c2690de..873181f967856759e3dc001e5bbe06e2 + } } diff --git a/src/vs/platform/product/common/product.ts b/src/vs/platform/product/common/product.ts -index bb33203d1727b1c076efac9113afc3b2580cdbd9..c53cea338cdaa0f0ac15542c129e1572b3f13b80 100644 +index 798faa74ae825de7449b74609ed649912531ec0f..487abd285a42407f669ce5f5396423644f822a83 100644 --- a/src/vs/platform/product/common/product.ts +++ b/src/vs/platform/product/common/product.ts -@@ -30,6 +30,12 @@ if (isWeb) { +@@ -37,6 +37,12 @@ if (isWeb || typeof require === 'undefined' || typeof require.__$__nodeRequire ! ], }); } @@ -674,12 +641,12 @@ index bb33203d1727b1c076efac9113afc3b2580cdbd9..c53cea338cdaa0f0ac15542c129e1572 + } } - // Node: AMD loader + // Native (non-sandboxed) diff --git a/src/vs/platform/product/common/productService.ts b/src/vs/platform/product/common/productService.ts -index d1cb00a6d63621a4873a6a5e815220d084ceac2a..1a69d6f63a7406d364aa3e2b32fb75309f212e98 100644 +index 53dc899d48d85d27042600962b27ab97fb68951f..4792fee15e308e7427bb24591b19bcdc97eff600 100644 --- a/src/vs/platform/product/common/productService.ts +++ b/src/vs/platform/product/common/productService.ts -@@ -30,6 +30,8 @@ export type ConfigurationSyncStore = { +@@ -32,6 +32,8 @@ export type ConfigurationSyncStore = { }; export interface IProductConfiguration { @@ -751,10 +718,10 @@ index 6611f1dae42055f69a55c1c154d9475f11cd4d0a..d598d4909d5ff6d1614e4a038b1865e1 /** * Delete an element stored under the provided key from storage. diff --git a/src/vs/platform/storage/node/storageService.ts b/src/vs/platform/storage/node/storageService.ts -index ac657056aa68549f0053cfb1ec68835ba4ce20f9..143f9b5681eb867c5e5c5437946ab785eb34e4b4 100644 +index 096b9e23493539c9937940a56e555d95bbae38d9..ef37e614004f550f7b64eacd362f6894fc523a42 100644 --- a/src/vs/platform/storage/node/storageService.ts +++ b/src/vs/platform/storage/node/storageService.ts -@@ -202,8 +202,8 @@ export class NativeStorageService extends Disposable implements IStorageService +@@ -201,8 +201,8 @@ export class NativeStorageService extends Disposable implements IStorageService return this.getStorage(scope).getNumber(key, fallbackValue); } @@ -1473,7 +1440,7 @@ index 0000000000000000000000000000000000000000..33b28cf2d53746ee9c50c056ac2e087d +} diff --git a/src/vs/server/node/channel.ts b/src/vs/server/node/channel.ts new file mode 100644 -index 0000000000000000000000000000000000000000..e10cc9c218b27d859a523be3db5b8a30ef90d953 +index 0000000000000000000000000000000000000000..609c4d1cb43f52f92906b901c14c790f4536468f --- /dev/null +++ b/src/vs/server/node/channel.ts @@ -0,0 +1,360 @@ @@ -1489,7 +1456,7 @@ index 0000000000000000000000000000000000000000..e10cc9c218b27d859a523be3db5b8a30 +import { transformOutgoingURIs } from 'vs/base/common/uriIpc'; +import { IServerChannel } from 'vs/base/parts/ipc/common/ipc'; +import { IDiagnosticInfo } from 'vs/platform/diagnostics/common/diagnostics'; -+import { INativeEnvironmentService } from 'vs/platform/environment/node/environmentService'; ++import { INativeEnvironmentService } from 'vs/platform/environment/common/environment'; +import { ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensions/common/extensions'; +import { FileDeleteOptions, FileOpenOptions, FileOverwriteOptions, FileReadStreamOptions, FileType, FileWriteOptions, IStat, IWatchOptions } from 'vs/platform/files/common/files'; +import { DiskFileSystemProvider } from 'vs/platform/files/node/diskFileSystemProvider'; @@ -1839,17 +1806,17 @@ index 0000000000000000000000000000000000000000..e10cc9c218b27d859a523be3db5b8a30 +} diff --git a/src/vs/server/node/connection.ts b/src/vs/server/node/connection.ts new file mode 100644 -index 0000000000000000000000000000000000000000..36e80fb6966ae2cb53c98f3d31e2193d00c509c3 +index 0000000000000000000000000000000000000000..eec198c948d48b1539ff46510016f759f396be18 --- /dev/null +++ b/src/vs/server/node/connection.ts @@ -0,0 +1,157 @@ +import * as cp from 'child_process'; -+import { getPathFromAmdModule } from 'vs/base/common/amd'; +import { VSBuffer } from 'vs/base/common/buffer'; +import { Emitter } from 'vs/base/common/event'; ++import { FileAccess } from 'vs/base/common/network'; +import { ISocket } from 'vs/base/parts/ipc/common/ipc.net'; +import { NodeSocket } from 'vs/base/parts/ipc/node/ipc.net'; -+import { INativeEnvironmentService } from 'vs/platform/environment/node/environmentService'; ++import { INativeEnvironmentService } from 'vs/platform/environment/common/environment'; +import { ILogService } from 'vs/platform/log/common/log'; +import { getNlsConfiguration } from 'vs/server/node/nls'; +import { Protocol } from 'vs/server/node/protocol'; @@ -1956,7 +1923,7 @@ index 0000000000000000000000000000000000000000..36e80fb6966ae2cb53c98f3d31e2193d + private async spawn(locale: string, buffer: VSBuffer): Promise { + const config = await getNlsConfiguration(locale, this.environment.userDataPath); + const proc = cp.fork( -+ getPathFromAmdModule(require, 'bootstrap-fork'), ++ FileAccess.asFileUri('bootstrap-fork', require).fsPath, + [ '--type=extensionHost' ], + { + env: { @@ -2560,10 +2527,10 @@ index 0000000000000000000000000000000000000000..3c74512192aec6220216bc8563b3127b +} diff --git a/src/vs/server/node/server.ts b/src/vs/server/node/server.ts new file mode 100644 -index 0000000000000000000000000000000000000000..4b88fedb2f05d300fb50978e63721d4d04b7fb5f +index 0000000000000000000000000000000000000000..a1289865858f405f93d3d396f41c6a0aadffd5e5 --- /dev/null +++ b/src/vs/server/node/server.ts -@@ -0,0 +1,285 @@ +@@ -0,0 +1,286 @@ +import * as fs from 'fs'; +import * as net from 'net'; +import * as path from 'path'; @@ -2577,9 +2544,9 @@ index 0000000000000000000000000000000000000000..4b88fedb2f05d300fb50978e63721d4d +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { ConfigurationService } from 'vs/platform/configuration/common/configurationService'; +import { ExtensionHostDebugBroadcastChannel } from 'vs/platform/debug/common/extensionHostDebugIpc'; -+import { IEnvironmentService } from 'vs/platform/environment/common/environment'; -+import { ParsedArgs } from 'vs/platform/environment/node/argv'; -+import { EnvironmentService, INativeEnvironmentService } from 'vs/platform/environment/node/environmentService'; ++import { NativeParsedArgs } from 'vs/platform/environment/common/argv'; ++import { IEnvironmentService, INativeEnvironmentService } from 'vs/platform/environment/common/environment'; ++import { NativeEnvironmentService } from 'vs/platform/environment/node/environmentService'; +import { ExtensionGalleryService } from 'vs/platform/extensionManagement/common/extensionGalleryService'; +import { IExtensionGalleryService, IExtensionManagementService } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { ExtensionManagementChannel } from 'vs/platform/extensionManagement/common/extensionManagementIpc'; @@ -2632,7 +2599,7 @@ index 0000000000000000000000000000000000000000..4b88fedb2f05d300fb50978e63721d4d + private readonly services = new ServiceCollection(); + private servicesPromise?: Promise; + -+ public async cli(args: ParsedArgs): Promise { ++ public async cli(args: NativeParsedArgs): Promise { + return main(args); + } + @@ -2766,8 +2733,8 @@ index 0000000000000000000000000000000000000000..4b88fedb2f05d300fb50978e63721d4d + } + } + -+ private async initializeServices(args: ParsedArgs): Promise { -+ const environmentService = new EnvironmentService(args, process.execPath); ++ private async initializeServices(args: NativeParsedArgs): Promise { ++ const environmentService = new NativeEnvironmentService(args); + // https://github.com/cdr/code-server/issues/1693 + fs.mkdirSync(environmentService.globalStorageHome.fsPath, { recursive: true }); + @@ -2789,6 +2756,7 @@ index 0000000000000000000000000000000000000000..4b88fedb2f05d300fb50978e63721d4d + + this.services.set(ILogService, logService); + this.services.set(IEnvironmentService, environmentService); ++ this.services.set(INativeEnvironmentService, environmentService); + + const configurationService = new ConfigurationService(environmentService.settingsResource, fileService); + await configurationService.initialize(); @@ -2869,10 +2837,10 @@ index 0000000000000000000000000000000000000000..fa47e993b46802f1a26457649e9e8bc4 + return path.split("/").map((p) => encodeURIComponent(p)).join("/"); +}; diff --git a/src/vs/workbench/api/browser/extensionHost.contribution.ts b/src/vs/workbench/api/browser/extensionHost.contribution.ts -index bfabf0008910c87146df53a2e10fe63bae517a86..32b3b1cf84c8d280fd7f03d541b867691d51c2fb 100644 +index 9e264fb33b9a282e3a5284bcd857e17a664107a7..a23a44a781cd1f9b7d432d79a46707c93f4008e7 100644 --- a/src/vs/workbench/api/browser/extensionHost.contribution.ts +++ b/src/vs/workbench/api/browser/extensionHost.contribution.ts -@@ -60,6 +60,7 @@ import './mainThreadComments'; +@@ -61,6 +61,7 @@ import './mainThreadComments'; import './mainThreadNotebook'; import './mainThreadTask'; import './mainThreadLabelService'; @@ -2899,7 +2867,7 @@ index 7bc3904963bed2925f3640b6bd929347159dd3cf..c6db2368ae9eaca61889efcf3c49763c return Promise.reject(err); } diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts -index 3595cd3e38136222044a13050b15105bbe539068..989caefff7c4b8203c03cec8fa451f5e70ea8964 100644 +index 2a0576b68f943f63c010dd496e094311bdc149f0..357c63f0fec08ddfb06b3579460fe1566fa5d852 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -68,6 +68,7 @@ import { IURITransformerService } from 'vs/workbench/api/common/extHostUriTransf @@ -2910,7 +2878,7 @@ index 3595cd3e38136222044a13050b15105bbe539068..989caefff7c4b8203c03cec8fa451f5e import { ExtHostTheming } from 'vs/workbench/api/common/extHostTheming'; import { IExtHostTunnelService } from 'vs/workbench/api/common/extHostTunnelService'; import { IExtHostApiDeprecationService } from 'vs/workbench/api/common/extHostApiDeprecationService'; -@@ -100,6 +101,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I +@@ -103,6 +104,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I const extHostStorage = accessor.get(IExtHostStorage); const extensionStoragePaths = accessor.get(IExtensionStoragePaths); const extHostLogService = accessor.get(ILogService); @@ -2918,7 +2886,7 @@ index 3595cd3e38136222044a13050b15105bbe539068..989caefff7c4b8203c03cec8fa451f5e const extHostTunnelService = accessor.get(IExtHostTunnelService); const extHostApiDeprecation = accessor.get(IExtHostApiDeprecationService); const extHostWindow = accessor.get(IExtHostWindow); -@@ -110,6 +112,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I +@@ -114,6 +116,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I rpcProtocol.set(ExtHostContext.ExtHostConfiguration, extHostConfiguration); rpcProtocol.set(ExtHostContext.ExtHostExtensionService, extensionService); rpcProtocol.set(ExtHostContext.ExtHostStorage, extHostStorage); @@ -2927,10 +2895,10 @@ index 3595cd3e38136222044a13050b15105bbe539068..989caefff7c4b8203c03cec8fa451f5e rpcProtocol.set(ExtHostContext.ExtHostWindow, extHostWindow); diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts -index 4b7946662950f18179a5b6e3552abd39e68ca80e..ca1352d311a94b42e18d0d9e4859b18ec2bb271d 100644 +index 3728f5602dffd0fd4b0cf326c5fa7d6d7c49c53e..233fbbce328094d014a375b1e9c11231ac9fafd9 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts -@@ -795,6 +795,16 @@ export interface MainThreadLabelServiceShape extends IDisposable { +@@ -807,6 +807,16 @@ export interface MainThreadLabelServiceShape extends IDisposable { $unregisterResourceLabelFormatter(handle: number): void; } @@ -2947,7 +2915,7 @@ index 4b7946662950f18179a5b6e3552abd39e68ca80e..ca1352d311a94b42e18d0d9e4859b18e export interface MainThreadSearchShape extends IDisposable { $registerFileSearchProvider(handle: number, scheme: string): void; $registerTextSearchProvider(handle: number, scheme: string): void; -@@ -1765,6 +1775,7 @@ export const MainContext = { +@@ -1784,6 +1794,7 @@ export const MainContext = { MainThreadWindow: createMainId('MainThreadWindow'), MainThreadLabelService: createMainId('MainThreadLabelService'), MainThreadNotebook: createMainId('MainThreadNotebook'), @@ -2955,7 +2923,7 @@ index 4b7946662950f18179a5b6e3552abd39e68ca80e..ca1352d311a94b42e18d0d9e4859b18e MainThreadTheming: createMainId('MainThreadTheming'), MainThreadTunnelService: createMainId('MainThreadTunnelService'), MainThreadTimeline: createMainId('MainThreadTimeline') -@@ -1806,6 +1817,7 @@ export const ExtHostContext = { +@@ -1826,6 +1837,7 @@ export const ExtHostContext = { ExtHostOutputService: createMainId('ExtHostOutputService'), ExtHosLabelService: createMainId('ExtHostLabelService'), ExtHostNotebook: createMainId('ExtHostNotebook'), @@ -2964,7 +2932,7 @@ index 4b7946662950f18179a5b6e3552abd39e68ca80e..ca1352d311a94b42e18d0d9e4859b18e ExtHostTunnelService: createMainId('ExtHostTunnelService'), ExtHostAuthentication: createMainId('ExtHostAuthentication'), diff --git a/src/vs/workbench/api/common/extHostExtensionService.ts b/src/vs/workbench/api/common/extHostExtensionService.ts -index 0bb5188614bcbf98b85c9208edc2b173f70b1670..38ff3e2e05645be8df619ed2b47fa2984b918741 100644 +index 311b529e5adb46014466bf1852aef05a20b7a724..bc1bbd0196e4baf089c2bc7e0b08ecd771232b5e 100644 --- a/src/vs/workbench/api/common/extHostExtensionService.ts +++ b/src/vs/workbench/api/common/extHostExtensionService.ts @@ -31,6 +31,7 @@ import { IExtHostInitDataService } from 'vs/workbench/api/common/extHostInitData @@ -2999,7 +2967,7 @@ index 0bb5188614bcbf98b85c9208edc2b173f70b1670..38ff3e2e05645be8df619ed2b47fa298 this._extHostTunnelService = extHostTunnelService; this._extHostTerminalService = extHostTerminalService; this._disposables = new DisposableStore(); -@@ -355,7 +359,7 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme +@@ -362,7 +366,7 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme const activationTimesBuilder = new ExtensionActivationTimesBuilder(reason.startup); return Promise.all([ @@ -3008,13 +2976,13 @@ index 0bb5188614bcbf98b85c9208edc2b173f70b1670..38ff3e2e05645be8df619ed2b47fa298 this._loadExtensionContext(extensionDescription) ]).then(values => { return AbstractExtHostExtensionService._callActivate(this._logService, extensionDescription.identifier, values[0], values[1], activationTimesBuilder); -@@ -746,7 +750,7 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme +@@ -753,7 +757,7 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme protected abstract _beforeAlmostReadyToRunExtensions(): Promise; protected abstract _getEntryPoint(extensionDescription: IExtensionDescription): string | undefined; - protected abstract _loadCommonJSModule(module: URI, activationTimesBuilder: ExtensionActivationTimesBuilder): Promise; + protected abstract _loadCommonJSModule(module: URI, activationTimesBuilder: ExtensionActivationTimesBuilder, isRemote?: boolean): Promise; - public abstract async $setRemoteEnvironment(env: { [key: string]: string | null }): Promise; + public abstract $setRemoteEnvironment(env: { [key: string]: string | null }): Promise; } diff --git a/src/vs/workbench/api/node/extHost.node.services.ts b/src/vs/workbench/api/node/extHost.node.services.ts @@ -3123,10 +3091,10 @@ index ced2d815834e40a1543e80516472799075980733..dfcae73e8a042307600c67f163aa00ba .monaco-workbench .activitybar > .content > .home-bar > .home-bar-icon-badge { diff --git a/src/vs/workbench/browser/web.main.ts b/src/vs/workbench/browser/web.main.ts -index 511d7376a2bfebde59b4c67fed54c39e9dd534c9..c7c45f8e4e4ffe56a8782f58af75c6a7835142cf 100644 +index 416938f141228faafc95dc765be7d5fbaf610a7e..25ff7f7c881458fbb7b5bd588704930b52f439e8 100644 --- a/src/vs/workbench/browser/web.main.ts +++ b/src/vs/workbench/browser/web.main.ts -@@ -45,6 +45,7 @@ import { FileLogService } from 'vs/platform/log/common/fileLogService'; +@@ -42,6 +42,7 @@ import { FileLogService } from 'vs/platform/log/common/fileLogService'; import { toLocalISOString } from 'vs/base/common/date'; import { isWorkspaceToOpen, isFolderToOpen } from 'vs/platform/windows/common/windows'; import { getWorkspaceIdentifier } from 'vs/workbench/services/workspaces/browser/workspaces'; @@ -3134,7 +3102,7 @@ index 511d7376a2bfebde59b4c67fed54c39e9dd534c9..c7c45f8e4e4ffe56a8782f58af75c6a7 import { coalesce } from 'vs/base/common/arrays'; import { InMemoryFileSystemProvider } from 'vs/platform/files/common/inMemoryFilesystemProvider'; import { WebResourceIdentityService, IResourceIdentityService } from 'vs/platform/resource/common/resourceIdentityService'; -@@ -87,6 +88,8 @@ class BrowserMain extends Disposable { +@@ -94,6 +95,8 @@ class BrowserMain extends Disposable { // Startup const instantiationService = workbench.startup(); @@ -3144,7 +3112,7 @@ index 511d7376a2bfebde59b4c67fed54c39e9dd534c9..c7c45f8e4e4ffe56a8782f58af75c6a7 return instantiationService.invokeFunction(accessor => { const commandService = accessor.get(ICommandService); diff --git a/src/vs/workbench/common/resources.ts b/src/vs/workbench/common/resources.ts -index 18ea0bfedb4492327429a38237b05915b29f6dd0..d59a17c17f4fffa23d786ce36b4ff624d5688a58 100644 +index 94e7e7a4bac154c45078a1b5034e50634a7a43af..8164200dcef1efbc65b50eef9c270af3ca655fbd 100644 --- a/src/vs/workbench/common/resources.ts +++ b/src/vs/workbench/common/resources.ts @@ -15,6 +15,7 @@ import { ParsedExpression, IExpression, parse } from 'vs/base/common/glob'; @@ -3155,7 +3123,7 @@ index 18ea0bfedb4492327429a38237b05915b29f6dd0..d59a17c17f4fffa23d786ce36b4ff624 export class ResourceContextKey extends Disposable implements IContextKey { -@@ -68,7 +69,8 @@ export class ResourceContextKey extends Disposable implements IContextKey { +@@ -74,7 +75,8 @@ export class ResourceContextKey extends Disposable implements IContextKey { if (!ResourceContextKey._uriEquals(this._resourceKey.get(), value)) { this._contextKeyService.bufferChangeEvents(() => { this._resourceKey.set(value); @@ -3163,13 +3131,13 @@ index 18ea0bfedb4492327429a38237b05915b29f6dd0..d59a17c17f4fffa23d786ce36b4ff624 + // NOTE@coder: Fixes source control context menus (#1104). + this._schemeKey.set(value ? (value.scheme === Schemas.vscodeRemote ? Schemas.file : value.scheme) : null); this._filenameKey.set(value ? basename(value) : null); - this._langIdKey.set(value ? this._modeService.getModeIdByFilepathOrFirstLine(value) : null); - this._extensionKey.set(value ? extname(value) : null); + this._dirnameKey.set(value ? dirname(value).fsPath : null); + this._pathKey.set(value ? value.fsPath : null); diff --git a/src/vs/workbench/contrib/scm/browser/media/scm.css b/src/vs/workbench/contrib/scm/browser/media/scm.css -index b1838de8f21c60141d01cc424a5e000a32f1c828..0a480032e0cc8d5219cd240f8807aa317718659d 100644 +index ac44ad3bae428def66e22fe9cc1c54648d429f6b..faa63023c4c586b51fa3c2a48ff3641b9cb0e145 100644 --- a/src/vs/workbench/contrib/scm/browser/media/scm.css +++ b/src/vs/workbench/contrib/scm/browser/media/scm.css -@@ -138,9 +138,11 @@ +@@ -149,9 +149,11 @@ margin-right: 8px; } @@ -3184,6 +3152,19 @@ index b1838de8f21c60141d01cc424a5e000a32f1c828..0a480032e0cc8d5219cd240f8807aa31 .scm-view .monaco-list .monaco-list-row .resource-group > .actions, .scm-view .monaco-list .monaco-list-row .resource > .name > .monaco-icon-label > .actions { +diff --git a/src/vs/workbench/electron-sandbox/sandbox.simpleservices.ts b/src/vs/workbench/electron-sandbox/sandbox.simpleservices.ts +index d57009a5a251ac5815c04e4f76bacef3e518a575..e825a8975449e95cf40504057d56579c12918a58 100644 +--- a/src/vs/workbench/electron-sandbox/sandbox.simpleservices.ts ++++ b/src/vs/workbench/electron-sandbox/sandbox.simpleservices.ts +@@ -127,6 +127,8 @@ export class SimpleNativeWorkbenchEnvironmentService implements INativeWorkbench + extensionsPath?: string | undefined; + extensionsDownloadPath: string = undefined!; + builtinExtensionsPath: string = undefined!; ++ extraExtensionPaths: string[] = undefined!; ++ extraBuiltinExtensionPaths: string[] = undefined!; + + driverHandle?: string | undefined; + driverVerbose = false; diff --git a/src/vs/workbench/services/dialogs/browser/dialogService.ts b/src/vs/workbench/services/dialogs/browser/dialogService.ts index 1360c248eb7ff937c92d08bbf30d2b76ea606dc0..adccf8b88d62381c3ec484df40c6d63142ec9ef5 100644 --- a/src/vs/workbench/services/dialogs/browser/dialogService.ts @@ -3204,10 +3185,10 @@ index 1360c248eb7ff937c92d08bbf30d2b76ea606dc0..adccf8b88d62381c3ec484df40c6d631 }; diff --git a/src/vs/workbench/services/environment/browser/environmentService.ts b/src/vs/workbench/services/environment/browser/environmentService.ts -index 819607be0c13fed28eb7fbe6d4a62c0b860b1aa9..b046943311b713a579cc3a94983ea1b7fca7b9b1 100644 +index 8d5e0fb796661c63bf46a377607977f4b1f0b2ad..9f5da5da9435626419be5447577d20dcb408e78d 100644 --- a/src/vs/workbench/services/environment/browser/environmentService.ts +++ b/src/vs/workbench/services/environment/browser/environmentService.ts -@@ -116,8 +116,18 @@ export class BrowserWorkbenchEnvironmentService implements IWorkbenchEnvironment +@@ -112,8 +112,18 @@ export class BrowserWorkbenchEnvironmentService implements IWorkbenchEnvironment @memoize get logFile(): URI { return joinPath(this.options.logsPath, 'window.log'); } @@ -3227,7 +3208,7 @@ index 819607be0c13fed28eb7fbe6d4a62c0b860b1aa9..b046943311b713a579cc3a94983ea1b7 @memoize get settingsResource(): URI { return joinPath(this.userRoamingDataHome, 'settings.json'); } -@@ -279,7 +289,12 @@ export class BrowserWorkbenchEnvironmentService implements IWorkbenchEnvironment +@@ -275,7 +285,12 @@ export class BrowserWorkbenchEnvironmentService implements IWorkbenchEnvironment extensionHostDebugEnvironment.params.port = parseInt(value); break; case 'enableProposedApi': @@ -3242,7 +3223,7 @@ index 819607be0c13fed28eb7fbe6d4a62c0b860b1aa9..b046943311b713a579cc3a94983ea1b7 } } diff --git a/src/vs/workbench/services/extensionManagement/common/extensionEnablementService.ts b/src/vs/workbench/services/extensionManagement/common/extensionEnablementService.ts -index 32f3dc52c1ff645df6471a03542d6ec3eb73a277..c2f4497d2eba13a771b2665ad58f12ecdfa7606a 100644 +index 6e7e8c5c9ed18d3a7e9f6d5d886adf0f49038f23..f4b17da7d718a2b60db7a44f936e7d0e93ffff6f 100644 --- a/src/vs/workbench/services/extensionManagement/common/extensionEnablementService.ts +++ b/src/vs/workbench/services/extensionManagement/common/extensionEnablementService.ts @@ -205,7 +205,7 @@ export class ExtensionEnablementService extends Disposable implements IWorkbench @@ -3284,10 +3265,10 @@ index a982b3ecc58c5a2f3a92be7b8cca3a1cacbb7d47..97f9bfcf0e679be683b1b09cd569149e const error = new Error(localize('cannot be installed', "Cannot install '{0}' because this extension has defined that it cannot run on the remote server.", gallery.displayName || gallery.name)); error.name = INSTALL_ERROR_NOT_SUPPORTED; diff --git a/src/vs/workbench/services/extensions/browser/extensionService.ts b/src/vs/workbench/services/extensions/browser/extensionService.ts -index 9e979d28691d0b0b26fde5e46b606731e31f3da5..dd31879c7dd899c73c4a1371996912f4513bfd0d 100644 +index 5eaec3499a3bd87ee4026c26a4b0e8c706978859..ee6801e5ddab91910930685c326aa6e344dd8d5d 100644 --- a/src/vs/workbench/services/extensions/browser/extensionService.ts +++ b/src/vs/workbench/services/extensions/browser/extensionService.ts -@@ -125,8 +125,10 @@ export class ExtensionService extends AbstractExtensionService implements IExten +@@ -161,8 +161,10 @@ export class ExtensionService extends AbstractExtensionService implements IExten this._remoteAgentService.getEnvironment(), this._remoteAgentService.scanExtensions() ]); @@ -3298,7 +3279,7 @@ index 9e979d28691d0b0b26fde5e46b606731e31f3da5..dd31879c7dd899c73c4a1371996912f4 + .concat(remoteExtensions.filter(ext => ext.extensionKind && (ext.extensionKind === "web" || ext.extensionKind.includes("web")))); const remoteAgentConnection = this._remoteAgentService.getConnection(); - this._runningLocation = _determineRunningLocation(this._productService, this._configService, localExtensions, remoteExtensions, Boolean(remoteEnv && remoteAgentConnection)); + this._runningLocation = this._runningLocationClassifier.determineRunningLocation(localExtensions, remoteExtensions); diff --git a/src/vs/workbench/services/extensions/common/extensionsUtil.ts b/src/vs/workbench/services/extensions/common/extensionsUtil.ts index 65e532ee58dfc06ed944846d01b885cb8f260ebc..0b6282fde7ad03c7ea9872a777cbf487253abed1 100644 --- a/src/vs/workbench/services/extensions/common/extensionsUtil.ts @@ -3314,7 +3295,7 @@ index 65e532ee58dfc06ed944846d01b885cb8f260ebc..0b6282fde7ad03c7ea9872a777cbf487 export function getExtensionKind(manifest: IExtensionManifest, productService: IProductService, configurationService: IConfigurationService): ExtensionKind[] { diff --git a/src/vs/workbench/services/extensions/node/extensionHostProcessSetup.ts b/src/vs/workbench/services/extensions/node/extensionHostProcessSetup.ts -index 49542eda74c65e485272cd37d586911886aa3ad7..de0e2da0a4c2dca91dc7e0e48c28a8a75ca3e7d4 100644 +index 4eb8204bf5ad61d4d292dad5c2490c25fbff497a..85f6006c1f125da283b2ba615bad2805fa3598fc 100644 --- a/src/vs/workbench/services/extensions/node/extensionHostProcessSetup.ts +++ b/src/vs/workbench/services/extensions/node/extensionHostProcessSetup.ts @@ -16,7 +16,7 @@ import { IInitData } from 'vs/workbench/api/common/extHost.protocol'; @@ -3371,15 +3352,15 @@ index 49542eda74c65e485272cd37d586911886aa3ad7..de0e2da0a4c2dca91dc7e0e48c28a8a7 console.error(e); } diff --git a/src/vs/workbench/services/extensions/worker/extensionHostWorkerMain.ts b/src/vs/workbench/services/extensions/worker/extensionHostWorkerMain.ts -index 79455414c06b95612a0dce2cad01f2bb2f40ef49..a407593b4dc6053309ed560898918cf67470e836 100644 +index b39a5cbb9eadbc046144d2e76d26a9b0e950ddaa..3b4cc7274e149ee10dba0dbbb09cf25939091f4b 100644 --- a/src/vs/workbench/services/extensions/worker/extensionHostWorkerMain.ts +++ b/src/vs/workbench/services/extensions/worker/extensionHostWorkerMain.ts -@@ -14,7 +14,11 @@ - +@@ -15,7 +15,11 @@ require.config({ baseUrl: monacoBaseUrl, -- catchError: true -+ catchError: true, + catchError: true, +- createTrustedScriptURL: (value: string) => value ++ createTrustedScriptURL: (value: string) => value, + paths: { + '@coder/node-browser': `../node_modules/@coder/node-browser/out/client/client.js`, + '@coder/requirefs': `../node_modules/@coder/requirefs/out/requirefs.js`, @@ -3388,10 +3369,10 @@ index 79455414c06b95612a0dce2cad01f2bb2f40ef49..a407593b4dc6053309ed560898918cf6 require(['vs/workbench/services/extensions/worker/extensionHostWorker'], () => { }, err => console.error(err)); diff --git a/src/vs/workbench/services/localizations/electron-browser/localizationsService.ts b/src/vs/workbench/services/localizations/electron-browser/localizationsService.ts -index 44999bd842eae12b752b2e7e8c4904272b111dc1..601b1c5408835c743fe07e34da4d4534873bf832 100644 +index d7aefde89c74bc6096d6e66c45368c8582594efa..9758f3bb96b48603251336e6a64e270ee89744f0 100644 --- a/src/vs/workbench/services/localizations/electron-browser/localizationsService.ts +++ b/src/vs/workbench/services/localizations/electron-browser/localizationsService.ts -@@ -5,17 +5,17 @@ +@@ -5,8 +5,8 @@ import { createChannelSender } from 'vs/base/parts/ipc/common/ipc'; import { ILocalizationsService } from 'vs/platform/localizations/common/localizations'; @@ -3399,8 +3380,9 @@ index 44999bd842eae12b752b2e7e8c4904272b111dc1..601b1c5408835c743fe07e34da4d4534 import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; +import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; - export class LocalizationsService { - + // @ts-ignore: interface is implemented via proxy + export class LocalizationsService implements ILocalizationsService { +@@ -14,9 +14,9 @@ export class LocalizationsService implements ILocalizationsService { declare readonly _serviceBrand: undefined; constructor( @@ -3413,7 +3395,7 @@ index 44999bd842eae12b752b2e7e8c4904272b111dc1..601b1c5408835c743fe07e34da4d4534 } diff --git a/src/vs/workbench/workbench.web.main.ts b/src/vs/workbench/workbench.web.main.ts -index f02bbbf874b5b18ac8d077ad56a8a4a57e77a4a6..86271940724aaf28e4eda93e59920820a7d93987 100644 +index 59208b87022c74342489288c1a3c89937aa6d37e..f49153db796e59a5aed0cc56ea5d01c0f10f963e 100644 --- a/src/vs/workbench/workbench.web.main.ts +++ b/src/vs/workbench/workbench.web.main.ts @@ -35,7 +35,8 @@ import 'vs/workbench/services/textfile/browser/browserTextFileService'; @@ -3427,7 +3409,7 @@ index f02bbbf874b5b18ac8d077ad56a8a4a57e77a4a6..86271940724aaf28e4eda93e59920820 import 'vs/workbench/services/credentials/browser/credentialsService'; import 'vs/workbench/services/url/browser/urlService'; diff --git a/yarn.lock b/yarn.lock -index 140ed883c1a92ebcd7a284b98ca71261fa9cb631..b363d7de5000fd370bb4221f48e193382648a185 100644 +index a38db6751b5bbe5949afeb4c29921e1cb88913ac..c78f065a5587d81427aaf951cbfbc5509e2096d5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -140,6 +140,23 @@ @@ -3454,7 +3436,7 @@ index 140ed883c1a92ebcd7a284b98ca71261fa9cb631..b363d7de5000fd370bb4221f48e19338 "@electron/get@^1.0.1": version "1.7.2" resolved "https://registry.yarnpkg.com/@electron/get/-/get-1.7.2.tgz#286436a9fb56ff1a1fcdf0e80131fd65f4d1e0fd" -@@ -5375,6 +5392,13 @@ jsprim@^1.2.2: +@@ -5383,6 +5400,13 @@ jsprim@^1.2.2: json-schema "0.2.3" verror "1.10.0" @@ -3468,7 +3450,7 @@ index 140ed883c1a92ebcd7a284b98ca71261fa9cb631..b363d7de5000fd370bb4221f48e19338 just-debounce@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/just-debounce/-/just-debounce-1.0.0.tgz#87fccfaeffc0b68cd19d55f6722943f929ea35ea" -@@ -5955,26 +5979,11 @@ minimatch@0.3: +@@ -5963,26 +5987,11 @@ minimatch@0.3: dependencies: brace-expansion "^1.1.7" @@ -3496,7 +3478,7 @@ index 140ed883c1a92ebcd7a284b98ca71261fa9cb631..b363d7de5000fd370bb4221f48e19338 minipass@^2.2.1, minipass@^2.3.3: version "2.3.3" resolved "https://registry.yarnpkg.com/minipass/-/minipass-2.3.3.tgz#a7dcc8b7b833f5d368759cce544dccb55f50f233" -@@ -6716,6 +6725,11 @@ p-try@^2.0.0: +@@ -6724,6 +6733,11 @@ p-try@^2.0.0: resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.0.0.tgz#85080bb87c64688fa47996fe8f7dfbe8211760b1" integrity sha512-hMp0onDKIajHfIkdRk3P4CdCmErkYAxxDtP3Wx/4nZ3aGlau2VKh3mZpcuFkH27WQkL/3WBCPOktzA9ZOAnMQQ== diff --git a/lib/vscode b/lib/vscode index 2af051012..93c2f0fbf 160000 --- a/lib/vscode +++ b/lib/vscode @@ -1 +1 @@ -Subproject commit 2af051012b66169dde0c4dfae3f5ef48f787ff69 +Subproject commit 93c2f0fbf16c5a4b10e4d5f89737d9c2c25488a3 From 07580e1fcbdf504e8b05249252d60ed032238408 Mon Sep 17 00:00:00 2001 From: Asher Date: Mon, 12 Oct 2020 15:42:33 -0500 Subject: [PATCH 079/247] Add path to loader for tas-client-umd It's a new module used by 1.50.0. --- src/browser/pages/vscode.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/browser/pages/vscode.ts b/src/browser/pages/vscode.ts index 3ca1db83e..4c9d9e1e0 100644 --- a/src/browser/pages/vscode.ts +++ b/src/browser/pages/vscode.ts @@ -41,6 +41,7 @@ try { "xterm-addon-unicode11": `../node_modules/xterm-addon-unicode11/lib/xterm-addon-unicode11.js`, "xterm-addon-webgl": `../node_modules/xterm-addon-webgl/lib/xterm-addon-webgl.js`, "semver-umd": `../node_modules/semver-umd/lib/semver-umd.js`, + "tas-client-umd": `../node_modules/tas-client-umd/lib/tas-client-umd.js`, "iconv-lite-umd": `../node_modules/iconv-lite-umd/lib/iconv-lite-umd.js`, jschardet: `../node_modules/jschardet/dist/jschardet.min.js`, }, From 30d05aeb4b9671198e8954b2e8e9f701c0261517 Mon Sep 17 00:00:00 2001 From: Asher Date: Mon, 12 Oct 2020 15:42:46 -0500 Subject: [PATCH 080/247] Update require base URL for VS Code loader It needs to have the scheme otherwise when resolving these modules the loader will default to the file scheme and fail to fetch. --- src/browser/pages/vscode.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/browser/pages/vscode.ts b/src/browser/pages/vscode.ts index 4c9d9e1e0..2cb7973f1 100644 --- a/src/browser/pages/vscode.ts +++ b/src/browser/pages/vscode.ts @@ -31,7 +31,8 @@ try { } ;(self.require as any) = { - baseUrl: `${options.csStaticBase}/lib/vscode/out`, + // Without the full URL VS Code will try to load file://. + baseUrl: `${window.location.origin}${options.csStaticBase}/lib/vscode/out`, recordStats: true, paths: { "vscode-textmate": `../node_modules/vscode-textmate/release/main`, From a7c43a8eb63c97ee1ac2c5e3675bab1e46ea02d7 Mon Sep 17 00:00:00 2001 From: Asher Date: Mon, 12 Oct 2020 17:08:15 -0500 Subject: [PATCH 081/247] Remove CSP tag from VS Code html This matches with the html in the VS Code repo and also fixes a problem with the worker which loads HTML using data: and then can't load any scripts because 'self' doesn't work. --- src/browser/pages/vscode.html | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/browser/pages/vscode.html b/src/browser/pages/vscode.html index accb07e26..d9305fe9d 100644 --- a/src/browser/pages/vscode.html +++ b/src/browser/pages/vscode.html @@ -9,11 +9,6 @@ - - Date: Wed, 14 Oct 2020 16:56:27 -0500 Subject: [PATCH 082/247] Move extension fetch to main thread This makes the fetch work independently of the worker's origin which is no longer the same as the main thread (the main problem is the inability to send cookies without setting SameSite to None). --- ci/dev/vscode.patch | 61 +++++++++++++++++++++++++++++---------------- 1 file changed, 39 insertions(+), 22 deletions(-) diff --git a/ci/dev/vscode.patch b/ci/dev/vscode.patch index 65d1c9d4e..9ee8c5682 100644 --- a/ci/dev/vscode.patch +++ b/ci/dev/vscode.patch @@ -929,11 +929,13 @@ index 0000000000000000000000000000000000000000..3c0703b7174ad792a4b42841e96ee937 +}; diff --git a/src/vs/server/browser/extHostNodeProxy.ts b/src/vs/server/browser/extHostNodeProxy.ts new file mode 100644 -index 0000000000000000000000000000000000000000..ed7c078077b0c375758529959b280e091436113a +index 0000000000000000000000000000000000000000..6c6b87a05610417d73635c5a151845000f216d28 --- /dev/null +++ b/src/vs/server/browser/extHostNodeProxy.ts -@@ -0,0 +1,46 @@ +@@ -0,0 +1,52 @@ ++import { VSBuffer } from 'vs/base/common/buffer'; +import { Emitter } from 'vs/base/common/event'; ++import { UriComponents } from 'vs/base/common/uri'; +import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { ExtHostNodeProxyShape, MainContext, MainThreadNodeProxyShape } from 'vs/workbench/api/common/extHost.protocol'; +import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService'; @@ -975,17 +977,24 @@ index 0000000000000000000000000000000000000000..ed7c078077b0c375758529959b280e09 + public send(message: string): void { + this.proxy.$send(message); + } ++ ++ public async fetchExtension(extensionUri: UriComponents): Promise { ++ return this.proxy.$fetchExtension(extensionUri).then(b => b.buffer); ++ } +} + +export interface IExtHostNodeProxy extends ExtHostNodeProxy { } +export const IExtHostNodeProxy = createDecorator('IExtHostNodeProxy'); diff --git a/src/vs/server/browser/mainThreadNodeProxy.ts b/src/vs/server/browser/mainThreadNodeProxy.ts new file mode 100644 -index 0000000000000000000000000000000000000000..0d2e93edae2baf34d27b7b52be0bf4960f244531 +index 0000000000000000000000000000000000000000..21a139288e5b8f56016491879d69d01da929decb --- /dev/null +++ b/src/vs/server/browser/mainThreadNodeProxy.ts -@@ -0,0 +1,37 @@ +@@ -0,0 +1,55 @@ ++import { VSBuffer } from 'vs/base/common/buffer'; +import { IDisposable } from 'vs/base/common/lifecycle'; ++import { FileAccess } from 'vs/base/common/network'; ++import { URI, UriComponents } from 'vs/base/common/uri'; +import { INodeProxyService } from 'vs/server/common/nodeProxy'; +import { ExtHostContext, IExtHostContext, MainContext, MainThreadNodeProxyShape } from 'vs/workbench/api/common/extHost.protocol'; +import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers'; @@ -1016,6 +1025,21 @@ index 0000000000000000000000000000000000000000..0d2e93edae2baf34d27b7b52be0bf496 + } + } + ++ async $fetchExtension(extensionUri: UriComponents): Promise { ++ const fetchUri = URI.from({ ++ scheme: window.location.protocol.replace(':', ''), ++ authority: window.location.host, ++ // Use FileAccess to get the static base path. ++ path: FileAccess.asBrowserUri("", require).path, ++ query: `tar=${encodeURIComponent(extensionUri.path)}`, ++ }); ++ const response = await fetch(fetchUri.toString(true)); ++ if (response.status !== 200) { ++ throw new Error(`Failed to download extension "${module}"`); ++ } ++ return VSBuffer.wrap(new Uint8Array(await response.arrayBuffer())); ++ } ++ + dispose(): void { + this.disposables.forEach((d) => d.dispose()); + this.disposables = []; @@ -1024,10 +1048,10 @@ index 0000000000000000000000000000000000000000..0d2e93edae2baf34d27b7b52be0bf496 +} diff --git a/src/vs/server/browser/worker.ts b/src/vs/server/browser/worker.ts new file mode 100644 -index 0000000000000000000000000000000000000000..5ae44cdc856bf81326a4c516b8be9afb2c746a67 +index 0000000000000000000000000000000000000000..1d47ede49b76b1774329269ab5c86fedb5712c19 --- /dev/null +++ b/src/vs/server/browser/worker.ts -@@ -0,0 +1,56 @@ +@@ -0,0 +1,48 @@ +import { Client } from '@coder/node-browser'; +import { fromTar } from '@coder/requirefs'; +import { URI } from 'vs/base/common/uri'; @@ -1042,19 +1066,11 @@ index 0000000000000000000000000000000000000000..5ae44cdc856bf81326a4c516b8be9afb + logService: ILogService, + vscode: any, +): Promise => { -+ const fetchUri = URI.from({ -+ scheme: self.location.protocol.replace(':', ''), -+ authority: self.location.host, -+ path: self.location.pathname.replace(/\/static\/([^\/]+)\/.*$/, '/static/$1\/'), -+ query: `tar=${encodeURIComponent(module.path)}`, -+ }); -+ const response = await fetch(fetchUri.toString(true)); -+ if (response.status !== 200) { -+ throw new Error(`Failed to download extension "${module}"`); -+ } + const client = new Client(nodeProxy, { logger: logService }); -+ const init = await client.handshake(); -+ const buffer = new Uint8Array(await response.arrayBuffer()); ++ const [buffer, init] = await Promise.all([ ++ nodeProxy.fetchExtension(module), ++ client.handshake(), ++ ]); + const rfs = fromTar(buffer); + (self).global = self; + rfs.provide('vscode', vscode); @@ -2895,15 +2911,16 @@ index 2a0576b68f943f63c010dd496e094311bdc149f0..357c63f0fec08ddfb06b3579460fe156 rpcProtocol.set(ExtHostContext.ExtHostWindow, extHostWindow); diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts -index 3728f5602dffd0fd4b0cf326c5fa7d6d7c49c53e..233fbbce328094d014a375b1e9c11231ac9fafd9 100644 +index 3728f5602dffd0fd4b0cf326c5fa7d6d7c49c53e..2521acff0e692e97b72deef758ce41b4cd54a724 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts -@@ -807,6 +807,16 @@ export interface MainThreadLabelServiceShape extends IDisposable { +@@ -807,6 +807,17 @@ export interface MainThreadLabelServiceShape extends IDisposable { $unregisterResourceLabelFormatter(handle: number): void; } +export interface MainThreadNodeProxyShape extends IDisposable { + $send(message: string): void; ++ $fetchExtension(extensionUri: UriComponents): Promise; +} +export interface ExtHostNodeProxyShape { + $onMessage(message: string): void; @@ -2915,7 +2932,7 @@ index 3728f5602dffd0fd4b0cf326c5fa7d6d7c49c53e..233fbbce328094d014a375b1e9c11231 export interface MainThreadSearchShape extends IDisposable { $registerFileSearchProvider(handle: number, scheme: string): void; $registerTextSearchProvider(handle: number, scheme: string): void; -@@ -1784,6 +1794,7 @@ export const MainContext = { +@@ -1784,6 +1795,7 @@ export const MainContext = { MainThreadWindow: createMainId('MainThreadWindow'), MainThreadLabelService: createMainId('MainThreadLabelService'), MainThreadNotebook: createMainId('MainThreadNotebook'), @@ -2923,7 +2940,7 @@ index 3728f5602dffd0fd4b0cf326c5fa7d6d7c49c53e..233fbbce328094d014a375b1e9c11231 MainThreadTheming: createMainId('MainThreadTheming'), MainThreadTunnelService: createMainId('MainThreadTunnelService'), MainThreadTimeline: createMainId('MainThreadTimeline') -@@ -1826,6 +1837,7 @@ export const ExtHostContext = { +@@ -1826,6 +1838,7 @@ export const ExtHostContext = { ExtHostOutputService: createMainId('ExtHostOutputService'), ExtHosLabelService: createMainId('ExtHostLabelService'), ExtHostNotebook: createMainId('ExtHostNotebook'), From daf204eeda5a937ef803b6fb45a84595b60f4fc9 Mon Sep 17 00:00:00 2001 From: Asher Date: Wed, 14 Oct 2020 17:21:05 -0500 Subject: [PATCH 083/247] Exclude browser-supported remote extensions Removing them just for peace of mind even though they seem to get filtered out later. This line is meant to only add remote extensions that aren't capable of running in the browser. If they are browser-capable they don't need to run in our shimmed Node environment. --- ci/dev/vscode.patch | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/ci/dev/vscode.patch b/ci/dev/vscode.patch index 9ee8c5682..b205aa83b 100644 --- a/ci/dev/vscode.patch +++ b/ci/dev/vscode.patch @@ -929,11 +929,10 @@ index 0000000000000000000000000000000000000000..3c0703b7174ad792a4b42841e96ee937 +}; diff --git a/src/vs/server/browser/extHostNodeProxy.ts b/src/vs/server/browser/extHostNodeProxy.ts new file mode 100644 -index 0000000000000000000000000000000000000000..6c6b87a05610417d73635c5a151845000f216d28 +index 0000000000000000000000000000000000000000..5dd5406befcb593ad6366d9e98f46485ed14fbc0 --- /dev/null +++ b/src/vs/server/browser/extHostNodeProxy.ts -@@ -0,0 +1,52 @@ -+import { VSBuffer } from 'vs/base/common/buffer'; +@@ -0,0 +1,51 @@ +import { Emitter } from 'vs/base/common/event'; +import { UriComponents } from 'vs/base/common/uri'; +import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; @@ -3282,7 +3281,7 @@ index a982b3ecc58c5a2f3a92be7b8cca3a1cacbb7d47..97f9bfcf0e679be683b1b09cd569149e const error = new Error(localize('cannot be installed', "Cannot install '{0}' because this extension has defined that it cannot run on the remote server.", gallery.displayName || gallery.name)); error.name = INSTALL_ERROR_NOT_SUPPORTED; diff --git a/src/vs/workbench/services/extensions/browser/extensionService.ts b/src/vs/workbench/services/extensions/browser/extensionService.ts -index 5eaec3499a3bd87ee4026c26a4b0e8c706978859..ee6801e5ddab91910930685c326aa6e344dd8d5d 100644 +index 5eaec3499a3bd87ee4026c26a4b0e8c706978859..829514442fe60e2999378af14fd38c71ee92d2b9 100644 --- a/src/vs/workbench/services/extensions/browser/extensionService.ts +++ b/src/vs/workbench/services/extensions/browser/extensionService.ts @@ -161,8 +161,10 @@ export class ExtensionService extends AbstractExtensionService implements IExten @@ -3293,7 +3292,7 @@ index 5eaec3499a3bd87ee4026c26a4b0e8c706978859..ee6801e5ddab91910930685c326aa6e3 remoteExtensions = this._checkEnabledAndProposedAPI(remoteExtensions); + // NOTE@coder: Include remotely hosted extensions that should run locally. + localExtensions = this._checkEnabledAndProposedAPI(localExtensions) -+ .concat(remoteExtensions.filter(ext => ext.extensionKind && (ext.extensionKind === "web" || ext.extensionKind.includes("web")))); ++ .concat(remoteExtensions.filter(ext => !ext.browser && ext.extensionKind && (ext.extensionKind === "web" || ext.extensionKind.includes("web")))); const remoteAgentConnection = this._remoteAgentService.getConnection(); this._runningLocation = this._runningLocationClassifier.determineRunningLocation(localExtensions, remoteExtensions); From dcb303a437be8c653eb78e67f314db3cb589a79a Mon Sep 17 00:00:00 2001 From: Asher Date: Thu, 15 Oct 2020 16:17:04 -0500 Subject: [PATCH 084/247] Move argument defaults into setDefaults --- src/node/cli.ts | 135 +++++++++++++++++++++++++++++++++++----------- src/node/entry.ts | 91 +++++++++++-------------------- src/node/http.ts | 12 ++--- test/cli.test.ts | 73 ++++++++++++++++++++++++- 4 files changed, 209 insertions(+), 102 deletions(-) diff --git a/src/node/cli.ts b/src/node/cli.ts index 1403d8920..ba8f5ae88 100644 --- a/src/node/cli.ts +++ b/src/node/cli.ts @@ -5,7 +5,7 @@ import * as os from "os" import * as path from "path" import { Args as VsArgs } from "../../lib/vscode/src/vs/server/ipc" import { AuthType } from "./http" -import { canConnect, generatePassword, humanPath, paths } from "./util" +import { canConnect, generateCertificate, generatePassword, humanPath, paths } from "./util" export class Optional { public constructor(public readonly value?: T) {} @@ -22,33 +22,33 @@ export enum LogLevel { export class OptionalString extends Optional {} export interface Args extends VsArgs { - readonly config?: string - readonly auth?: AuthType - readonly password?: string - readonly cert?: OptionalString - readonly "cert-key"?: string - readonly "disable-telemetry"?: boolean - readonly help?: boolean - readonly host?: string - readonly json?: boolean + config?: string + auth?: AuthType + password?: string + cert?: OptionalString + "cert-key"?: string + "disable-telemetry"?: boolean + help?: boolean + host?: string + json?: boolean log?: LogLevel - readonly open?: boolean - readonly port?: number - readonly "bind-addr"?: string - readonly socket?: string - readonly version?: boolean - readonly force?: boolean - readonly "list-extensions"?: boolean - readonly "install-extension"?: string[] - readonly "show-versions"?: boolean - readonly "uninstall-extension"?: string[] - readonly "proxy-domain"?: string[] - readonly locale?: string - readonly _: string[] - readonly "reuse-window"?: boolean - readonly "new-window"?: boolean + open?: boolean + port?: number + "bind-addr"?: string + socket?: string + version?: boolean + force?: boolean + "list-extensions"?: boolean + "install-extension"?: string[] + "show-versions"?: boolean + "uninstall-extension"?: string[] + "proxy-domain"?: string[] + locale?: string + _: string[] + "reuse-window"?: boolean + "new-window"?: boolean - readonly link?: OptionalString + link?: OptionalString } interface Option { @@ -325,13 +325,37 @@ export const parse = ( args._.push(arg) } + // If a cert was provided a key must also be provided. + if (args.cert && args.cert.value && !args["cert-key"]) { + throw new Error("--cert-key is missing") + } + logger.debug("parsed command line", field("args", args)) return args } -export async function setDefaults(args: Args): Promise { - args = { ...args } +export interface DefaultedArgs extends ConfigArgs { + auth: AuthType + cert?: { + value: string + } + host: string + port: number + "proxy-domain": string[] + verbose: boolean + usingEnvPassword: boolean + "extensions-dir": string + "user-data-dir": string +} + +/** + * Take CLI and config arguments (optional) and return a single set of arguments + * with the defaults set. Arguments from the CLI are prioritized over config + * arguments. + */ +export async function setDefaults(cliArgs: Args, configArgs?: ConfigArgs): Promise { + const args = Object.assign({}, configArgs || {}, cliArgs) if (!args["user-data-dir"]) { await copyOldMacOSDataDir() @@ -381,7 +405,52 @@ export async function setDefaults(args: Args): Promise { break } - return args + // Default to using a password. + if (!args.auth) { + args.auth = AuthType.Password + } + + const [host, port] = bindAddrFromAllSources(args, configArgs || { _: [] }) + args.host = host + args.port = port + + // If we're being exposed to the cloud, we listen on a random address and + // disable auth. + if (args.link) { + args.host = "localhost" + args.port = 0 + args.socket = undefined + args.cert = undefined + + if (args.auth !== AuthType.None) { + args.auth = AuthType.None + } + } + + if (args.cert && !args.cert.value) { + const { cert, certKey } = await generateCertificate() + args.cert = { + value: cert, + } + args["cert-key"] = certKey + } + + const usingEnvPassword = !!process.env.PASSWORD + if (process.env.PASSWORD) { + args.password = process.env.PASSWORD + } + + // Ensure it's not readable by child processes. + delete process.env.PASSWORD + + // Filter duplicate proxy domains and remove any leading `*.`. + const proxyDomains = new Set((args["proxy-domain"] || []).map((d) => d.replace(/^\*\./, ""))) + args["proxy-domain"] = Array.from(proxyDomains) + + return { + ...args, + usingEnvPassword, + } as DefaultedArgs // TODO: Technically no guarantee this is fulfilled. } async function defaultConfigFile(): Promise { @@ -392,12 +461,16 @@ cert: false ` } +interface ConfigArgs extends Args { + config: string +} + /** * Reads the code-server yaml config file and returns it as Args. * * @param configPath Read the config from configPath instead of $CODE_SERVER_CONFIG or the default. */ -export async function readConfigFile(configPath?: string): Promise { +export async function readConfigFile(configPath?: string): Promise { if (!configPath) { configPath = process.env.CODE_SERVER_CONFIG if (!configPath) { @@ -466,7 +539,7 @@ function bindAddrFromArgs(addr: Addr, args: Args): Addr { return addr } -export function bindAddrFromAllSources(cliArgs: Args, configArgs: Args): [string, number] { +function bindAddrFromAllSources(cliArgs: Args, configArgs: Args): [string, number] { let addr: Addr = { host: "localhost", port: 8080, diff --git a/src/node/entry.ts b/src/node/entry.ts index 96db046e2..826d9a484 100644 --- a/src/node/entry.ts +++ b/src/node/entry.ts @@ -12,8 +12,7 @@ import { StaticHttpProvider } from "./app/static" import { UpdateHttpProvider } from "./app/update" import { VscodeHttpProvider } from "./app/vscode" import { - Args, - bindAddrFromAllSources, + DefaultedArgs, optionDescriptions, parse, readConfigFile, @@ -24,7 +23,7 @@ import { import { coderCloudBind } from "./coder-cloud" import { AuthType, HttpServer, HttpServerOptions } from "./http" import { loadPlugins } from "./plugin" -import { generateCertificate, hash, humanPath, open } from "./util" +import { hash, humanPath, open } from "./util" import { ipcMain, WrapperProcess } from "./wrapper" let pkg: { version?: string; commit?: string } = {} @@ -37,7 +36,7 @@ try { const version = pkg.version || "development" const commit = pkg.commit || "development" -export const runVsCodeCli = (args: Args): void => { +export const runVsCodeCli = (args: DefaultedArgs): void => { logger.debug("forking vs code cli...") const vscode = cp.fork(path.resolve(__dirname, "../../lib/vscode/out/vs/server/fork"), [], { env: { @@ -61,7 +60,7 @@ export const runVsCodeCli = (args: Args): void => { vscode.on("exit", (code) => process.exit(code || 0)) } -export const openInExistingInstance = async (args: Args, socketPath: string): Promise => { +export const openInExistingInstance = async (args: DefaultedArgs, socketPath: string): Promise => { const pipeArgs: OpenCommandPipeArgs & { fileURIs: string[] } = { type: "open", folderURIs: [], @@ -117,54 +116,26 @@ export const openInExistingInstance = async (args: Args, socketPath: string): Pr vscode.end() } -const main = async (args: Args, configArgs: Args): Promise => { - if (args.link) { - // If we're being exposed to the cloud, we listen on a random address and disable auth. - args = { - ...args, - host: "localhost", - port: 0, - auth: AuthType.None, - socket: undefined, - cert: undefined, - } - logger.info("link: disabling auth and listening on random localhost port for cloud agent") - } - - if (!args.auth) { - args = { - ...args, - auth: AuthType.Password, - } - } - +const main = async (args: DefaultedArgs): Promise => { logger.info(`Using user-data-dir ${humanPath(args["user-data-dir"])}`) - logger.trace(`Using extensions-dir ${humanPath(args["extensions-dir"])}`) - const envPassword = !!process.env.PASSWORD - const password = args.auth === AuthType.Password && (process.env.PASSWORD || args.password) - if (args.auth === AuthType.Password && !password) { + if (args.auth === AuthType.Password && !args.password) { throw new Error("Please pass in a password via the config file or $PASSWORD") } - const [host, port] = bindAddrFromAllSources(args, configArgs) // Spawn the main HTTP server. const options: HttpServerOptions = { auth: args.auth, commit, - host: host, + host: args.host, // The hash does not add any actual security but we do it for obfuscation purposes. - password: password ? hash(password) : undefined, - port: port, + password: args.password ? hash(args.password) : undefined, + port: args.port, proxyDomains: args["proxy-domain"], socket: args.socket, - ...(args.cert && !args.cert.value - ? await generateCertificate() - : { - cert: args.cert && args.cert.value, - certKey: args["cert-key"], - }), + cert: args.cert && args.cert.value, + certKey: args["cert-key"], } if (options.cert && !options.certKey) { @@ -175,7 +146,7 @@ const main = async (args: Args, configArgs: Args): Promise => { httpServer.registerHttpProvider(["/", "/vscode"], VscodeHttpProvider, args) httpServer.registerHttpProvider("/update", UpdateHttpProvider, false) httpServer.registerHttpProvider("/proxy", ProxyHttpProvider) - httpServer.registerHttpProvider("/login", LoginHttpProvider, args.config!, envPassword) + httpServer.registerHttpProvider("/login", LoginHttpProvider, args.config!, args.usingEnvPassword) httpServer.registerHttpProvider("/static", StaticHttpProvider) httpServer.registerHttpProvider("/healthz", HealthHttpProvider, httpServer.heart) @@ -191,19 +162,18 @@ const main = async (args: Args, configArgs: Args): Promise => { logger.info(`Using config file ${humanPath(args.config)}`) const serverAddress = await httpServer.listen() - logger.info(`HTTP server listening on ${serverAddress}`) + logger.info(`HTTP server listening on ${serverAddress} ${args.link ? "(randomized by --link)" : ""}`) if (args.auth === AuthType.Password) { - if (envPassword) { + if (args.usingEnvPassword) { logger.info(" - Using password from $PASSWORD") } else { logger.info(` - Using password from ${humanPath(args.config)}`) } logger.info(" - To disable use `--auth none`") } else { - logger.info(" - No authentication") + logger.info(` - No authentication ${args.link ? "(disabled by --link)" : ""}`) } - delete process.env.PASSWORD if (httpServer.protocol === "https") { logger.info( @@ -215,9 +185,19 @@ const main = async (args: Args, configArgs: Args): Promise => { logger.info(" - Not serving HTTPS") } - if (httpServer.proxyDomains.size > 0) { - logger.info(` - ${plural(httpServer.proxyDomains.size, "Proxying the following domain")}:`) - httpServer.proxyDomains.forEach((domain) => logger.info(` - *.${domain}`)) + if (args["proxy-domain"].length > 0) { + logger.info(` - ${plural(args["proxy-domain"].length, "Proxying the following domain")}:`) + args["proxy-domain"].forEach((domain) => logger.info(` - *.${domain}`)) + } + + if (args.link) { + try { + await coderCloudBind(serverAddress!, args.link.value) + logger.info(" - Connected to cloud agent") + } catch (err) { + logger.error(err.message) + ipcMain.exit(1) + } } if (serverAddress && !options.socket && args.open) { @@ -228,23 +208,12 @@ const main = async (args: Args, configArgs: Args): Promise => { }) logger.info(`Opened ${openAddress}`) } - - if (args.link) { - try { - await coderCloudBind(serverAddress!, args.link.value) - } catch (err) { - logger.error(err.message) - ipcMain.exit(1) - } - } } async function entry(): Promise { const cliArgs = parse(process.argv.slice(2)) const configArgs = await readConfigFile(cliArgs.config) - // This prioritizes the flags set in args over the ones in the config file. - let args = Object.assign(configArgs, cliArgs) - args = await setDefaults(args) + const args = await setDefaults(cliArgs, configArgs) // There's no need to check flags like --help or to spawn in an existing // instance for the child process because these would have already happened in @@ -252,7 +221,7 @@ async function entry(): Promise { if (ipcMain.isChild) { await ipcMain.handshake() ipcMain.preventExit() - return main(args, configArgs) + return main(args) } if (args.help) { diff --git a/src/node/http.ts b/src/node/http.ts index c616c8837..93d6e725c 100644 --- a/src/node/http.ts +++ b/src/node/http.ts @@ -130,7 +130,7 @@ export interface HttpServerOptions { readonly host?: string readonly password?: string readonly port?: number - readonly proxyDomains?: string[] + readonly proxyDomains: string[] readonly socket?: string } @@ -463,18 +463,12 @@ export class HttpServer { public readonly heart: Heart private readonly socketProvider = new SocketProxyProvider() - /** - * Proxy domains are stored here without the leading `*.` - */ - public readonly proxyDomains: Set - /** * Provides the actual proxying functionality. */ private readonly proxy = proxy.createProxyServer({}) public constructor(private readonly options: HttpServerOptions) { - this.proxyDomains = new Set((options.proxyDomains || []).map((d) => d.replace(/^\*\./, ""))) this.heart = new Heart(path.join(paths.data, "heartbeat"), async () => { const connections = await this.getConnections() logger.trace(plural(connections, `${connections} active connection`)) @@ -892,7 +886,7 @@ export class HttpServer { return undefined } - this.proxyDomains.forEach((domain) => { + this.options.proxyDomains.forEach((domain) => { if (host.endsWith(domain) && domain.length < host.length) { host = domain } @@ -922,7 +916,7 @@ export class HttpServer { // There must be an exact match. const port = parts.shift() const proxyDomain = parts.join(".") - if (!port || !this.proxyDomains.has(proxyDomain)) { + if (!port || !this.options.proxyDomains.includes(proxyDomain)) { return undefined } diff --git a/test/cli.test.ts b/test/cli.test.ts index ae5256142..a59aac995 100644 --- a/test/cli.test.ts +++ b/test/cli.test.ts @@ -14,17 +14,23 @@ type Mutable = { describe("parser", () => { beforeEach(() => { delete process.env.LOG_LEVEL + delete process.env.PASSWORD }) // The parser should not set any defaults so the caller can determine what // values the user actually set. These are only set after explicitly calling // `setDefaults`. const defaults = { + auth: "password", + host: "localhost", + port: 8080, + "proxy-domain": [], + usingEnvPassword: false, "extensions-dir": path.join(paths.data, "extensions"), "user-data-dir": paths.data, } - it("should set defaults", () => { + it("should parse nothing", () => { assert.deepEqual(parse([]), { _: [] }) }) @@ -232,6 +238,71 @@ describe("parser", () => { "proxy-domain": ["*.coder.com", "test.com"], }) }) + + it("should enforce cert-key with cert value or otherwise generate one", async () => { + const args = parse(["--cert"]) + assert.deepEqual(args, { + _: [], + cert: { + value: undefined, + }, + }) + assert.throws(() => parse(["--cert", "test"]), /--cert-key is missing/) + assert.deepEqual(await setDefaults(args), { + _: [], + ...defaults, + cert: { + value: path.join(tmpdir, "self-signed.cert"), + }, + "cert-key": path.join(tmpdir, "self-signed.key"), + }) + }) + + it("should override with --link", async () => { + const args = parse("--cert test --cert-key test --socket test --host 0.0.0.0 --port 8888 --link test".split(" ")) + assert.deepEqual(await setDefaults(args), { + _: [], + ...defaults, + auth: "none", + host: "localhost", + link: { + value: "test", + }, + port: 0, + cert: undefined, + "cert-key": path.resolve("test"), + socket: undefined, + }) + }) + + it("should use env var password", async () => { + process.env.PASSWORD = "test" + const args = parse([]) + assert.deepEqual(args, { + _: [], + }) + + assert.deepEqual(await setDefaults(args), { + ...defaults, + _: [], + password: "test", + usingEnvPassword: true, + }) + }) + + it("should filter proxy domains", async () => { + const args = parse(["--proxy-domain", "*.coder.com", "--proxy-domain", "coder.com", "--proxy-domain", "coder.org"]) + assert.deepEqual(args, { + _: [], + "proxy-domain": ["*.coder.com", "coder.com", "coder.org"], + }) + + assert.deepEqual(await setDefaults(args), { + ...defaults, + _: [], + "proxy-domain": ["coder.com", "coder.org"], + }) + }) }) describe("cli", () => { From 2928d362fa7e40ba2cbeecb57275a296b1903ca3 Mon Sep 17 00:00:00 2001 From: Asher Date: Thu, 15 Oct 2020 17:00:21 -0500 Subject: [PATCH 085/247] Move heart and AuthType out of http This file is going to get blasted in favor of Express. --- src/node/app/health.ts | 3 ++- src/node/app/login.ts | 3 ++- src/node/cli.ts | 6 ++++- src/node/heart.ts | 46 +++++++++++++++++++++++++++++++++++++ src/node/http.ts | 51 ++---------------------------------------- test/update.test.ts | 5 ++--- 6 files changed, 59 insertions(+), 55 deletions(-) create mode 100644 src/node/heart.ts diff --git a/src/node/app/health.ts b/src/node/app/health.ts index 48d6897cf..4e505f57e 100644 --- a/src/node/app/health.ts +++ b/src/node/app/health.ts @@ -1,4 +1,5 @@ -import { HttpProvider, HttpResponse, Heart, HttpProviderOptions } from "../http" +import { Heart } from "../heart" +import { HttpProvider, HttpProviderOptions, HttpResponse } from "../http" /** * Check the heartbeat. diff --git a/src/node/app/login.ts b/src/node/app/login.ts index 0fe1e0b6e..58bd55e0a 100644 --- a/src/node/app/login.ts +++ b/src/node/app/login.ts @@ -2,7 +2,8 @@ import * as http from "http" import * as limiter from "limiter" import * as querystring from "querystring" import { HttpCode, HttpError } from "../../common/http" -import { AuthType, HttpProvider, HttpProviderOptions, HttpResponse, Route } from "../http" +import { AuthType } from "../cli" +import { HttpProvider, HttpProviderOptions, HttpResponse, Route } from "../http" import { hash, humanPath } from "../util" interface LoginPayload { diff --git a/src/node/cli.ts b/src/node/cli.ts index ba8f5ae88..7f44f9ea5 100644 --- a/src/node/cli.ts +++ b/src/node/cli.ts @@ -4,9 +4,13 @@ import yaml from "js-yaml" import * as os from "os" import * as path from "path" import { Args as VsArgs } from "../../lib/vscode/src/vs/server/ipc" -import { AuthType } from "./http" import { canConnect, generateCertificate, generatePassword, humanPath, paths } from "./util" +export enum AuthType { + Password = "password", + None = "none", +} + export class Optional { public constructor(public readonly value?: T) {} } diff --git a/src/node/heart.ts b/src/node/heart.ts new file mode 100644 index 000000000..5198e33d8 --- /dev/null +++ b/src/node/heart.ts @@ -0,0 +1,46 @@ +import { logger } from "@coder/logger" +import { promises as fs } from "fs" + +/** + * Provides a heartbeat using a local file to indicate activity. + */ +export class Heart { + private heartbeatTimer?: NodeJS.Timeout + private heartbeatInterval = 60000 + public lastHeartbeat = 0 + + public constructor(private readonly heartbeatPath: string, private readonly isActive: () => Promise) {} + + public alive(): boolean { + const now = Date.now() + return now - this.lastHeartbeat < this.heartbeatInterval + } + /** + * Write to the heartbeat file if we haven't already done so within the + * timeout and start or reset a timer that keeps running as long as there is + * activity. Failures are logged as warnings. + */ + public beat(): void { + if (!this.alive()) { + logger.trace("heartbeat") + fs.writeFile(this.heartbeatPath, "").catch((error) => { + logger.warn(error.message) + }) + this.lastHeartbeat = Date.now() + if (typeof this.heartbeatTimer !== "undefined") { + clearTimeout(this.heartbeatTimer) + } + this.heartbeatTimer = setTimeout(() => { + this.isActive() + .then((active) => { + if (active) { + this.beat() + } + }) + .catch((error) => { + logger.warn(error.message) + }) + }, this.heartbeatInterval) + } + } +} diff --git a/src/node/http.ts b/src/node/http.ts index 93d6e725c..4aa6dc067 100644 --- a/src/node/http.ts +++ b/src/node/http.ts @@ -13,6 +13,8 @@ import * as tls from "tls" import * as url from "url" import { HttpCode, HttpError } from "../common/http" import { arrayify, normalize, Options, plural, split, trimSlashes } from "../common/util" +import { AuthType } from "./cli" +import { Heart } from "./heart" import { SocketProxyProvider } from "./socket" import { getMediaMime, paths } from "./util" @@ -27,11 +29,6 @@ interface AuthPayload extends Cookies { key?: string[] } -export enum AuthType { - Password = "password", - None = "none", -} - export type Query = { [key: string]: string | string[] | undefined } export interface ProxyOptions { @@ -390,50 +387,6 @@ export abstract class HttpProvider { } } -/** - * Provides a heartbeat using a local file to indicate activity. - */ -export class Heart { - private heartbeatTimer?: NodeJS.Timeout - private heartbeatInterval = 60000 - public lastHeartbeat = 0 - - public constructor(private readonly heartbeatPath: string, private readonly isActive: () => Promise) {} - - public alive(): boolean { - const now = Date.now() - return now - this.lastHeartbeat < this.heartbeatInterval - } - /** - * Write to the heartbeat file if we haven't already done so within the - * timeout and start or reset a timer that keeps running as long as there is - * activity. Failures are logged as warnings. - */ - public beat(): void { - if (!this.alive()) { - logger.trace("heartbeat") - fs.outputFile(this.heartbeatPath, "").catch((error) => { - logger.warn(error.message) - }) - this.lastHeartbeat = Date.now() - if (typeof this.heartbeatTimer !== "undefined") { - clearTimeout(this.heartbeatTimer) - } - this.heartbeatTimer = setTimeout(() => { - this.isActive() - .then((active) => { - if (active) { - this.beat() - } - }) - .catch((error) => { - logger.warn(error.message) - }) - }, this.heartbeatInterval) - } - } -} - export interface HttpProvider0 { new (options: HttpProviderOptions): T } diff --git a/test/update.test.ts b/test/update.test.ts index 7e4b80f21..9e27eefaf 100644 --- a/test/update.test.ts +++ b/test/update.test.ts @@ -3,12 +3,11 @@ import * as fs from "fs-extra" import * as http from "http" import * as path from "path" import { LatestResponse, UpdateHttpProvider } from "../src/node/app/update" -import { AuthType } from "../src/node/http" +import { AuthType } from "../src/node/cli" import { SettingsProvider, UpdateSettings } from "../src/node/settings" import { tmpdir } from "../src/node/util" -describe("update", () => { - return +describe.skip("update", () => { let version = "1.0.0" let spy: string[] = [] const server = http.createServer((request: http.IncomingMessage, response: http.ServerResponse) => { From 6000e389bc6778f86f1ef1c18c2ff4d8362f27ec Mon Sep 17 00:00:00 2001 From: Asher Date: Thu, 15 Oct 2020 17:05:55 -0500 Subject: [PATCH 086/247] Add Express as a dep and regenerate lockfile The Express types were throwing errors but regenerating the lockfile resolved them. --- package.json | 2 + yarn.lock | 1034 ++++++++++++++++++++++++++++++++------------------ 2 files changed, 675 insertions(+), 361 deletions(-) diff --git a/package.json b/package.json index cc3edd30a..e6197065f 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ }, "main": "out/node/entry.js", "devDependencies": { + "@types/express": "^4.17.8", "@types/fs-extra": "^8.0.1", "@types/http-proxy": "^1.17.4", "@types/js-yaml": "^3.12.3", @@ -67,6 +68,7 @@ "dependencies": { "@coder/logger": "1.1.16", "env-paths": "^2.2.0", + "express": "^4.17.1", "fs-extra": "^9.0.1", "http-proxy": "^1.18.0", "httpolyglot": "^0.1.2", diff --git a/yarn.lock b/yarn.lock index 6f388626b..d1224a32f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9,28 +9,24 @@ dependencies: "@babel/highlight" "^7.10.4" -"@babel/compat-data@^7.10.4", "@babel/compat-data@^7.11.0": - version "7.11.0" - resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.11.0.tgz#e9f73efe09af1355b723a7f39b11bad637d7c99c" - integrity sha512-TPSvJfv73ng0pfnEOh17bYMPQbI95+nGWc71Ss4vZdRBHTDqmM9Z8ZV4rYz8Ks7sfzc95n30k6ODIq5UGnXcYQ== - dependencies: - browserslist "^4.12.0" - invariant "^2.2.4" - semver "^5.5.0" +"@babel/compat-data@^7.12.0": + version "7.12.0" + resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.12.0.tgz#443aea07a5aeba7942cb067de6b8272f2ab36b9e" + integrity sha512-jAbCtMANC9ptXxbSVXIqV/3H0bkh7iyyv6JS5lu10av45bcc2QmDNJXkASZCFwbBt75Q0AEq/BB+bNa3x1QgYQ== "@babel/core@>=7.9.0", "@babel/core@^7.4.4": - version "7.11.4" - resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.11.4.tgz#4301dfdfafa01eeb97f1896c5501a3f0655d4229" - integrity sha512-5deljj5HlqRXN+5oJTY7Zs37iH3z3b++KjiKtIsJy1NrjOOVSEaJHEetLBhyu0aQOSNNZ/0IuEAan9GzRuDXHg== + version "7.12.0" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.12.0.tgz#e42e07a086e978cdd4c61f4078d8230fb817cc86" + integrity sha512-iV7Gwg0DePKvdDZZWRTkj4MW+6/AbVWd4ZCg+zk8H1RVt5xBpUZS6vLQWwb3pyLg4BFTaGiQCPoJ4Ibmbne4fA== dependencies: "@babel/code-frame" "^7.10.4" - "@babel/generator" "^7.11.4" - "@babel/helper-module-transforms" "^7.11.0" + "@babel/generator" "^7.12.0" + "@babel/helper-module-transforms" "^7.12.0" "@babel/helpers" "^7.10.4" - "@babel/parser" "^7.11.4" + "@babel/parser" "^7.12.0" "@babel/template" "^7.10.4" - "@babel/traverse" "^7.11.0" - "@babel/types" "^7.11.0" + "@babel/traverse" "^7.12.0" + "@babel/types" "^7.12.0" convert-source-map "^1.7.0" debug "^4.1.0" gensync "^1.0.0-beta.1" @@ -40,12 +36,12 @@ semver "^5.4.1" source-map "^0.5.0" -"@babel/generator@^7.11.0", "@babel/generator@^7.11.4", "@babel/generator@^7.4.4": - version "7.11.4" - resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.11.4.tgz#1ec7eec00defba5d6f83e50e3ee72ae2fee482be" - integrity sha512-Rn26vueFx0eOoz7iifCN2UHT6rGtnkSGWSoDRIy8jZN3B91PzeSULbswfLoOWuTuAcNwpG/mxy+uCTDnZ9Mp1g== +"@babel/generator@^7.12.0", "@babel/generator@^7.4.4": + version "7.12.0" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.12.0.tgz#91a45f1c18ca8d895a35a04da1a4cf7ea3f37f98" + integrity sha512-8lnf4QcyiQMf5XQp47BltuMTocsOh6P0z/vueEh8GzhmWWlDbdvOoI5Ziddg0XYhmnx35HyByUW51/9NprF8cA== dependencies: - "@babel/types" "^7.11.0" + "@babel/types" "^7.12.0" jsesc "^2.5.1" source-map "^0.5.0" @@ -65,13 +61,13 @@ "@babel/types" "^7.10.4" "@babel/helper-builder-react-jsx-experimental@^7.10.4": - version "7.10.5" - resolved "https://registry.yarnpkg.com/@babel/helper-builder-react-jsx-experimental/-/helper-builder-react-jsx-experimental-7.10.5.tgz#f35e956a19955ff08c1258e44a515a6d6248646b" - integrity sha512-Buewnx6M4ttG+NLkKyt7baQn7ScC/Td+e99G914fRU8fGIUivDDgVIQeDHFa5e4CRSJQt58WpNHhsAZgtzVhsg== + version "7.12.0" + resolved "https://registry.yarnpkg.com/@babel/helper-builder-react-jsx-experimental/-/helper-builder-react-jsx-experimental-7.12.0.tgz#e8655888d0d36fd2a15c02decf77923fc18e95cd" + integrity sha512-AFzu6ib4i56olCtulkbIifcTay0O5tv8ZVK8hZMzrpu+YjsIDEcesF1DMqqTzV65clu3X61aE7qeHcJsY/gmnA== dependencies: "@babel/helper-annotate-as-pure" "^7.10.4" "@babel/helper-module-imports" "^7.10.4" - "@babel/types" "^7.10.5" + "@babel/types" "^7.12.0" "@babel/helper-builder-react-jsx@^7.10.4": version "7.10.4" @@ -81,37 +77,36 @@ "@babel/helper-annotate-as-pure" "^7.10.4" "@babel/types" "^7.10.4" -"@babel/helper-compilation-targets@^7.10.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.10.4.tgz#804ae8e3f04376607cc791b9d47d540276332bd2" - integrity sha512-a3rYhlsGV0UHNDvrtOXBg8/OpfV0OKTkxKPzIplS1zpx7CygDcWWxckxZeDd3gzPzC4kUT0A4nVFDK0wGMh4MQ== +"@babel/helper-compilation-targets@^7.12.0": + version "7.12.0" + resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.12.0.tgz#c477d89a1f4d626c8149b9b88802f78d66d0c99a" + integrity sha512-NbDFJNjDgxE7IkrHp5gq2+Tr8bEdCLKYN90YDQEjMiTMUAFAcShNkaH8kydcmU0mEQTiQY0Ydy/+1xfS2OCEnw== dependencies: - "@babel/compat-data" "^7.10.4" + "@babel/compat-data" "^7.12.0" + "@babel/helper-validator-option" "^7.12.0" browserslist "^4.12.0" - invariant "^2.2.4" - levenary "^1.1.1" semver "^5.5.0" "@babel/helper-create-class-features-plugin@^7.10.4": - version "7.10.5" - resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.10.5.tgz#9f61446ba80e8240b0a5c85c6fdac8459d6f259d" - integrity sha512-0nkdeijB7VlZoLT3r/mY3bUkw3T8WG/hNw+FATs/6+pG2039IJWjTYL0VTISqsNHMUTEnwbVnc89WIJX9Qed0A== + version "7.12.0" + resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.12.0.tgz#f3f2fc77bacc89e59ce6764daeabc1fb23e79a05" + integrity sha512-9tD1r9RK928vxvxcoNK8/7uwT7Q2DJZP1dnJmyMAJPwOF0yr8PPwqdpyw33lUpCfrJ765bOs5XNa4KSfUDWFSw== dependencies: "@babel/helper-function-name" "^7.10.4" - "@babel/helper-member-expression-to-functions" "^7.10.5" + "@babel/helper-member-expression-to-functions" "^7.12.0" "@babel/helper-optimise-call-expression" "^7.10.4" "@babel/helper-plugin-utils" "^7.10.4" - "@babel/helper-replace-supers" "^7.10.4" + "@babel/helper-replace-supers" "^7.12.0" "@babel/helper-split-export-declaration" "^7.10.4" "@babel/helper-create-regexp-features-plugin@^7.10.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.10.4.tgz#fdd60d88524659a0b6959c0579925e425714f3b8" - integrity sha512-2/hu58IEPKeoLF45DBwx3XFqsbCXmkdAay4spVr2x0jYgRxrSNp+ePwvSsy9g6YSaNDcKIQVPXk1Ov8S2edk2g== + version "7.12.0" + resolved "https://registry.yarnpkg.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.12.0.tgz#858cef57039f3b3a9012273597288a71e1dff8ca" + integrity sha512-YBqH+3wLcom+tko8/JLgRcG8DMqORgmjqNRNI751gTioJSZHWFybO1mRoLtJtWIlYSHY+zT9LqqnbbK1c3KIVQ== dependencies: "@babel/helper-annotate-as-pure" "^7.10.4" "@babel/helper-regex" "^7.10.4" - regexpu-core "^4.7.0" + regexpu-core "^4.7.1" "@babel/helper-define-map@^7.10.4": version "7.10.5" @@ -152,12 +147,12 @@ dependencies: "@babel/types" "^7.10.4" -"@babel/helper-member-expression-to-functions@^7.10.4", "@babel/helper-member-expression-to-functions@^7.10.5": - version "7.11.0" - resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.11.0.tgz#ae69c83d84ee82f4b42f96e2a09410935a8f26df" - integrity sha512-JbFlKHFntRV5qKw3YC0CvQnDZ4XMwgzzBbld7Ly4Mj4cbFy3KywcR8NtNctRToMWJOVvLINJv525Gd6wwVEx/Q== +"@babel/helper-member-expression-to-functions@^7.12.0": + version "7.12.0" + resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.12.0.tgz#48f605fa801764f3e5b2e301e49d35fe1820c4f3" + integrity sha512-I0d/bgzgzgLsJMk7UZ0TN2KV3OGjC/t/9Saz8PKb9jrcEAXhgjGysOgp4PDKydIKjUv/gj2St4ae+ov8l+T9Xg== dependencies: - "@babel/types" "^7.11.0" + "@babel/types" "^7.12.0" "@babel/helper-module-imports@^7.10.4": version "7.10.4" @@ -166,17 +161,19 @@ dependencies: "@babel/types" "^7.10.4" -"@babel/helper-module-transforms@^7.10.4", "@babel/helper-module-transforms@^7.10.5", "@babel/helper-module-transforms@^7.11.0": - version "7.11.0" - resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.11.0.tgz#b16f250229e47211abdd84b34b64737c2ab2d359" - integrity sha512-02EVu8COMuTRO1TAzdMtpBPbe6aQ1w/8fePD2YgQmxZU4gpNWaL9gK3Jp7dxlkUlUCJOTaSeA+Hrm1BRQwqIhg== +"@babel/helper-module-transforms@^7.10.4", "@babel/helper-module-transforms@^7.10.5", "@babel/helper-module-transforms@^7.12.0": + version "7.12.0" + resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.12.0.tgz#8ac7d9e8716f94549a42e577c5429391950e33f3" + integrity sha512-1ZTMoCiLSzTJLbq7mSaTHki4oIrBIf/dUbzdhwTrvtMU3ZNVKwQmGae3gSiqppo7G8HAgnXmc43rfEaD8yYLLQ== dependencies: "@babel/helper-module-imports" "^7.10.4" - "@babel/helper-replace-supers" "^7.10.4" + "@babel/helper-replace-supers" "^7.12.0" "@babel/helper-simple-access" "^7.10.4" "@babel/helper-split-export-declaration" "^7.11.0" + "@babel/helper-validator-identifier" "^7.10.4" "@babel/template" "^7.10.4" - "@babel/types" "^7.11.0" + "@babel/traverse" "^7.12.0" + "@babel/types" "^7.12.0" lodash "^4.17.19" "@babel/helper-optimise-call-expression@^7.10.4": @@ -208,15 +205,15 @@ "@babel/template" "^7.10.4" "@babel/types" "^7.10.4" -"@babel/helper-replace-supers@^7.10.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.10.4.tgz#d585cd9388ea06e6031e4cd44b6713cbead9e6cf" - integrity sha512-sPxZfFXocEymYTdVK1UNmFPBN+Hv5mJkLPsYWwGBxZAxaWfFu+xqp7b6qWD0yjNuNL2VKc6L5M18tOXUP7NU0A== +"@babel/helper-replace-supers@^7.10.4", "@babel/helper-replace-supers@^7.12.0": + version "7.12.0" + resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.12.0.tgz#98d3f3eb779752e59c7422ab387c9b444323be60" + integrity sha512-9kycFdq2c9e7PXZOr2z/ZqTFF9OzFu287iFwYS+CiDVPuoTCfY8hoTsIqNQNetQjlqoRsRyJFrMG1uhGAR4EEw== dependencies: - "@babel/helper-member-expression-to-functions" "^7.10.4" + "@babel/helper-member-expression-to-functions" "^7.12.0" "@babel/helper-optimise-call-expression" "^7.10.4" - "@babel/traverse" "^7.10.4" - "@babel/types" "^7.10.4" + "@babel/traverse" "^7.12.0" + "@babel/types" "^7.12.0" "@babel/helper-simple-access@^7.10.4": version "7.10.4" @@ -245,6 +242,11 @@ resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz#a78c7a7251e01f616512d31b10adcf52ada5e0d2" integrity sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw== +"@babel/helper-validator-option@^7.12.0": + version "7.12.0" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.12.0.tgz#1d1fc48a9b69763da61b892774b0df89aee1c969" + integrity sha512-NRfKaAQw/JCMsTFUdJI6cp4MoJGGVBRQTRSiW1nwlGldNqzjB9jqWI0SZqQksC724dJoKqwG+QqfS9ib7SoVsw== + "@babel/helper-wrap-function@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/helper-wrap-function/-/helper-wrap-function-7.10.4.tgz#8a6f701eab0ff39f765b5a1cfef409990e624b87" @@ -273,10 +275,10 @@ chalk "^2.0.0" js-tokens "^4.0.0" -"@babel/parser@^7.10.4", "@babel/parser@^7.11.0", "@babel/parser@^7.11.4", "@babel/parser@^7.4.4": - version "7.11.4" - resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.11.4.tgz#6fa1a118b8b0d80d0267b719213dc947e88cc0ca" - integrity sha512-MggwidiH+E9j5Sh8pbrX5sJvMcsqS5o+7iB42M9/k0CD63MjYbdP4nhSh7uB5wnv2/RVzTZFTxzF/kIa5mrCqA== +"@babel/parser@^7.10.4", "@babel/parser@^7.12.0", "@babel/parser@^7.4.4": + version "7.12.0" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.12.0.tgz#2ad388f3960045b22f9b7d4bf85e80b15a1c9e3a" + integrity sha512-dYmySMYnlus2jwl7JnnajAj11obRStZoW9cG04wh4ZuhozDn11tDUrhHcUZ9iuNHqALAhh60XqNaYXpvuuE/Gg== "@babel/plugin-proposal-async-generator-functions@^7.10.4": version "7.10.5" @@ -303,10 +305,10 @@ "@babel/helper-plugin-utils" "^7.10.4" "@babel/plugin-syntax-dynamic-import" "^7.8.0" -"@babel/plugin-proposal-export-namespace-from@^7.10.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-export-namespace-from/-/plugin-proposal-export-namespace-from-7.10.4.tgz#570d883b91031637b3e2958eea3c438e62c05f54" - integrity sha512-aNdf0LY6/3WXkhh0Fdb6Zk9j1NMD8ovj3F6r0+3j837Pn1S1PdNtcwJ5EG9WkVPNHPxyJDaxMaAOVq4eki0qbg== +"@babel/plugin-proposal-export-namespace-from@^7.12.0": + version "7.12.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-export-namespace-from/-/plugin-proposal-export-namespace-from-7.12.0.tgz#08b0f8100bbae1199a5f5294f38a1b0b8d8402fc" + integrity sha512-ao43U2ptSe+mIZAQo2nBV5Wx2Ie3i2XbLt8jCXZpv+bvLY1Twv0lak4YZ1Ps5OwbeLMAl3iOVScgGMOImBae1g== dependencies: "@babel/helper-plugin-utils" "^7.10.4" "@babel/plugin-syntax-export-namespace-from" "^7.8.3" @@ -319,26 +321,26 @@ "@babel/helper-plugin-utils" "^7.10.4" "@babel/plugin-syntax-json-strings" "^7.8.0" -"@babel/plugin-proposal-logical-assignment-operators@^7.11.0": - version "7.11.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-logical-assignment-operators/-/plugin-proposal-logical-assignment-operators-7.11.0.tgz#9f80e482c03083c87125dee10026b58527ea20c8" - integrity sha512-/f8p4z+Auz0Uaf+i8Ekf1iM7wUNLcViFUGiPxKeXvxTSl63B875YPiVdUDdem7hREcI0E0kSpEhS8tF5RphK7Q== +"@babel/plugin-proposal-logical-assignment-operators@^7.12.0": + version "7.12.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-logical-assignment-operators/-/plugin-proposal-logical-assignment-operators-7.12.0.tgz#830d8ff4984d800b2824e8eac0005ecb7430328e" + integrity sha512-dssjXHzdMQal4q6GCSwDTVPEbyBLdd9+7aSlzAkQbrGEKq5xG8pvhQ7u2ktUrCLRmzQphZnSzILBL5ta4xSRlA== dependencies: "@babel/helper-plugin-utils" "^7.10.4" "@babel/plugin-syntax-logical-assignment-operators" "^7.10.4" -"@babel/plugin-proposal-nullish-coalescing-operator@^7.10.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.10.4.tgz#02a7e961fc32e6d5b2db0649e01bf80ddee7e04a" - integrity sha512-wq5n1M3ZUlHl9sqT2ok1T2/MTt6AXE0e1Lz4WzWBr95LsAZ5qDXe4KnFuauYyEyLiohvXFMdbsOTMyLZs91Zlw== +"@babel/plugin-proposal-nullish-coalescing-operator@^7.12.0": + version "7.12.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.12.0.tgz#d82174a531305df4d7079ce3782269b35b810b82" + integrity sha512-JpNWix2VP2ue31r72fKytTE13nPX1fxl1mudfTaTwcDhl3iExz5NZjQBq012b/BQ6URWoc/onI73pZdYlAfihg== dependencies: "@babel/helper-plugin-utils" "^7.10.4" "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.0" -"@babel/plugin-proposal-numeric-separator@^7.10.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.10.4.tgz#ce1590ff0a65ad12970a609d78855e9a4c1aef06" - integrity sha512-73/G7QoRoeNkLZFxsoCCvlg4ezE4eM+57PnOqgaPOozd5myfj7p0muD1mRVJvbUWbOzD+q3No2bWbaKy+DJ8DA== +"@babel/plugin-proposal-numeric-separator@^7.12.0": + version "7.12.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.12.0.tgz#76de244152abaf2e72800ab0aebc9771f6de3e9a" + integrity sha512-iON65YmIy/IpEgteYJ4HfO2q30SLdIxiyjNNlsSjSl0tUxLhSH9PljE5r6sczwdW64ZZzznYNcezdcROB+rDDw== dependencies: "@babel/helper-plugin-utils" "^7.10.4" "@babel/plugin-syntax-numeric-separator" "^7.10.4" @@ -360,10 +362,10 @@ "@babel/helper-plugin-utils" "^7.10.4" "@babel/plugin-syntax-optional-catch-binding" "^7.8.0" -"@babel/plugin-proposal-optional-chaining@^7.11.0": - version "7.11.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.11.0.tgz#de5866d0646f6afdaab8a566382fe3a221755076" - integrity sha512-v9fZIu3Y8562RRwhm1BbMRxtqZNFmFA2EG+pT2diuU8PT3H6T/KXoZ54KgYisfOFZHV6PfvAiBIZ9Rcz+/JCxA== +"@babel/plugin-proposal-optional-chaining@^7.12.0": + version "7.12.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.12.0.tgz#0159b549f165016fc9f284b8607a58a37a3b71fe" + integrity sha512-CXu9aw32FH/MksqdKvhpiH8pSvxnXJ33E7I7BGNE9VzNRpWgpNzvPpds/tW9E0pjmX9+D1zAHRyHbtyeTboo2g== dependencies: "@babel/helper-plugin-utils" "^7.10.4" "@babel/helper-skip-transparent-expression-wrappers" "^7.11.0" @@ -620,14 +622,15 @@ "@babel/helper-simple-access" "^7.10.4" babel-plugin-dynamic-import-node "^2.3.3" -"@babel/plugin-transform-modules-systemjs@^7.10.4": - version "7.10.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.10.5.tgz#6270099c854066681bae9e05f87e1b9cadbe8c85" - integrity sha512-f4RLO/OL14/FP1AEbcsWMzpbUz6tssRaeQg11RH1BP/XnPpRoVwgeYViMFacnkaw4k4wjRSjn3ip1Uw9TaXuMw== +"@babel/plugin-transform-modules-systemjs@^7.12.0": + version "7.12.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.12.0.tgz#bca842db6980cfc98ae7d0f2c907c9b1df3f874e" + integrity sha512-h2fDMnwRwBiNMmTGAWqUo404Z3oLbrPE6hyATecyIbsEsrbM5gjLbfKQLb6hjiouMlGHH+yliYBbc4NPgWKE/g== dependencies: "@babel/helper-hoist-variables" "^7.10.4" - "@babel/helper-module-transforms" "^7.10.5" + "@babel/helper-module-transforms" "^7.12.0" "@babel/helper-plugin-utils" "^7.10.4" + "@babel/helper-validator-identifier" "^7.10.4" babel-plugin-dynamic-import-node "^2.3.3" "@babel/plugin-transform-modules-umd@^7.10.4": @@ -753,25 +756,26 @@ "@babel/helper-plugin-utils" "^7.10.4" "@babel/preset-env@^7.4.4": - version "7.11.0" - resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.11.0.tgz#860ee38f2ce17ad60480c2021ba9689393efb796" - integrity sha512-2u1/k7rG/gTh02dylX2kL3S0IJNF+J6bfDSp4DI2Ma8QN6Y9x9pmAax59fsCk6QUQG0yqH47yJWA+u1I1LccAg== + version "7.12.0" + resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.12.0.tgz#7d2d0c4f4a14ca0fd7d905a741070ab4745177b7" + integrity sha512-jSIHvHSuF+hBUIrvA2/61yIzhH+ceLOXGLTH1nwPvQlso/lNxXsoE/nvrCzY5M77KRzhKegB1CvdhWPZmYDZ5A== dependencies: - "@babel/compat-data" "^7.11.0" - "@babel/helper-compilation-targets" "^7.10.4" + "@babel/compat-data" "^7.12.0" + "@babel/helper-compilation-targets" "^7.12.0" "@babel/helper-module-imports" "^7.10.4" "@babel/helper-plugin-utils" "^7.10.4" + "@babel/helper-validator-option" "^7.12.0" "@babel/plugin-proposal-async-generator-functions" "^7.10.4" "@babel/plugin-proposal-class-properties" "^7.10.4" "@babel/plugin-proposal-dynamic-import" "^7.10.4" - "@babel/plugin-proposal-export-namespace-from" "^7.10.4" + "@babel/plugin-proposal-export-namespace-from" "^7.12.0" "@babel/plugin-proposal-json-strings" "^7.10.4" - "@babel/plugin-proposal-logical-assignment-operators" "^7.11.0" - "@babel/plugin-proposal-nullish-coalescing-operator" "^7.10.4" - "@babel/plugin-proposal-numeric-separator" "^7.10.4" + "@babel/plugin-proposal-logical-assignment-operators" "^7.12.0" + "@babel/plugin-proposal-nullish-coalescing-operator" "^7.12.0" + "@babel/plugin-proposal-numeric-separator" "^7.12.0" "@babel/plugin-proposal-object-rest-spread" "^7.11.0" "@babel/plugin-proposal-optional-catch-binding" "^7.10.4" - "@babel/plugin-proposal-optional-chaining" "^7.11.0" + "@babel/plugin-proposal-optional-chaining" "^7.12.0" "@babel/plugin-proposal-private-methods" "^7.10.4" "@babel/plugin-proposal-unicode-property-regex" "^7.10.4" "@babel/plugin-syntax-async-generators" "^7.8.0" @@ -802,7 +806,7 @@ "@babel/plugin-transform-member-expression-literals" "^7.10.4" "@babel/plugin-transform-modules-amd" "^7.10.4" "@babel/plugin-transform-modules-commonjs" "^7.10.4" - "@babel/plugin-transform-modules-systemjs" "^7.10.4" + "@babel/plugin-transform-modules-systemjs" "^7.12.0" "@babel/plugin-transform-modules-umd" "^7.10.4" "@babel/plugin-transform-named-capturing-groups-regex" "^7.10.4" "@babel/plugin-transform-new-target" "^7.10.4" @@ -819,17 +823,15 @@ "@babel/plugin-transform-unicode-escapes" "^7.10.4" "@babel/plugin-transform-unicode-regex" "^7.10.4" "@babel/preset-modules" "^0.1.3" - "@babel/types" "^7.11.0" + "@babel/types" "^7.12.0" browserslist "^4.12.0" core-js-compat "^3.6.2" - invariant "^2.2.2" - levenary "^1.1.1" semver "^5.5.0" "@babel/preset-modules@^0.1.3": - version "0.1.3" - resolved "https://registry.yarnpkg.com/@babel/preset-modules/-/preset-modules-0.1.3.tgz#13242b53b5ef8c883c3cf7dddd55b36ce80fbc72" - integrity sha512-Ra3JXOHBq2xd56xSF7lMKXdjBn3T772Y1Wet3yWnkDly9zHvJki029tAFzvAAK5cf4YV3yoxuP61crYRol6SVg== + version "0.1.4" + resolved "https://registry.yarnpkg.com/@babel/preset-modules/-/preset-modules-0.1.4.tgz#362f2b68c662842970fdb5e254ffc8fc1c2e415e" + integrity sha512-J36NhwnfdzpmH41M1DrnkkgAqhZaqr/NBdPfQ677mLzlaXo+oDiv1deyCDtgAhz8p328otdob0Du7+xgHGZbKg== dependencies: "@babel/helper-plugin-utils" "^7.0.0" "@babel/plugin-proposal-unicode-property-regex" "^7.4.4" @@ -838,9 +840,9 @@ esutils "^2.0.2" "@babel/runtime@^7.4.4", "@babel/runtime@^7.8.4": - version "7.11.2" - resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.11.2.tgz#f549c13c754cc40b87644b9fa9f09a6a95fe0736" - integrity sha512-TeWkU52so0mPtDcaCTxNBI/IHiz0pZgr8VEFqXFtZWpYD08ZB6FaSwVAS8MKRQAP3bYKiVjwysOJgMFY28o6Tw== + version "7.12.0" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.12.0.tgz#98bd7666186969c04be893d747cf4a6c6c8fa6b0" + integrity sha512-lS4QLXQ2Vbw2ubfQjeQcn+BZgZ5+ROHW9f+DWjEp5Y+NHYmkRGKqHSJ1tuhbUauKu2nhZNTBIvsIQ8dXfY5Gjw== dependencies: regenerator-runtime "^0.13.4" @@ -853,25 +855,25 @@ "@babel/parser" "^7.10.4" "@babel/types" "^7.10.4" -"@babel/traverse@^7.10.4", "@babel/traverse@^7.11.0", "@babel/traverse@^7.4.4": - version "7.11.0" - resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.11.0.tgz#9b996ce1b98f53f7c3e4175115605d56ed07dd24" - integrity sha512-ZB2V+LskoWKNpMq6E5UUCrjtDUh5IOTAyIl0dTjIEoXum/iKWkoIEKIRDnUucO6f+2FzNkE0oD4RLKoPIufDtg== +"@babel/traverse@^7.10.4", "@babel/traverse@^7.12.0", "@babel/traverse@^7.4.4": + version "7.12.0" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.12.0.tgz#ed31953d6e708cdd34443de2fcdb55f72cdfb266" + integrity sha512-ZU9e79xpOukCNPkQ1UzR4gJKCruGckr6edd8v8lmKpSk8iakgUIvb+5ZtaKKV9f7O+x5r+xbMDDIbzVpUoiIuw== dependencies: "@babel/code-frame" "^7.10.4" - "@babel/generator" "^7.11.0" + "@babel/generator" "^7.12.0" "@babel/helper-function-name" "^7.10.4" "@babel/helper-split-export-declaration" "^7.11.0" - "@babel/parser" "^7.11.0" - "@babel/types" "^7.11.0" + "@babel/parser" "^7.12.0" + "@babel/types" "^7.12.0" debug "^4.1.0" globals "^11.1.0" lodash "^4.17.19" -"@babel/types@^7.10.4", "@babel/types@^7.10.5", "@babel/types@^7.11.0", "@babel/types@^7.4.4": - version "7.11.0" - resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.11.0.tgz#2ae6bf1ba9ae8c3c43824e5861269871b206e90d" - integrity sha512-O53yME4ZZI0jO1EVGtF1ePGl0LHirG4P1ibcD80XyzZcKhcMFeCXmh4Xb1ifGBIV233Qg12x4rBfQgA+tmOukA== +"@babel/types@^7.10.4", "@babel/types@^7.10.5", "@babel/types@^7.11.0", "@babel/types@^7.12.0", "@babel/types@^7.4.4": + version "7.12.0" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.12.0.tgz#b6b49f425ee59043fbc89c61b11a13d5eae7b5c6" + integrity sha512-ggIyFmT2zMaYRheOfPDQ4gz7QqV3B+t2rjqjbttDJxMcb7/LukvWCmlIl1sWcOxrvwpTDd+z0OytzqsbGeb3/g== dependencies: "@babel/helper-validator-identifier" "^7.10.4" lodash "^4.17.19" @@ -882,6 +884,22 @@ resolved "https://registry.yarnpkg.com/@coder/logger/-/logger-1.1.16.tgz#ee5b1b188f680733f35c11b065bbd139d618c1e1" integrity sha512-X6VB1++IkosYY6amRAiMvuvCf12NA4+ooX+gOuu5bJIkdjmh4Lz7QpJcWRdgxesvo1msriDDr9E/sDbIWf6vsQ== +"@eslint/eslintrc@^0.1.3": + version "0.1.3" + resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-0.1.3.tgz#7d1a2b2358552cc04834c0979bd4275362e37085" + integrity sha512-4YVwPkANLeNtRjMekzux1ci8hIaH5eGKktGqR0d3LWsKNn5B2X/1Z6Trxy7jQXl9EBGE6Yj02O+t09FMeRllaA== + dependencies: + ajv "^6.12.4" + debug "^4.1.1" + espree "^7.3.0" + globals "^12.1.0" + ignore "^4.0.6" + import-fresh "^3.2.1" + js-yaml "^3.13.1" + lodash "^4.17.19" + minimatch "^3.0.4" + strip-json-comments "^3.1.1" + "@iarna/toml@^2.2.0": version "2.2.5" resolved "https://registry.yarnpkg.com/@iarna/toml/-/toml-2.2.5.tgz#b32366c89b43c6f8cefbdefac778b9c828e3ba8c" @@ -962,7 +980,7 @@ "@parcel/utils" "^1.11.0" physical-cpu-count "^2.0.0" -"@stylelint/postcss-css-in-js@^0.37.1": +"@stylelint/postcss-css-in-js@^0.37.2": version "0.37.2" resolved "https://registry.yarnpkg.com/@stylelint/postcss-css-in-js/-/postcss-css-in-js-0.37.2.tgz#7e5a84ad181f4234a2480803422a47b8749af3d2" integrity sha512-nEhsFoJurt8oUmieT8qy4nk81WRHmJynmVwn/Vts08PL9fhgIsMhk1GId5yAN643OzqEEb5S/6At2TZW7pqPDA== @@ -995,10 +1013,20 @@ traverse "^0.6.6" unified "^6.1.6" -"@types/color-name@^1.1.1": - version "1.1.1" - resolved "https://registry.yarnpkg.com/@types/color-name/-/color-name-1.1.1.tgz#1c1261bbeaa10a8055bbc5d8ab84b7b2afc846a0" - integrity sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ== +"@types/body-parser@*": + version "1.19.0" + resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.0.tgz#0685b3c47eb3006ffed117cdd55164b61f80538f" + integrity sha512-W98JrE0j2K78swW4ukqMleo8R7h/pFETjM2DQ90MF6XK2i4LO4W3gQ71Lt4w3bfm2EvVSyWHplECvB5sK22yFQ== + dependencies: + "@types/connect" "*" + "@types/node" "*" + +"@types/connect@*": + version "3.4.33" + resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.33.tgz#31610c901eca573b8713c3330abc6e6b9f588546" + integrity sha512-2+FrkXY4zllzTNfJth7jOqEHC+enpLeGslEhpnTAkg21GkRrWV4SsAtqchtT4YS9/nODBU2/ZfsBY2X4J/dX7A== + dependencies: + "@types/node" "*" "@types/eslint-visitor-keys@^1.0.0": version "1.0.0" @@ -1006,14 +1034,24 @@ integrity sha512-OCutwjDZ4aFS6PB1UZ988C4YgwlBHJd6wCeQqaLdmadZ/7e+w79+hbMUFC1QXDNCmdyoRfAFdm0RypzwR+Qpag== "@types/express-serve-static-core@*": - version "4.17.9" - resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.17.9.tgz#2d7b34dcfd25ec663c25c85d76608f8b249667f1" - integrity sha512-DG0BYg6yO+ePW+XoDENYz8zhNGC3jDDEpComMYn7WJc4mY1Us8Rw9ax2YhJXxpyk2SF47PQAoQ0YyVT1a0bEkA== + version "4.17.13" + resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.17.13.tgz#d9af025e925fc8b089be37423b8d1eac781be084" + integrity sha512-RgDi5a4nuzam073lRGKTUIaL3eF2+H7LJvJ8eUnCI0wA6SNjXc44DCmWNiTLs/AZ7QlsFWZiw/gTG3nSQGL0fA== dependencies: "@types/node" "*" "@types/qs" "*" "@types/range-parser" "*" +"@types/express@^4.17.8": + version "4.17.8" + resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.8.tgz#3df4293293317e61c60137d273a2e96cd8d5f27a" + integrity sha512-wLhcKh3PMlyA2cNAB9sjM1BntnhPMiM0JOBwPBqttjHev2428MLEB4AYVN+d8s2iyCVZac+o41Pflm/ZH5vLXQ== + dependencies: + "@types/body-parser" "*" + "@types/express-serve-static-core" "*" + "@types/qs" "*" + "@types/serve-static" "*" + "@types/fs-extra@^8.0.1": version "8.1.1" resolved "https://registry.yarnpkg.com/@types/fs-extra/-/fs-extra-8.1.1.tgz#1e49f22d09aa46e19b51c0b013cb63d0d923a068" @@ -1034,15 +1072,20 @@ integrity sha512-JCcp6J0GV66Y4ZMDAQCXot4xprYB+Zfd3meK9+INSJeVZwJmHAW30BBEEkPzXswMXuiyReUGOP3GxrADc9wPww== "@types/json-schema@^7.0.3": - version "7.0.5" - resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.5.tgz#dcce4430e64b443ba8945f0290fb564ad5bac6dd" - integrity sha512-7+2BITlgjgDhH0vvwZU/HZJVyk+2XUlvxXe8dFMedNX/aMkaOq++rMAFXc0tM7ij15QaWlbdQASBR9dihi+bDQ== + version "7.0.6" + resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.6.tgz#f4c7ec43e81b319a9815115031709f26987891f0" + integrity sha512-3c+yGKvVP5Y9TYBEibGNR+kLtijnj7mYrXRg+WpFb2X9xm04g/DXYkfg4hmzJQosc9snFNUPkbYIhu+KAm6jJw== "@types/json5@^0.0.29": version "0.0.29" resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" integrity sha1-7ihweulOEdK4J7y+UnC86n8+ce4= +"@types/mime@*": + version "2.0.3" + resolved "https://registry.yarnpkg.com/@types/mime/-/mime-2.0.3.tgz#c893b73721db73699943bfc3653b1deb7faa4a3a" + integrity sha512-Jus9s4CDbqwocc5pOAnh8ShfrnMcPHuJYzVcSUU7lrh8Ni5HuIqX3oilL86p3dlTrk0LzHRCgA/GQ7uNCw6l2Q== + "@types/minimist@^1.2.0": version "1.2.0" resolved "https://registry.yarnpkg.com/@types/minimist/-/minimist-1.2.0.tgz#69a23a3ad29caf0097f06eda59b361ee2f0639f6" @@ -1054,9 +1097,9 @@ integrity sha512-vyxR57nv8NfcU0GZu8EUXZLTbCMupIUwy95LJ6lllN+JRPG25CwMHoB1q5xKh8YKhQnHYRAn4yW2yuHbf/5xgg== "@types/node@*", "@types/node@^12.12.7": - version "12.12.54" - resolved "https://registry.yarnpkg.com/@types/node/-/node-12.12.54.tgz#a4b58d8df3a4677b6c08bfbc94b7ad7a7a5f82d1" - integrity sha512-ge4xZ3vSBornVYlDnk7yZ0gK6ChHf/CHB7Gl1I0Jhah8DDnEQqBzgohYG4FX4p81TNirSETOiSyn+y1r9/IR6w== + version "12.12.67" + resolved "https://registry.yarnpkg.com/@types/node/-/node-12.12.67.tgz#4f86badb292e822e3b13730a1f9713ed2377f789" + integrity sha512-R48tgL2izApf+9rYNH+3RBMbRpPeW3N8f0I9HMhggeq4UXwBDqumJ14SDs4ctTMhG11pIOduZ4z3QWGOiMc9Vg== "@types/normalize-package-data@^2.4.0": version "2.4.0" @@ -1088,9 +1131,9 @@ integrity sha512-1HcDas8SEj4z1Wc696tH56G8OlRaH/sqZOynNNB+HF0WOeXPaxTtbYzJY2oEfiUxjSKjhCKr+MvR7dCHcEelug== "@types/qs@*": - version "6.9.4" - resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.4.tgz#a59e851c1ba16c0513ea123830dd639a0a15cb6a" - integrity sha512-+wYo+L6ZF6BMoEjtf8zB2esQsqdV6WsjRK/GP9WOgLPrq87PbNWgIxS76dS5uvl/QXtHGakZmwTznIfcPXcKlQ== + version "6.9.5" + resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.5.tgz#434711bdd49eb5ee69d90c1d67c354a9a8ecb18b" + integrity sha512-/JHkVHtx/REVG0VVToGRGH2+23hsYLHdyG+GrvoUGlGAd0ErauXDyvHtRI/7H7mzLm+tBCKA7pfcpkQ1lf58iQ== "@types/range-parser@*": version "1.2.3" @@ -1103,9 +1146,17 @@ integrity sha512-1ri+LJhh0gRxIa37IpGytdaW7yDEHeJniBSMD1BmitS07R1j63brcYCzry+l0WJvGdEKQNQ7DYXO2epgborWPw== "@types/semver@^7.1.0": - version "7.3.3" - resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.3.3.tgz#3ad6ed949e7487e7bda6f886b4a2434a2c3d7b1a" - integrity sha512-jQxClWFzv9IXdLdhSaTf16XI3NYe6zrEbckSpb5xhKfPbWgIyAY0AFyWWWfaiDcBuj3UHmMkCIwSRqpKMTZL2Q== + version "7.3.4" + resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.3.4.tgz#43d7168fec6fa0988bb1a513a697b29296721afb" + integrity sha512-+nVsLKlcUCeMzD2ufHEYuJ9a2ovstb6Dp52A5VsoKxDXgvE051XgHI/33I1EymwkRGQkwnA0LkhnUzituGs4EQ== + +"@types/serve-static@*": + version "1.13.5" + resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.13.5.tgz#3d25d941a18415d3ab092def846e135a08bbcf53" + integrity sha512-6M64P58N+OXjU432WoLLBQxbA0LRGBCRm7aAGQJ+SMC1IMl0dgRVi9EFfoDcS2a7Xogygk/eGN94CfwU9UF7UQ== + dependencies: + "@types/express-serve-static-core" "*" + "@types/mime" "*" "@types/split2@^2.1.6": version "2.1.6" @@ -1135,9 +1186,9 @@ integrity sha512-FvUupuM3rlRsRtCN+fDudtmytGO6iHJuuRKS1Ss0pG5z8oX0diNEw94UEL7hgDbpN94rgaK5R7sWm6RrSkZuAQ== "@types/ws@^7.2.6": - version "7.2.6" - resolved "https://registry.yarnpkg.com/@types/ws/-/ws-7.2.6.tgz#516cbfb818310f87b43940460e065eb912a4178d" - integrity sha512-Q07IrQUSNpr+cXU4E4LtkSIBPie5GLZyyMC1QtQYRLWz701+XcoVygGUZgvLqElq1nU4ICldMYPnexlBsg3dqQ== + version "7.2.7" + resolved "https://registry.yarnpkg.com/@types/ws/-/ws-7.2.7.tgz#362ad1a1d62721bdb725e72c8cccf357078cf5a3" + integrity sha512-UUFC/xxqFLP17hTva8/lVT0SybLUrfSD9c+iapKb0fEiC8uoDbA+xuZ3pAN603eW+bY8ebSMLm9jXdIPnD0ZgA== dependencies: "@types/node" "*" @@ -1202,9 +1253,17 @@ eslint-visitor-keys "^1.1.0" abab@^2.0.0: - version "2.0.4" - resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.4.tgz#6dfa57b417ca06d21b2478f0e638302f99c2405c" - integrity sha512-Eu9ELJWCz/c1e9gTiCY+FceWxcqzjYEbqMgtndnuSqZSUCOL73TWNK2mHfIj4Cw2E/ongOp+JISVNCmovt2KYQ== + version "2.0.5" + resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.5.tgz#c0b678fb32d60fc1219c784d6a826fe385aeb79a" + integrity sha512-9IK9EadsbHo6jLWIpxpR6pL0sazTXV6+SQv25ZB+F7Bj9mJNaOc4nCRabwd5M/JwmUa8idz6Eci6eKfJryPs6Q== + +accepts@~1.3.7: + version "1.3.7" + resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.7.tgz#531bc726517a3b2b41f850021c6cc15eaab507cd" + integrity sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA== + dependencies: + mime-types "~2.1.24" + negotiator "0.6.2" acorn-globals@^4.3.0: version "4.3.4" @@ -1215,9 +1274,9 @@ acorn-globals@^4.3.0: acorn-walk "^6.0.1" acorn-jsx@^5.2.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.2.0.tgz#4c66069173d6fdd68ed85239fc256226182b2ebe" - integrity sha512-HiUX/+K2YpkpJ+SzBffkM/AQ2YE03S0U1kjTLVpoJdhZMOWy8qvXVN9JdLqv2QsaQ6MPYQIuNmwD8zOiYUofLQ== + version "5.3.1" + resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.1.tgz#fc8661e11b7ac1539c47dbfea2e72b3af34d267b" + integrity sha512-K0Ptm/47OKfQRpNQ2J/oIN/3QYiK6FwW+eJbILhsdxh2WTLdl+30o8aGdTbm5JbffpFFAg/g+zi1E+jvJha5ng== acorn-walk@^6.0.1: version "6.2.0" @@ -1225,19 +1284,19 @@ acorn-walk@^6.0.1: integrity sha512-7evsyfH1cLOCdAzZAd43Cic04yKydNx0cF+7tiA19p1XnLLPU4dpCQOqpjqwokFe//vS0QqfqqjCS2JkiIs0cA== acorn@^6.0.1, acorn@^6.0.4: - version "6.4.1" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.4.1.tgz#531e58ba3f51b9dacb9a6646ca4debf5b14ca474" - integrity sha512-ZVA9k326Nwrj3Cj9jlh3wGFutC2ZornPNARZwsNYqQYgN0EsV2d53w5RN/co65Ohn4sUAUtb1rSUAOD6XN9idA== + version "6.4.2" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.4.2.tgz#35866fd710528e92de10cf06016498e47e39e1e6" + integrity sha512-XtGIhXwF8YM8bJhGxG5kXgjkEuNGLTkoYqVE+KMR+aspr4KGYmKYg7yUe3KghyQ9yheNwLnjmzh/7+gfDBmHCQ== acorn@^7.1.1, acorn@^7.4.0: - version "7.4.0" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.0.tgz#e1ad486e6c54501634c6c397c5c121daa383607c" - integrity sha512-+G7P8jJmCHr+S+cLfQxygbWhXy+8YTVGzAkpEbcLo2mLoL7tij/VG41QSHACSf5QgYRhMZYHuNc6drJaO0Da+w== + version "7.4.1" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.1.tgz#feaed255973d2e77555b83dbc08851a6c63520fa" + integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A== -ajv@^6.10.0, ajv@^6.10.2, ajv@^6.12.3: - version "6.12.4" - resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.4.tgz#0614facc4522127fa713445c6bfd3ebd376e2234" - integrity sha512-eienB2c9qVQs2KWexhkrdMLVDoIQCz5KSeLxwg9Lzk4DOfBtIK9PQwwufcsn1jjGuf9WZmqPMbGxOzfcuphJCQ== +ajv@^6.10.0, ajv@^6.10.2, ajv@^6.12.3, ajv@^6.12.4: + version "6.12.6" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" + integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== dependencies: fast-deep-equal "^3.1.1" fast-json-stable-stringify "^2.0.0" @@ -1293,12 +1352,11 @@ ansi-styles@^3.2.0, ansi-styles@^3.2.1: dependencies: color-convert "^1.9.0" -ansi-styles@^4.1.0: - version "4.2.1" - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.2.1.tgz#90ae75c424d008d2624c5bf29ead3177ebfcf359" - integrity sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA== +ansi-styles@^4.0.0, ansi-styles@^4.1.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" + integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== dependencies: - "@types/color-name" "^1.1.1" color-convert "^2.0.1" ansi-to-html@^0.6.4: @@ -1356,6 +1414,11 @@ array-equal@^1.0.0: resolved "https://registry.yarnpkg.com/array-equal/-/array-equal-1.0.0.tgz#8c2a5ef2472fd9ea742b04c77a75093ba2757c93" integrity sha1-jCpe8kcv2ep0KwTHenUJO6J1fJM= +array-flatten@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" + integrity sha1-ml9pkFGx5wczKPKgCJaLZOopVdI= + array-includes@^3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/array-includes/-/array-includes-3.1.1.tgz#cdd67e6852bdf9c1215460786732255ed2459348" @@ -1438,6 +1501,11 @@ astral-regex@^1.0.0: resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-1.0.0.tgz#6c8c3fb827dd43ee3918f27b82782ab7658a6fd9" integrity sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg== +astral-regex@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-2.0.0.tgz#483143c567aeed4785759c0865786dc77d7d2e31" + integrity sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ== + async-each@^1.0.1: version "1.0.3" resolved "https://registry.yarnpkg.com/async-each/-/async-each-1.0.3.tgz#b727dbf87d7651602f06f4d4ac387f47d91b0cbf" @@ -1463,7 +1531,7 @@ atob@^2.1.2: resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9" integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg== -autoprefixer@^9.8.0: +autoprefixer@^9.8.6: version "9.8.6" resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-9.8.6.tgz#3b73594ca1bf9266320c5acf1588d74dea74210f" integrity sha512-XrvP4VVHdRBCdX1S3WXVD8+RyG9qeb1D5Sn1DeLiG2xfSpzellk5k54xbUERJ3M5DggQxes39UGOTP8CFrEGbg== @@ -1572,7 +1640,7 @@ bindings@^1.5.0: dependencies: file-uri-to-path "1.0.0" -bl@^4.0.1: +bl@^4.0.3: version "4.0.3" resolved "https://registry.yarnpkg.com/bl/-/bl-4.0.3.tgz#12d6287adc29080e22a705e5764b2a9522cdc489" integrity sha512-fs4G6/Hu4/EE+F75J8DuN/0IpQqNjAdC7aEQv7Qt8MHGUH7Ckv2MwTEEeN9QehD0pfIDkMI1bkHYkKy7xHyKIg== @@ -1591,6 +1659,22 @@ bn.js@^5.1.1: resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-5.1.3.tgz#beca005408f642ebebea80b042b4d18d2ac0ee6b" integrity sha512-GkTiFpjFtUzU9CbMeJ5iazkCzGL3jrhzerzZIuqLABjbwRaFt33I9tUdSNryIptM+RxDet6OKm2WnLXzW51KsQ== +body-parser@1.19.0: + version "1.19.0" + resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.19.0.tgz#96b2709e57c9c4e09a6fd66a8fd979844f69f08a" + integrity sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw== + dependencies: + bytes "3.1.0" + content-type "~1.0.4" + debug "2.6.9" + depd "~1.1.2" + http-errors "1.7.2" + iconv-lite "0.4.24" + on-finished "~2.3.0" + qs "6.7.0" + raw-body "2.4.0" + type-is "~1.6.17" + boolbase@^1.0.0, boolbase@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e" @@ -1719,14 +1803,14 @@ browserify-zlib@^0.2.0: pako "~1.0.5" browserslist@^4.0.0, browserslist@^4.1.0, browserslist@^4.12.0, browserslist@^4.8.5: - version "4.14.0" - resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.14.0.tgz#2908951abfe4ec98737b72f34c3bcedc8d43b000" - integrity sha512-pUsXKAF2lVwhmtpeA3LJrZ76jXuusrNyhduuQs7CDFf9foT4Y38aQOserd2lMe5DSSrjf3fx34oHwryuvxAUgQ== + version "4.14.5" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.14.5.tgz#1c751461a102ddc60e40993639b709be7f2c4015" + integrity sha512-Z+vsCZIvCBvqLoYkBFTwEYH3v5MCQbsAjp50ERycpOjnPmolg1Gjy4+KaWWpm8QOJt9GHkhdqAl14NpCX73CWA== dependencies: - caniuse-lite "^1.0.30001111" - electron-to-chromium "^1.3.523" - escalade "^3.0.2" - node-releases "^1.1.60" + caniuse-lite "^1.0.30001135" + electron-to-chromium "^1.3.571" + escalade "^3.1.0" + node-releases "^1.1.61" buffer-alloc-unsafe@^1.1.0: version "1.1.0" @@ -1783,6 +1867,11 @@ builtin-status-codes@^3.0.0: resolved "https://registry.yarnpkg.com/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz#85982878e21b98e1c66425e03d0174788f569ee8" integrity sha1-hZgoeOIbmOHGZCXgPQF0eI9Wnug= +bytes@3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.0.tgz#f6cf7933a360e0588fa9fde85651cdc7f805d1f6" + integrity sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg== + cache-base@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/cache-base/-/cache-base-1.0.1.tgz#0a7f46416831c8b662ee36fe4e7c59d76f666ab2" @@ -1851,10 +1940,10 @@ caniuse-api@^3.0.0: lodash.memoize "^4.1.2" lodash.uniq "^4.5.0" -caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001109, caniuse-lite@^1.0.30001111: - version "1.0.30001118" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001118.tgz#116a9a670e5264aec895207f5e918129174c6f62" - integrity sha512-RNKPLojZo74a0cP7jFMidQI7nvLER40HgNfgKQEJ2PFm225L0ectUungNQoK3Xk3StQcFbpBPNEvoWD59436Hg== +caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001109, caniuse-lite@^1.0.30001135: + version "1.0.30001148" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001148.tgz#dc97c7ed918ab33bf8706ddd5e387287e015d637" + integrity sha512-E66qcd0KMKZHNJQt9hiLZGE3J4zuTqE1OnU53miEVtylFbwOEmeA5OsRu90noZful+XGSQOni1aT2tiqu/9yYw== caseless@~0.12.0: version "0.12.0" @@ -2065,21 +2154,21 @@ color-name@^1.0.0, color-name@~1.1.4: resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== -color-string@^1.5.2: - version "1.5.3" - resolved "https://registry.yarnpkg.com/color-string/-/color-string-1.5.3.tgz#c9bbc5f01b58b5492f3d6857459cb6590ce204cc" - integrity sha512-dC2C5qeWoYkxki5UAXapdjqO672AM4vZuPGRQfO8b5HKuKGBbKWpITyDYN7TOFKvRW7kOgAn3746clDBMDJyQw== +color-string@^1.5.4: + version "1.5.4" + resolved "https://registry.yarnpkg.com/color-string/-/color-string-1.5.4.tgz#dd51cd25cfee953d138fe4002372cc3d0e504cb6" + integrity sha512-57yF5yt8Xa3czSEW1jfQDE79Idk0+AkN/4KWad6tbdxUmAs3MvjxlWSWD4deYytcRfoZ9nhKyFl1kj5tBvidbw== dependencies: color-name "^1.0.0" simple-swizzle "^0.2.2" color@^3.0.0: - version "3.1.2" - resolved "https://registry.yarnpkg.com/color/-/color-3.1.2.tgz#68148e7f85d41ad7649c5fa8c8106f098d229e10" - integrity sha512-vXTJhHebByxZn3lDvDJYw4lR5+uB3vuoHsuYA5AKuxRVn5wzzIfQKGLBmgdVRHKTJYeK5rvJcHnrd0Li49CFpg== + version "3.1.3" + resolved "https://registry.yarnpkg.com/color/-/color-3.1.3.tgz#ca67fb4e7b97d611dcde39eceed422067d91596e" + integrity sha512-xgXAcTHa2HeFCGLE9Xs/R82hujGtu9Jd9x4NW3T34+OMs7VoPsjwzRczKHvTAHeJwWFwX5j15+MgAppE8ztObQ== dependencies: color-convert "^1.9.1" - color-string "^1.5.2" + color-string "^1.5.4" colorette@^1.2.1: version "1.2.1" @@ -2143,6 +2232,18 @@ contains-path@^0.1.0: resolved "https://registry.yarnpkg.com/contains-path/-/contains-path-0.1.0.tgz#fe8cf184ff6670b6baef01a9d4861a5cbec4120a" integrity sha1-/ozxhP9mcLa67wGp1IYaXL7EEgo= +content-disposition@0.5.3: + version "0.5.3" + resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.3.tgz#e130caf7e7279087c5616c2007d0485698984fbd" + integrity sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g== + dependencies: + safe-buffer "5.1.2" + +content-type@~1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b" + integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA== + convert-source-map@^1.5.1, convert-source-map@^1.7.0: version "1.7.0" resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.7.0.tgz#17a2cb882d7f77d3490585e2ce6c524424a3a442" @@ -2150,6 +2251,16 @@ convert-source-map@^1.5.1, convert-source-map@^1.7.0: dependencies: safe-buffer "~5.1.1" +cookie-signature@1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" + integrity sha1-4wOogrNCzD7oylE6eZmXNNqzriw= + +cookie@0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.0.tgz#beb437e7022b3b6d49019d088665303ebe9c14ba" + integrity sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg== + copy-descriptor@^0.1.0: version "0.1.1" resolved "https://registry.yarnpkg.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d" @@ -2183,16 +2294,16 @@ cosmiconfig@^5.0.0: js-yaml "^3.13.1" parse-json "^4.0.0" -cosmiconfig@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-6.0.0.tgz#da4fee853c52f6b1e6935f41c1a2fc50bd4a9982" - integrity sha512-xb3ZL6+L8b9JLLCx3ZdoZy4+2ECphCMo2PwqgP1tlfVq6M6YReyzBJtvWWtbDSpNr9hn96pkCiZqUcFEc+54Qg== +cosmiconfig@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-7.0.0.tgz#ef9b44d773959cae63ddecd122de23853b60f8d3" + integrity sha512-pondGvTuVYDk++upghXJabWzL6Kxu6f26ljFw64Swq9v6sQPUL3EUlVDV56diOjpCayKihL6hVe8exIACU4XcA== dependencies: "@types/parse-json" "^4.0.0" - import-fresh "^3.1.0" + import-fresh "^3.2.1" parse-json "^5.0.0" path-type "^4.0.0" - yaml "^1.7.2" + yaml "^1.10.0" create-ecdh@^4.0.0: version "4.0.4" @@ -2332,9 +2443,9 @@ css-tree@1.0.0-alpha.39: source-map "^0.6.1" css-what@^3.2.1: - version "3.3.0" - resolved "https://registry.yarnpkg.com/css-what/-/css-what-3.3.0.tgz#10fec696a9ece2e591ac772d759aacabac38cd39" - integrity sha512-pv9JPyatiPaQ6pf4OvD/dbfm0o5LviWmwxNWzblYf/1u9QZd0ihV+PMwy5jdQWQ3349kZmKEx9WXuSka2dM4cg== + version "3.4.2" + resolved "https://registry.yarnpkg.com/css-what/-/css-what-3.4.2.tgz#ea7026fcb01777edbde52124e21f327e7ae950e4" + integrity sha512-ACUm3L0/jiZTqfzRM3Hi9Q8eZqd6IK37mMWPLz9PJxkLWllYeRf+EHUSHYEtFop2Eqytaq1FizFVh7XfBnXCDQ== cssesc@^3.0.0: version "3.0.0" @@ -2459,13 +2570,20 @@ debug@2.6.9, debug@^2.1.3, debug@^2.2.0, debug@^2.3.3, debug@^2.6.9: dependencies: ms "2.0.0" -debug@4.1.1, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1: +debug@4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/debug/-/debug-4.1.1.tgz#3b72260255109c6b589cee050f1d516139664791" integrity sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw== dependencies: ms "^2.1.1" +debug@^4.0.1, debug@^4.1.0, debug@^4.1.1: + version "4.2.0" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.2.0.tgz#7f150f93920e94c58f5574c2fd01a3110effe7f1" + integrity sha512-IX2ncY78vDTjZMFUdmsvIRFY2Cf4FnD0wRs+nQwJU8Lu99/tPFdb0VybiiMTPe3I6rQmwsqQqRBvxU+bZ/I8sg== + dependencies: + ms "2.1.2" + decamelize-keys@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/decamelize-keys/-/decamelize-keys-1.1.0.tgz#d171a87933252807eb3cb61dc1c1445d078df2d9" @@ -2615,9 +2733,9 @@ domelementtype@1, domelementtype@^1.3.0, domelementtype@^1.3.1: integrity sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w== domelementtype@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.0.1.tgz#1f8bdfe91f5a78063274e803b4bdcedf6e94f94d" - integrity sha512-5HOHUDsYZWV8FGWN0Njbr/Rn7f/eWSQi1v7+HsUVwXgn8nWWlL64zKDkS0n8ZmQ3mlWOMuXOnR+7Nx/5tMO5AQ== + version "2.0.2" + resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.0.2.tgz#f3b6e549201e46f588b59463dd77187131fe6971" + integrity sha512-wFwTwCVebUrMgGeAwRL/NhZtHAUyT9n9yg4IMDwf10+6iCMxSkVq9MGCVEH+QZWo1nNidy8kNvwmv4zWHDTqvA== domexception@^1.0.1: version "1.0.1" @@ -2642,9 +2760,9 @@ domutils@^1.5.1, domutils@^1.7.0: domelementtype "1" dot-prop@^5.2.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/dot-prop/-/dot-prop-5.2.0.tgz#c34ecc29556dc45f1f4c22697b6f4904e0cc4fcb" - integrity sha512-uEUyaDKoSQ1M4Oq8l45hSE26SnTxL6snNnqvK/VWx5wJhmff5z0FUVJDKDanor/6w3kzE3i7XZOk+7wC0EXr1A== + version "5.3.0" + resolved "https://registry.yarnpkg.com/dot-prop/-/dot-prop-5.3.0.tgz#90ccce708cd9cd82cc4dc8c3ddd9abdd55b20e88" + integrity sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q== dependencies: is-obj "^2.0.0" @@ -2678,10 +2796,10 @@ ee-first@1.1.1: resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0= -electron-to-chromium@^1.3.523: - version "1.3.549" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.549.tgz#bf500c8eb75a7286a895e34f41aa144384ac613b" - integrity sha512-q09qZdginlqDH3+Y1P6ch5UDTW8nZ1ijwMkxFs15J/DAWOwqolIx8HZH1UP0vReByBigk/dPlU22xS1MaZ+kpQ== +electron-to-chromium@^1.3.571: + version "1.3.581" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.581.tgz#7f796fe92cdc18f5013769dc6f45f4536315a183" + integrity sha512-ALORbI23YkYJoVJWusSdmTq8vXH3TLFzniILE47uZkZOim135ZhoTCM7QlIuvmK78As5kLdANfy7kDIQvJ+iPw== elliptic@^6.5.3: version "6.5.3" @@ -2758,19 +2876,37 @@ error-ex@^1.2.0, error-ex@^1.3.1: is-arrayish "^0.2.1" es-abstract@^1.17.0, es-abstract@^1.17.0-next.1, es-abstract@^1.17.2, es-abstract@^1.17.4, es-abstract@^1.17.5: - version "1.17.6" - resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.17.6.tgz#9142071707857b2cacc7b89ecb670316c3e2d52a" - integrity sha512-Fr89bON3WFyUi5EvAeI48QTWX0AyekGgLA8H+c+7fbfCkJwRWRMLd8CQedNEyJuoYYhmtEqY92pgte1FAhBlhw== + version "1.17.7" + resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.17.7.tgz#a4de61b2f66989fc7421676c1cb9787573ace54c" + integrity sha512-VBl/gnfcJ7OercKA9MVaegWsBHFjV492syMudcnQZvt/Dw8ezpcOHYZXa/J96O8vx+g4x65YKhxOwDUh63aS5g== dependencies: es-to-primitive "^1.2.1" function-bind "^1.1.1" has "^1.0.3" has-symbols "^1.0.1" - is-callable "^1.2.0" - is-regex "^1.1.0" - object-inspect "^1.7.0" + is-callable "^1.2.2" + is-regex "^1.1.1" + object-inspect "^1.8.0" object-keys "^1.1.1" - object.assign "^4.1.0" + object.assign "^4.1.1" + string.prototype.trimend "^1.0.1" + string.prototype.trimstart "^1.0.1" + +es-abstract@^1.18.0-next.0: + version "1.18.0-next.1" + resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.18.0-next.1.tgz#6e3a0a4bda717e5023ab3b8e90bec36108d22c68" + integrity sha512-I4UGspA0wpZXWENrdA0uHbnhte683t3qT/1VFH9aX2dA5PPSf6QW5HHXf5HImaqPmjXaVeVk4RGWnaylmV7uAA== + dependencies: + es-to-primitive "^1.2.1" + function-bind "^1.1.1" + has "^1.0.3" + has-symbols "^1.0.1" + is-callable "^1.2.2" + is-negative-zero "^2.0.0" + is-regex "^1.1.1" + object-inspect "^1.8.0" + object-keys "^1.1.1" + object.assign "^4.1.1" string.prototype.trimend "^1.0.1" string.prototype.trimstart "^1.0.1" @@ -2806,10 +2942,10 @@ es6-promisify@^6.0.0: resolved "https://registry.yarnpkg.com/es6-promisify/-/es6-promisify-6.1.1.tgz#46837651b7b06bf6fff893d03f29393668d01621" integrity sha512-HBL8I3mIki5C1Cc9QjKUenHtnG0A5/xA8Q/AllRcfiwl2CZFXGK7ddBiCoRwAix4i2KxcQfjtIVcrVbB3vbmwg== -escalade@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.0.2.tgz#6a580d70edb87880f22b4c91d0d56078df6962c4" - integrity sha512-gPYAU37hYCUhW5euPeR+Y74F7BL+IBsV93j5cvGriSaD1aG6MGsqsV1yamRdrWrb2j3aiZvb0X+UBOWpx3JWtQ== +escalade@^3.1.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40" + integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw== escape-html@~1.0.3: version "1.0.3" @@ -2851,13 +2987,13 @@ escodegen@~1.9.0: source-map "~0.6.1" eslint-config-prettier@^6.0.0: - version "6.11.0" - resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-6.11.0.tgz#f6d2238c1290d01c859a8b5c1f7d352a0b0da8b1" - integrity sha512-oB8cpLWSAjOVFEJhhyMZh6NOEOtBVziaqdDQ86+qhDHFbZXoRTM7pNSvFRfW/W/L/LrQ38C99J5CGuRBBzBsdA== + version "6.12.0" + resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-6.12.0.tgz#9eb2bccff727db1c52104f0b49e87ea46605a0d2" + integrity sha512-9jWPlFlgNwRUYVoujvWTQ1aMO8o6648r+K7qU7K5Jmkbyqav1fuEZC0COYpGBxyiAJb65Ra9hrmFx19xRGwXWw== dependencies: get-stdin "^6.0.0" -eslint-import-resolver-node@^0.3.3: +eslint-import-resolver-node@^0.3.4: version "0.3.4" resolved "https://registry.yarnpkg.com/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.4.tgz#85ffa81942c25012d8231096ddf679c03042c717" integrity sha512-ogtf+5AB/O+nM6DIeBUNr2fuT7ot9Qg/1harBfBtaP13ekEWFQEEMP94BCB7zaNW3gyY+8SHYF00rnqYwXKWOA== @@ -2874,16 +3010,16 @@ eslint-module-utils@^2.6.0: pkg-dir "^2.0.0" eslint-plugin-import@^2.18.2: - version "2.22.0" - resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.22.0.tgz#92f7736fe1fde3e2de77623c838dd992ff5ffb7e" - integrity sha512-66Fpf1Ln6aIS5Gr/55ts19eUuoDhAbZgnr6UxK5hbDx6l/QgQgx61AePq+BV4PP2uXQFClgMVzep5zZ94qqsxg== + version "2.22.1" + resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.22.1.tgz#0896c7e6a0cf44109a2d97b95903c2bb689d7702" + integrity sha512-8K7JjINHOpH64ozkAhpT3sd+FswIZTfMZTjdx052pnWrgRCVfp8op9tbjpAk3DdUeI/Ba4C8OjdC0r90erHEOw== dependencies: array-includes "^3.1.1" array.prototype.flat "^1.2.3" contains-path "^0.1.0" debug "^2.6.9" doctrine "1.5.0" - eslint-import-resolver-node "^0.3.3" + eslint-import-resolver-node "^0.3.4" eslint-module-utils "^2.6.0" has "^1.0.3" minimatch "^3.0.4" @@ -2899,12 +3035,12 @@ eslint-plugin-prettier@^3.1.0: dependencies: prettier-linter-helpers "^1.0.0" -eslint-scope@^5.0.0, eslint-scope@^5.1.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.1.0.tgz#d0f971dfe59c69e0cada684b23d49dbf82600ce5" - integrity sha512-iiGRvtxWqgtx5m8EyQUJihBloE4EnYeGE/bz1wSPwJE6tZuJUtHlhqDM4Xj2ukE8Dyy1+HCZ4hE0fzIVMzb58w== +eslint-scope@^5.0.0, eslint-scope@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.1.1.tgz#e786e59a66cb92b3f6c1fb0d508aab174848f48c" + integrity sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw== dependencies: - esrecurse "^4.1.0" + esrecurse "^4.3.0" estraverse "^4.1.1" eslint-utils@^2.0.0, eslint-utils@^2.1.0: @@ -2919,22 +3055,28 @@ eslint-visitor-keys@^1.1.0, eslint-visitor-keys@^1.3.0: resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz#30ebd1ef7c2fdff01c3a4f151044af25fab0523e" integrity sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ== +eslint-visitor-keys@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-2.0.0.tgz#21fdc8fbcd9c795cc0321f0563702095751511a8" + integrity sha512-QudtT6av5WXels9WjIM7qz1XD1cWGvX4gGXvp/zBn9nXG02D0utdU3Em2m/QjTnrsk6bBjmCygl3rmj118msQQ== + eslint@^7.7.0: - version "7.7.0" - resolved "https://registry.yarnpkg.com/eslint/-/eslint-7.7.0.tgz#18beba51411927c4b64da0a8ceadefe4030d6073" - integrity sha512-1KUxLzos0ZVsyL81PnRN335nDtQ8/vZUD6uMtWbF+5zDtjKcsklIi78XoE0MVL93QvWTu+E5y44VyyCsOMBrIg== + version "7.11.0" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-7.11.0.tgz#aaf2d23a0b5f1d652a08edacea0c19f7fadc0b3b" + integrity sha512-G9+qtYVCHaDi1ZuWzBsOWo2wSwd70TXnU6UHA3cTYHp7gCTXZcpggWFoUVAMRarg68qtPoNfFbzPh+VdOgmwmw== dependencies: "@babel/code-frame" "^7.0.0" + "@eslint/eslintrc" "^0.1.3" ajv "^6.10.0" chalk "^4.0.0" cross-spawn "^7.0.2" debug "^4.0.1" doctrine "^3.0.0" enquirer "^2.3.5" - eslint-scope "^5.1.0" + eslint-scope "^5.1.1" eslint-utils "^2.1.0" - eslint-visitor-keys "^1.3.0" - espree "^7.2.0" + eslint-visitor-keys "^2.0.0" + espree "^7.3.0" esquery "^1.2.0" esutils "^2.0.2" file-entry-cache "^5.0.1" @@ -2961,7 +3103,7 @@ eslint@^7.7.0: text-table "^0.2.0" v8-compile-cache "^2.0.3" -espree@^7.2.0: +espree@^7.3.0: version "7.3.0" resolved "https://registry.yarnpkg.com/espree/-/espree-7.3.0.tgz#dc30437cf67947cf576121ebd780f15eeac72348" integrity sha512-dksIWsvKCixn1yrEXO8UosNSxaDoSYpq9reEjZSbHLpT5hpaCAKTLBwq0RHtLrIr+c0ByiYzWT8KTMRzoRCNlw== @@ -2987,19 +3129,19 @@ esquery@^1.2.0: dependencies: estraverse "^5.1.0" -esrecurse@^4.1.0: - version "4.2.1" - resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.2.1.tgz#007a3b9fdbc2b3bb87e4879ea19c92fdbd3942cf" - integrity sha512-64RBB++fIOAXPw3P9cy89qfMlvZEXZkqqJkjqqXIvzP5ezRZjW+lPWjw35UX/3EhUPFYbg5ER4JYgDw4007/DQ== +esrecurse@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.3.0.tgz#7ad7964d679abb28bee72cec63758b1c5d2c9921" + integrity sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag== dependencies: - estraverse "^4.1.0" + estraverse "^5.2.0" -estraverse@^4.1.0, estraverse@^4.1.1, estraverse@^4.2.0: +estraverse@^4.1.1, estraverse@^4.2.0: version "4.3.0" resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d" integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw== -estraverse@^5.1.0: +estraverse@^5.1.0, estraverse@^5.2.0: version "5.2.0" resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.2.0.tgz#307df42547e6cc7324d3cf03c155d5cdb8c53880" integrity sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ== @@ -3015,9 +3157,9 @@ etag@~1.8.1: integrity sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc= eventemitter3@^4.0.0: - version "4.0.6" - resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.6.tgz#1258f6fa51b4908aadc2cd624fcd6e64f99f49d6" - integrity sha512-s3GJL04SQoM+gn2c14oyqxvZ3Pcq7cduSDqy3sBFXx6UPSUmgVYwQM9zwkTn9je0lrfg0gHEwR42pF3Q2dCQkQ== + version "4.0.7" + resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f" + integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw== events@^3.0.0: version "3.2.0" @@ -3052,6 +3194,42 @@ expand-brackets@^2.1.4: snapdragon "^0.8.1" to-regex "^3.0.1" +express@^4.17.1: + version "4.17.1" + resolved "https://registry.yarnpkg.com/express/-/express-4.17.1.tgz#4491fc38605cf51f8629d39c2b5d026f98a4c134" + integrity sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g== + dependencies: + accepts "~1.3.7" + array-flatten "1.1.1" + body-parser "1.19.0" + content-disposition "0.5.3" + content-type "~1.0.4" + cookie "0.4.0" + cookie-signature "1.0.6" + debug "2.6.9" + depd "~1.1.2" + encodeurl "~1.0.2" + escape-html "~1.0.3" + etag "~1.8.1" + finalhandler "~1.1.2" + fresh "0.5.2" + merge-descriptors "1.0.1" + methods "~1.1.2" + on-finished "~2.3.0" + parseurl "~1.3.3" + path-to-regexp "0.1.7" + proxy-addr "~2.0.5" + qs "6.7.0" + range-parser "~1.2.1" + safe-buffer "5.1.2" + send "0.17.1" + serve-static "1.14.1" + setprototypeof "1.1.1" + statuses "~1.5.0" + type-is "~1.6.18" + utils-merge "1.0.1" + vary "~1.1.2" + extend-shallow@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-2.0.1.tgz#51af7d614ad9a9f610ea1bafbb989d6b1c56890f" @@ -3128,7 +3306,7 @@ fast-glob@^2.2.2: merge2 "^1.2.3" micromatch "^3.1.10" -fast-glob@^3.1.1: +fast-glob@^3.1.1, fast-glob@^3.2.4: version "3.2.4" resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.4.tgz#d20aefbf99579383e7f3cc66529158c9b98554d3" integrity sha512-kr/Oo6PX51265qeuCYsyGypiO5uJFgBS0jksyG7FUeCyQzNwYnzrNIMR1NXfkZXsMYXYLRAHgISHBz8gQcxKHQ== @@ -3150,6 +3328,11 @@ fast-levenshtein@^2.0.6, fast-levenshtein@~2.0.6: resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc= +fastest-levenshtein@^1.0.12: + version "1.0.12" + resolved "https://registry.yarnpkg.com/fastest-levenshtein/-/fastest-levenshtein-1.0.12.tgz#9990f7d3a88cc5a9ffd1f1745745251700d497e2" + integrity sha512-On2N+BpYJ15xIC974QNVuYGMOlEVt4s0EOI3wwMqOmK1fdDY+FN/zltPV8vosq4ad4c/gJ1KHScUn/6AWIgiow== + fastparse@^1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/fastparse/-/fastparse-1.1.2.tgz#91728c5a5942eced8531283c79441ee4122c35a9" @@ -3203,6 +3386,19 @@ fill-range@^7.0.1: dependencies: to-regex-range "^5.0.1" +finalhandler@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.1.2.tgz#b7e7d000ffd11938d0fdb053506f6ebabe9f587d" + integrity sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA== + dependencies: + debug "2.6.9" + encodeurl "~1.0.2" + escape-html "~1.0.3" + on-finished "~2.3.0" + parseurl "~1.3.3" + statuses "~1.5.0" + unpipe "~1.0.0" + find-up@5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/find-up/-/find-up-5.0.0.tgz#4c92819ecb7083561e4f4a240a86be5198f536fc" @@ -3243,9 +3439,9 @@ flat-cache@^2.0.1: write "1.0.3" flat@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/flat/-/flat-4.1.0.tgz#090bec8b05e39cba309747f1d588f04dbaf98db2" - integrity sha512-Px/TiLIznH7gEDlPXcUD4KnBusa6kR6ayRUVcnEAbreRIuhkqow/mun59BuRXwoYk7ZQOLW1ZM05ilIvK38hFw== + version "4.1.1" + resolved "https://registry.yarnpkg.com/flat/-/flat-4.1.1.tgz#a392059cc382881ff98642f5da4dde0a959f309b" + integrity sha512-FmTtBsHskrU6FJ2VxCnsDb84wu9zhmO3cUX2kGFb5tuwhfXxGciiT0oRY+cck35QmG+NmGh5eLz6lLCpWTqwpA== dependencies: is-buffer "~2.0.3" @@ -3288,6 +3484,11 @@ format@^0.2.0: resolved "https://registry.yarnpkg.com/format/-/format-0.2.2.tgz#d6170107e9efdc4ed30c9dc39016df942b5cb58b" integrity sha1-1hcBB+nv3E7TDJ3DkBbflCtctYs= +forwarded@~0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.1.2.tgz#98c23dab1175657b8c0573e8ceccd91b0ff18c84" + integrity sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ= + fragment-cache@^0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/fragment-cache/-/fragment-cache-0.2.1.tgz#4290fad27f13e89be7f33799c6bc5a0abfff0d19" @@ -3683,6 +3884,17 @@ htmlparser2@~3.9.2: inherits "^2.0.1" readable-stream "^2.0.2" +http-errors@1.7.2: + version "1.7.2" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.2.tgz#4f5029cf13239f31036e5b2e55292bcfbcc85c8f" + integrity sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg== + dependencies: + depd "~1.1.2" + inherits "2.0.3" + setprototypeof "1.1.1" + statuses ">= 1.5.0 < 2" + toidentifier "1.0.0" + http-errors@~1.7.2: version "1.7.3" resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.3.tgz#6c619e4f9c60308c38519498c14fbb10aacebb06" @@ -3757,7 +3969,7 @@ import-fresh@^2.0.0: caller-path "^2.0.0" resolve-from "^3.0.0" -import-fresh@^3.0.0, import-fresh@^3.1.0: +import-fresh@^3.0.0, import-fresh@^3.2.1: version "3.2.1" resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.2.1.tgz#633ff618506e793af5ac91bf48b72677e15cbe66" integrity sha512-6e1q1cnWP2RXD9/keSkxHScg508CdXqXWgWBaETNhyuBFz+kUZlKboh+ISK+bU++DmbHimVBrOz/zzPe0sZ3sQ== @@ -3813,12 +4025,10 @@ ini@^1.3.5: resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927" integrity sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw== -invariant@^2.2.2, invariant@^2.2.4: - version "2.2.4" - resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6" - integrity sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA== - dependencies: - loose-envify "^1.0.0" +ipaddr.js@1.9.1: + version "1.9.1" + resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3" + integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g== is-absolute-url@^2.0.0: version "2.1.0" @@ -3901,10 +4111,10 @@ is-buffer@^2.0.0, is-buffer@~2.0.3: resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-2.0.4.tgz#3e572f23c8411a5cfd9557c849e3665e0b290623" integrity sha512-Kq1rokWXOPXWuaMAqZiJW4XxsmD9zGx9q4aePabbn3qCRGedtH7Cm+zV8WETitMfu1wdh+Rvd6w5egwSngUX2A== -is-callable@^1.1.4, is-callable@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.0.tgz#83336560b54a38e35e3a2df7afd0454d691468bb" - integrity sha512-pyVD9AaGLxtg6srb2Ng6ynWJqkHU9bEM087AKck0w8QwDarTfNcpIYoU8x8Hv2Icm8u6kFJM18Dag8lyqGkviw== +is-callable@^1.1.4, is-callable@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.2.tgz#c7c6715cd22d4ddb48d3e19970223aceabb080d9" + integrity sha512-dnMqspv5nU3LoewK2N/y7KLtxtakvTuaCsU9FU50/QDmdbHNy/4/JuRtMHqRU22o3q+W89YQndQEeCVwK+3qrA== is-color-stop@^1.0.0: version "1.1.0" @@ -4023,6 +4233,11 @@ is-map@^2.0.1: resolved "https://registry.yarnpkg.com/is-map/-/is-map-2.0.1.tgz#520dafc4307bb8ebc33b813de5ce7c9400d644a1" integrity sha512-T/S49scO8plUiAOA2DBTBG3JHpn1yiw0kRp6dgiZ0v2/6twi5eiB0rHtHFH9ZIrvlWc6+4O+m4zg5+Z833aXgw== +is-negative-zero@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.0.tgz#9553b121b0fac28869da9ed459e20c7543788461" + integrity sha1-lVOxIbD6wohp2p7UWeIMdUN4hGE= + is-number@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/is-number/-/is-number-3.0.0.tgz#24fd6201a4782cf50561c810276afc7d12d71195" @@ -4057,7 +4272,7 @@ is-plain-object@^2.0.3, is-plain-object@^2.0.4: dependencies: isobject "^3.0.1" -is-regex@^1.1.0: +is-regex@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.1.tgz#c6f98aacc546f6cec5468a07b7b153ab564a57b9" integrity sha512-1+QkEcxiLlB7VEyFtyBg94e08OAsvq7FUBgApTq/w2ymCLyKJgDPsybBENVtA7XCQEgEXxKPonG+mvYRxh/LIg== @@ -4173,7 +4388,7 @@ iterate-value@^1.0.0: es-get-iterator "^1.0.2" iterate-iterator "^1.0.1" -"js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0: +js-tokens@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== @@ -4239,9 +4454,9 @@ json-parse-better-errors@^1.0.1: integrity sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw== json-parse-even-better-errors@^2.3.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.0.tgz#371873c5ffa44304a6ba12419bcfa95f404ae081" - integrity sha512-o3aP+RsWDJZayj1SbHNQAI8x0v3T3SKiGoZlNYfbUP1S3omJQ6i9CnqADqkSPaOAxwua4/1YWx5CM7oiChJt2Q== + version "2.3.1" + resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d" + integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w== json-schema-traverse@^0.4.1: version "0.4.1" @@ -4334,18 +4549,6 @@ leaked-handles@^5.2.0: weakmap-shim "^1.1.0" xtend "^4.0.0" -leven@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/leven/-/leven-3.1.0.tgz#77891de834064cccba82ae7842bb6b14a13ed7f2" - integrity sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A== - -levenary@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/levenary/-/levenary-1.1.1.tgz#842a9ee98d2075aa7faeedbe32679e9205f46f77" - integrity sha512-mkAdOIt79FD6irqjYSs4rdbnlT5vRonMEvBVPVb3XmevfS8kgRXwfes0dhPdEtzTWD/1eNE/Bm/G1iRt6DcnQQ== - dependencies: - leven "^3.1.0" - levn@^0.4.1: version "0.4.1" resolved "https://registry.yarnpkg.com/levn/-/levn-0.4.1.tgz#ae4562c007473b932a6200d403268dd2fffc6ade" @@ -4432,7 +4635,7 @@ lodash.uniq@^4.5.0: resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773" integrity sha1-0CJTc662Uq3BvILklFM5qEJ1R3M= -lodash@^4.17.11, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.4: +lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17.4: version "4.17.20" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52" integrity sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA== @@ -4456,13 +4659,6 @@ longest-streak@^2.0.1: resolved "https://registry.yarnpkg.com/longest-streak/-/longest-streak-2.0.4.tgz#b8599957da5b5dab64dee3fe316fa774597d90e4" integrity sha512-vM6rUVCVUJJt33bnmHiZEvr7wPT78ztX7rojL+LW51bHtLh6HTjx84LA5W4+oa6aKEJA7jJu5LR6vQRBpA5DVg== -loose-envify@^1.0.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" - integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== - dependencies: - js-tokens "^3.0.0 || ^4.0.0" - magic-string@^0.22.4: version "0.22.5" resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.22.5.tgz#8e9cf5afddf44385c1da5bc2a6a0dbd10b03657e" @@ -4549,10 +4745,15 @@ mdn-data@2.0.6: resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.6.tgz#852dc60fcaa5daa2e8cf6c9189c440ed3e042978" integrity sha512-rQvjv71olwNHgiTbfPZFkJtjNMciWgswYeciZhtvWLO8bmX3TnhyA62I6sTWOyZssWHJJjY6/KiWwqQsWWsqOA== -meow@^7.0.1: - version "7.1.0" - resolved "https://registry.yarnpkg.com/meow/-/meow-7.1.0.tgz#50ecbcdafa16f8b58fb7eb9675b933f6473b3a59" - integrity sha512-kq5F0KVteskZ3JdfyQFivJEj2RaA8NFsS4+r9DaMKLcUHpk5OcHS3Q0XkCXONB1mZRPsu/Y/qImKri0nwSEZog== +media-typer@0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" + integrity sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g= + +meow@^7.1.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/meow/-/meow-7.1.1.tgz#7c01595e3d337fcb0ec4e8eed1666ea95903d306" + integrity sha512-GWHvA5QOcS412WCo8vwKDlTelGLsCGBVevQB5Kva961rmNfun0PCbv5+xta2kUMFJyR8/oWnn7ddeKdosbAPbA== dependencies: "@types/minimist" "^1.2.0" camelcase-keys "^6.2.2" @@ -4566,6 +4767,11 @@ meow@^7.0.1: type-fest "^0.13.1" yargs-parser "^18.1.3" +merge-descriptors@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" + integrity sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E= + merge-source-map@1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/merge-source-map/-/merge-source-map-1.0.4.tgz#a5de46538dae84d4114cc5ea02b4772a6346701f" @@ -4578,6 +4784,11 @@ merge2@^1.2.3, merge2@^1.3.0: resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== +methods@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" + integrity sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4= + micromatch@^3.0.4, micromatch@^3.1.10, micromatch@^3.1.4: version "3.1.10" resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-3.1.10.tgz#70859bc95c9840952f359a068a3fc49f9ecfac23" @@ -4618,7 +4829,7 @@ mime-db@1.44.0: resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.44.0.tgz#fa11c5eb0aca1334b4233cb4d52f10c5a6272f92" integrity sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg== -mime-types@^2.1.12, mime-types@~2.1.19: +mime-types@^2.1.12, mime-types@~2.1.19, mime-types@~2.1.24: version "2.1.27" resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.27.tgz#47949f98e279ea53119f5722e0f34e529bec009f" integrity sha512-JIhqnCasI9yD+SsmkquHBxTSEuZdQX5BuQnS2Vc7puQQQ+8yiP5AY5uWhpdv4YL4VM5c6iliiYWPgJ/nJQLp7w== @@ -4712,9 +4923,9 @@ mkdirp@^1.0.3: integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== mocha@^8.1.2: - version "8.1.2" - resolved "https://registry.yarnpkg.com/mocha/-/mocha-8.1.2.tgz#d67fad13300e4f5cd48135a935ea566f96caf827" - integrity sha512-I8FRAcuACNMLQn3lS4qeWLxXqLvGf6r2CaLstDpZmMUUSmvW6Cnm1AuHxgbc7ctZVRcfwspCRbDHymPsi3dkJw== + version "8.1.3" + resolved "https://registry.yarnpkg.com/mocha/-/mocha-8.1.3.tgz#5e93f873e35dfdd69617ea75f9c68c2ca61c2ac5" + integrity sha512-ZbaYib4hT4PpF4bdSO2DohooKXIn4lDeiYqB+vTmCdr6l2woW0b6H3pf5x4sM5nwQMru9RvjjHYWVGltR50ZBw== dependencies: ansi-colors "4.1.1" browser-stdout "1.3.1" @@ -4758,9 +4969,9 @@ ms@2.1.2, ms@^2.1.1: integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== nan@^2.12.1: - version "2.14.1" - resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.1.tgz#d7be34dfa3105b91494c3147089315eff8874b01" - integrity sha512-isWHgVjnFjh2x2yuJ/tj3JbwoHu3UC2dX5G/88Cm24yB6YopVgxvBObDY7n5xW6ExmFhJpSEQqFPvq9zaXc8Jw== + version "2.14.2" + resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.2.tgz#f5376400695168f4cc694ac9393d0c9585eeea19" + integrity sha512-M2ufzIiINKCuDfBSAUr1vWQ+vuVcA9kqx8JJUsbQi6yf1uGRyb7HfpdfUr5qLXf3B/t8dPvcjhKMmlfnP47EzQ== nanomatch@^1.2.9: version "1.2.13" @@ -4784,6 +4995,11 @@ natural-compare@^1.4.0: resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc= +negotiator@0.6.2: + version "0.6.2" + resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.2.tgz#feacf7ccf525a77ae9634436a64883ffeca346fb" + integrity sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw== + nice-try@^1.0.4: version "1.0.5" resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366" @@ -4828,10 +5044,10 @@ node-libs-browser@^2.0.0: util "^0.11.0" vm-browserify "^1.0.1" -node-releases@^1.1.60: - version "1.1.60" - resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.60.tgz#6948bdfce8286f0b5d0e5a88e8384e954dfe7084" - integrity sha512-gsO4vjEdQaTusZAEebUWp2a5d7dF5DYoIpDG7WySnk7BuZDW+GPpHXoXXuYawRBr/9t5q54tirPz79kFIWg4dA== +node-releases@^1.1.61: + version "1.1.63" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.63.tgz#db6dbb388544c31e888216304e8fd170efee3ff5" + integrity sha512-ukW3iCfQaoxJkSPN+iK7KznTeqDGVJatAEuXsJERYHa9tn/KaT5lBdIyxQjLEVTzSkyjJEuQ17/vaEjrOauDkg== normalize-html-whitespace@^1.0.0: version "1.0.0" @@ -4911,7 +5127,7 @@ object-copy@^0.1.0: define-property "^0.2.5" kind-of "^3.0.3" -object-inspect@^1.7.0: +object-inspect@^1.8.0: version "1.8.0" resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.8.0.tgz#df807e5ecf53a609cc6bfe93eac3cc7be5b3a9d0" integrity sha512-jLdtEOB112fORuypAyl/50VRVIBIdVQOSUUGQHzJ4xBSbit81zRarz7GThkEFZy1RceYrWYcPcBFPQwHyAc1gA== @@ -4933,7 +5149,7 @@ object-visit@^1.0.0: dependencies: isobject "^3.0.0" -object.assign@4.1.0, object.assign@^4.1.0: +object.assign@4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.0.tgz#968bf1100d7956bb3ca086f006f846b3bc4008da" integrity sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w== @@ -4943,6 +5159,16 @@ object.assign@4.1.0, object.assign@^4.1.0: has-symbols "^1.0.0" object-keys "^1.0.11" +object.assign@^4.1.0, object.assign@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.1.tgz#303867a666cdd41936ecdedfb1f8f3e32a478cdd" + integrity sha512-VT/cxmx5yaoHSOTSyrCygIDFco+RsibY2NM0a4RdEeY/4KgqezwFtK1yr3U67xYhqJSlASm2pKhLVzPj2lr4bA== + dependencies: + define-properties "^1.1.3" + es-abstract "^1.18.0-next.0" + has-symbols "^1.0.1" + object-keys "^1.1.1" + object.getownpropertydescriptors@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.0.tgz#369bf1f9592d8ab89d712dced5cb81c7c5352649" @@ -5298,6 +5524,11 @@ path-parse@^1.0.6: resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.6.tgz#d62dbb5679405d72c4737ec58600e9ddcf06d24c" integrity sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw== +path-to-regexp@0.1.7: + version "0.1.7" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" + integrity sha1-32BBeABfUi8V60SQ5yR6G/qmf4w= + path-type@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/path-type/-/path-type-2.0.0.tgz#f012ccb8415b7096fc2daa1054c3d72389594c73" @@ -5369,9 +5600,9 @@ posix-character-classes@^0.1.0: integrity sha1-AerA/jta9xoqbAL+q7jB/vfgDqs= postcss-calc@^7.0.1: - version "7.0.3" - resolved "https://registry.yarnpkg.com/postcss-calc/-/postcss-calc-7.0.3.tgz#d65cca92a3c52bf27ad37a5f732e0587b74f1623" - integrity sha512-IB/EAEmZhIMEIhG7Ov4x+l47UaXOS1n2f4FBUk/aKllQhtSCxWhTzn0nJgkqN7fo/jcWySvWTSB6Syk9L+31bA== + version "7.0.5" + resolved "https://registry.yarnpkg.com/postcss-calc/-/postcss-calc-7.0.5.tgz#f8a6e99f12e619c2ebc23cf6c486fdc15860933e" + integrity sha512-1tKHutbGtLtEZF6PT4JSihCHfIVldU72mZ8SdZHIYriIZ9fh9k9aWSppaT8rHsyI3dX+KSR+W+Ix9BMY3AODrg== dependencies: postcss "^7.0.27" postcss-selector-parser "^6.0.2" @@ -5646,16 +5877,6 @@ postcss-reduce-transforms@^4.0.2: postcss "^7.0.0" postcss-value-parser "^3.0.0" -postcss-reporter@^6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/postcss-reporter/-/postcss-reporter-6.0.1.tgz#7c055120060a97c8837b4e48215661aafb74245f" - integrity sha512-LpmQjfRWyabc+fRygxZjpRxfhRf9u/fdlKf4VHG4TSPbV2XNsuISzYW1KL+1aQzx53CAppa1bKG4APIB/DOXXw== - dependencies: - chalk "^2.4.1" - lodash "^4.17.11" - log-symbols "^2.2.0" - postcss "^7.0.7" - postcss-resolve-nested-selector@^0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/postcss-resolve-nested-selector/-/postcss-resolve-nested-selector-0.1.1.tgz#29ccbc7c37dedfac304e9fff0bf1596b3f6a0e4e" @@ -5683,7 +5904,7 @@ postcss-scss@^2.1.1: dependencies: postcss "^7.0.6" -postcss-selector-parser@6.0.2, postcss-selector-parser@^6.0.2: +postcss-selector-parser@6.0.2: version "6.0.2" resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.2.tgz#934cf799d016c83411859e09dcecade01286ec5c" integrity sha512-36P2QR59jDTOAiIkqEprfJDsoNrvwFei3eCqKd1Y0tUsBimsq39BLp7RD+JWny3WgB1zGhJX8XVePwm9k4wdBg== @@ -5701,6 +5922,16 @@ postcss-selector-parser@^3.0.0: indexes-of "^1.0.1" uniq "^1.0.1" +postcss-selector-parser@^6.0.2: + version "6.0.4" + resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.4.tgz#56075a1380a04604c38b063ea7767a129af5c2b3" + integrity sha512-gjMeXBempyInaBqpp8gODmwZ52WaYsVOsfr4L4lDQ7n3ncD6mEyySiDtgzCT+NYC0mmeOLvtsF8iaEf0YT6dBw== + dependencies: + cssesc "^3.0.0" + indexes-of "^1.0.1" + uniq "^1.0.1" + util-deprecate "^1.0.2" + postcss-svgo@^4.0.2: version "4.0.2" resolved "https://registry.yarnpkg.com/postcss-svgo/-/postcss-svgo-4.0.2.tgz#17b997bc711b333bab143aaed3b8d3d6e3d38258" @@ -5744,7 +5975,7 @@ postcss@6.0.1: source-map "^0.5.6" supports-color "^3.2.3" -postcss@7.0.32, postcss@^7.0.0, postcss@^7.0.1, postcss@^7.0.11, postcss@^7.0.14, postcss@^7.0.17, postcss@^7.0.2, postcss@^7.0.21, postcss@^7.0.26, postcss@^7.0.27, postcss@^7.0.32, postcss@^7.0.6, postcss@^7.0.7: +postcss@7.0.32: version "7.0.32" resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.32.tgz#4310d6ee347053da3433db2be492883d62cec59d" integrity sha512-03eXong5NLnNCD05xscnGKGDZ98CyzoqPSMjOe6SuoQY7Z2hIj0Ld1g/O/UQRuOle2aRtiIRDg9tDcTGAkLfKw== @@ -5762,6 +5993,15 @@ postcss@^6.0.1: source-map "^0.6.1" supports-color "^5.4.0" +postcss@^7.0.0, postcss@^7.0.1, postcss@^7.0.11, postcss@^7.0.14, postcss@^7.0.17, postcss@^7.0.2, postcss@^7.0.21, postcss@^7.0.26, postcss@^7.0.27, postcss@^7.0.32, postcss@^7.0.6: + version "7.0.35" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.35.tgz#d2be00b998f7f211d8a276974079f2e92b970e24" + integrity sha512-3QT8bBJeX/S5zKTTjTCIjRF3If4avAT6kqxcASlTWEtAFCb9NH0OUxNDfgZSWdP5fJnBYCMEWkIFfWeugjzYMg== + dependencies: + chalk "^2.4.2" + source-map "^0.6.1" + supports-color "^6.1.0" + posthtml-parser@^0.4.0, posthtml-parser@^0.4.1: version "0.4.2" resolved "https://registry.yarnpkg.com/posthtml-parser/-/posthtml-parser-0.4.2.tgz#a132bbdf0cd4bc199d34f322f5c1599385d7c6c1" @@ -5790,9 +6030,9 @@ posthtml@^0.11.2: posthtml-render "^1.1.5" posthtml@^0.13.1: - version "0.13.3" - resolved "https://registry.yarnpkg.com/posthtml/-/posthtml-0.13.3.tgz#9702d745108d532a9d5808985e0dafd81b09f7bd" - integrity sha512-5NL2bBc4ihAyoYnY0EAQrFQbJNE1UdvgC1wjYts0hph7jYeU2fa5ki3/9U45ce9V6M1vLMEgUX2NXe/bYL+bCQ== + version "0.13.4" + resolved "https://registry.yarnpkg.com/posthtml/-/posthtml-0.13.4.tgz#ad81b3fa62b85f81ccdb5710f4ec375a4ed94934" + integrity sha512-i2oTo/+dwXGC6zaAQSF6WZEQSbEqu10hsvg01DWzGAfZmy31Iiy9ktPh9nnXDfZiYytjxTIvxoK4TI0uk4QWpw== dependencies: posthtml-parser "^0.5.0" posthtml-render "^1.2.3" @@ -5815,9 +6055,9 @@ prettier-linter-helpers@^1.0.0: fast-diff "^1.1.2" prettier@^2.0.5: - version "2.1.1" - resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.1.1.tgz#d9485dd5e499daa6cb547023b87a6cf51bee37d6" - integrity sha512-9bY+5ZWCfqj3ghYBLxApy2zf6m+NJo5GzmLTpr9FsApsfjriNnS2dahWReHMi7qNPhhHl9SYHJs2cHZLgexNIw== + version "2.1.2" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.1.2.tgz#3050700dae2e4c8b67c4c3f666cdb8af405e1ce5" + integrity sha512-16c7K+x4qVlJg9rEbXl7HEGmQyZlG4R9AgP+oHKRMsMsuk8s+ATStlf1NpDqyBI1HpVyfjLOeMhH2LvuNvV5Vg== process-nextick-args@~2.0.0: version "2.0.1" @@ -5850,6 +6090,14 @@ promise.allsettled@1.0.2: function-bind "^1.1.1" iterate-value "^1.0.0" +proxy-addr@~2.0.5: + version "2.0.6" + resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.6.tgz#fdc2336505447d3f2f2c638ed272caf614bbb2bf" + integrity sha512-dh/frvCBVmSsDYzw6n926jv974gddhkFPfiN8hPOi30Wax25QZyZEGveluCgliBnqmuM+UJmBErbAUFIoDbjOw== + dependencies: + forwarded "~0.1.2" + ipaddr.js "1.9.1" + psl@^1.1.28: version "1.8.0" resolved "https://registry.yarnpkg.com/psl/-/psl-1.8.0.tgz#9326f8bcfb013adcc005fdff056acce020e51c24" @@ -5905,6 +6153,11 @@ q@^1.1.2: resolved "https://registry.yarnpkg.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7" integrity sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc= +qs@6.7.0: + version "6.7.0" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.7.0.tgz#41dc1a015e3d581f1621776be31afb2876a9b1bc" + integrity sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ== + qs@~6.5.2: version "6.5.2" resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36" @@ -5954,6 +6207,16 @@ range-parser@~1.2.1: resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== +raw-body@2.4.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.4.0.tgz#a1ce6fb9c9bc356ca52e89256ab59059e13d0332" + integrity sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q== + dependencies: + bytes "3.1.0" + http-errors "1.7.2" + iconv-lite "0.4.24" + unpipe "1.0.0" + read-pkg-up@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-2.0.0.tgz#6b72a8048984e0c41e79510fd5e9fa99b3b549be" @@ -6078,10 +6341,10 @@ regexpp@^3.0.0, regexpp@^3.1.0: resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-3.1.0.tgz#206d0ad0a5648cffbdb8ae46438f3dc51c9f78e2" integrity sha512-ZOIzd8yVsQQA7j8GCSlPGXwg5PfmA1mrq0JP4nGhh54LaKN3xdai/vHUDu74pKwV8OxseMS65u2NImosQcSD0Q== -regexpu-core@^4.7.0: - version "4.7.0" - resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-4.7.0.tgz#fcbf458c50431b0bb7b45d6967b8192d91f3d938" - integrity sha512-TQ4KXRnIn6tz6tjnrXEkD/sshygKH/j5KzK86X8MkeHyZ8qst/LZ89j3X4/8HEIfHANTFIP/AbXakeRhWIl5YQ== +regexpu-core@^4.7.1: + version "4.7.1" + resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-4.7.1.tgz#2dea5a9a07233298fbf0db91fa9abc4c6e0f8ad6" + integrity sha512-ywH2VUraA44DZQuRKzARmw6S66mr48pQVva4LBeRhcOltJ6hExvWly5ZjFLYo67xbIxb6W1q4bAGtgfEl20zfQ== dependencies: regenerate "^1.4.0" regenerate-unicode-properties "^8.2.0" @@ -6346,7 +6609,7 @@ run-parallel@^1.1.9: resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.1.9.tgz#c9dd3a7cf9f4b2c4b6244e173a6ed866e61dd679" integrity sha512-DEqnSRTDw/Tc3FXf49zedI638Z9onwUotBMiUFKmrO2sdFKIbXamXGQ3Axd4qgphxKB4kw/qP1w5kTxnfU1B9Q== -safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@^5.1.2, safe-buffer@^5.2.0, safe-buffer@~5.1.0, safe-buffer@~5.1.1, safe-buffer@~5.2.0: +safe-buffer@5.1.2, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@^5.1.2, safe-buffer@^5.2.0, safe-buffer@~5.1.0, safe-buffer@~5.1.1, safe-buffer@~5.2.0: version "5.2.1" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== @@ -6428,7 +6691,7 @@ serialize-to-js@^3.0.0: resolved "https://registry.yarnpkg.com/serialize-to-js/-/serialize-to-js-3.1.1.tgz#b3e77d0568ee4a60bfe66287f991e104d3a1a4ac" integrity sha512-F+NGU0UHMBO4Q965tjw7rvieNVjlH6Lqi2emq/Lc9LUURYJbiCzmpi4Cy1OOjjVPtxu0c+NE85LU6968Wko5ZA== -serve-static@^1.12.4: +serve-static@1.14.1, serve-static@^1.12.4: version "1.14.1" resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.14.1.tgz#666e636dc4f010f7ef29970a88a674320898b2f9" integrity sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg== @@ -6526,6 +6789,15 @@ slice-ansi@^2.1.0: astral-regex "^1.0.0" is-fullwidth-code-point "^2.0.0" +slice-ansi@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-4.0.0.tgz#500e8dd0fd55b05815086255b3195adf2a45fe6b" + integrity sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ== + dependencies: + ansi-styles "^4.0.0" + astral-regex "^2.0.0" + is-fullwidth-code-point "^3.0.0" + snapdragon-node@^2.0.1: version "2.1.1" resolved "https://registry.yarnpkg.com/snapdragon-node/-/snapdragon-node-2.1.1.tgz#6c175f86ff14bdb0724563e8f3c1b021a286853b" @@ -6612,9 +6884,9 @@ spdx-expression-parse@^3.0.0: spdx-license-ids "^3.0.0" spdx-license-ids@^3.0.0: - version "3.0.5" - resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.5.tgz#3694b5804567a458d3c8045842a6358632f62654" - integrity sha512-J+FWzZoynJEXGphVIS+XEh3kFSjZX/1i9gFBaWQcB+/tmpe2qUsSBABpcxqxnAxFdiUFEgAX1bjYGQvIZmoz9Q== + version "3.0.6" + resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.6.tgz#c80757383c28abf7296744998cbc106ae8b854ce" + integrity sha512-+orQK83kyMva3WyPf59k1+Y525csj5JejicWut55zeTWANuN17qSiSLUXWtzHeNWORSvT7GLDJ/E/XiIWoXBTw== specificity@^0.4.1: version "0.4.1" @@ -6841,7 +7113,7 @@ strip-json-comments@3.0.1: resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.0.1.tgz#85713975a91fb87bf1b305cca77395e40d2a64a7" integrity sha512-VTyMAUfdm047mwKl+u79WIdrZxtFtn+nBxHeb844XBQ9uMNTuTHdx2hc5RiAJYqwTj3wc/xe5HLSdJSkJ+WfZw== -strip-json-comments@^3.1.0: +strip-json-comments@^3.1.0, strip-json-comments@^3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== @@ -6873,18 +7145,20 @@ stylelint-config-recommended@^3.0.0: integrity sha512-F6yTRuc06xr1h5Qw/ykb2LuFynJ2IxkKfCMf+1xqPffkxh0S09Zc902XCffcsw/XMFq/OzQ1w54fLIDtmRNHnQ== stylelint@^13.0.0: - version "13.6.1" - resolved "https://registry.yarnpkg.com/stylelint/-/stylelint-13.6.1.tgz#cc1d76338116d55e8ff2be94c4a4386c1239b878" - integrity sha512-XyvKyNE7eyrqkuZ85Citd/Uv3ljGiuYHC6UiztTR6sWS9rza8j3UeQv/eGcQS9NZz/imiC4GKdk1EVL3wst5vw== + version "13.7.2" + resolved "https://registry.yarnpkg.com/stylelint/-/stylelint-13.7.2.tgz#6f3c58eea4077680ed0ceb0d064b22b100970486" + integrity sha512-mmieorkfmO+ZA6CNDu1ic9qpt4tFvH2QUB7vqXgrMVHe5ENU69q7YDq0YUg/UHLuCsZOWhUAvcMcLzLDIERzSg== dependencies: - "@stylelint/postcss-css-in-js" "^0.37.1" + "@stylelint/postcss-css-in-js" "^0.37.2" "@stylelint/postcss-markdown" "^0.36.1" - autoprefixer "^9.8.0" + autoprefixer "^9.8.6" balanced-match "^1.0.0" chalk "^4.1.0" - cosmiconfig "^6.0.0" + cosmiconfig "^7.0.0" debug "^4.1.1" execall "^2.0.0" + fast-glob "^3.2.4" + fastest-levenshtein "^1.0.12" file-entry-cache "^5.0.1" get-stdin "^8.0.0" global-modules "^2.0.0" @@ -6895,18 +7169,16 @@ stylelint@^13.0.0: import-lazy "^4.0.0" imurmurhash "^0.1.4" known-css-properties "^0.19.0" - leven "^3.1.0" - lodash "^4.17.15" + lodash "^4.17.20" log-symbols "^4.0.0" mathml-tag-names "^2.1.3" - meow "^7.0.1" + meow "^7.1.1" micromatch "^4.0.2" normalize-selector "^0.2.0" postcss "^7.0.32" postcss-html "^0.36.0" postcss-less "^3.1.4" postcss-media-query-parser "^0.2.3" - postcss-reporter "^6.0.1" postcss-resolve-nested-selector "^0.1.1" postcss-safe-parser "^4.0.2" postcss-sass "^0.4.4" @@ -6922,7 +7194,7 @@ stylelint@^13.0.0: style-search "^0.1.0" sugarss "^2.0.0" svg-tags "^1.0.0" - table "^5.4.6" + table "^6.0.1" v8-compile-cache "^2.1.1" write-file-atomic "^3.0.3" @@ -6933,7 +7205,7 @@ sugarss@^2.0.0: dependencies: postcss "^7.0.2" -supports-color@7.1.0, supports-color@^7.1.0: +supports-color@7.1.0: version "7.1.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.1.0.tgz#68e32591df73e25ad1c4b49108a2ec507962bfd1" integrity sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g== @@ -6966,6 +7238,13 @@ supports-color@^6.1.0: dependencies: has-flag "^3.0.0" +supports-color@^7.1.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" + integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== + dependencies: + has-flag "^4.0.0" + svg-tags@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/svg-tags/-/svg-tags-1.0.0.tgz#58f71cee3bd519b59d4b2a843b6c7de64ac04764" @@ -6995,7 +7274,7 @@ symbol-tree@^3.2.2: resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2" integrity sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw== -table@^5.2.3, table@^5.4.6: +table@^5.2.3: version "5.4.6" resolved "https://registry.yarnpkg.com/table/-/table-5.4.6.tgz#1292d19500ce3f86053b05f0e8e7e4a3bb21079e" integrity sha512-wmEc8m4fjnob4gt5riFRtTu/6+4rSe12TpAELNSqHMfF3IqnA+CH37USM6/YR3qRZv7e56kAEAtd6nKZaxe0Ug== @@ -7005,6 +7284,16 @@ table@^5.2.3, table@^5.4.6: slice-ansi "^2.1.0" string-width "^3.0.0" +table@^6.0.1: + version "6.0.3" + resolved "https://registry.yarnpkg.com/table/-/table-6.0.3.tgz#e5b8a834e37e27ad06de2e0fda42b55cfd8a0123" + integrity sha512-8321ZMcf1B9HvVX/btKv8mMZahCjn2aYrDlpqHaBFCfnox64edeH9kEid0vTLTRR8gWR2A20aDgeuTTea4sVtw== + dependencies: + ajv "^6.12.4" + lodash "^4.17.20" + slice-ansi "^4.0.0" + string-width "^4.2.0" + tar-fs@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/tar-fs/-/tar-fs-2.1.0.tgz#d1cdd121ab465ee0eb9ccde2d35049d3f3daf0d5" @@ -7016,11 +7305,11 @@ tar-fs@^2.0.0: tar-stream "^2.0.0" tar-stream@^2.0.0: - version "2.1.3" - resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-2.1.3.tgz#1e2022559221b7866161660f118255e20fa79e41" - integrity sha512-Z9yri56Dih8IaK8gncVPx4Wqt86NDmQTSh49XLZgjWpGZL9GK9HKParS2scqHCC4w6X9Gh2jwaU45V47XTKwVA== + version "2.1.4" + resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-2.1.4.tgz#c4fb1a11eb0da29b893a5b25476397ba2d053bfa" + integrity sha512-o3pS2zlG4gxr67GmFYBLlq+dM8gyRGUOvsrHclSkvtVtQbjV0s/+ZE8OpICbaj8clrX3tjeHngYGP7rweaBnuw== dependencies: - bl "^4.0.1" + bl "^4.0.3" end-of-stream "^1.4.1" fs-constants "^1.0.0" inherits "^2.0.3" @@ -7200,9 +7489,9 @@ tsconfig-paths@^3.9.0: strip-bom "^3.0.0" tslib@^1.8.1: - version "1.13.0" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.13.0.tgz#c881e13cc7015894ed914862d276436fa9a47043" - integrity sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q== + version "1.14.1" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" + integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== tsutils@^3.17.1: version "3.17.1" @@ -7257,6 +7546,14 @@ type-fest@^0.8.1: resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d" integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA== +type-is@~1.6.17, type-is@~1.6.18: + version "1.6.18" + resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" + integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g== + dependencies: + media-typer "0.3.0" + mime-types "~2.1.24" + typedarray-to-buffer@^3.1.5: version "3.1.5" resolved "https://registry.yarnpkg.com/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz#a97ee7a9ff42691b9f783ff1bc5112fe3fca9080" @@ -7456,6 +7753,11 @@ universalify@^1.0.0: resolved "https://registry.yarnpkg.com/universalify/-/universalify-1.0.0.tgz#b61a1da173e8435b2fe3c67d29b9adf8594bd16d" integrity sha512-rb6X1W158d7pRQBg5gkR8uPaSfiids68LTJQYOtEUhoJUWBdaQHsuT/EUduxXYxcrt4r5PJ4fuHW1MHT6p0qug== +unpipe@1.0.0, unpipe@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" + integrity sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw= + unquote@~1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/unquote/-/unquote-1.1.1.tgz#8fded7324ec6e88a0ff8b905e7c098cdc086d544" @@ -7480,9 +7782,9 @@ update-section@^0.3.0: integrity sha1-RY8Xgg03gg3GDiC4bZQ5GwASMVg= uri-js@^4.2.2: - version "4.2.2" - resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.2.2.tgz#94c540e1ff772956e2299507c010aea6c8838eb0" - integrity sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ== + version "4.4.0" + resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.0.tgz#aa714261de793e8a82347a7bcc9ce74e86f28602" + integrity sha512-B0yRTzYdUCCn9n+F4+Gh4yIDtMQcaJsmYBDsTSG8g/OejKBodLQ2IHfN3bM7jUsRXndopT7OIXWdYqc1fjmV6g== dependencies: punycode "^2.1.0" @@ -7504,7 +7806,7 @@ use@^3.1.0: resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f" integrity sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ== -util-deprecate@^1.0.1, util-deprecate@~1.0.1: +util-deprecate@^1.0.1, util-deprecate@^1.0.2, util-deprecate@~1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8= @@ -7533,6 +7835,11 @@ util@^0.11.0: dependencies: inherits "2.0.3" +utils-merge@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" + integrity sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM= + uuid@^3.3.2: version "3.4.0" resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee" @@ -7551,6 +7858,11 @@ validate-npm-package-license@^3.0.1: spdx-correct "^3.0.0" spdx-expression-parse "^3.0.0" +vary@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" + integrity sha1-IpnwLG3tMNSllhsLn3RSShj2NPw= + vendors@^1.0.0: version "1.0.4" resolved "https://registry.yarnpkg.com/vendors/-/vendors-1.0.4.tgz#e2b800a53e7a29b93506c3cf41100d16c4c4ad8e" @@ -7789,7 +8101,7 @@ yallist@^4.0.0: resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== -yaml@^1.7.2: +yaml@^1.10.0: version "1.10.0" resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.0.tgz#3b593add944876077d4d683fee01081bd9fff31e" integrity sha512-yr2icI4glYaNG+KWONODapy2/jDdMSDnrONSjblABjD9B4Z5LgiircSt8m8sRZFNi08kG9Sm0uSHtEmP3zaEGg== @@ -7863,9 +8175,9 @@ yargs@^14.2.3: yargs-parser "^15.0.1" yarn@^1.22.4: - version "1.22.4" - resolved "https://registry.yarnpkg.com/yarn/-/yarn-1.22.4.tgz#01c1197ca5b27f21edc8bc472cd4c8ce0e5a470e" - integrity sha512-oYM7hi/lIWm9bCoDMEWgffW8aiNZXCWeZ1/tGy0DWrN6vmzjCXIKu2Y21o8DYVBUtiktwKcNoxyGl/2iKLUNGA== + version "1.22.10" + resolved "https://registry.yarnpkg.com/yarn/-/yarn-1.22.10.tgz#c99daa06257c80f8fa2c3f1490724e394c26b18c" + integrity sha512-IanQGI9RRPAN87VGTF7zs2uxkSyQSrSPsju0COgbsKQOOXr5LtcVPeyXWgwVa0ywG3d8dg6kSYKGBuYK021qeA== yn@3.1.1: version "3.1.1" From 9f25cc6d5d1c08cc7d1a4fd583a491680da9cd6a Mon Sep 17 00:00:00 2001 From: Asher Date: Thu, 15 Oct 2020 17:47:20 -0500 Subject: [PATCH 087/247] Move providers from `app` to `routes` --- src/node/entry.ts | 12 ++++++------ src/node/{app => routes}/health.ts | 0 src/node/{app => routes}/login.ts | 0 src/node/{app => routes}/proxy.ts | 0 src/node/{app => routes}/static.ts | 0 src/node/{app => routes}/update.ts | 0 src/node/{app => routes}/vscode.ts | 0 test/update.test.ts | 2 +- 8 files changed, 7 insertions(+), 7 deletions(-) rename src/node/{app => routes}/health.ts (100%) rename src/node/{app => routes}/login.ts (100%) rename src/node/{app => routes}/proxy.ts (100%) rename src/node/{app => routes}/static.ts (100%) rename src/node/{app => routes}/update.ts (100%) rename src/node/{app => routes}/vscode.ts (100%) diff --git a/src/node/entry.ts b/src/node/entry.ts index 826d9a484..630e4b0b7 100644 --- a/src/node/entry.ts +++ b/src/node/entry.ts @@ -5,12 +5,12 @@ import http from "http" import * as path from "path" import { CliMessage, OpenCommandPipeArgs } from "../../lib/vscode/src/vs/server/ipc" import { plural } from "../common/util" -import { HealthHttpProvider } from "./app/health" -import { LoginHttpProvider } from "./app/login" -import { ProxyHttpProvider } from "./app/proxy" -import { StaticHttpProvider } from "./app/static" -import { UpdateHttpProvider } from "./app/update" -import { VscodeHttpProvider } from "./app/vscode" +import { HealthHttpProvider } from "./routes/health" +import { LoginHttpProvider } from "./routes/login" +import { ProxyHttpProvider } from "./routes/proxy" +import { StaticHttpProvider } from "./routes/static" +import { UpdateHttpProvider } from "./routes/update" +import { VscodeHttpProvider } from "./routes/vscode" import { DefaultedArgs, optionDescriptions, diff --git a/src/node/app/health.ts b/src/node/routes/health.ts similarity index 100% rename from src/node/app/health.ts rename to src/node/routes/health.ts diff --git a/src/node/app/login.ts b/src/node/routes/login.ts similarity index 100% rename from src/node/app/login.ts rename to src/node/routes/login.ts diff --git a/src/node/app/proxy.ts b/src/node/routes/proxy.ts similarity index 100% rename from src/node/app/proxy.ts rename to src/node/routes/proxy.ts diff --git a/src/node/app/static.ts b/src/node/routes/static.ts similarity index 100% rename from src/node/app/static.ts rename to src/node/routes/static.ts diff --git a/src/node/app/update.ts b/src/node/routes/update.ts similarity index 100% rename from src/node/app/update.ts rename to src/node/routes/update.ts diff --git a/src/node/app/vscode.ts b/src/node/routes/vscode.ts similarity index 100% rename from src/node/app/vscode.ts rename to src/node/routes/vscode.ts diff --git a/test/update.test.ts b/test/update.test.ts index 9e27eefaf..093429be3 100644 --- a/test/update.test.ts +++ b/test/update.test.ts @@ -2,8 +2,8 @@ import * as assert from "assert" import * as fs from "fs-extra" import * as http from "http" import * as path from "path" -import { LatestResponse, UpdateHttpProvider } from "../src/node/app/update" import { AuthType } from "../src/node/cli" +import { LatestResponse, UpdateHttpProvider } from "../src/node/routes/update" import { SettingsProvider, UpdateSettings } from "../src/node/settings" import { tmpdir } from "../src/node/util" From 8e93e281628a09d66cdc131cf31816de8a529bd9 Mon Sep 17 00:00:00 2001 From: Asher Date: Fri, 16 Oct 2020 12:31:22 -0500 Subject: [PATCH 088/247] Strip config file password from debug log --- src/node/cli.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/node/cli.ts b/src/node/cli.ts index 7f44f9ea5..a5757aa6b 100644 --- a/src/node/cli.ts +++ b/src/node/cli.ts @@ -334,7 +334,7 @@ export const parse = ( throw new Error("--cert-key is missing") } - logger.debug("parsed command line", field("args", args)) + logger.debug(() => ["parsed command line", field("args", { ...args, password: undefined })]) return args } From 4a3d2e5a9430756f68602deaa29b6349982b461b Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Wed, 21 Oct 2020 16:39:30 -0400 Subject: [PATCH 089/247] Remove unnecessary whoami Closes #2213 --- ci/release-image/Dockerfile | 1 + ci/release-image/entrypoint.sh | 4 ---- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/ci/release-image/Dockerfile b/ci/release-image/Dockerfile index 5c31ecbe7..a0b6aed71 100644 --- a/ci/release-image/Dockerfile +++ b/ci/release-image/Dockerfile @@ -43,5 +43,6 @@ EXPOSE 8080 # the uid will remain the same. note: only relevant if -u isn't passed to # docker-run. USER 1000 +ENV USER=coder WORKDIR /home/coder ENTRYPOINT ["/usr/bin/entrypoint.sh", "--bind-addr", "0.0.0.0:8080", "."] diff --git a/ci/release-image/entrypoint.sh b/ci/release-image/entrypoint.sh index b4343e7ed..abf9fddb3 100755 --- a/ci/release-image/entrypoint.sh +++ b/ci/release-image/entrypoint.sh @@ -1,10 +1,6 @@ #!/bin/sh set -eu -# This isn't set by default. -USER="$(whoami)" -export USER - if [ "${DOCKER_USER-}" ] && [ "$DOCKER_USER" != "$USER" ]; then echo "$DOCKER_USER ALL=(ALL) NOPASSWD:ALL" | sudo tee -a /etc/sudoers.d/nopasswd > /dev/null # Unfortunately we cannot change $HOME as we cannot move any bind mounts From bca1bcfc03f5bfb0671b62b29766f6cbb271a27e Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Wed, 21 Oct 2020 16:45:53 -0400 Subject: [PATCH 090/247] Fix README formatting --- README.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 433413bb9..8e68c3e16 100644 --- a/README.md +++ b/README.md @@ -43,11 +43,12 @@ The install script will print out how to run and start using code-server. Docs on the install script, manual installation and docker image are at [./doc/install.md](./doc/install.md). ### Alpha Program 🐣 + We're working on a cloud platform to make deploying and managing code-server easier. If you don't want to worry about -* TLS -* Authentication -* Port Forwarding +- TLS +- Authentication +- Port Forwarding consider [joining our alpha program](https://codercom.typeform.com/to/U4IKyv0W). From 759a78d9d8cb372a8d7436eb88575b074a9c54ef Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Wed, 21 Oct 2020 17:00:20 -0400 Subject: [PATCH 091/247] install.sh: Rename SSH_FLAGS to RSH_FLAGS --- install.sh | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/install.sh b/install.sh index 08e3a51e8..4865d4621 100755 --- a/install.sh +++ b/install.sh @@ -109,7 +109,7 @@ main() { VERSION \ OPTIONAL \ ALL_FLAGS \ - SSH_ARGS + RSH_ARGS ALL_FLAGS="" while [ "$#" -gt 0 ]; do @@ -152,7 +152,7 @@ main() { shift # We remove the -- added above. ALL_FLAGS="${ALL_FLAGS% --}" - SSH_ARGS="$*" + RSH_ARGS="$*" break ;; -*) @@ -161,7 +161,7 @@ main() { exit 1 ;; *) - SSH_ARGS="$*" + RSH_ARGS="$*" break ;; esac @@ -169,9 +169,9 @@ main() { shift done - if [ "${SSH_ARGS-}" ]; then - echoh "Installing remotely with ssh $SSH_ARGS" - curl -fsSL https://code-server.dev/install.sh | prefix "$SSH_ARGS" ssh "$SSH_ARGS" sh -s -- "$ALL_FLAGS" + if [ "${RSH_ARGS-}" ]; then + echoh "Installing remotely with ssh $RSH_ARGS" + curl -fsSL https://code-server.dev/install.sh | prefix "$RSH_ARGS" ssh "$RSH_ARGS" sh -s -- "$ALL_FLAGS" return fi From 30f30305309338217e9634c8fb99ccaf88a07383 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Wed, 21 Oct 2020 17:05:58 -0400 Subject: [PATCH 092/247] install.sh: Allow customizing remote shell with --rsh --- install.sh | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/install.sh b/install.sh index 4865d4621..2fd90d71b 100755 --- a/install.sh +++ b/install.sh @@ -24,7 +24,7 @@ ${not_curl_usage-} Usage: $arg0 [--dry-run] [--version X.X.X] [--method detect] \ - [--prefix ~/.local] [user@host] + [--prefix ~/.local] [--rsh ssh] [user@host] --dry-run Echo the commands for the install process without running them. @@ -45,6 +45,9 @@ Usage: and the binary symlinked into ~/.local/bin/code-server To install system wide pass ---prefix=/usr/local + --rsh + Specifies the remote shell for remote installation. Defaults to ssh. + - For Debian, Ubuntu and Raspbian it will install the latest deb package. - For Fedora, CentOS, RHEL and openSUSE it will install the latest rpm package. - For Arch Linux it will install the AUR package. @@ -109,7 +112,8 @@ main() { VERSION \ OPTIONAL \ ALL_FLAGS \ - RSH_ARGS + RSH_ARGS \ + RSH ALL_FLAGS="" while [ "$#" -gt 0 ]; do @@ -144,6 +148,13 @@ main() { --version=*) VERSION="$(parse_arg "$@")" ;; + --rsh) + RSH="$(parse_arg "$@")" + shift + ;; + --rsh=*) + RSH="$(parse_arg "$@")" + ;; -h | --h | -help | --help) usage exit 0 @@ -170,8 +181,8 @@ main() { done if [ "${RSH_ARGS-}" ]; then - echoh "Installing remotely with ssh $RSH_ARGS" - curl -fsSL https://code-server.dev/install.sh | prefix "$RSH_ARGS" ssh "$RSH_ARGS" sh -s -- "$ALL_FLAGS" + echoh "Installing remotely with $RSH $RSH_ARGS" + curl -fsSL https://code-server.dev/install.sh | prefix "$RSH_ARGS" "$RSH" "$RSH_ARGS" sh -s -- "$ALL_FLAGS" return fi From a0b7bf218042b5b1b8f10e73fa50f1809718ba55 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Thu, 22 Oct 2020 01:43:22 -0400 Subject: [PATCH 093/247] install.sh: Default $RSH to ssh --- install.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/install.sh b/install.sh index 2fd90d71b..2c0240297 100755 --- a/install.sh +++ b/install.sh @@ -181,6 +181,7 @@ main() { done if [ "${RSH_ARGS-}" ]; then + RSH="${RSH-ssh}" echoh "Installing remotely with $RSH $RSH_ARGS" curl -fsSL https://code-server.dev/install.sh | prefix "$RSH_ARGS" "$RSH" "$RSH_ARGS" sh -s -- "$ALL_FLAGS" return From 79443c14ff41f38d3f05e0a53e4aa592e3c0e418 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Fri, 23 Oct 2020 12:07:08 -0400 Subject: [PATCH 094/247] release-image: Remap UID within the image before handling $DOCKER_USER (#2223) If do not update the UID within the passwd database to match whatever uid the container is being ran as, then sudo will not work when renaming the user to match $DOCKER_USER as it will complain about the current user being non-existent. --- ci/release-image/entrypoint.sh | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/ci/release-image/entrypoint.sh b/ci/release-image/entrypoint.sh index abf9fddb3..4f2f7cfe2 100755 --- a/ci/release-image/entrypoint.sh +++ b/ci/release-image/entrypoint.sh @@ -1,7 +1,11 @@ #!/bin/sh set -eu -if [ "${DOCKER_USER-}" ] && [ "$DOCKER_USER" != "$USER" ]; then +# We do this first to ensure sudo works below when renaming the user. +# Otherwise the current container UID may not exist in the passwd database. +eval "$(fixuid -q)" + +if [ "${DOCKER_USER-}" ]; then echo "$DOCKER_USER ALL=(ALL) NOPASSWD:ALL" | sudo tee -a /etc/sudoers.d/nopasswd > /dev/null # Unfortunately we cannot change $HOME as we cannot move any bind mounts # nor can we bind mount $HOME into a new home as that requires a privileged container. @@ -11,7 +15,6 @@ if [ "${DOCKER_USER-}" ] && [ "$DOCKER_USER" != "$USER" ]; then USER="$DOCKER_USER" sudo sed -i "/coder/d" /etc/sudoers.d/nopasswd - sudo sed -i "s/coder/$DOCKER_USER/g" /etc/fixuid/config.yml fi -dumb-init fixuid -q /usr/bin/code-server "$@" +dumb-init /usr/bin/code-server "$@" From 62735da69466a444561ab9b1115dc7c4d496d455 Mon Sep 17 00:00:00 2001 From: Asher Date: Wed, 14 Oct 2020 17:06:42 -0500 Subject: [PATCH 095/247] v3.6.1 --- doc/install.md | 14 +++++++------- package.json | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/doc/install.md b/doc/install.md index b53a60675..c33299cf6 100644 --- a/doc/install.md +++ b/doc/install.md @@ -79,8 +79,8 @@ commands presented in the rest of this document. ## Debian, Ubuntu ```bash -curl -fOL https://github.com/cdr/code-server/releases/download/v3.6.0/code-server_3.6.0_amd64.deb -sudo dpkg -i code-server_3.6.0_amd64.deb +curl -fOL https://github.com/cdr/code-server/releases/download/v3.6.1/code-server_3.6.1_amd64.deb +sudo dpkg -i code-server_3.6.1_amd64.deb sudo systemctl enable --now code-server@$USER # Now visit http://127.0.0.1:8080. Your password is in ~/.config/code-server/config.yaml ``` @@ -88,8 +88,8 @@ sudo systemctl enable --now code-server@$USER ## Fedora, CentOS, RHEL, SUSE ```bash -curl -fOL https://github.com/cdr/code-server/releases/download/v3.6.0/code-server-3.6.0-amd64.rpm -sudo rpm -i code-server-3.6.0-amd64.rpm +curl -fOL https://github.com/cdr/code-server/releases/download/v3.6.1/code-server-3.6.1-amd64.rpm +sudo rpm -i code-server-3.6.1-amd64.rpm sudo systemctl enable --now code-server@$USER # Now visit http://127.0.0.1:8080. Your password is in ~/.config/code-server/config.yaml ``` @@ -158,10 +158,10 @@ Here is an example script for installing and using a standalone `code-server` re ```bash mkdir -p ~/.local/lib ~/.local/bin -curl -fL https://github.com/cdr/code-server/releases/download/v3.6.0/code-server-3.6.0-linux-amd64.tar.gz \ +curl -fL https://github.com/cdr/code-server/releases/download/v3.6.1/code-server-3.6.1-linux-amd64.tar.gz \ | tar -C ~/.local/lib -xz -mv ~/.local/lib/code-server-3.6.0-linux-amd64 ~/.local/lib/code-server-3.6.0 -ln -s ~/.local/lib/code-server-3.6.0/bin/code-server ~/.local/bin/code-server +mv ~/.local/lib/code-server-3.6.1-linux-amd64 ~/.local/lib/code-server-3.6.1 +ln -s ~/.local/lib/code-server-3.6.1/bin/code-server ~/.local/bin/code-server PATH="~/.local/bin:$PATH" code-server # Now visit http://127.0.0.1:8080. Your password is in ~/.config/code-server/config.yaml diff --git a/package.json b/package.json index cc3edd30a..6cb9dd17a 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "code-server", "license": "MIT", - "version": "3.6.0", + "version": "3.6.1", "description": "Run VS Code on a remote server.", "homepage": "https://github.com/cdr/code-server", "bugs": { From 71dc5c75427c653a472b2cad6a07495e0827e9bb Mon Sep 17 00:00:00 2001 From: Asher Date: Fri, 16 Oct 2020 12:43:49 -0500 Subject: [PATCH 096/247] Switch to Express It doesn't do anything yet. --- src/node/app.ts | 57 ++++++++++++++++++++++++++++++++++++++++ src/node/entry.ts | 65 ++++++++++++---------------------------------- src/node/plugin.ts | 20 +++++++------- 3 files changed, 84 insertions(+), 58 deletions(-) create mode 100644 src/node/app.ts diff --git a/src/node/app.ts b/src/node/app.ts new file mode 100644 index 000000000..7e5e7a2ef --- /dev/null +++ b/src/node/app.ts @@ -0,0 +1,57 @@ +import { logger } from "@coder/logger" +import express, { Express } from "express" +import { promises as fs } from "fs" +import http from "http" +import * as httpolyglot from "httpolyglot" +import { DefaultedArgs } from "./cli" + +/** + * Create an Express app and an HTTP/S server to serve it. + */ +export const createApp = async (args: DefaultedArgs): Promise<[Express, http.Server]> => { + const app = express() + + const server = args.cert + ? httpolyglot.createServer( + { + cert: args.cert && (await fs.readFile(args.cert.value)), + key: args["cert-key"] && (await fs.readFile(args["cert-key"])), + }, + app, + ) + : http.createServer(app) + + await new Promise(async (resolve, reject) => { + server.on("error", reject) + if (args.socket) { + try { + await fs.unlink(args.socket) + } catch (error) { + if (error.code !== "ENOENT") { + logger.error(error.message) + } + } + server.listen(args.socket, resolve) + } else { + // [] is the correct format when using :: but Node errors with them. + server.listen(args.port, args.host.replace(/^\[|\]$/g, ""), resolve) + } + }) + + return [app, server] +} + +/** + * Get the address of a server as a string (protocol not included) while + * ensuring there is one (will throw if there isn't). + */ +export const ensureAddress = (server: http.Server): string => { + const addr = server.address() + if (!addr) { + throw new Error("server has no address") + } + if (typeof addr !== "string") { + return `${addr.address}:${addr.port}` + } + return addr +} diff --git a/src/node/entry.ts b/src/node/entry.ts index 630e4b0b7..e8c6b4e1f 100644 --- a/src/node/entry.ts +++ b/src/node/entry.ts @@ -5,13 +5,9 @@ import http from "http" import * as path from "path" import { CliMessage, OpenCommandPipeArgs } from "../../lib/vscode/src/vs/server/ipc" import { plural } from "../common/util" -import { HealthHttpProvider } from "./routes/health" -import { LoginHttpProvider } from "./routes/login" -import { ProxyHttpProvider } from "./routes/proxy" -import { StaticHttpProvider } from "./routes/static" -import { UpdateHttpProvider } from "./routes/update" -import { VscodeHttpProvider } from "./routes/vscode" +import { createApp, ensureAddress } from "./app" import { + AuthType, DefaultedArgs, optionDescriptions, parse, @@ -21,9 +17,8 @@ import { shouldRunVsCodeCli, } from "./cli" import { coderCloudBind } from "./coder-cloud" -import { AuthType, HttpServer, HttpServerOptions } from "./http" import { loadPlugins } from "./plugin" -import { hash, humanPath, open } from "./util" +import { humanPath, open } from "./util" import { ipcMain, WrapperProcess } from "./wrapper" let pkg: { version?: string; commit?: string } = {} @@ -117,65 +112,39 @@ export const openInExistingInstance = async (args: DefaultedArgs, socketPath: st } const main = async (args: DefaultedArgs): Promise => { + logger.info(`code-server ${version} ${commit}`) + logger.info(`Using user-data-dir ${humanPath(args["user-data-dir"])}`) logger.trace(`Using extensions-dir ${humanPath(args["extensions-dir"])}`) if (args.auth === AuthType.Password && !args.password) { throw new Error("Please pass in a password via the config file or $PASSWORD") } - - // Spawn the main HTTP server. - const options: HttpServerOptions = { - auth: args.auth, - commit, - host: args.host, - // The hash does not add any actual security but we do it for obfuscation purposes. - password: args.password ? hash(args.password) : undefined, - port: args.port, - proxyDomains: args["proxy-domain"], - socket: args.socket, - cert: args.cert && args.cert.value, - certKey: args["cert-key"], - } - - if (options.cert && !options.certKey) { - throw new Error("--cert-key is missing") - } - - const httpServer = new HttpServer(options) - httpServer.registerHttpProvider(["/", "/vscode"], VscodeHttpProvider, args) - httpServer.registerHttpProvider("/update", UpdateHttpProvider, false) - httpServer.registerHttpProvider("/proxy", ProxyHttpProvider) - httpServer.registerHttpProvider("/login", LoginHttpProvider, args.config!, args.usingEnvPassword) - httpServer.registerHttpProvider("/static", StaticHttpProvider) - httpServer.registerHttpProvider("/healthz", HealthHttpProvider, httpServer.heart) - - await loadPlugins(httpServer, args) - ipcMain.onDispose(() => { - httpServer.dispose().then((errors) => { - errors.forEach((error) => logger.error(error.message)) - }) + // TODO: register disposables }) - logger.info(`code-server ${version} ${commit}`) - logger.info(`Using config file ${humanPath(args.config)}`) + const [app, server] = await createApp(args) + const serverAddress = ensureAddress(server) - const serverAddress = await httpServer.listen() + // TODO: register routes + await loadPlugins(app, args) + + logger.info(`Using config file ${humanPath(args.config)}`) logger.info(`HTTP server listening on ${serverAddress} ${args.link ? "(randomized by --link)" : ""}`) if (args.auth === AuthType.Password) { + logger.info(" - Authentication is enabled") if (args.usingEnvPassword) { logger.info(" - Using password from $PASSWORD") } else { logger.info(` - Using password from ${humanPath(args.config)}`) } - logger.info(" - To disable use `--auth none`") } else { - logger.info(` - No authentication ${args.link ? "(disabled by --link)" : ""}`) + logger.info(` - Authentication is disabled ${args.link ? "(disabled by --link)" : ""}`) } - if (httpServer.protocol === "https") { + if (args.cert) { logger.info( args.cert && args.cert.value ? ` - Using provided certificate and key for HTTPS` @@ -192,7 +161,7 @@ const main = async (args: DefaultedArgs): Promise => { if (args.link) { try { - await coderCloudBind(serverAddress!, args.link.value) + await coderCloudBind(serverAddress, args.link.value) logger.info(" - Connected to cloud agent") } catch (err) { logger.error(err.message) @@ -200,7 +169,7 @@ const main = async (args: DefaultedArgs): Promise => { } } - if (serverAddress && !options.socket && args.open) { + if (serverAddress && !args.socket && args.open) { // The web socket doesn't seem to work if browsing with 0.0.0.0. const openAddress = serverAddress.replace(/:\/\/0.0.0.0/, "://localhost") await open(openAddress).catch((error: Error) => { diff --git a/src/node/plugin.ts b/src/node/plugin.ts index 7469f317d..20c19d3e7 100644 --- a/src/node/plugin.ts +++ b/src/node/plugin.ts @@ -1,14 +1,14 @@ import { field, logger } from "@coder/logger" +import { Express } from "express" import * as fs from "fs" import * as path from "path" import * as util from "util" import { Args } from "./cli" -import { HttpServer } from "./http" import { paths } from "./util" /* eslint-disable @typescript-eslint/no-var-requires */ -export type Activate = (httpServer: HttpServer, args: Args) => void +export type Activate = (app: Express, args: Args) => void /** * Plugins must implement this interface. @@ -30,10 +30,10 @@ require("module")._load = function (request: string, parent: object, isMain: boo /** * Load a plugin and run its activation function. */ -const loadPlugin = async (pluginPath: string, httpServer: HttpServer, args: Args): Promise => { +const loadPlugin = async (pluginPath: string, app: Express, args: Args): Promise => { try { const plugin: Plugin = require(pluginPath) - plugin.activate(httpServer, args) + plugin.activate(app, args) const packageJson = require(path.join(pluginPath, "package.json")) logger.debug( @@ -50,12 +50,12 @@ const loadPlugin = async (pluginPath: string, httpServer: HttpServer, args: Args /** * Load all plugins in the specified directory. */ -const _loadPlugins = async (pluginDir: string, httpServer: HttpServer, args: Args): Promise => { +const _loadPlugins = async (pluginDir: string, app: Express, args: Args): Promise => { try { const files = await util.promisify(fs.readdir)(pluginDir, { withFileTypes: true, }) - await Promise.all(files.map((file) => loadPlugin(path.join(pluginDir, file.name), httpServer, args))) + await Promise.all(files.map((file) => loadPlugin(path.join(pluginDir, file.name), app, args))) } catch (error) { if (error.code !== "ENOENT") { logger.warn(error.message) @@ -68,17 +68,17 @@ const _loadPlugins = async (pluginDir: string, httpServer: HttpServer, args: Arg * `CS_PLUGIN_PATH` (colon-separated), and individual plugins specified by * `CS_PLUGIN` (also colon-separated). */ -export const loadPlugins = async (httpServer: HttpServer, args: Args): Promise => { +export const loadPlugins = async (app: Express, args: Args): Promise => { const pluginPath = process.env.CS_PLUGIN_PATH || `${path.join(paths.data, "plugins")}:/usr/share/code-server/plugins` const plugin = process.env.CS_PLUGIN || "" await Promise.all([ // Built-in plugins. - _loadPlugins(path.resolve(__dirname, "../../plugins"), httpServer, args), + _loadPlugins(path.resolve(__dirname, "../../plugins"), app, args), // User-added plugins. ...pluginPath .split(":") .filter((p) => !!p) - .map((dir) => _loadPlugins(path.resolve(dir), httpServer, args)), + .map((dir) => _loadPlugins(path.resolve(dir), app, args)), // Individual plugins so you don't have to symlink or move them into a // directory specifically for plugins. This lets you load plugins that are // on the same level as other directories that are not plugins (if you tried @@ -87,6 +87,6 @@ export const loadPlugins = async (httpServer: HttpServer, args: Args): Promise !!p) - .map((dir) => loadPlugin(path.resolve(dir), httpServer, args)), + .map((dir) => loadPlugin(path.resolve(dir), app, args)), ]) } From 4b6cbacbad522ceb259845bc6d315aa8310a2945 Mon Sep 17 00:00:00 2001 From: Asher Date: Fri, 16 Oct 2020 14:45:49 -0500 Subject: [PATCH 097/247] Add file for global constants --- src/node/constants.ts | 13 +++++++++++++ src/node/entry.ts | 11 +---------- 2 files changed, 14 insertions(+), 10 deletions(-) create mode 100644 src/node/constants.ts diff --git a/src/node/constants.ts b/src/node/constants.ts new file mode 100644 index 000000000..d6ba953ea --- /dev/null +++ b/src/node/constants.ts @@ -0,0 +1,13 @@ +import { logger } from "@coder/logger" +import * as path from "path" + +let pkg: { version?: string; commit?: string } = {} +try { + pkg = require("../../package.json") +} catch (error) { + logger.warn(error.message) +} + +export const version = pkg.version || "development" +export const commit = pkg.commit || "development" +export const rootPath = path.resolve(__dirname, "../..") diff --git a/src/node/entry.ts b/src/node/entry.ts index e8c6b4e1f..ce0d2980f 100644 --- a/src/node/entry.ts +++ b/src/node/entry.ts @@ -17,20 +17,11 @@ import { shouldRunVsCodeCli, } from "./cli" import { coderCloudBind } from "./coder-cloud" +import { commit, version } from "./constants" import { loadPlugins } from "./plugin" import { humanPath, open } from "./util" import { ipcMain, WrapperProcess } from "./wrapper" -let pkg: { version?: string; commit?: string } = {} -try { - pkg = require("../../package.json") -} catch (error) { - logger.warn(error.message) -} - -const version = pkg.version || "development" -const commit = pkg.commit || "development" - export const runVsCodeCli = (args: DefaultedArgs): void => { logger.debug("forking vs code cli...") const vscode = cp.fork(path.resolve(__dirname, "../../lib/vscode/out/vs/server/fork"), [], { From 112eda46052ef5ea72b71b68817e8c00121a5140 Mon Sep 17 00:00:00 2001 From: Asher Date: Tue, 20 Oct 2020 18:05:58 -0500 Subject: [PATCH 098/247] Convert routes to Express --- .eslintrc.yaml | 2 + ci/dev/vscode.patch | 4 +- package.json | 5 + src/common/http.ts | 2 +- src/node/app.ts | 3 + src/node/entry.ts | 7 +- src/node/http.ts | 1092 ++++++++----------------------------- src/node/proxy.ts | 73 +++ src/node/routes/health.ts | 28 +- src/node/routes/index.ts | 123 +++++ src/node/routes/login.ts | 210 +++---- src/node/routes/proxy.ts | 76 +-- src/node/routes/static.ts | 96 ++-- src/node/routes/update.ts | 196 +------ src/node/routes/vscode.ts | 304 +++-------- src/node/settings.ts | 4 +- src/node/update.ts | 133 +++++ src/node/vscode.ts | 150 +++++ test/update.test.ts | 25 +- yarn.lock | 21 +- 20 files changed, 1031 insertions(+), 1523 deletions(-) create mode 100644 src/node/proxy.ts create mode 100644 src/node/routes/index.ts create mode 100644 src/node/update.ts create mode 100644 src/node/vscode.ts diff --git a/.eslintrc.yaml b/.eslintrc.yaml index 92657d629..231c2dbca 100644 --- a/.eslintrc.yaml +++ b/.eslintrc.yaml @@ -31,6 +31,8 @@ rules: import/order: [error, { alphabetize: { order: "asc" }, groups: [["builtin", "external", "internal"], "parent", "sibling"] }] no-async-promise-executor: off + # This isn't a real module, just types, which apparently doesn't resolve. + import/no-unresolved: [error, { ignore: ["express-serve-static-core"] }] settings: # Does not work with CommonJS unfortunately. diff --git a/ci/dev/vscode.patch b/ci/dev/vscode.patch index b205aa83b..9d759649e 100644 --- a/ci/dev/vscode.patch +++ b/ci/dev/vscode.patch @@ -1318,7 +1318,7 @@ index 0000000000000000000000000000000000000000..56331ff1fc32bbd82e769aaecb551e42 +require('../../bootstrap-amd').load('vs/server/entry'); diff --git a/src/vs/server/ipc.d.ts b/src/vs/server/ipc.d.ts new file mode 100644 -index 0000000000000000000000000000000000000000..33b28cf2d53746ee9c50c056ac2e087dcee0a4e2 +index 0000000000000000000000000000000000000000..6ce56bec114a6d8daf5dd3ded945ea78fc72a5c6 --- /dev/null +++ b/src/vs/server/ipc.d.ts @@ -0,0 +1,131 @@ @@ -1336,7 +1336,7 @@ index 0000000000000000000000000000000000000000..33b28cf2d53746ee9c50c056ac2e087d + options: VscodeOptions; +} + -+export type Query = { [key: string]: string | string[] | undefined }; ++export type Query = { [key: string]: string | string[] | undefined | Query | Query[] }; + +export interface SocketMessage { + type: 'socket'; diff --git a/package.json b/package.json index e6197065f..187052f8d 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,8 @@ }, "main": "out/node/entry.js", "devDependencies": { + "@types/body-parser": "^1.19.0", + "@types/cookie-parser": "^1.4.2", "@types/express": "^4.17.8", "@types/fs-extra": "^8.0.1", "@types/http-proxy": "^1.17.4", @@ -67,6 +69,8 @@ }, "dependencies": { "@coder/logger": "1.1.16", + "body-parser": "^1.19.0", + "cookie-parser": "^1.4.5", "env-paths": "^2.2.0", "express": "^4.17.1", "fs-extra": "^9.0.1", @@ -75,6 +79,7 @@ "js-yaml": "^3.13.1", "limiter": "^1.1.5", "pem": "^1.14.2", + "qs": "6.7.0", "rotating-file-stream": "^2.1.1", "safe-buffer": "^5.1.1", "safe-compare": "^1.1.4", diff --git a/src/common/http.ts b/src/common/http.ts index 4749247d7..5279bf44f 100644 --- a/src/common/http.ts +++ b/src/common/http.ts @@ -9,7 +9,7 @@ export enum HttpCode { } export class HttpError extends Error { - public constructor(message: string, public readonly code: number, public readonly details?: object) { + public constructor(message: string, public readonly status: number, public readonly details?: object) { super(message) this.name = this.constructor.name } diff --git a/src/node/app.ts b/src/node/app.ts index 7e5e7a2ef..5fde27675 100644 --- a/src/node/app.ts +++ b/src/node/app.ts @@ -4,6 +4,7 @@ import { promises as fs } from "fs" import http from "http" import * as httpolyglot from "httpolyglot" import { DefaultedArgs } from "./cli" +import { handleUpgrade } from "./http" /** * Create an Express app and an HTTP/S server to serve it. @@ -38,6 +39,8 @@ export const createApp = async (args: DefaultedArgs): Promise<[Express, http.Ser } }) + handleUpgrade(app, server) + return [app, server] } diff --git a/src/node/entry.ts b/src/node/entry.ts index ce0d2980f..30dd93d8f 100644 --- a/src/node/entry.ts +++ b/src/node/entry.ts @@ -18,7 +18,7 @@ import { } from "./cli" import { coderCloudBind } from "./coder-cloud" import { commit, version } from "./constants" -import { loadPlugins } from "./plugin" +import { register } from "./routes" import { humanPath, open } from "./util" import { ipcMain, WrapperProcess } from "./wrapper" @@ -111,15 +111,14 @@ const main = async (args: DefaultedArgs): Promise => { if (args.auth === AuthType.Password && !args.password) { throw new Error("Please pass in a password via the config file or $PASSWORD") } + ipcMain.onDispose(() => { // TODO: register disposables }) const [app, server] = await createApp(args) const serverAddress = ensureAddress(server) - - // TODO: register routes - await loadPlugins(app, args) + await register(app, server, args) logger.info(`Using config file ${humanPath(args.config)}`) logger.info(`HTTP server listening on ${serverAddress} ${args.link ? "(randomized by --link)" : ""}`) diff --git a/src/node/http.ts b/src/node/http.ts index 4aa6dc067..960669100 100644 --- a/src/node/http.ts +++ b/src/node/http.ts @@ -1,885 +1,253 @@ import { field, logger } from "@coder/logger" -import * as fs from "fs-extra" +import * as express from "express" +import * as expressCore from "express-serve-static-core" import * as http from "http" -import proxy from "http-proxy" -import * as httpolyglot from "httpolyglot" -import * as https from "https" import * as net from "net" -import * as path from "path" -import * as querystring from "querystring" +import qs from "qs" import safeCompare from "safe-compare" -import { Readable } from "stream" -import * as tls from "tls" -import * as url from "url" import { HttpCode, HttpError } from "../common/http" -import { arrayify, normalize, Options, plural, split, trimSlashes } from "../common/util" +import { normalize, Options } from "../common/util" import { AuthType } from "./cli" +import { commit, rootPath } from "./constants" import { Heart } from "./heart" -import { SocketProxyProvider } from "./socket" -import { getMediaMime, paths } from "./util" +import { hash } from "./util" -export type Cookies = { [key: string]: string[] | undefined } -export type PostData = { [key: string]: string | string[] | undefined } - -interface ProxyRequest extends http.IncomingMessage { - base?: string -} - -interface AuthPayload extends Cookies { - key?: string[] -} - -export type Query = { [key: string]: string | string[] | undefined } - -export interface ProxyOptions { - /** - * A path to strip from from the beginning of the request before proxying - */ - strip?: string - /** - * A path to add to the beginning of the request before proxying. - */ - prepend?: string - /** - * The port to proxy. - */ - port: string -} - -export interface HttpResponse { - /* - * Whether to set cache-control headers for this response. - */ - cache?: boolean - /** - * If the code cannot be determined automatically set it here. The - * defaults are 302 for redirects and 200 for successful requests. For errors - * you should throw an HttpError and include the code there. If you - * use Error it will default to 404 for ENOENT and EISDIR and 500 otherwise. - */ - code?: number - /** - * Content to write in the response. Mutually exclusive with stream. - */ - content?: T - /** - * Cookie to write with the response. - * NOTE: Cookie paths must be absolute. The default is /. - */ - cookie?: { key: string; value: string; path?: string } - /** - * Used to automatically determine the appropriate mime type. - */ - filePath?: string - /** - * Additional headers to include. - */ - headers?: http.OutgoingHttpHeaders - /** - * If the mime type cannot be determined automatically set it here. - */ - mime?: string - /** - * Redirect to this path. This is constructed against the site base (not the - * provider's base). - */ - redirect?: string - /** - * Stream this to the response. Mutually exclusive with content. - */ - stream?: Readable - /** - * Query variables to add in addition to current ones when redirecting. Use - * `undefined` to remove a query variable. - */ - query?: Query - /** - * Indicates the request should be proxied. - */ - proxy?: ProxyOptions -} - -export interface WsResponse { - /** - * Indicates the web socket should be proxied. - */ - proxy?: ProxyOptions +export interface Locals { + heart: Heart } /** - * Use when you need to run search and replace on a file's content before - * sending it. + * Replace common variable strings in HTML templates. */ -export interface HttpStringFileResponse extends HttpResponse { - content: string - filePath: string -} - -export interface RedirectResponse extends HttpResponse { - redirect: string -} - -export interface HttpServerOptions { - readonly auth?: AuthType - readonly cert?: string - readonly certKey?: string - readonly commit?: string - readonly host?: string - readonly password?: string - readonly port?: number - readonly proxyDomains: string[] - readonly socket?: string -} - -export interface Route { - /** - * Provider base path part (for /provider/base/path it would be /provider). - */ - providerBase: string - /** - * Base path part (for /provider/base/path it would be /base). - */ - base: string - /** - * Remaining part of the route after factoring out the base and provider base - * (for /provider/base/path it would be /path). It can be blank. - */ - requestPath: string - /** - * Query variables included in the request. - */ - query: querystring.ParsedUrlQuery - /** - * Normalized version of `originalPath`. - */ - fullPath: string - /** - * Original path of the request without any modifications. - */ - originalPath: string -} - -interface ProviderRoute extends Route { - provider: HttpProvider -} - -export interface HttpProviderOptions { - readonly auth: AuthType - readonly commit: string - readonly password?: string +export const replaceTemplates = ( + req: express.Request, + content: string, + extraOpts?: Omit, +): string => { + const base = relativeRoot(req) + const options: Options = { + base, + csStaticBase: base + "/static/" + commit + rootPath, + logLevel: logger.level, + ...extraOpts, + } + return content + .replace(/{{TO}}/g, (typeof req.query.to === "string" && req.query.to) || "/") + .replace(/{{BASE}}/g, options.base) + .replace(/{{CS_STATIC_BASE}}/g, options.csStaticBase) + .replace(/"{{OPTIONS}}"/, `'${JSON.stringify(options)}'`) } /** - * Provides HTTP responses. This abstract class provides some helpers for - * interpreting, creating, and authenticating responses. + * Throw an error if not authorized. */ -export abstract class HttpProvider { - protected readonly rootPath = path.resolve(__dirname, "../..") - - public constructor(protected readonly options: HttpProviderOptions) {} - - public async dispose(): Promise { - // No default behavior. +export const ensureAuthenticated = (req: express.Request): void => { + if (!authenticated(req)) { + throw new HttpError("Unauthorized", HttpCode.Unauthorized) } - - /** - * Handle web sockets on the registered endpoint. Normally the provider - * handles the request itself but it can return a response when necessary. The - * default is to throw a 404. - */ - public handleWebSocket( - /* eslint-disable @typescript-eslint/no-unused-vars */ - _route: Route, - _request: http.IncomingMessage, - _socket: net.Socket, - _head: Buffer, - /* eslint-enable @typescript-eslint/no-unused-vars */ - ): Promise { - throw new HttpError("Not found", HttpCode.NotFound) - } - - /** - * Handle requests to the registered endpoint. - */ - public abstract handleRequest(route: Route, request: http.IncomingMessage): Promise - - /** - * Get the base relative to the provided route. For each slash we need to go - * up a directory. For example: - * / => . - * /foo => . - * /foo/ => ./.. - * /foo/bar => ./.. - * /foo/bar/ => ./../.. - */ - public base(route: Route): string { - const depth = (route.originalPath.match(/\//g) || []).length - return normalize("./" + (depth > 1 ? "../".repeat(depth - 1) : "")) - } - - /** - * Get error response. - */ - public async getErrorRoot(route: Route, title: string, header: string, body: string): Promise { - const response = await this.getUtf8Resource(this.rootPath, "src/browser/pages/error.html") - response.content = response.content - .replace(/{{ERROR_TITLE}}/g, title) - .replace(/{{ERROR_HEADER}}/g, header) - .replace(/{{ERROR_BODY}}/g, body) - return this.replaceTemplates(route, response) - } - - /** - * Replace common templates strings. - */ - protected replaceTemplates( - route: Route, - response: HttpStringFileResponse, - extraOptions?: Omit, - ): HttpStringFileResponse { - const base = this.base(route) - const options: Options = { - base, - csStaticBase: base + "/static/" + this.options.commit + this.rootPath, - logLevel: logger.level, - ...extraOptions, - } - response.content = response.content - .replace(/{{TO}}/g, Array.isArray(route.query.to) ? route.query.to[0] : route.query.to || "/dashboard") - .replace(/{{BASE}}/g, options.base) - .replace(/{{CS_STATIC_BASE}}/g, options.csStaticBase) - .replace(/"{{OPTIONS}}"/, `'${JSON.stringify(options)}'`) - return response - } - - protected get isDev(): boolean { - return this.options.commit === "development" - } - - /** - * Get a file resource. - * TODO: Would a stream be faster, at least for large files? - */ - protected async getResource(...parts: string[]): Promise { - const filePath = path.join(...parts) - return { content: await fs.readFile(filePath), filePath } - } - - /** - * Get a file resource as a string. - */ - protected async getUtf8Resource(...parts: string[]): Promise { - const filePath = path.join(...parts) - return { content: await fs.readFile(filePath, "utf8"), filePath } - } - - /** - * Helper to error on invalid methods (default GET). - */ - protected ensureMethod(request: http.IncomingMessage, method?: string | string[]): void { - const check = arrayify(method || "GET") - if (!request.method || !check.includes(request.method)) { - throw new HttpError(`Unsupported method ${request.method}`, HttpCode.BadRequest) - } - } - - /** - * Helper to error if not authorized. - */ - public ensureAuthenticated(request: http.IncomingMessage): void { - if (!this.authenticated(request)) { - throw new HttpError("Unauthorized", HttpCode.Unauthorized) - } - } - - /** - * Use the first query value or the default if there isn't one. - */ - protected queryOrDefault(value: string | string[] | undefined, def: string): string { - if (Array.isArray(value)) { - value = value[0] - } - return typeof value !== "undefined" ? value : def - } - - /** - * Return the provided password value if the payload contains the right - * password otherwise return false. If no payload is specified use cookies. - */ - public authenticated(request: http.IncomingMessage, payload?: AuthPayload): string | boolean { - switch (this.options.auth) { - case AuthType.None: - return true - case AuthType.Password: - if (typeof payload === "undefined") { - payload = this.parseCookies(request) - } - if (this.options.password && payload.key) { - for (let i = 0; i < payload.key.length; ++i) { - if (safeCompare(payload.key[i], this.options.password)) { - return payload.key[i] - } - } - } - return false - default: - throw new Error(`Unsupported auth type ${this.options.auth}`) - } - } - - /** - * Parse POST data. - */ - protected getData(request: http.IncomingMessage): Promise { - return request.method === "POST" || request.method === "DELETE" - ? new Promise((resolve, reject) => { - let body = "" - const onEnd = (): void => { - off() // eslint-disable-line @typescript-eslint/no-use-before-define - resolve(body || undefined) - } - const onError = (error: Error): void => { - off() // eslint-disable-line @typescript-eslint/no-use-before-define - reject(error) - } - const onData = (d: Buffer): void => { - body += d - if (body.length > 1e6) { - onError(new HttpError("Payload is too large", HttpCode.LargePayload)) - request.connection.destroy() - } - } - const off = (): void => { - request.off("error", onError) - request.off("data", onError) - request.off("end", onEnd) - } - request.on("error", onError) - request.on("data", onData) - request.on("end", onEnd) - }) - : Promise.resolve(undefined) - } - - /** - * Parse cookies. - */ - protected parseCookies(request: http.IncomingMessage): T { - const cookies: { [key: string]: string[] } = {} - if (request.headers.cookie) { - request.headers.cookie.split(";").forEach((keyValue) => { - const [key, value] = split(keyValue, "=") - if (!cookies[key]) { - cookies[key] = [] - } - cookies[key].push(decodeURI(value)) - }) - } - return cookies as T - } - - /** - * Return true if the route is for the root page. For example /base, /base/, - * or /base/index.html but not /base/path or /base/file.js. - */ - protected isRoot(route: Route): boolean { - return !route.requestPath || route.requestPath === "/index.html" - } -} - -export interface HttpProvider0 { - new (options: HttpProviderOptions): T -} - -export interface HttpProvider1 { - new (options: HttpProviderOptions, a1: A1): T -} - -export interface HttpProvider2 { - new (options: HttpProviderOptions, a1: A1, a2: A2): T -} - -export interface HttpProvider3 { - new (options: HttpProviderOptions, a1: A1, a2: A2, a3: A3): T } /** - * An HTTP server. Its main role is to route incoming HTTP requests to the - * appropriate provider for that endpoint then write out the response. It also - * covers some common use cases like redirects and caching. + * Return true if authenticated via cookies. */ -export class HttpServer { - protected readonly server: http.Server | https.Server - private listenPromise: Promise | undefined - public readonly protocol: "http" | "https" - private readonly providers = new Map() - public readonly heart: Heart - private readonly socketProvider = new SocketProxyProvider() - - /** - * Provides the actual proxying functionality. - */ - private readonly proxy = proxy.createProxyServer({}) - - public constructor(private readonly options: HttpServerOptions) { - this.heart = new Heart(path.join(paths.data, "heartbeat"), async () => { - const connections = await this.getConnections() - logger.trace(plural(connections, `${connections} active connection`)) - return connections !== 0 - }) - this.protocol = this.options.cert ? "https" : "http" - if (this.protocol === "https") { - this.server = httpolyglot.createServer( - { - cert: this.options.cert && fs.readFileSync(this.options.cert), - key: this.options.certKey && fs.readFileSync(this.options.certKey), - }, - this.onRequest, - ) - } else { - this.server = http.createServer(this.onRequest) - } - this.proxy.on("error", (error, _request, response) => { - response.writeHead(HttpCode.ServerError) - response.end(error.message) - }) - // Intercept the response to rewrite absolute redirects against the base path. - this.proxy.on("proxyRes", (response, request: ProxyRequest) => { - if (response.headers.location && response.headers.location.startsWith("/") && request.base) { - response.headers.location = request.base + response.headers.location - } - }) - } - - /** - * Stop and dispose everything. Return an array of disposal errors. - */ - public async dispose(): Promise { - this.socketProvider.stop() - const providers = Array.from(this.providers.values()) - // Catch so all the errors can be seen rather than just the first one. - const responses = await Promise.all(providers.map((p) => p.dispose().catch((e) => e))) - return responses.filter((r): r is Error => typeof r !== "undefined") - } - - public async getConnections(): Promise { - return new Promise((resolve, reject) => { - this.server.getConnections((error, count) => { - return error ? reject(error) : resolve(count) - }) - }) - } - - /** - * Register a provider for a top-level endpoint. - */ - public registerHttpProvider(endpoint: string | string[], provider: HttpProvider0): T - public registerHttpProvider( - endpoint: string | string[], - provider: HttpProvider1, - a1: A1, - ): T - public registerHttpProvider( - endpoint: string | string[], - provider: HttpProvider2, - a1: A1, - a2: A2, - ): T - public registerHttpProvider( - endpoint: string | string[], - provider: HttpProvider3, - a1: A1, - a2: A2, - a3: A3, - ): T - // eslint-disable-next-line @typescript-eslint/no-explicit-any - public registerHttpProvider(endpoint: string | string[], provider: any, ...args: any[]): void { - const p = new provider( - { - auth: this.options.auth || AuthType.None, - commit: this.options.commit, - password: this.options.password, - }, - ...args, - ) - const endpoints = arrayify(endpoint).map(trimSlashes) - endpoints.forEach((endpoint) => { - if (/\//.test(endpoint)) { - throw new Error(`Only top-level endpoints are supported (got ${endpoint})`) - } - const existingProvider = this.providers.get(`/${endpoint}`) - this.providers.set(`/${endpoint}`, p) - if (existingProvider) { - logger.debug(`Overridding existing /${endpoint} provider`) - // If the existing provider isn't registered elsewhere we can dispose. - if (!Array.from(this.providers.values()).find((p) => p === existingProvider)) { - logger.debug(`Disposing existing /${endpoint} provider`) - existingProvider.dispose() - } - } - }) - } - - /** - * Start listening on the specified port. - */ - public listen(): Promise { - if (!this.listenPromise) { - this.listenPromise = new Promise(async (resolve, reject) => { - this.server.on("error", reject) - this.server.on("upgrade", this.onUpgrade) - const onListen = (): void => resolve(this.address()) - if (this.options.socket) { - try { - await fs.unlink(this.options.socket) - } catch (err) { - if (err.code !== "ENOENT") { - logger.warn(err.message) - } - } - this.server.listen(this.options.socket, onListen) - } else if (this.options.host) { - // [] is the correct format when using :: but Node errors with them. - this.server.listen(this.options.port, this.options.host.replace(/^\[|\]$/g, ""), onListen) - } else { - this.server.listen(this.options.port, onListen) - } - }) - } - return this.listenPromise - } - - /** - * The *local* address of the server. - */ - public address(): string | null { - const address = this.server.address() - const endpoint = - typeof address !== "string" && address !== null - ? (address.address === "::" ? "localhost" : address.address) + ":" + address.port - : address - return endpoint && `${this.protocol}://${endpoint}` - } - - private onRequest = async (request: http.IncomingMessage, response: http.ServerResponse): Promise => { - const route = this.parseUrl(request) - if (route.providerBase !== "/healthz") { - this.heart.beat() - } - const write = (payload: HttpResponse): void => { - response.writeHead(payload.redirect ? HttpCode.Redirect : payload.code || HttpCode.Ok, { - "Content-Type": payload.mime || getMediaMime(payload.filePath), - ...(payload.redirect ? { Location: this.constructRedirect(request, route, payload as RedirectResponse) } : {}), - ...(request.headers["service-worker"] ? { "Service-Worker-Allowed": route.provider.base(route) } : {}), - ...(payload.cache ? { "Cache-Control": "public, max-age=31536000" } : {}), - ...(payload.cookie - ? { - "Set-Cookie": [ - `${payload.cookie.key}=${payload.cookie.value}`, - `Path=${normalize(payload.cookie.path || "/", true)}`, - this.getCookieDomain(request.headers.host || ""), - // "HttpOnly", - "SameSite=lax", - ] - .filter((l) => !!l) - .join(";"), - } - : {}), - ...payload.headers, - }) - if (payload.stream) { - payload.stream.on("error", (error: NodeJS.ErrnoException) => { - response.writeHead(error.code === "ENOENT" ? HttpCode.NotFound : HttpCode.ServerError) - response.end(error.message) - }) - payload.stream.on("close", () => response.end()) - payload.stream.pipe(response) - } else if (typeof payload.content === "string" || payload.content instanceof Buffer) { - response.end(payload.content) - } else if (payload.content && typeof payload.content === "object") { - response.end(JSON.stringify(payload.content)) - } else { - response.end() - } - } - - try { - const payload = (await this.handleRequest(route, request)) || (await route.provider.handleRequest(route, request)) - if (payload.proxy) { - this.doProxy(route, request, response, payload.proxy) - } else { - write(payload) - } - } catch (error) { - let e = error - if (error.code === "ENOENT" || error.code === "EISDIR") { - e = new HttpError("Not found", HttpCode.NotFound) - } - const code = typeof e.code === "number" ? e.code : HttpCode.ServerError - logger.debug("Request error", field("url", request.url), field("code", code), field("error", error)) - if (code >= HttpCode.ServerError) { - logger.error(error.stack) - } - if (request.headers["content-type"] === "application/json") { - write({ - code, - mime: "application/json", - content: { - error: e.message, - ...(e.details || {}), - }, - }) - } else { - write({ - code, - ...(await route.provider.getErrorRoot(route, code, code, e.message)), - }) - } - } - } - - /** - * Handle requests that are always in effect no matter what provider is - * registered at the route. - */ - private async handleRequest(route: ProviderRoute, request: http.IncomingMessage): Promise { - // If we're handling TLS ensure all requests are redirected to HTTPS. - if (this.options.cert && !(request.connection as tls.TLSSocket).encrypted) { - return { redirect: route.fullPath } - } - - // Return robots.txt. - if (route.fullPath === "/robots.txt") { - const filePath = path.resolve(__dirname, "../../src/browser/robots.txt") - return { content: await fs.readFile(filePath), filePath } - } - - // Handle proxy domains. - return this.maybeProxy(route, request) - } - - /** - * Given a path that goes from the base, construct a relative redirect URL - * that will get you there considering that the app may be served from an - * unknown base path. If handling TLS, also ensure HTTPS. - */ - private constructRedirect(request: http.IncomingMessage, route: ProviderRoute, payload: RedirectResponse): string { - const query = { - ...route.query, - ...(payload.query || {}), - } - - Object.keys(query).forEach((key) => { - if (typeof query[key] === "undefined") { - delete query[key] - } - }) - - const secure = (request.connection as tls.TLSSocket).encrypted - const redirect = - (this.options.cert && !secure ? `${this.protocol}://${request.headers.host}/` : "") + - normalize(`${route.provider.base(route)}/${payload.redirect}`, true) + - (Object.keys(query).length > 0 ? `?${querystring.stringify(query)}` : "") - logger.debug("redirecting", field("secure", !!secure), field("from", request.url), field("to", redirect)) - return redirect - } - - private onUpgrade = async (request: http.IncomingMessage, socket: net.Socket, head: Buffer): Promise => { - try { - this.heart.beat() - socket.on("error", () => socket.destroy()) - - if (this.options.cert && !(socket as tls.TLSSocket).encrypted) { - throw new HttpError("HTTP websocket", HttpCode.BadRequest) - } - - if (!request.headers.upgrade || request.headers.upgrade.toLowerCase() !== "websocket") { - throw new HttpError("HTTP/1.1 400 Bad Request", HttpCode.BadRequest) - } - - const route = this.parseUrl(request) - if (!route.provider) { - throw new HttpError("Not found", HttpCode.NotFound) - } - - // The socket proxy is so we can pass them to child processes (TLS sockets - // can't be transferred so we need an in-between). - const socketProxy = await this.socketProvider.createProxy(socket) - const payload = - this.maybeProxy(route, request) || (await route.provider.handleWebSocket(route, request, socketProxy, head)) - if (payload && payload.proxy) { - this.doProxy(route, request, { socket: socketProxy, head }, payload.proxy) - } - } catch (error) { - socket.destroy(error) - logger.warn(`discarding socket connection: ${error.message}`) - } - } - - /** - * Parse a request URL so we can route it. - */ - private parseUrl(request: http.IncomingMessage): ProviderRoute { - const parse = (fullPath: string): { base: string; requestPath: string } => { - const match = fullPath.match(/^(\/?[^/]*)(.*)$/) - let [, /* ignore */ base, requestPath] = match ? match.map((p) => p.replace(/\/+$/, "")) : ["", "", ""] - if (base.indexOf(".") !== -1) { - // Assume it's a file at the root. - requestPath = base - base = "/" - } else if (base === "") { - // Happens if it's a plain `domain.com`. - base = "/" - } - return { base, requestPath } - } - - const parsedUrl = request.url ? url.parse(request.url, true) : { query: {}, pathname: "" } - const originalPath = parsedUrl.pathname || "/" - const fullPath = normalize(originalPath, true) - const { base, requestPath } = parse(fullPath) - - // Providers match on the path after their base so we need to account for - // that by shifting the next base out of the request path. - let provider = this.providers.get(base) - if (base !== "/" && provider) { - return { ...parse(requestPath), providerBase: base, fullPath, query: parsedUrl.query, provider, originalPath } - } - - // Fall back to the top-level provider. - provider = this.providers.get("/") - if (!provider) { - throw new Error(`No provider for ${base}`) - } - return { base, providerBase: "/", fullPath, requestPath, query: parsedUrl.query, provider, originalPath } - } - - /** - * Proxy a request to the target. - */ - private doProxy( - route: Route, - request: http.IncomingMessage, - response: http.ServerResponse, - options: ProxyOptions, - ): void - /** - * Proxy a web socket to the target. - */ - private doProxy( - route: Route, - request: http.IncomingMessage, - response: { socket: net.Socket; head: Buffer }, - options: ProxyOptions, - ): void - /** - * Proxy a request or web socket to the target. - */ - private doProxy( - route: Route, - request: http.IncomingMessage, - response: http.ServerResponse | { socket: net.Socket; head: Buffer }, - options: ProxyOptions, - ): void { - const port = parseInt(options.port, 10) - if (isNaN(port)) { - throw new HttpError(`"${options.port}" is not a valid number`, HttpCode.BadRequest) - } - - // REVIEW: Absolute redirects need to be based on the subpath but I'm not - // sure how best to get this information to the `proxyRes` event handler. - // For now I'm sticking it on the request object which is passed through to - // the event. - ;(request as ProxyRequest).base = options.strip - - const isHttp = response instanceof http.ServerResponse - const base = options.strip ? route.fullPath.replace(options.strip, "") : route.fullPath - const path = normalize("/" + (options.prepend || "") + "/" + base, true) - const proxyOptions: proxy.ServerOptions = { - changeOrigin: true, - ignorePath: true, - target: `${isHttp ? "http" : "ws"}://127.0.0.1:${port}${path}${ - Object.keys(route.query).length > 0 ? `?${querystring.stringify(route.query)}` : "" - }`, - ws: !isHttp, - } - - if (response instanceof http.ServerResponse) { - this.proxy.web(request, response, proxyOptions) - } else { - this.proxy.ws(request, response.socket, response.head, proxyOptions) - } - } - - /** - * Get the value that should be used for setting a cookie domain. This will - * allow the user to authenticate only once. This will use the highest level - * domain (e.g. `coder.com` over `test.coder.com` if both are specified). - */ - private getCookieDomain(host: string): string | undefined { - const idx = host.lastIndexOf(":") - host = idx !== -1 ? host.substring(0, idx) : host - if ( - // Might be blank/missing, so there's nothing more to do. - !host || - // IP addresses can't have subdomains so there's no value in setting the - // domain for them. Assume anything with a : is ipv6 (valid domain name - // characters are alphanumeric or dashes). - host.includes(":") || - // Assume anything entirely numbers and dots is ipv4 (currently tlds - // cannot be entirely numbers). - !/[^0-9.]/.test(host) || - // localhost subdomains don't seem to work at all (browser bug?). - host.endsWith(".localhost") || - // It might be localhost (or an IP, see above) if it's a proxy and it - // isn't setting the host header to match the access domain. - host === "localhost" - ) { - logger.debug("no valid cookie doman", field("host", host)) - return undefined - } - - this.options.proxyDomains.forEach((domain) => { - if (host.endsWith(domain) && domain.length < host.length) { - host = domain - } - }) - - logger.debug("got cookie doman", field("host", host)) - return host ? `Domain=${host}` : undefined - } - - /** - * Return a response if the request should be proxied. Anything that ends in a - * proxy domain and has a *single* subdomain should be proxied. Anything else - * should return `undefined` and will be handled as normal. - * - * For example if `coder.com` is specified `8080.coder.com` will be proxied - * but `8080.test.coder.com` and `test.8080.coder.com` will not. - * - * Throw an error if proxying but the user isn't authenticated. - */ - public maybeProxy(route: ProviderRoute, request: http.IncomingMessage): HttpResponse | undefined { - // Split into parts. - const host = request.headers.host || "" - const idx = host.indexOf(":") - const domain = idx !== -1 ? host.substring(0, idx) : host - const parts = domain.split(".") - - // There must be an exact match. - const port = parts.shift() - const proxyDomain = parts.join(".") - if (!port || !this.options.proxyDomains.includes(proxyDomain)) { - return undefined - } - - // Must be authenticated to use the proxy. - route.provider.ensureAuthenticated(request) - - return { - proxy: { - port, - }, - } +export const authenticated = (req: express.Request): boolean => { + switch (req.args.auth) { + case AuthType.None: + return true + case AuthType.Password: + // The password is stored in the cookie after being hashed. + return req.args.password && req.cookies.key && safeCompare(req.cookies.key, hash(req.args.password)) + default: + throw new Error(`Unsupported auth type ${req.args.auth}`) } } + +/** + * Get the relative path that will get us to the root of the page. For each + * slash we need to go up a directory. For example: + * / => . + * /foo => . + * /foo/ => ./.. + * /foo/bar => ./.. + * /foo/bar/ => ./../.. + */ +export const relativeRoot = (req: express.Request): string => { + const depth = (req.originalUrl.split("?", 1)[0].match(/\//g) || []).length + return normalize("./" + (depth > 1 ? "../".repeat(depth - 1) : "")) +} + +/** + * Redirect relatively to `/${to}`. Query variables will be preserved. + * `override` will merge with the existing query (use `undefined` to unset). + */ +export const redirect = ( + req: express.Request, + res: express.Response, + to: string, + override: expressCore.Query = {}, +): void => { + const query = Object.assign({}, req.query, override) + Object.keys(override).forEach((key) => { + if (typeof override[key] === "undefined") { + delete query[key] + } + }) + + const relativePath = normalize(`${relativeRoot(req)}/${to}`, true) + const queryString = qs.stringify(query) + const redirectPath = `${relativePath}${queryString ? `?${queryString}` : ""}` + logger.debug(`redirecting from ${req.originalUrl} to ${redirectPath}`) + res.redirect(redirectPath) +} + +/** + * Get the value that should be used for setting a cookie domain. This will + * allow the user to authenticate only once. This will use the highest level + * domain (e.g. `coder.com` over `test.coder.com` if both are specified). + */ +export const getCookieDomain = (host: string, proxyDomains: string[]): string | undefined => { + const idx = host.lastIndexOf(":") + host = idx !== -1 ? host.substring(0, idx) : host + if ( + // Might be blank/missing, so there's nothing more to do. + !host || + // IP addresses can't have subdomains so there's no value in setting the + // domain for them. Assume anything with a : is ipv6 (valid domain name + // characters are alphanumeric or dashes). + host.includes(":") || + // Assume anything entirely numbers and dots is ipv4 (currently tlds + // cannot be entirely numbers). + !/[^0-9.]/.test(host) || + // localhost subdomains don't seem to work at all (browser bug?). + host.endsWith(".localhost") || + // It might be localhost (or an IP, see above) if it's a proxy and it + // isn't setting the host header to match the access domain. + host === "localhost" + ) { + logger.debug("no valid cookie doman", field("host", host)) + return undefined + } + + proxyDomains.forEach((domain) => { + if (host.endsWith(domain) && domain.length < host.length) { + host = domain + } + }) + + logger.debug("got cookie doman", field("host", host)) + return host ? `Domain=${host}` : undefined +} + +declare module "express" { + function Router(options?: express.RouterOptions): express.Router & WithWebsocketMethod + + type WebsocketRequestHandler = ( + socket: net.Socket, + head: Buffer, + req: express.Request, + next: express.NextFunction, + ) => void | Promise + + type WebsocketMethod = (route: expressCore.PathParams, ...handlers: WebsocketRequestHandler[]) => T + + interface WithWebsocketMethod { + ws: WebsocketMethod + } +} + +export const handleUpgrade = (app: express.Express, server: http.Server): void => { + server.on("upgrade", (req, socket, head) => { + socket.on("error", () => socket.destroy()) + + req.ws = socket + req.head = head + req._ws_handled = false + + const res = new http.ServerResponse(req) + res.writeHead = function writeHead(statusCode: number) { + if (statusCode > 200) { + socket.destroy(new Error(`${statusCode}`)) + } + return res + } + + // Send the request off to be handled by Express. + ;(app as any).handle(req, res, () => { + if (!req._ws_handled) { + socket.destroy(new Error("Not found")) + } + }) + }) +} + +/** + * Patch Express routers to handle web sockets and async routes (since we have + * to patch `get` anyway). + * + * Not using express-ws since the ws-wrapped sockets don't work with the proxy + * and wildcards don't work correctly. + */ +function patchRouter(): void { + // Apparently this all works because Router is also the prototype assigned to + // the routers it returns. + + // Store these since the original methods will be overridden. + const originalGet = (express.Router as any).get + const originalPost = (express.Router as any).post + + // Inject the `ws` method. + ;(express.Router as any).ws = function ws( + route: expressCore.PathParams, + ...handlers: express.WebsocketRequestHandler[] + ) { + originalGet.apply(this, [ + route, + ...handlers.map((handler) => { + const wrapped: express.Handler = (req, _, next) => { + if ((req as any).ws) { + ;(req as any)._ws_handled = true + Promise.resolve(handler((req as any).ws, (req as any).head, req, next)).catch(next) + } else { + next() + } + } + return wrapped + }), + ]) + return this + } + // Overwrite `get` so we can distinguish between websocket and non-websocket + // routes. While we're at it handle async responses. + ;(express.Router as any).get = function get(route: expressCore.PathParams, ...handlers: express.Handler[]) { + originalGet.apply(this, [ + route, + ...handlers.map((handler) => { + const wrapped: express.Handler = (req, res, next) => { + if (!(req as any).ws) { + Promise.resolve(handler(req, res, next)).catch(next) + } else { + next() + } + } + return wrapped + }), + ]) + return this + } + // Handle async responses for `post` as well since we're in here anyway. + ;(express.Router as any).post = function post(route: expressCore.PathParams, ...handlers: express.Handler[]) { + originalPost.apply(this, [ + route, + ...handlers.map((handler) => { + const wrapped: express.Handler = (req, res, next) => { + Promise.resolve(handler(req, res, next)).catch(next) + } + return wrapped + }), + ]) + return this + } +} + +// This needs to happen before anything uses the router. +patchRouter() diff --git a/src/node/proxy.ts b/src/node/proxy.ts new file mode 100644 index 000000000..8f5dd684b --- /dev/null +++ b/src/node/proxy.ts @@ -0,0 +1,73 @@ +import { Request, Router } from "express" +import proxyServer from "http-proxy" +import { HttpCode } from "../common/http" +import { ensureAuthenticated } from "./http" + +export const proxy = proxyServer.createProxyServer({}) +proxy.on("error", (error, _, res) => { + res.writeHead(HttpCode.ServerError) + res.end(error.message) +}) + +// Intercept the response to rewrite absolute redirects against the base path. +proxy.on("proxyRes", (res, req) => { + if (res.headers.location && res.headers.location.startsWith("/") && (req as any).base) { + res.headers.location = (req as any).base + res.headers.location + } +}) + +export const router = Router() + +/** + * Return the port if the request should be proxied. Anything that ends in a + * proxy domain and has a *single* subdomain should be proxied. Anything else + * should return `undefined` and will be handled as normal. + * + * For example if `coder.com` is specified `8080.coder.com` will be proxied + * but `8080.test.coder.com` and `test.8080.coder.com` will not. + * + * Throw an error if proxying but the user isn't authenticated. + */ +const maybeProxy = (req: Request): string | undefined => { + // Split into parts. + const host = req.headers.host || "" + const idx = host.indexOf(":") + const domain = idx !== -1 ? host.substring(0, idx) : host + const parts = domain.split(".") + + // There must be an exact match. + const port = parts.shift() + const proxyDomain = parts.join(".") + if (!port || !req.args["proxy-domain"].includes(proxyDomain)) { + return undefined + } + + // Must be authenticated to use the proxy. + ensureAuthenticated(req) + + return port +} + +router.all("*", (req, res, next) => { + const port = maybeProxy(req) + if (!port) { + return next() + } + + proxy.web(req, res, { + ignorePath: true, + target: `http://127.0.0.1:${port}${req.originalUrl}`, + }) +}) + +router.ws("*", (socket, head, req, next) => { + const port = maybeProxy(req) + if (!port) { + return next() + } + + proxy.ws(req, socket, head, { + ignorePath: true, + target: `http://127.0.0.1:${port}${req.originalUrl}`, + }) +}) diff --git a/src/node/routes/health.ts b/src/node/routes/health.ts index 4e505f57e..20dab71a5 100644 --- a/src/node/routes/health.ts +++ b/src/node/routes/health.ts @@ -1,22 +1,10 @@ -import { Heart } from "../heart" -import { HttpProvider, HttpProviderOptions, HttpResponse } from "../http" +import { Router } from "express" -/** - * Check the heartbeat. - */ -export class HealthHttpProvider extends HttpProvider { - public constructor(options: HttpProviderOptions, private readonly heart: Heart) { - super(options) - } +export const router = Router() - public async handleRequest(): Promise { - return { - cache: false, - mime: "application/json", - content: { - status: this.heart.alive() ? "alive" : "expired", - lastHeartbeat: this.heart.lastHeartbeat, - }, - } - } -} +router.get("/", (req, res) => { + res.json({ + status: req.heart.alive() ? "alive" : "expired", + lastHeartbeat: req.heart.lastHeartbeat, + }) +}) diff --git a/src/node/routes/index.ts b/src/node/routes/index.ts new file mode 100644 index 000000000..82a68866b --- /dev/null +++ b/src/node/routes/index.ts @@ -0,0 +1,123 @@ +import { logger } from "@coder/logger" +import bodyParser from "body-parser" +import cookieParser from "cookie-parser" +import { Express } from "express" +import { promises as fs } from "fs" +import http from "http" +import * as path from "path" +import * as tls from "tls" +import { HttpCode, HttpError } from "../../common/http" +import { plural } from "../../common/util" +import { AuthType, DefaultedArgs } from "../cli" +import { rootPath } from "../constants" +import { Heart } from "../heart" +import { replaceTemplates } from "../http" +import { loadPlugins } from "../plugin" +import * as domainProxy from "../proxy" +import { getMediaMime, paths } from "../util" +import * as health from "./health" +import * as login from "./login" +import * as proxy from "./proxy" +// static is a reserved keyword. +import * as _static from "./static" +import * as update from "./update" +import * as vscode from "./vscode" + +declare global { + // eslint-disable-next-line @typescript-eslint/no-namespace + namespace Express { + export interface Request { + args: DefaultedArgs + heart: Heart + } + } +} + +/** + * Register all routes and middleware. + */ +export const register = async (app: Express, server: http.Server, args: DefaultedArgs): Promise => { + const heart = new Heart(path.join(paths.data, "heartbeat"), async () => { + return new Promise((resolve, reject) => { + server.getConnections((error, count) => { + if (error) { + return reject(error) + } + logger.trace(plural(count, `${count} active connection`)) + resolve(count > 0) + }) + }) + }) + + app.disable("x-powered-by") + + app.use(cookieParser()) + app.use(bodyParser.json()) + app.use(bodyParser.urlencoded({ extended: true })) + + server.on("upgrade", () => { + heart.beat() + }) + + app.use(async (req, res, next) => { + heart.beat() + + // If we're handling TLS ensure all requests are redirected to HTTPS. + // TODO: This does *NOT* work if you have a base path since to specify the + // protocol we need to specify the whole path. + if (args.cert && !(req.connection as tls.TLSSocket).encrypted) { + return res.redirect(`https://${req.headers.host}${req.originalUrl}`) + } + + // Return robots.txt. + if (req.originalUrl === "/robots.txt") { + const resourcePath = path.resolve(rootPath, "src/browser/robots.txt") + res.set("Content-Type", getMediaMime(resourcePath)) + return res.send(await fs.readFile(resourcePath)) + } + + // Add common variables routes can use. + req.args = args + req.heart = heart + + return next() + }) + + app.use("/", domainProxy.router) + app.use("/", vscode.router) + app.use("/healthz", health.router) + if (args.auth === AuthType.Password) { + app.use("/login", login.router) + } + app.use("/proxy", proxy.router) + app.use("/static", _static.router) + app.use("/update", update.router) + app.use("/vscode", vscode.router) + + await loadPlugins(app, args) + + app.use(() => { + throw new HttpError("Not Found", HttpCode.NotFound) + }) + + // Handle errors. + // TODO: The types are broken; says they're all implicitly `any`. + app.use(async (err: any, req: any, res: any, next: any) => { + const resourcePath = path.resolve(rootPath, "src/browser/pages/error.html") + res.set("Content-Type", getMediaMime(resourcePath)) + try { + const content = await fs.readFile(resourcePath, "utf8") + if (err.code === "ENOENT" || err.code === "EISDIR") { + err.status = HttpCode.NotFound + } + res.status(err.status || 500).send( + replaceTemplates(req, content) + .replace(/{{ERROR_TITLE}}/g, err.status || "Error") + .replace(/{{ERROR_HEADER}}/g, err.status || "Error") + .replace(/{{ERROR_BODY}}/g, err.message), + ) + } catch (error) { + next(error) + } + }) +} diff --git a/src/node/routes/login.ts b/src/node/routes/login.ts index 58bd55e0a..bf1058e5e 100644 --- a/src/node/routes/login.ts +++ b/src/node/routes/login.ts @@ -1,140 +1,21 @@ -import * as http from "http" -import * as limiter from "limiter" -import * as querystring from "querystring" -import { HttpCode, HttpError } from "../../common/http" -import { AuthType } from "../cli" -import { HttpProvider, HttpProviderOptions, HttpResponse, Route } from "../http" +import { Router, Request } from "express" +import { promises as fs } from "fs" +import { RateLimiter as Limiter } from "limiter" +import * as path from "path" +import safeCompare from "safe-compare" +import { rootPath } from "../constants" +import { authenticated, getCookieDomain, redirect, replaceTemplates } from "../http" import { hash, humanPath } from "../util" -interface LoginPayload { - password?: string - /** - * Since we must set a cookie with an absolute path, we need to know the full - * base path. - */ - base?: string -} - -/** - * Login HTTP provider. - */ -export class LoginHttpProvider extends HttpProvider { - public constructor( - options: HttpProviderOptions, - private readonly configFile: string, - private readonly envPassword: boolean, - ) { - super(options) - } - - public async handleRequest(route: Route, request: http.IncomingMessage): Promise { - if (this.options.auth !== AuthType.Password || !this.isRoot(route)) { - throw new HttpError("Not found", HttpCode.NotFound) - } - switch (route.base) { - case "/": - switch (request.method) { - case "POST": - this.ensureMethod(request, ["GET", "POST"]) - return this.tryLogin(route, request) - default: - this.ensureMethod(request) - if (this.authenticated(request)) { - return { - redirect: (Array.isArray(route.query.to) ? route.query.to[0] : route.query.to) || "/", - query: { to: undefined }, - } - } - return this.getRoot(route) - } - } - - throw new HttpError("Not found", HttpCode.NotFound) - } - - public async getRoot(route: Route, error?: Error): Promise { - const response = await this.getUtf8Resource(this.rootPath, "src/browser/pages/login.html") - response.content = response.content.replace(/{{ERROR}}/, error ? `
${error.message}
` : "") - let passwordMsg = `Check the config file at ${humanPath(this.configFile)} for the password.` - if (this.envPassword) { - passwordMsg = "Password was set from $PASSWORD." - } - response.content = response.content.replace(/{{PASSWORD_MSG}}/g, passwordMsg) - return this.replaceTemplates(route, response) - } - - private readonly limiter = new RateLimiter() - - /** - * Try logging in. On failure, show the login page with an error. - */ - private async tryLogin(route: Route, request: http.IncomingMessage): Promise { - // Already authenticated via cookies? - const providedPassword = this.authenticated(request) - if (providedPassword) { - return { code: HttpCode.Ok } - } - - try { - if (!this.limiter.try()) { - throw new Error("Login rate limited!") - } - - const data = await this.getData(request) - const payload = data ? querystring.parse(data) : {} - return await this.login(payload, route, request) - } catch (error) { - return this.getRoot(route, error) - } - } - - /** - * Return a cookie if the user is authenticated otherwise throw an error. - */ - private async login(payload: LoginPayload, route: Route, request: http.IncomingMessage): Promise { - const password = this.authenticated(request, { - key: typeof payload.password === "string" ? [hash(payload.password)] : undefined, - }) - - if (password) { - return { - redirect: (Array.isArray(route.query.to) ? route.query.to[0] : route.query.to) || "/", - query: { to: undefined }, - cookie: - typeof password === "string" - ? { - key: "key", - value: password, - path: payload.base, - } - : undefined, - } - } - - // Only log if it was an actual login attempt. - if (payload && payload.password) { - console.error( - "Failed login attempt", - JSON.stringify({ - xForwardedFor: request.headers["x-forwarded-for"], - remoteAddress: request.connection.remoteAddress, - userAgent: request.headers["user-agent"], - timestamp: Math.floor(new Date().getTime() / 1000), - }), - ) - - throw new Error("Incorrect password") - } - - throw new Error("Missing password") - } +enum Cookie { + Key = "key", } // RateLimiter wraps around the limiter library for logins. // It allows 2 logins every minute and 12 logins every hour. class RateLimiter { - private readonly minuteLimiter = new limiter.RateLimiter(2, "minute") - private readonly hourLimiter = new limiter.RateLimiter(12, "hour") + private readonly minuteLimiter = new Limiter(2, "minute") + private readonly hourLimiter = new Limiter(12, "hour") public try(): boolean { if (this.minuteLimiter.tryRemoveTokens(1)) { @@ -143,3 +24,72 @@ class RateLimiter { return this.hourLimiter.tryRemoveTokens(1) } } + +const getRoot = async (req: Request, error?: Error): Promise => { + const content = await fs.readFile(path.join(rootPath, "src/browser/pages/login.html"), "utf8") + let passwordMsg = `Check the config file at ${humanPath(req.args.config)} for the password.` + if (req.args.usingEnvPassword) { + passwordMsg = "Password was set from $PASSWORD." + } + return replaceTemplates( + req, + content + .replace(/{{PASSWORD_MSG}}/g, passwordMsg) + .replace(/{{ERROR}}/, error ? `
${error.message}
` : ""), + ) +} + +const limiter = new RateLimiter() + +export const router = Router() + +router.use((req, res, next) => { + const to = (typeof req.query.to === "string" && req.query.to) || "/" + if (authenticated(req)) { + return redirect(req, res, to, { to: undefined }) + } + next() +}) + +router.get("/", async (req, res) => { + res.send(await getRoot(req)) +}) + +router.post("/", async (req, res) => { + try { + if (!limiter.try()) { + throw new Error("Login rate limited!") + } + + if (!req.body.password) { + throw new Error("Missing password") + } + + if (req.args.password && safeCompare(req.body.password, req.args.password)) { + // The hash does not add any actual security but we do it for + // obfuscation purposes (and as a side effect it handles escaping). + res.cookie(Cookie.Key, hash(req.body.password), { + domain: getCookieDomain(req.headers.host || "", req.args["proxy-domain"]), + path: req.body.base || "/", + sameSite: "lax", + }) + + const to = (typeof req.query.to === "string" && req.query.to) || "/" + return redirect(req, res, to, { to: undefined }) + } + + console.error( + "Failed login attempt", + JSON.stringify({ + xForwardedFor: req.headers["x-forwarded-for"], + remoteAddress: req.connection.remoteAddress, + userAgent: req.headers["user-agent"], + timestamp: Math.floor(new Date().getTime() / 1000), + }), + ) + + throw new Error("Incorrect password") + } catch (error) { + res.send(await getRoot(req, error)) + } +}) diff --git a/src/node/routes/proxy.ts b/src/node/routes/proxy.ts index a332cc055..8c83827a6 100644 --- a/src/node/routes/proxy.ts +++ b/src/node/routes/proxy.ts @@ -1,43 +1,43 @@ -import * as http from "http" +import { Request, Router } from "express" +import qs from "qs" import { HttpCode, HttpError } from "../../common/http" -import { HttpProvider, HttpResponse, Route, WsResponse } from "../http" +import { authenticated, redirect } from "../http" +import { proxy } from "../proxy" -/** - * Proxy HTTP provider. - */ -export class ProxyHttpProvider extends HttpProvider { - public async handleRequest(route: Route, request: http.IncomingMessage): Promise { - if (!this.authenticated(request)) { - if (this.isRoot(route)) { - return { redirect: "/login", query: { to: route.fullPath } } - } - throw new HttpError("Unauthorized", HttpCode.Unauthorized) - } +export const router = Router() - // Ensure there is a trailing slash so relative paths work correctly. - if (this.isRoot(route) && !route.fullPath.endsWith("/")) { - return { - redirect: `${route.fullPath}/`, - } - } - - const port = route.base.replace(/^\//, "") - return { - proxy: { - strip: `${route.providerBase}/${port}`, - port, - }, - } - } - - public async handleWebSocket(route: Route, request: http.IncomingMessage): Promise { - this.ensureAuthenticated(request) - const port = route.base.replace(/^\//, "") - return { - proxy: { - strip: `${route.providerBase}/${port}`, - port, - }, - } +const getProxyTarget = (req: Request, rewrite: boolean): string => { + if (rewrite) { + const query = qs.stringify(req.query) + return `http://127.0.0.1:${req.params.port}/${req.params[0] || ""}${query ? `?${query}` : ""}` } + return `http://127.0.0.1:${req.params.port}/${req.originalUrl}` } + +router.all("/(:port)(/*)?", (req, res) => { + if (!authenticated(req)) { + // If visiting the root (/proxy/:port and nothing else) redirect to the + // login page. + if (!req.params[0] || req.params[0] === "/") { + return redirect(req, res, "login", { + to: `${req.baseUrl}${req.path}` || "/", + }) + } + throw new HttpError("Unauthorized", HttpCode.Unauthorized) + } + + // Absolute redirects need to be based on the subpath when rewriting. + ;(req as any).base = `${req.baseUrl}/${req.params.port}` + + proxy.web(req, res, { + ignorePath: true, + target: getProxyTarget(req, true), + }) +}) + +router.ws("/(:port)(/*)?", (socket, head, req) => { + proxy.ws(req, socket, head, { + ignorePath: true, + target: getProxyTarget(req, true), + }) +}) diff --git a/src/node/routes/static.ts b/src/node/routes/static.ts index 471d0c987..0678c2313 100644 --- a/src/node/routes/static.ts +++ b/src/node/routes/static.ts @@ -1,73 +1,61 @@ import { field, logger } from "@coder/logger" -import * as http from "http" +import { Router } from "express" +import { promises as fs } from "fs" import * as path from "path" import { Readable } from "stream" import * as tarFs from "tar-fs" import * as zlib from "zlib" -import { HttpProvider, HttpResponse, Route } from "../http" -import { pathToFsPath } from "../util" +import { HttpCode, HttpError } from "../../common/http" +import { rootPath } from "../constants" +import { authenticated, replaceTemplates } from "../http" +import { getMediaMime, pathToFsPath } from "../util" -/** - * Static file HTTP provider. Static requests do not require authentication if - * the resource is in the application's directory except requests to serve a - * directory as a tar which always requires authentication. - */ -export class StaticHttpProvider extends HttpProvider { - public async handleRequest(route: Route, request: http.IncomingMessage): Promise { - this.ensureMethod(request) +export const router = Router() - if (typeof route.query.tar === "string") { - this.ensureAuthenticated(request) - return this.getTarredResource(request, pathToFsPath(route.query.tar)) - } - - const response = await this.getReplacedResource(request, route) - if (!this.isDev) { - response.cache = true - } - return response +// The commit is for caching. +router.get("/(:commit)(/*)?", async (req, res) => { + if (!req.params[0]) { + throw new HttpError("Not Found", HttpCode.NotFound) } - /** - * Return a resource with variables replaced where necessary. - */ - protected async getReplacedResource(request: http.IncomingMessage, route: Route): Promise { - // The first part is always the commit (for caching purposes). - const split = route.requestPath.split("/").slice(1) + const resourcePath = path.resolve(req.params[0]) - const resourcePath = path.resolve("/", ...split) - - // Make sure it's in code-server or a plugin. - const validPaths = [this.rootPath, process.env.PLUGIN_DIR] - if (!validPaths.find((p) => p && resourcePath.startsWith(p))) { - this.ensureAuthenticated(request) - } - - switch (split[split.length - 1]) { - case "manifest.json": { - const response = await this.getUtf8Resource(resourcePath) - return this.replaceTemplates(route, response) - } - } - return this.getResource(resourcePath) + // Make sure it's in code-server if you aren't authenticated. This lets + // unauthenticated users load the login assets. + if (!resourcePath.startsWith(rootPath) && !authenticated(req)) { + throw new HttpError("Unauthorized", HttpCode.Unauthorized) } - /** - * Tar up and stream a directory. - */ - private async getTarredResource(request: http.IncomingMessage, ...parts: string[]): Promise { - const filePath = path.join(...parts) - let stream: Readable = tarFs.pack(filePath) - const headers: http.OutgoingHttpHeaders = {} - if (request.headers["accept-encoding"] && request.headers["accept-encoding"].includes("gzip")) { - logger.debug("gzipping tar", field("filePath", filePath)) + // Don't cache during development. - can also be used if you want to make a + // static request without caching. + if (req.params.commit !== "development" && req.params.commit !== "-") { + res.header("Cache-Control", "public, max-age=31536000") + } + + const tar = Array.isArray(req.query.tar) ? req.query.tar[0] : req.query.tar + if (typeof tar === "string") { + let stream: Readable = tarFs.pack(pathToFsPath(tar)) + if (req.headers["accept-encoding"] && req.headers["accept-encoding"].includes("gzip")) { + logger.debug("gzipping tar", field("path", resourcePath)) const compress = zlib.createGzip() stream.pipe(compress) stream.on("error", (error) => compress.destroy(error)) stream.on("close", () => compress.end()) stream = compress - headers["content-encoding"] = "gzip" + res.header("content-encoding", "gzip") } - return { stream, filePath, mime: "application/x-tar", cache: true, headers } + res.set("Content-Type", "application/x-tar") + stream.on("close", () => res.end()) + return stream.pipe(res) } -} + + res.set("Content-Type", getMediaMime(resourcePath)) + + if (resourcePath.endsWith("manifest.json")) { + const content = await fs.readFile(resourcePath, "utf8") + return res.send(replaceTemplates(req, content)) + } + + const content = await fs.readFile(resourcePath) + return res.send(content) +}) diff --git a/src/node/routes/update.ts b/src/node/routes/update.ts index a83f578e1..ea7479fe0 100644 --- a/src/node/routes/update.ts +++ b/src/node/routes/update.ts @@ -1,172 +1,34 @@ -import { field, logger } from "@coder/logger" -import * as http from "http" -import * as https from "https" -import * as path from "path" -import * as semver from "semver" -import * as url from "url" -import { HttpCode, HttpError } from "../../common/http" -import { HttpProvider, HttpProviderOptions, HttpResponse, Route } from "../http" -import { settings as globalSettings, SettingsProvider, UpdateSettings } from "../settings" +import { Router } from "express" +import { version } from "../constants" +import { ensureAuthenticated } from "../http" +import { UpdateProvider } from "../update" -export interface Update { - checked: number - version: string -} +export const router = Router() -export interface LatestResponse { - name: string -} +const provider = new UpdateProvider() -/** - * HTTP provider for checking updates (does not download/install them). - */ -export class UpdateHttpProvider extends HttpProvider { - private update?: Promise - private updateInterval = 1000 * 60 * 60 * 24 // Milliseconds between update checks. +router.use((req, _, next) => { + ensureAuthenticated(req) + next() +}) - public constructor( - options: HttpProviderOptions, - public readonly enabled: boolean, - /** - * The URL for getting the latest version of code-server. Should return JSON - * that fulfills `LatestResponse`. - */ - private readonly latestUrl = "https://api.github.com/repos/cdr/code-server/releases/latest", - /** - * Update information will be stored here. If not provided, the global - * settings will be used. - */ - private readonly settings: SettingsProvider = globalSettings, - ) { - super(options) - } +router.get("/", async (_, res) => { + const update = await provider.getUpdate() + res.json({ + checked: update.checked, + latest: update.version, + current: version, + isLatest: provider.isLatestVersion(update), + }) +}) - public async handleRequest(route: Route, request: http.IncomingMessage): Promise { - this.ensureAuthenticated(request) - this.ensureMethod(request) - - if (!this.isRoot(route)) { - throw new HttpError("Not found", HttpCode.NotFound) - } - - if (!this.enabled) { - throw new Error("update checks are disabled") - } - - switch (route.base) { - case "/check": - case "/": { - const update = await this.getUpdate(route.base === "/check") - return { - content: { - ...update, - isLatest: this.isLatestVersion(update), - }, - } - } - } - - throw new HttpError("Not found", HttpCode.NotFound) - } - - /** - * Query for and return the latest update. - */ - public async getUpdate(force?: boolean): Promise { - // Don't run multiple requests at a time. - if (!this.update) { - this.update = this._getUpdate(force) - this.update.then(() => (this.update = undefined)) - } - - return this.update - } - - private async _getUpdate(force?: boolean): Promise { - const now = Date.now() - try { - let { update } = !force ? await this.settings.read() : { update: undefined } - if (!update || update.checked + this.updateInterval < now) { - const buffer = await this.request(this.latestUrl) - const data = JSON.parse(buffer.toString()) as LatestResponse - update = { checked: now, version: data.name } - await this.settings.write({ update }) - } - logger.debug("got latest version", field("latest", update.version)) - return update - } catch (error) { - logger.error("Failed to get latest version", field("error", error.message)) - return { - checked: now, - version: "unknown", - } - } - } - - public get currentVersion(): string { - return require(path.resolve(__dirname, "../../../package.json")).version - } - - /** - * Return true if the currently installed version is the latest. - */ - public isLatestVersion(latest: Update): boolean { - const version = this.currentVersion - logger.debug("comparing versions", field("current", version), field("latest", latest.version)) - try { - return latest.version === version || semver.lt(latest.version, version) - } catch (error) { - return true - } - } - - private async request(uri: string): Promise { - const response = await this.requestResponse(uri) - return new Promise((resolve, reject) => { - const chunks: Buffer[] = [] - let bufferLength = 0 - response.on("data", (chunk) => { - bufferLength += chunk.length - chunks.push(chunk) - }) - response.on("error", reject) - response.on("end", () => { - resolve(Buffer.concat(chunks, bufferLength)) - }) - }) - } - - private async requestResponse(uri: string): Promise { - let redirects = 0 - const maxRedirects = 10 - return new Promise((resolve, reject) => { - const request = (uri: string): void => { - logger.debug("Making request", field("uri", uri)) - const httpx = uri.startsWith("https") ? https : http - const client = httpx.get(uri, { headers: { "User-Agent": "code-server" } }, (response) => { - if ( - response.statusCode && - response.statusCode >= 300 && - response.statusCode < 400 && - response.headers.location - ) { - ++redirects - if (redirects > maxRedirects) { - return reject(new Error("reached max redirects")) - } - response.destroy() - return request(url.resolve(uri, response.headers.location)) - } - - if (!response.statusCode || response.statusCode < 200 || response.statusCode >= 400) { - return reject(new Error(`${uri}: ${response.statusCode || "500"}`)) - } - - resolve(response) - }) - client.on("error", reject) - } - request(uri) - }) - } -} +// This route will force a check. +router.get("/check", async (_, res) => { + const update = await provider.getUpdate(true) + res.json({ + checked: update.checked, + latest: update.version, + current: version, + isLatest: provider.isLatestVersion(update), + }) +}) diff --git a/src/node/routes/vscode.ts b/src/node/routes/vscode.ts index ed4f714e5..5f8dd7584 100644 --- a/src/node/routes/vscode.ts +++ b/src/node/routes/vscode.ts @@ -1,237 +1,99 @@ -import { field, logger } from "@coder/logger" -import * as cp from "child_process" import * as crypto from "crypto" -import * as fs from "fs-extra" -import * as http from "http" -import * as net from "net" +import { Router } from "express" +import { promises as fs } from "fs" import * as path from "path" -import { - CodeServerMessage, - Options, - StartPath, - VscodeMessage, - VscodeOptions, - WorkbenchOptions, -} from "../../../lib/vscode/src/vs/server/ipc" -import { HttpCode, HttpError } from "../../common/http" -import { arrayify, generateUuid } from "../../common/util" -import { Args } from "../cli" -import { HttpProvider, HttpProviderOptions, HttpResponse, Route } from "../http" -import { settings } from "../settings" -import { pathToFsPath } from "../util" +import { commit, rootPath, version } from "../constants" +import { authenticated, ensureAuthenticated, redirect, replaceTemplates } from "../http" +import { getMediaMime, pathToFsPath } from "../util" +import { VscodeProvider } from "../vscode" -export class VscodeHttpProvider extends HttpProvider { - private readonly serverRootPath: string - private readonly vsRootPath: string - private _vscode?: Promise +export const router = Router() - public constructor(options: HttpProviderOptions, private readonly args: Args) { - super(options) - this.vsRootPath = path.resolve(this.rootPath, "lib/vscode") - this.serverRootPath = path.join(this.vsRootPath, "out/vs/server") - } +const vscode = new VscodeProvider() - public get running(): boolean { - return !!this._vscode - } - - public async dispose(): Promise { - if (this._vscode) { - const vscode = await this._vscode - vscode.removeAllListeners() - this._vscode = undefined - vscode.kill() - } - } - - private async initialize(options: VscodeOptions): Promise { - const id = generateUuid() - const vscode = await this.fork() - - logger.debug("setting up vs code...") - return new Promise((resolve, reject) => { - vscode.once("message", (message: VscodeMessage) => { - logger.debug("got message from vs code", field("message", message)) - return message.type === "options" && message.id === id - ? resolve(message.options) - : reject(new Error("Unexpected response during initialization")) - }) - vscode.once("error", reject) - vscode.once("exit", (code) => reject(new Error(`VS Code exited unexpectedly with code ${code}`))) - this.send({ type: "init", id, options }, vscode) +router.get("/", async (req, res) => { + if (!authenticated(req)) { + return redirect(req, res, "login", { + to: req.baseUrl || "/", }) } - private fork(): Promise { - if (!this._vscode) { - logger.debug("forking vs code...") - const vscode = cp.fork(path.join(this.serverRootPath, "fork")) - vscode.on("error", (error) => { - logger.error(error.message) - this._vscode = undefined - }) - vscode.on("exit", (code) => { - logger.error(`VS Code exited unexpectedly with code ${code}`) - this._vscode = undefined - }) - - this._vscode = new Promise((resolve, reject) => { - vscode.once("message", (message: VscodeMessage) => { - logger.debug("got message from vs code", field("message", message)) - return message.type === "ready" - ? resolve(vscode) - : reject(new Error("Unexpected response waiting for ready response")) - }) - vscode.once("error", reject) - vscode.once("exit", (code) => reject(new Error(`VS Code exited unexpectedly with code ${code}`))) - }) - } - - return this._vscode - } - - public async handleWebSocket(route: Route, request: http.IncomingMessage, socket: net.Socket): Promise { - if (!this.authenticated(request)) { - throw new Error("not authenticated") - } - - // VS Code expects a raw socket. It will handle all the web socket frames. - // We just need to handle the initial upgrade. - // This magic value is specified by the websocket spec. - const magic = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" - const reply = crypto - .createHash("sha1") - .update(request.headers["sec-websocket-key"] + magic) - .digest("base64") - socket.write( - [ - "HTTP/1.1 101 Switching Protocols", - "Upgrade: websocket", - "Connection: Upgrade", - `Sec-WebSocket-Accept: ${reply}`, - ].join("\r\n") + "\r\n\r\n", - ) - - const vscode = await this._vscode - this.send({ type: "socket", query: route.query }, vscode, socket) - } - - private send(message: CodeServerMessage, vscode?: cp.ChildProcess, socket?: net.Socket): void { - if (!vscode || vscode.killed) { - throw new Error("vscode is not running") - } - vscode.send(message, socket) - } - - public async handleRequest(route: Route, request: http.IncomingMessage): Promise { - this.ensureMethod(request) - - switch (route.base) { - case "/": - if (!this.isRoot(route)) { - throw new HttpError("Not found", HttpCode.NotFound) - } else if (!this.authenticated(request)) { - return { redirect: "/login", query: { to: route.providerBase } } - } - try { - return await this.getRoot(request, route) - } catch (error) { - const message = `
VS Code failed to load.
${ - this.isDev - ? `
It might not have finished compiling.
` + - `Check for Finished compilation in the output.` - : "" - }

${error}` - return this.getErrorRoot(route, "VS Code failed to load", "500", message) - } - } - - this.ensureAuthenticated(request) - - switch (route.base) { - case "/resource": - case "/vscode-remote-resource": - if (typeof route.query.path === "string") { - return this.getResource(pathToFsPath(route.query.path)) - } - break - case "/webview": - if (/^\/vscode-resource/.test(route.requestPath)) { - return this.getResource(route.requestPath.replace(/^\/vscode-resource(\/file)?/, "")) - } - return this.getResource(this.vsRootPath, "out/vs/workbench/contrib/webview/browser/pre", route.requestPath) - } - - throw new HttpError("Not found", HttpCode.NotFound) - } - - private async getRoot(request: http.IncomingMessage, route: Route): Promise { - const remoteAuthority = request.headers.host as string - const { lastVisited } = await settings.read() - const startPath = await this.getFirstPath([ - { url: route.query.workspace, workspace: true }, - { url: route.query.folder, workspace: false }, - this.args._ && this.args._.length > 0 ? { url: path.resolve(this.args._[this.args._.length - 1]) } : undefined, - lastVisited, - ]) - const [response, options] = await Promise.all([ - await this.getUtf8Resource(this.rootPath, "src/browser/pages/vscode.html"), - this.initialize({ - args: this.args, - remoteAuthority, - startPath, + const [content, options] = await Promise.all([ + await fs.readFile(path.join(rootPath, "src/browser/pages/vscode.html"), "utf8"), + vscode + .initialize( + { + args: req.args, + remoteAuthority: req.headers.host || "", + }, + req.query, + ) + .catch((error) => { + const devMessage = commit === "development" ? "It might not have finished compiling." : "" + throw new Error(`VS Code failed to load. ${devMessage} ${error.message}`) }), - ]) + ]) - settings.write({ - lastVisited: startPath || lastVisited, // If startpath is undefined, then fallback to lastVisited - query: route.query, - }) + options.productConfiguration.codeServerVersion = version - if (!this.isDev) { - response.content = response.content.replace(//g, "") - } - - options.productConfiguration.codeServerVersion = require("../../../package.json").version - - response.content = response.content + res.send( + replaceTemplates( + req, + // Uncomment prod blocks if not in development. TODO: Would this be + // better as a build step? Or maintain two HTML files again? + commit !== "development" ? content.replace(//g, "") : content, + { + disableTelemetry: !!req.args["disable-telemetry"], + }, + ) .replace(`"{{REMOTE_USER_DATA_URI}}"`, `'${JSON.stringify(options.remoteUserDataUri)}'`) .replace(`"{{PRODUCT_CONFIGURATION}}"`, `'${JSON.stringify(options.productConfiguration)}'`) .replace(`"{{WORKBENCH_WEB_CONFIGURATION}}"`, `'${JSON.stringify(options.workbenchWebConfiguration)}'`) - .replace(`"{{NLS_CONFIGURATION}}"`, `'${JSON.stringify(options.nlsConfiguration)}'`) - return this.replaceTemplates(route, response, { - disableTelemetry: !!this.args["disable-telemetry"], - }) - } + .replace(`"{{NLS_CONFIGURATION}}"`, `'${JSON.stringify(options.nlsConfiguration)}'`), + ) +}) - /** - * Choose the first non-empty path. - */ - private async getFirstPath( - startPaths: Array<{ url?: string | string[]; workspace?: boolean } | undefined>, - ): Promise { - const isFile = async (path: string): Promise => { - try { - const stat = await fs.stat(path) - return stat.isFile() - } catch (error) { - logger.warn(error.message) - return false - } - } - for (let i = 0; i < startPaths.length; ++i) { - const startPath = startPaths[i] - const url = arrayify(startPath && startPath.url).find((p) => !!p) - if (startPath && url) { - return { - url, - // The only time `workspace` is undefined is for the command-line - // argument, in which case it's a path (not a URL) so we can stat it - // without having to parse it. - workspace: typeof startPath.workspace !== "undefined" ? startPath.workspace : await isFile(url), - } - } - } - return undefined +router.ws("/", async (socket, _, req) => { + ensureAuthenticated(req) + const magic = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" + const reply = crypto + .createHash("sha1") + .update(req.headers["sec-websocket-key"] + magic) + .digest("base64") + socket.write( + [ + "HTTP/1.1 101 Switching Protocols", + "Upgrade: websocket", + "Connection: Upgrade", + `Sec-WebSocket-Accept: ${reply}`, + ].join("\r\n") + "\r\n\r\n", + ) + await vscode.sendWebsocket(socket, req.query) +}) + +router.get("/resource(/*)?", async (req, res) => { + ensureAuthenticated(req) + if (typeof req.query.path === "string") { + res.set("Content-Type", getMediaMime(req.query.path)) + res.send(await fs.readFile(pathToFsPath(req.query.path))) } -} +}) + +router.get("/vscode-remote-resource(/*)?", async (req, res) => { + ensureAuthenticated(req) + if (typeof req.query.path === "string") { + res.set("Content-Type", getMediaMime(req.query.path)) + res.send(await fs.readFile(pathToFsPath(req.query.path))) + } +}) + +router.get("/webview/*", async (req, res) => { + ensureAuthenticated(req) + res.set("Content-Type", getMediaMime(req.path)) + if (/^\/vscode-resource/.test(req.path)) { + return res.send(await fs.readFile(req.path.replace(/^\/vscode-resource(\/file)?/, ""))) + } + return res.send( + await fs.readFile(path.join(vscode.vsRootPath, "out/vs/workbench/contrib/webview/browser/pre", req.params[0])), + ) +}) diff --git a/src/node/settings.ts b/src/node/settings.ts index d68e8e3bd..5f9427aa1 100644 --- a/src/node/settings.ts +++ b/src/node/settings.ts @@ -1,7 +1,7 @@ import { logger } from "@coder/logger" +import { Query } from "express-serve-static-core" import * as fs from "fs-extra" import * as path from "path" -import { Route } from "./http" import { paths } from "./util" export type Settings = { [key: string]: Settings | string | boolean | number } @@ -58,7 +58,7 @@ export interface CoderSettings extends UpdateSettings { url: string workspace: boolean } - query: Route["query"] + query: Query } /** diff --git a/src/node/update.ts b/src/node/update.ts new file mode 100644 index 000000000..42baa1848 --- /dev/null +++ b/src/node/update.ts @@ -0,0 +1,133 @@ +import { field, logger } from "@coder/logger" +import * as http from "http" +import * as https from "https" +import * as semver from "semver" +import * as url from "url" +import { version } from "./constants" +import { settings as globalSettings, SettingsProvider, UpdateSettings } from "./settings" + +export interface Update { + checked: number + version: string +} + +export interface LatestResponse { + name: string +} + +/** + * Provide update information. + */ +export class UpdateProvider { + private update?: Promise + private updateInterval = 1000 * 60 * 60 * 24 // Milliseconds between update checks. + + public constructor( + /** + * The URL for getting the latest version of code-server. Should return JSON + * that fulfills `LatestResponse`. + */ + private readonly latestUrl = "https://api.github.com/repos/cdr/code-server/releases/latest", + /** + * Update information will be stored here. If not provided, the global + * settings will be used. + */ + private readonly settings: SettingsProvider = globalSettings, + ) {} + + /** + * Query for and return the latest update. + */ + public async getUpdate(force?: boolean): Promise { + // Don't run multiple requests at a time. + if (!this.update) { + this.update = this._getUpdate(force) + this.update.then(() => (this.update = undefined)) + } + + return this.update + } + + private async _getUpdate(force?: boolean): Promise { + const now = Date.now() + try { + let { update } = !force ? await this.settings.read() : { update: undefined } + if (!update || update.checked + this.updateInterval < now) { + const buffer = await this.request(this.latestUrl) + const data = JSON.parse(buffer.toString()) as LatestResponse + update = { checked: now, version: data.name.replace(/^v/, "") } + await this.settings.write({ update }) + } + logger.debug("got latest version", field("latest", update.version)) + return update + } catch (error) { + logger.error("Failed to get latest version", field("error", error.message)) + return { + checked: now, + version: "unknown", + } + } + } + + /** + * Return true if the currently installed version is the latest. + */ + public isLatestVersion(latest: Update): boolean { + logger.debug("comparing versions", field("current", version), field("latest", latest.version)) + try { + return latest.version === version || semver.lt(latest.version, version) + } catch (error) { + return true + } + } + + private async request(uri: string): Promise { + const response = await this.requestResponse(uri) + return new Promise((resolve, reject) => { + const chunks: Buffer[] = [] + let bufferLength = 0 + response.on("data", (chunk) => { + bufferLength += chunk.length + chunks.push(chunk) + }) + response.on("error", reject) + response.on("end", () => { + resolve(Buffer.concat(chunks, bufferLength)) + }) + }) + } + + private async requestResponse(uri: string): Promise { + let redirects = 0 + const maxRedirects = 10 + return new Promise((resolve, reject) => { + const request = (uri: string): void => { + logger.debug("Making request", field("uri", uri)) + const httpx = uri.startsWith("https") ? https : http + const client = httpx.get(uri, { headers: { "User-Agent": "code-server" } }, (response) => { + if ( + response.statusCode && + response.statusCode >= 300 && + response.statusCode < 400 && + response.headers.location + ) { + ++redirects + if (redirects > maxRedirects) { + return reject(new Error("reached max redirects")) + } + response.destroy() + return request(url.resolve(uri, response.headers.location)) + } + + if (!response.statusCode || response.statusCode < 200 || response.statusCode >= 400) { + return reject(new Error(`${uri}: ${response.statusCode || "500"}`)) + } + + resolve(response) + }) + client.on("error", reject) + } + request(uri) + }) + } +} diff --git a/src/node/vscode.ts b/src/node/vscode.ts new file mode 100644 index 000000000..5cf4e4b76 --- /dev/null +++ b/src/node/vscode.ts @@ -0,0 +1,150 @@ +import { field, logger } from "@coder/logger" +import * as cp from "child_process" +import * as fs from "fs-extra" +import * as net from "net" +import * as path from "path" +import * as ipc from "../../lib/vscode/src/vs/server/ipc" +import { arrayify, generateUuid } from "../common/util" +import { rootPath } from "./constants" +import { settings } from "./settings" + +export class VscodeProvider { + public readonly serverRootPath: string + public readonly vsRootPath: string + private _vscode?: Promise + + public constructor() { + this.vsRootPath = path.resolve(rootPath, "lib/vscode") + this.serverRootPath = path.join(this.vsRootPath, "out/vs/server") + } + + public async dispose(): Promise { + if (this._vscode) { + const vscode = await this._vscode + vscode.removeAllListeners() + this._vscode = undefined + vscode.kill() + } + } + + public async initialize( + options: Omit, + query: ipc.Query, + ): Promise { + const { lastVisited } = await settings.read() + const startPath = await this.getFirstPath([ + { url: query.workspace, workspace: true }, + { url: query.folder, workspace: false }, + options.args._ && options.args._.length > 0 + ? { url: path.resolve(options.args._[options.args._.length - 1]) } + : undefined, + lastVisited, + ]) + + settings.write({ + lastVisited: startPath, + query, + }) + + const id = generateUuid() + const vscode = await this.fork() + + logger.debug("setting up vs code...") + return new Promise((resolve, reject) => { + vscode.once("message", (message: ipc.VscodeMessage) => { + logger.debug("got message from vs code", field("message", message)) + return message.type === "options" && message.id === id + ? resolve(message.options) + : reject(new Error("Unexpected response during initialization")) + }) + vscode.once("error", reject) + vscode.once("exit", (code) => reject(new Error(`VS Code exited unexpectedly with code ${code}`))) + this.send( + { + type: "init", + id, + options: { + ...options, + startPath, + }, + }, + vscode, + ) + }) + } + + private fork(): Promise { + if (!this._vscode) { + logger.debug("forking vs code...") + const vscode = cp.fork(path.join(this.serverRootPath, "fork")) + vscode.on("error", (error) => { + logger.error(error.message) + this._vscode = undefined + }) + vscode.on("exit", (code) => { + logger.error(`VS Code exited unexpectedly with code ${code}`) + this._vscode = undefined + }) + + this._vscode = new Promise((resolve, reject) => { + vscode.once("message", (message: ipc.VscodeMessage) => { + logger.debug("got message from vs code", field("message", message)) + return message.type === "ready" + ? resolve(vscode) + : reject(new Error("Unexpected response waiting for ready response")) + }) + vscode.once("error", reject) + vscode.once("exit", (code) => reject(new Error(`VS Code exited unexpectedly with code ${code}`))) + }) + } + + return this._vscode + } + + /** + * VS Code expects a raw socket. It will handle all the web socket frames. + */ + public async sendWebsocket(socket: net.Socket, query: ipc.Query): Promise { + // TODO: TLS socket proxy. + const vscode = await this._vscode + this.send({ type: "socket", query }, vscode, socket) + } + + private send(message: ipc.CodeServerMessage, vscode?: cp.ChildProcess, socket?: net.Socket): void { + if (!vscode || vscode.killed) { + throw new Error("vscode is not running") + } + vscode.send(message, socket) + } + + /** + * Choose the first non-empty path. + */ + private async getFirstPath( + startPaths: Array<{ url?: string | string[] | ipc.Query | ipc.Query[]; workspace?: boolean } | undefined>, + ): Promise { + const isFile = async (path: string): Promise => { + try { + const stat = await fs.stat(path) + return stat.isFile() + } catch (error) { + logger.warn(error.message) + return false + } + } + for (let i = 0; i < startPaths.length; ++i) { + const startPath = startPaths[i] + const url = arrayify(startPath && startPath.url).find((p) => !!p) + if (startPath && url && typeof url === "string") { + return { + url, + // The only time `workspace` is undefined is for the command-line + // argument, in which case it's a path (not a URL) so we can stat it + // without having to parse it. + workspace: typeof startPath.workspace !== "undefined" ? startPath.workspace : await isFile(url), + } + } + } + return undefined + } +} diff --git a/test/update.test.ts b/test/update.test.ts index 093429be3..29c558f56 100644 --- a/test/update.test.ts +++ b/test/update.test.ts @@ -2,9 +2,8 @@ import * as assert from "assert" import * as fs from "fs-extra" import * as http from "http" import * as path from "path" -import { AuthType } from "../src/node/cli" -import { LatestResponse, UpdateHttpProvider } from "../src/node/routes/update" import { SettingsProvider, UpdateSettings } from "../src/node/settings" +import { LatestResponse, UpdateProvider } from "../src/node/update" import { tmpdir } from "../src/node/util" describe.skip("update", () => { @@ -34,22 +33,14 @@ describe.skip("update", () => { const jsonPath = path.join(tmpdir, "tests/updates/update.json") const settings = new SettingsProvider(jsonPath) - let _provider: UpdateHttpProvider | undefined - const provider = (): UpdateHttpProvider => { + let _provider: UpdateProvider | undefined + const provider = (): UpdateProvider => { if (!_provider) { const address = server.address() if (!address || typeof address === "string" || !address.port) { throw new Error("unexpected address") } - _provider = new UpdateHttpProvider( - { - auth: AuthType.None, - commit: "test", - }, - true, - `http://${address.address}:${address.port}/latest`, - settings, - ) + _provider = new UpdateProvider(`http://${address.address}:${address.port}/latest`, settings) } return _provider } @@ -153,14 +144,10 @@ describe.skip("update", () => { }) it("should not reject if unable to fetch", async () => { - const options = { - auth: AuthType.None, - commit: "test", - } - let provider = new UpdateHttpProvider(options, true, "invalid", settings) + let provider = new UpdateProvider("invalid", settings) await assert.doesNotReject(() => provider.getUpdate(true)) - provider = new UpdateHttpProvider(options, true, "http://probably.invalid.dev.localhost/latest", settings) + provider = new UpdateProvider("http://probably.invalid.dev.localhost/latest", settings) await assert.doesNotReject(() => provider.getUpdate(true)) }) }) diff --git a/yarn.lock b/yarn.lock index d1224a32f..1d98caab6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1013,7 +1013,7 @@ traverse "^0.6.6" unified "^6.1.6" -"@types/body-parser@*": +"@types/body-parser@*", "@types/body-parser@^1.19.0": version "1.19.0" resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.0.tgz#0685b3c47eb3006ffed117cdd55164b61f80538f" integrity sha512-W98JrE0j2K78swW4ukqMleo8R7h/pFETjM2DQ90MF6XK2i4LO4W3gQ71Lt4w3bfm2EvVSyWHplECvB5sK22yFQ== @@ -1028,6 +1028,13 @@ dependencies: "@types/node" "*" +"@types/cookie-parser@^1.4.2": + version "1.4.2" + resolved "https://registry.yarnpkg.com/@types/cookie-parser/-/cookie-parser-1.4.2.tgz#e4d5c5ffda82b80672a88a4281aaceefb1bd9df5" + integrity sha512-uwcY8m6SDQqciHsqcKDGbo10GdasYsPCYkH3hVegj9qAah6pX5HivOnOuI3WYmyQMnOATV39zv/Ybs0bC/6iVg== + dependencies: + "@types/express" "*" + "@types/eslint-visitor-keys@^1.0.0": version "1.0.0" resolved "https://registry.yarnpkg.com/@types/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz#1ee30d79544ca84d68d4b3cdb0af4f205663dd2d" @@ -1042,7 +1049,7 @@ "@types/qs" "*" "@types/range-parser" "*" -"@types/express@^4.17.8": +"@types/express@*", "@types/express@^4.17.8": version "4.17.8" resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.8.tgz#3df4293293317e61c60137d273a2e96cd8d5f27a" integrity sha512-wLhcKh3PMlyA2cNAB9sjM1BntnhPMiM0JOBwPBqttjHev2428MLEB4AYVN+d8s2iyCVZac+o41Pflm/ZH5vLXQ== @@ -1659,7 +1666,7 @@ bn.js@^5.1.1: resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-5.1.3.tgz#beca005408f642ebebea80b042b4d18d2ac0ee6b" integrity sha512-GkTiFpjFtUzU9CbMeJ5iazkCzGL3jrhzerzZIuqLABjbwRaFt33I9tUdSNryIptM+RxDet6OKm2WnLXzW51KsQ== -body-parser@1.19.0: +body-parser@1.19.0, body-parser@^1.19.0: version "1.19.0" resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.19.0.tgz#96b2709e57c9c4e09a6fd66a8fd979844f69f08a" integrity sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw== @@ -2251,6 +2258,14 @@ convert-source-map@^1.5.1, convert-source-map@^1.7.0: dependencies: safe-buffer "~5.1.1" +cookie-parser@^1.4.5: + version "1.4.5" + resolved "https://registry.yarnpkg.com/cookie-parser/-/cookie-parser-1.4.5.tgz#3e572d4b7c0c80f9c61daf604e4336831b5d1d49" + integrity sha512-f13bPUj/gG/5mDr+xLmSxxDsB9DQiTIfhJS/sqjrmfAWiAN+x2O4i/XguTL9yDZ+/IFDanJ+5x7hC4CXT9Tdzw== + dependencies: + cookie "0.4.0" + cookie-signature "1.0.6" + cookie-signature@1.0.6: version "1.0.6" resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" From 257d9a4fa4de1b50af26204a61a7528a3ead25ce Mon Sep 17 00:00:00 2001 From: Asher Date: Mon, 26 Oct 2020 15:13:52 -0500 Subject: [PATCH 099/247] Make authentication work with sub-domain proxy --- src/node/proxy.ts | 34 +++++++++++++++++++++++++++++----- 1 file changed, 29 insertions(+), 5 deletions(-) diff --git a/src/node/proxy.ts b/src/node/proxy.ts index 8f5dd684b..d6dd795de 100644 --- a/src/node/proxy.ts +++ b/src/node/proxy.ts @@ -1,7 +1,7 @@ import { Request, Router } from "express" import proxyServer from "http-proxy" -import { HttpCode } from "../common/http" -import { ensureAuthenticated } from "./http" +import { HttpCode, HttpError } from "../common/http" +import { authenticated, ensureAuthenticated } from "./http" export const proxy = proxyServer.createProxyServer({}) proxy.on("error", (error, _, res) => { @@ -42,18 +42,39 @@ const maybeProxy = (req: Request): string | undefined => { return undefined } - // Must be authenticated to use the proxy. - ensureAuthenticated(req) - return port } +/** + * Determine if the user is browsing /, /login, or static assets and if so fall + * through to allow the redirect and login flow. + */ +const shouldFallThrough = (req: Request): boolean => { + // The ideal would be to have a reliable way to detect if this is a request + // for (or originating from) our root or login HTML. But requests for HTML + // don't seem to set any content type. + return ( + req.headers["content-type"] !== "application/json" && + ((req.originalUrl.startsWith("/") && req.method === "GET") || + (req.originalUrl.startsWith("/static") && req.method === "GET") || + (req.originalUrl.startsWith("/login") && (req.method === "GET" || req.method === "POST"))) + ) +} + router.all("*", (req, res, next) => { const port = maybeProxy(req) if (!port) { return next() } + // Must be authenticated to use the proxy. + if (!authenticated(req)) { + if (shouldFallThrough(req)) { + return next() + } + throw new HttpError("Unauthorized", HttpCode.Unauthorized) + } + proxy.web(req, res, { ignorePath: true, target: `http://127.0.0.1:${port}${req.originalUrl}`, @@ -66,6 +87,9 @@ router.ws("*", (socket, head, req, next) => { return next() } + // Must be authenticated to use the proxy. + ensureAuthenticated(req) + proxy.ws(req, socket, head, { ignorePath: true, target: `http://127.0.0.1:${port}${req.originalUrl}`, From 6422a8d74b0b644189e6589dbb4f8d7945008421 Mon Sep 17 00:00:00 2001 From: Asher Date: Tue, 27 Oct 2020 17:17:05 -0500 Subject: [PATCH 100/247] Fix webview resource path --- src/node/routes/vscode.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/node/routes/vscode.ts b/src/node/routes/vscode.ts index 5f8dd7584..adef232fd 100644 --- a/src/node/routes/vscode.ts +++ b/src/node/routes/vscode.ts @@ -90,8 +90,8 @@ router.get("/vscode-remote-resource(/*)?", async (req, res) => { router.get("/webview/*", async (req, res) => { ensureAuthenticated(req) res.set("Content-Type", getMediaMime(req.path)) - if (/^\/vscode-resource/.test(req.path)) { - return res.send(await fs.readFile(req.path.replace(/^\/vscode-resource(\/file)?/, ""))) + if (/^vscode-resource/.test(req.params[0])) { + return res.send(await fs.readFile(req.params[0].replace(/^vscode-resource(\/file)?/, ""))) } return res.send( await fs.readFile(path.join(vscode.vsRootPath, "out/vs/workbench/contrib/webview/browser/pre", req.params[0])), From 6ab6cb4f0731ce122f75204c06ef85e6ae1bc4d8 Mon Sep 17 00:00:00 2001 From: Asher Date: Tue, 27 Oct 2020 17:18:44 -0500 Subject: [PATCH 101/247] Fix error handler types --- src/node/routes/index.ts | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/node/routes/index.ts b/src/node/routes/index.ts index 82a68866b..8c541d555 100644 --- a/src/node/routes/index.ts +++ b/src/node/routes/index.ts @@ -1,7 +1,7 @@ import { logger } from "@coder/logger" import bodyParser from "body-parser" import cookieParser from "cookie-parser" -import { Express } from "express" +import { ErrorRequestHandler, Express } from "express" import { promises as fs } from "fs" import http from "http" import * as path from "path" @@ -100,9 +100,7 @@ export const register = async (app: Express, server: http.Server, args: Defaulte throw new HttpError("Not Found", HttpCode.NotFound) }) - // Handle errors. - // TODO: The types are broken; says they're all implicitly `any`. - app.use(async (err: any, req: any, res: any, next: any) => { + const errorHandler: ErrorRequestHandler = async (err, req, res, next) => { const resourcePath = path.resolve(rootPath, "src/browser/pages/error.html") res.set("Content-Type", getMediaMime(resourcePath)) try { @@ -110,14 +108,17 @@ export const register = async (app: Express, server: http.Server, args: Defaulte if (err.code === "ENOENT" || err.code === "EISDIR") { err.status = HttpCode.NotFound } - res.status(err.status || 500).send( + const status = err.status ?? err.statusCode ?? 500 + res.status(status).send( replaceTemplates(req, content) - .replace(/{{ERROR_TITLE}}/g, err.status || "Error") - .replace(/{{ERROR_HEADER}}/g, err.status || "Error") + .replace(/{{ERROR_TITLE}}/g, status) + .replace(/{{ERROR_HEADER}}/g, status) .replace(/{{ERROR_BODY}}/g, err.message), ) } catch (error) { next(error) } - }) + } + + app.use(errorHandler) } From 305348f0aca6e6944943ec807675f7af4b02ab72 Mon Sep 17 00:00:00 2001 From: Asher Date: Tue, 27 Oct 2020 17:26:22 -0500 Subject: [PATCH 102/247] Improve proxy fallthrough logic - Use accept header. - Match /login and /login/ exactly. - Match /static/ (trailing slash). - Use req.path. Same result but feels more accurate to me. --- src/node/proxy.ts | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/src/node/proxy.ts b/src/node/proxy.ts index d6dd795de..4c861d0fe 100644 --- a/src/node/proxy.ts +++ b/src/node/proxy.ts @@ -50,15 +50,18 @@ const maybeProxy = (req: Request): string | undefined => { * through to allow the redirect and login flow. */ const shouldFallThrough = (req: Request): boolean => { - // The ideal would be to have a reliable way to detect if this is a request - // for (or originating from) our root or login HTML. But requests for HTML - // don't seem to set any content type. - return ( - req.headers["content-type"] !== "application/json" && - ((req.originalUrl.startsWith("/") && req.method === "GET") || - (req.originalUrl.startsWith("/static") && req.method === "GET") || - (req.originalUrl.startsWith("/login") && (req.method === "GET" || req.method === "POST"))) - ) + // See if it looks like a request for the root or login HTML. + if (req.accepts("text/html")) { + if ( + (req.path === "/" && req.method === "GET") || + (/\/login\/?/.test(req.path) && (req.method === "GET" || req.method === "POST")) + ) { + return true + } + } + + // See if it looks like a request for a static asset. + return req.path.startsWith("/static/") && req.method === "GET" } router.all("*", (req, res, next) => { From cde94d5ed4c8e12baa9ff7f2d43ae9a835906095 Mon Sep 17 00:00:00 2001 From: Asher Date: Tue, 27 Oct 2020 17:35:42 -0500 Subject: [PATCH 103/247] Remove redundant serverAddress check We now guarantee there is an address. --- src/node/entry.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/node/entry.ts b/src/node/entry.ts index 30dd93d8f..3b95f9e90 100644 --- a/src/node/entry.ts +++ b/src/node/entry.ts @@ -159,7 +159,7 @@ const main = async (args: DefaultedArgs): Promise => { } } - if (serverAddress && !args.socket && args.open) { + if (!args.socket && args.open) { // The web socket doesn't seem to work if browsing with 0.0.0.0. const openAddress = serverAddress.replace(/:\/\/0.0.0.0/, "://localhost") await open(openAddress).catch((error: Error) => { From dc177ab50580a5df971b0f1fbe539a9259b28139 Mon Sep 17 00:00:00 2001 From: Asher Date: Tue, 27 Oct 2020 17:38:54 -0500 Subject: [PATCH 104/247] Unambiguify address replacement Co-authored-by: Teffen Ellis --- src/node/entry.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/node/entry.ts b/src/node/entry.ts index 3b95f9e90..890ad6ae8 100644 --- a/src/node/entry.ts +++ b/src/node/entry.ts @@ -161,7 +161,7 @@ const main = async (args: DefaultedArgs): Promise => { if (!args.socket && args.open) { // The web socket doesn't seem to work if browsing with 0.0.0.0. - const openAddress = serverAddress.replace(/:\/\/0.0.0.0/, "://localhost") + const openAddress = serverAddress.replace("://0.0.0.0", "://localhost") await open(openAddress).catch((error: Error) => { logger.error("Failed to open", field("address", openAddress), field("error", error)) }) From 504d89638b4b55a62ffc65144cce6a140aedcde5 Mon Sep 17 00:00:00 2001 From: Asher Date: Tue, 27 Oct 2020 17:41:11 -0500 Subject: [PATCH 105/247] Fix open line being printed when open fails Opening the URL can fail if the user doesn't have something appropriate installed to handle it. --- src/node/entry.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/node/entry.ts b/src/node/entry.ts index 890ad6ae8..df218a52d 100644 --- a/src/node/entry.ts +++ b/src/node/entry.ts @@ -162,10 +162,12 @@ const main = async (args: DefaultedArgs): Promise => { if (!args.socket && args.open) { // The web socket doesn't seem to work if browsing with 0.0.0.0. const openAddress = serverAddress.replace("://0.0.0.0", "://localhost") - await open(openAddress).catch((error: Error) => { + try { + await open(openAddress) + logger.info(`Opened ${openAddress}`) + } catch (error) { logger.error("Failed to open", field("address", openAddress), field("error", error)) - }) - logger.info(`Opened ${openAddress}`) + } } } From f2f1fee6f179059a1a5ed40e36192946c0d4bc86 Mon Sep 17 00:00:00 2001 From: Asher Date: Tue, 27 Oct 2020 17:48:37 -0500 Subject: [PATCH 106/247] Short-circuit heartbeat when alive --- src/node/heart.ts | 42 ++++++++++++++++++++++-------------------- 1 file changed, 22 insertions(+), 20 deletions(-) diff --git a/src/node/heart.ts b/src/node/heart.ts index 5198e33d8..eed070e4e 100644 --- a/src/node/heart.ts +++ b/src/node/heart.ts @@ -21,26 +21,28 @@ export class Heart { * activity. Failures are logged as warnings. */ public beat(): void { - if (!this.alive()) { - logger.trace("heartbeat") - fs.writeFile(this.heartbeatPath, "").catch((error) => { - logger.warn(error.message) - }) - this.lastHeartbeat = Date.now() - if (typeof this.heartbeatTimer !== "undefined") { - clearTimeout(this.heartbeatTimer) - } - this.heartbeatTimer = setTimeout(() => { - this.isActive() - .then((active) => { - if (active) { - this.beat() - } - }) - .catch((error) => { - logger.warn(error.message) - }) - }, this.heartbeatInterval) + if (this.alive()) { + return } + + logger.trace("heartbeat") + fs.writeFile(this.heartbeatPath, "").catch((error) => { + logger.warn(error.message) + }) + this.lastHeartbeat = Date.now() + if (typeof this.heartbeatTimer !== "undefined") { + clearTimeout(this.heartbeatTimer) + } + this.heartbeatTimer = setTimeout(() => { + this.isActive() + .then((active) => { + if (active) { + this.beat() + } + }) + .catch((error) => { + logger.warn(error.message) + }) + }, this.heartbeatInterval) } } From 860c99e3b8b8c9f0b3627806a39e6b8b876a216e Mon Sep 17 00:00:00 2001 From: Katie Horne Date: Thu, 29 Oct 2020 16:21:47 -0500 Subject: [PATCH 107/247] Docs copyedits: README.md + CONTRIBUTING.md (#2242) * Edit README * Edit CONTRIBUTING * Format CONTRIBUTING.MD * Incorporate feedback * Revert movement of CONTRIBUTING.MD and format --- README.md | 46 ++++++---------- doc/CONTRIBUTING.md | 127 ++++++++++++++++++++++---------------------- 2 files changed, 79 insertions(+), 94 deletions(-) diff --git a/README.md b/README.md index 8e68c3e16..ee0b8700b 100644 --- a/README.md +++ b/README.md @@ -6,72 +6,58 @@ Run [VS Code](https://github.com/Microsoft/vscode) on any machine anywhere and a ## Highlights -- **Code everywhere** - - Code on your Chromebook, tablet, and laptop with a consistent development environment. - - Develop on a Linux machine and pick up from any device with a web browser. -- **Server-powered** - - Take advantage of large cloud servers to speed up tests, compilations, downloads, and more. - - Preserve battery life when you're on the go as all intensive tasks run on your server. - - Make use of a spare computer you have lying around and turn it into a full development environment. +- Code on any device with a consistent development environment +- Use cloud servers to speed up tests, compilations, downloads, and more +- Preserve battery life when you're on the go; all intensive tasks run on your server ## Getting Started -For a full setup and walkthrough, please see [./doc/guide.md](./doc/guide.md). +There are two ways to get started: -### Quick Install +1. Using the [install script](./install.sh), which automates most of the process. The script uses the system package manager (if possible) +2. Manually installing code-server; see [Installation](./doc/install.md) for instructions applicable to most use cases -We have a [script](./install.sh) to install code-server for Linux, macOS and FreeBSD. - -It tries to use the system package manager if possible. - -First run to print out the install process: +If you choose to use the install script, you can preview what occurs during the install process: ```bash curl -fsSL https://code-server.dev/install.sh | sh -s -- --dry-run ``` -Now to actually install: +To install, run: ```bash curl -fsSL https://code-server.dev/install.sh | sh ``` -The install script will print out how to run and start using code-server. +When done, the install script prints out instructions for running and starting code-server. -### Manual Install - -Docs on the install script, manual installation and docker image are at [./doc/install.md](./doc/install.md). +We also have an in-depth [setup and configuration](./doc/guide.md) guide. ### Alpha Program 🐣 -We're working on a cloud platform to make deploying and managing code-server easier. If you don't want to worry about +We're working on a cloud platform that makes deploying and managing code-server easier. Consider [joining our alpha program](https://codercom.typeform.com/to/U4IKyv0W) if you don't want to worry about - TLS - Authentication - Port Forwarding -consider [joining our alpha program](https://codercom.typeform.com/to/U4IKyv0W). - ## FAQ See [./doc/FAQ.md](./doc/FAQ.md). -## Contributing +## Want to help? -See [./doc/CONTRIBUTING.md](./doc/CONTRIBUTING.md). +See [CONTRIBUTING](./doc/CONTRIBUTING.md) for details. ## Hiring -We ([@cdr](https://github.com/cdr)) are looking for engineers to help maintain -code-server, innovate on open source and streamline dev workflows. +We ([@cdr](https://github.com/cdr)) are looking for engineers to help [maintain +code-server](https://jobs.lever.co/coder/e40becde-2cbd-4885-9029-e5c7b0a734b8), innovate on open source, and streamline dev workflows. Our main office is in Austin, Texas. Remote is ok as long as you're in North America or Europe. -Please get in [touch](mailto:jobs@coder.com) with your resume/github if interested. - -We're also hiring someone specifically to help maintain code-server. -See the listing [here](https://jobs.lever.co/coder/e40becde-2cbd-4885-9029-e5c7b0a734b8). +Please get in [touch](mailto:jobs@coder.com) with your resume/GitHub if interested. ## For Organizations diff --git a/doc/CONTRIBUTING.md b/doc/CONTRIBUTING.md index 62c20f915..52b4153a5 100644 --- a/doc/CONTRIBUTING.md +++ b/doc/CONTRIBUTING.md @@ -8,6 +8,7 @@ - [Build](#build) - [Structure](#structure) - [VS Code Patch](#vs-code-patch) + - [Currently Known Issues](#currently-known-issues) @@ -15,24 +16,26 @@ ## Pull Requests -Please link to the issue each PR solves. -If there is no existing issue, please first create one unless the fix is minor. +Please create a [GitHub Issue](https://github.com/cdr/code-server/issues) for each issue +you'd like to address unless the proposed fix is minor. -Please make sure the base of your PR is the master branch. We keep the GitHub -default branch the latest release branch to avoid confusion as the -documentation is on GitHub and we don't want users to see docs on unreleased -features. +In your Pull Requests (PR), link to the issue that the PR solves. + +Please ensure that the base of your PR is the **master** branch. (Note: The default +GitHub branch is the latest release branch, though you should point all of your changes to be merged into +master). ## Requirements -Please refer to [VS Code's prerequisites](https://github.com/Microsoft/vscode/wiki/How-to-Contribute#prerequisites). +The prerequisites for contributing to code-server are almost the same as those for +[VS Code](https://github.com/Microsoft/vscode/wiki/How-to-Contribute#prerequisites). +There are several differences, however. You must: -Differences: +- Use Node.js version 12.x (or greater) +- Have [nfpm](https://github.com/goreleaser/nfpm) (which is used to build `.deb` and `.rpm` packages and [jq](https://stedolan.github.io/jq/) (used to build code-server releases) installed -- We require a minimum of node v12 but later versions should work. -- We use [nfpm](https://github.com/goreleaser/nfpm) to build `.deb` and `.rpm` packages. -- We use [jq](https://stedolan.github.io/jq/) to build code-server releases. -- The [CI container](../ci/images/debian10/Dockerfile) is a useful reference for all our dependencies. +The [CI container](../ci/images/debian8/Dockerfile) is a useful reference for all +of the dependencies code-server uses. ## Development Workflow @@ -40,10 +43,10 @@ Differences: yarn yarn vscode yarn watch -# Visit http://localhost:8080 once the build completed. +# Visit http://localhost:8080 once the build is completed. ``` -To develop inside of an isolated docker container: +To develop inside an isolated Docker container: ```shell ./ci/dev/image/run.sh yarn @@ -53,12 +56,12 @@ To develop inside of an isolated docker container: `yarn watch` will live reload changes to the source. -If changes are made to the patch and you've built previously you must manually -reset VS Code then run `yarn vscode:patch`. +If you introduce changes to the patch and you've previously built, you +must (1) manually reset VS Code and (2) run `yarn vscode:patch`. ## Build -You can build with: +You can build using: ```shell ./ci/dev/image/run.sh ./ci/steps/release.sh @@ -66,22 +69,22 @@ You can build with: Run your build with: -``` +```shell cd release yarn --production # Runs the built JavaScript with Node. node . ``` -Build release packages (make sure you run `./ci/steps/release.sh` first): +Build the release packages (make sure that you run `./ci/steps/release.sh` first): -``` +```shell IMAGE=centos7 ./ci/dev/image/run.sh ./ci/steps/release-packages.sh # The standalone release is in ./release-standalone # .deb, .rpm and the standalone archive are in ./release-packages ``` -The `release.sh` script is the equivalent of: +The `release.sh` script is equal to running: ```shell yarn @@ -91,73 +94,69 @@ yarn build:vscode yarn release ``` -And `release-packages.sh` is: +And `release-packages.sh` is equal to: -``` +```shell yarn release:standalone yarn test:standalone-release yarn package ``` -For a faster release build you can also run: +For a faster release build, you can run instead: -``` +```shell KEEP_MODULES=1 ./ci/steps/release.sh node ./release ``` ## Structure -The `code-server` script serves an HTTP API to login and start a remote VS Code process. +The `code-server` script serves an HTTP API for login and starting a remote VS Code process. The CLI code is in [./src/node](./src/node) and the HTTP routes are implemented in [./src/node/app](./src/node/app). -Most of the meaty parts are in our VS Code patch which is described next. +Most of the meaty parts are in the VS Code patch, which we described next. ### VS Code Patch -Back in v1 of code-server, we had an extensive patch of VS Code that split the codebase -into a frontend and server. The frontend consisted of all UI code and the server ran -the extensions and exposed an API to the frontend for file access and everything else -that the UI needed. +In v1 of code-server, we had a patch of VS Code that split the codebase into a front-end +and a server. The front-end consisted of all UI code, while the server ran the extensions +and exposed an API to the front-end for file access and all UI needs. -This worked but eventually Microsoft added support to VS Code to run it in the web. -They have open sourced the frontend but have kept the server closed source. - -So in interest of piggy backing off their work, v2 and beyond use the VS Code -web frontend and fill in the server. This is contained in our +Over time, Microsoft added support to VS Code to run it on the web. They have made +the front-end open source, but not the server. As such, code-server v2 (and later) uses +the VS Code front-end and implements the server. You can find this in [./ci/dev/vscode.patch](../ci/dev/vscode.patch) under the path `src/vs/server`. Other notable changes in our patch include: -- Add our own build file which includes our code and VS Code's web code. -- Allow multiple extension directories (both user and built-in). -- Modify the loader, websocket, webview, service worker, and asset requests to - use the URL of the page as a base (and TLS if necessary for the websocket). -- Send client-side telemetry through the server. -- Allow modification of the display language. -- Make it possible for us to load code on the client. -- Make extensions work in the browser. -- Make it possible to install extensions of any kind. -- Fix getting permanently disconnected when you sleep or hibernate for a while. -- Add connection type to web socket query parameters. +- Adding our build file, which includes our code and VS Code's web code +- Allowing multiple extension directories (both user and built-in) +- Modifying the loader, websocket, webview, service worker, and asset requests to + use the URL of the page as a base (and TLS, if necessary for the websocket) +- Sending client-side telemetry through the server +- Allowing modification of the display language +- Making it possible for us to load code on the client +- Making extensions work in the browser +- Making it possible to install extensions of any kind +- Fixing issue with getting disconnected when your machine sleeps or hibernates +- Adding connection type to web socket query parameters -Some known issues presently: - -- Creating custom VS Code extensions and debugging them doesn't work. -- Extension profiling and tips are currently disabled. - -As the web portion of VS Code matures, we'll be able to shrink and maybe even entirely -eliminate our patch. In the meantime, however, upgrading the VS Code version requires -ensuring that the patch still applies and has the intended effects. - -To generate a new patch run `yarn vscode:diff`. - -**note**: We have extension docs on the CI and build system at [./ci/README.md](../ci/README.md) - -If functionality doesn't depend on code from VS Code then it should be moved -into code-server otherwise it should be in the patch. - -In the future we'd like to run VS Code unit tests against our builds to ensure features +As the web portion of VS Code matures, we'll be able to shrink and possibly +eliminate our patch. In the meantime, upgrading the VS Code version requires +us to ensure that the patch is applied and works as intended. In the future, +we'd like to run VS Code unit tests against our builds to ensure that features work as expected. + +To generate a new patch, run `yarn vscode:diff` + +**Note**: We have [extension docs](../ci/README.md) on the CI and build system. + +If the functionality you're working on does NOT depend on code from VS Code, please +move it out and into code-server. + +### Currently Known Issues + +- Creating custom VS Code extensions and debugging them doesn't work +- Extension profiling and tips are currently disabled From 10b3028196192607dab139ea01b58b94a2336eae Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Fri, 30 Oct 2020 04:13:22 -0400 Subject: [PATCH 108/247] util: Generate self signed certificate into data directory Closes #1778 --- doc/FAQ.md | 3 +++ src/node/entry.ts | 2 +- src/node/util.ts | 18 ++++++++++-------- 3 files changed, 14 insertions(+), 9 deletions(-) diff --git a/doc/FAQ.md b/doc/FAQ.md index 370dd6660..694a07229 100644 --- a/doc/FAQ.md +++ b/doc/FAQ.md @@ -144,6 +144,9 @@ For HTTPS, you can use a self signed certificate by passing in just `--cert` or pass in an existing certificate by providing the path to `--cert` and the path to the key with `--cert-key`. +The self signed certificate will be generated into +`~/.local/share/code-server/self-signed.cert`. + If `code-server` has been passed a certificate it will also respond to HTTPS requests and will redirect all HTTP requests to HTTPS. diff --git a/src/node/entry.ts b/src/node/entry.ts index 96db046e2..3fbbb4cf1 100644 --- a/src/node/entry.ts +++ b/src/node/entry.ts @@ -209,7 +209,7 @@ const main = async (args: Args, configArgs: Args): Promise => { logger.info( args.cert && args.cert.value ? ` - Using provided certificate and key for HTTPS` - : ` - Using generated certificate and key for HTTPS`, + : ` - Using generated certificate and key for HTTPS: ${humanPath(options.cert)}`, ) } else { logger.info(" - Not serving HTTPS") diff --git a/src/node/util.ts b/src/node/util.ts index 75122fe76..ee1e85be9 100644 --- a/src/node/util.ts +++ b/src/node/util.ts @@ -55,11 +55,10 @@ export function humanPath(p?: string): string { } export const generateCertificate = async (): Promise<{ cert: string; certKey: string }> => { - const paths = { - cert: path.join(tmpdir, "self-signed.cert"), - certKey: path.join(tmpdir, "self-signed.key"), - } - const checks = await Promise.all([fs.pathExists(paths.cert), fs.pathExists(paths.certKey)]) + const certPath = path.join(paths.data, "self-signed.cert") + const certKeyPath = path.join(paths.data, "self-signed.key") + + const checks = await Promise.all([fs.pathExists(certPath), fs.pathExists(certKeyPath)]) if (!checks[0] || !checks[1]) { // Require on demand so openssl isn't required if you aren't going to // generate certificates. @@ -69,10 +68,13 @@ export const generateCertificate = async (): Promise<{ cert: string; certKey: st return error ? reject(error) : resolve(result) }) }) - await fs.mkdirp(tmpdir) - await Promise.all([fs.writeFile(paths.cert, certs.certificate), fs.writeFile(paths.certKey, certs.serviceKey)]) + await fs.mkdirp(paths.data) + await Promise.all([fs.writeFile(certPath, certs.certificate), fs.writeFile(certKeyPath, certs.serviceKey)]) + } + return { + cert: certPath, + certKey: certKeyPath, } - return paths } export const generatePassword = async (length = 24): Promise => { From 8b85006996847580b10092c4b57697f2c3eef5e4 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Fri, 30 Oct 2020 04:35:08 -0400 Subject: [PATCH 109/247] src/node/util.ts: Make certificate generation "modern" Now we add a subject alt name, set extendedKeyUsage and use the correct certificate extension. The above allow it to be properly trusted by iOS. See https://support.apple.com/en-us/HT210176 *.cert isn't a real extension for certificates, *.crt is correct for it to be recognized by e.g. keychain or when importing as a profile into iOS. Updates #1566 I've been able to successfully connect from my iPad Pro now to my code-server instance with a self signed certificate! Next commit will be docs. --- doc/FAQ.md | 2 +- src/node/util.ts | 24 ++++++++++++++++++++---- 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/doc/FAQ.md b/doc/FAQ.md index 694a07229..5d1407d14 100644 --- a/doc/FAQ.md +++ b/doc/FAQ.md @@ -145,7 +145,7 @@ pass in an existing certificate by providing the path to `--cert` and the path t the key with `--cert-key`. The self signed certificate will be generated into -`~/.local/share/code-server/self-signed.cert`. +`~/.local/share/code-server/self-signed.crt`. If `code-server` has been passed a certificate it will also respond to HTTPS requests and will redirect all HTTP requests to HTTPS. diff --git a/src/node/util.ts b/src/node/util.ts index ee1e85be9..20880913b 100644 --- a/src/node/util.ts +++ b/src/node/util.ts @@ -55,7 +55,7 @@ export function humanPath(p?: string): string { } export const generateCertificate = async (): Promise<{ cert: string; certKey: string }> => { - const certPath = path.join(paths.data, "self-signed.cert") + const certPath = path.join(paths.data, "self-signed.crt") const certKeyPath = path.join(paths.data, "self-signed.key") const checks = await Promise.all([fs.pathExists(certPath), fs.pathExists(certKeyPath)]) @@ -64,9 +64,25 @@ export const generateCertificate = async (): Promise<{ cert: string; certKey: st // generate certificates. const pem = require("pem") as typeof import("pem") const certs = await new Promise((resolve, reject): void => { - pem.createCertificate({ selfSigned: true }, (error, result) => { - return error ? reject(error) : resolve(result) - }) + pem.createCertificate( + { + selfSigned: true, + config: ` +[req] +req_extensions = v3_req + +[ v3_req ] +extendedKeyUsage = serverAuth +subjectAltName = @alt_names + +[alt_names] +DNS.1 = localhost +`, + }, + (error, result) => { + return error ? reject(error) : resolve(result) + }, + ) }) await fs.mkdirp(paths.data) await Promise.all([fs.writeFile(certPath, certs.certificate), fs.writeFile(certKeyPath, certs.serviceKey)]) From bae28727bd00e02601b618fe3efe720a675a3e3a Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Fri, 30 Oct 2020 05:26:40 -0400 Subject: [PATCH 110/247] src/node/cli.ts: Add --cert-host to configure generated certificate hostname --- src/node/cli.ts | 7 ++++++- src/node/entry.ts | 2 +- src/node/util.ts | 9 +++++---- test/socket.test.ts | 2 +- 4 files changed, 13 insertions(+), 7 deletions(-) diff --git a/src/node/cli.ts b/src/node/cli.ts index 1403d8920..4ff35b445 100644 --- a/src/node/cli.ts +++ b/src/node/cli.ts @@ -26,6 +26,7 @@ export interface Args extends VsArgs { readonly auth?: AuthType readonly password?: string readonly cert?: OptionalString + readonly "cert-host"?: string readonly "cert-key"?: string readonly "disable-telemetry"?: boolean readonly help?: boolean @@ -101,7 +102,11 @@ const options: Options> = { cert: { type: OptionalString, path: true, - description: "Path to certificate. Generated if no path is provided.", + description: "Path to certificate. A self signed certificate is generated if none is provided.", + }, + "cert-host": { + type: "string", + description: "Hostname to use when generating a self signed certificate.", }, "cert-key": { type: "string", path: true, description: "Path to certificate key when using non-generated cert." }, "disable-telemetry": { type: "boolean", description: "Disable telemetry." }, diff --git a/src/node/entry.ts b/src/node/entry.ts index 3fbbb4cf1..5184c4344 100644 --- a/src/node/entry.ts +++ b/src/node/entry.ts @@ -160,7 +160,7 @@ const main = async (args: Args, configArgs: Args): Promise => { proxyDomains: args["proxy-domain"], socket: args.socket, ...(args.cert && !args.cert.value - ? await generateCertificate() + ? await generateCertificate(args["cert-host"] || "localhost") : { cert: args.cert && args.cert.value, certKey: args["cert-key"], diff --git a/src/node/util.ts b/src/node/util.ts index 20880913b..b4e175a3e 100644 --- a/src/node/util.ts +++ b/src/node/util.ts @@ -54,9 +54,9 @@ export function humanPath(p?: string): string { return p.replace(os.homedir(), "~") } -export const generateCertificate = async (): Promise<{ cert: string; certKey: string }> => { - const certPath = path.join(paths.data, "self-signed.crt") - const certKeyPath = path.join(paths.data, "self-signed.key") +export const generateCertificate = async (hostname: string): Promise<{ cert: string; certKey: string }> => { + const certPath = path.join(paths.data, `${hostname.replace(/\./g, "_")}.crt`) + const certKeyPath = path.join(paths.data, `${hostname.replace(/\./g, "_")}.key`) const checks = await Promise.all([fs.pathExists(certPath), fs.pathExists(certKeyPath)]) if (!checks[0] || !checks[1]) { @@ -67,6 +67,7 @@ export const generateCertificate = async (): Promise<{ cert: string; certKey: st pem.createCertificate( { selfSigned: true, + commonName: hostname, config: ` [req] req_extensions = v3_req @@ -76,7 +77,7 @@ extendedKeyUsage = serverAuth subjectAltName = @alt_names [alt_names] -DNS.1 = localhost +DNS.1 = ${hostname} `, }, (error, result) => { diff --git a/test/socket.test.ts b/test/socket.test.ts index 7d4de985f..b1e974ad0 100644 --- a/test/socket.test.ts +++ b/test/socket.test.ts @@ -45,7 +45,7 @@ describe("SocketProxyProvider", () => { } before(async () => { - const cert = await generateCertificate() + const cert = await generateCertificate("localhost") const options = { cert: fs.readFileSync(cert.cert), key: fs.readFileSync(cert.certKey), From a1b61d165935e647e16570c304feb7b3df801afd Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Fri, 30 Oct 2020 05:42:42 -0400 Subject: [PATCH 111/247] src/node/util.ts: Mark generated certificates as CA Required for access under iPad. --- src/node/util.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/node/util.ts b/src/node/util.ts index b4e175a3e..b50916b5c 100644 --- a/src/node/util.ts +++ b/src/node/util.ts @@ -73,6 +73,7 @@ export const generateCertificate = async (hostname: string): Promise<{ cert: str req_extensions = v3_req [ v3_req ] +basicConstraints = CA:true extendedKeyUsage = serverAuth subjectAltName = @alt_names From c63dc3a1ea8d4b339f970738ad773d37d13f79a7 Mon Sep 17 00:00:00 2001 From: Asher Date: Thu, 29 Oct 2020 14:48:01 -0500 Subject: [PATCH 112/247] Add more logging around connections --- ci/build/build-release.sh | 2 +- ci/dev/vscode.patch | 99 +++++++++++++++++++++++++-------------- 2 files changed, 64 insertions(+), 37 deletions(-) diff --git a/ci/build/build-release.sh b/ci/build/build-release.sh index 95579eb82..3b88ed7e9 100755 --- a/ci/build/build-release.sh +++ b/ci/build/build-release.sh @@ -67,7 +67,7 @@ EOF bundle_vscode() { mkdir -p "$VSCODE_OUT_PATH" rsync "$VSCODE_SRC_PATH/yarn.lock" "$VSCODE_OUT_PATH" - rsync "$VSCODE_SRC_PATH/out-vscode${MINIFY+-min}/" "$VSCODE_OUT_PATH/out" + rsync "$VSCODE_SRC_PATH/out-vscode${MINIFY:+-min}/" "$VSCODE_OUT_PATH/out" rsync "$VSCODE_SRC_PATH/.build/extensions/" "$VSCODE_OUT_PATH/extensions" if [ "$KEEP_MODULES" = 0 ]; then diff --git a/ci/dev/vscode.patch b/ci/dev/vscode.patch index b205aa83b..f1eaef04a 100644 --- a/ci/dev/vscode.patch +++ b/ci/dev/vscode.patch @@ -1225,10 +1225,10 @@ index 0000000000000000000000000000000000000000..4ea6d95d36aaac07dbd4d0e16ab3c1bb +} diff --git a/src/vs/server/entry.ts b/src/vs/server/entry.ts new file mode 100644 -index 0000000000000000000000000000000000000000..ab020fbb4e4ab3748cc807765ff9c362389faafa +index 0000000000000000000000000000000000000000..8482c48bae007ed6b39183001ae2cc6d140fcd50 --- /dev/null +++ b/src/vs/server/entry.ts -@@ -0,0 +1,78 @@ +@@ -0,0 +1,79 @@ +import { field } from '@coder/logger'; +import { setUnexpectedErrorHandler } from 'vs/base/common/errors'; +import { CodeServerMessage, VscodeMessage } from 'vs/server/ipc'; @@ -1273,7 +1273,8 @@ index 0000000000000000000000000000000000000000..ab020fbb4e4ab3748cc807765ff9c362 +// Wait for the init message then start up VS Code. Subsequent messages will +// return new workbench options without starting a new instance. +process.on('message', async (message: CodeServerMessage, socket) => { -+ logger.debug('got message from code-server', field('message', message)); ++ logger.debug('got message from code-server', field('type', message.type)); ++ logger.trace('code-server message content', field('message', message)); + switch (message.type) { + case 'init': + try { @@ -1821,10 +1822,11 @@ index 0000000000000000000000000000000000000000..609c4d1cb43f52f92906b901c14c790f +} diff --git a/src/vs/server/node/connection.ts b/src/vs/server/node/connection.ts new file mode 100644 -index 0000000000000000000000000000000000000000..eec198c948d48b1539ff46510016f759f396be18 +index 0000000000000000000000000000000000000000..93062cadc627c61e0829c27a72894b81e6a0e039 --- /dev/null +++ b/src/vs/server/node/connection.ts -@@ -0,0 +1,157 @@ +@@ -0,0 +1,171 @@ ++import { field, Logger, logger } from '@coder/logger'; +import * as cp from 'child_process'; +import { VSBuffer } from 'vs/base/common/buffer'; +import { Emitter } from 'vs/base/common/event'; @@ -1832,10 +1834,8 @@ index 0000000000000000000000000000000000000000..eec198c948d48b1539ff46510016f759 +import { ISocket } from 'vs/base/parts/ipc/common/ipc.net'; +import { NodeSocket } from 'vs/base/parts/ipc/node/ipc.net'; +import { INativeEnvironmentService } from 'vs/platform/environment/common/environment'; -+import { ILogService } from 'vs/platform/log/common/log'; +import { getNlsConfiguration } from 'vs/server/node/nls'; +import { Protocol } from 'vs/server/node/protocol'; -+import { IExtHostReadyMessage } from 'vs/workbench/services/extensions/common/extensionHostProtocol'; + +export abstract class Connection { + private readonly _onClose = new Emitter(); @@ -1899,13 +1899,14 @@ index 0000000000000000000000000000000000000000..eec198c948d48b1539ff46510016f759 + +export class ExtensionHostConnection extends Connection { + private process?: cp.ChildProcess; ++ private readonly logger: Logger; + + public constructor( + locale:string, protocol: Protocol, buffer: VSBuffer, token: string, -+ private readonly log: ILogService, + private readonly environment: INativeEnvironmentService, + ) { + super(protocol, token); ++ this.logger = logger.named("exthost", field("token", token)); + this.protocol.dispose(); + this.spawn(locale, buffer).then((p) => this.process = p); + this.protocol.getUnderlyingSocket().pause(); @@ -1928,6 +1929,7 @@ index 0000000000000000000000000000000000000000..eec198c948d48b1539ff46510016f759 + private sendInitMessage(buffer: VSBuffer): void { + const socket = this.protocol.getUnderlyingSocket(); + socket.pause(); ++ this.logger.trace('Sending socket'); + this.process!.send({ // Process must be set at this point. + type: 'VSCODE_EXTHOST_IPC_SOCKET', + initialDataChunk: (buffer.buffer as Buffer).toString('base64'), @@ -1936,7 +1938,9 @@ index 0000000000000000000000000000000000000000..eec198c948d48b1539ff46510016f759 + } + + private async spawn(locale: string, buffer: VSBuffer): Promise { ++ this.logger.trace('Getting NLS configuration...'); + const config = await getNlsConfiguration(locale, this.environment.userDataPath); ++ this.logger.trace('Spawning extension host...'); + const proc = cp.fork( + FileAccess.asFileUri('bootstrap-fork', require).fsPath, + [ '--type=extensionHost' ], @@ -1956,30 +1960,41 @@ index 0000000000000000000000000000000000000000..eec198c948d48b1539ff46510016f759 + }, + ); + -+ proc.on('error', () => this.dispose()); -+ proc.on('exit', () => this.dispose()); ++ proc.on('error', (error) => { ++ this.logger.error('Exited unexpectedly', field('error', error)); ++ this.dispose(); ++ }); ++ proc.on('exit', (code) => { ++ this.logger.trace('Exited', field('code', code)); ++ this.dispose(); ++ }); + if (proc.stdout && proc.stderr) { -+ proc.stdout.setEncoding('utf8').on('data', (d) => this.log.info('Extension host stdout', d)); -+ proc.stderr.setEncoding('utf8').on('data', (d) => this.log.error('Extension host stderr', d)); ++ proc.stdout.setEncoding('utf8').on('data', (d) => this.logger.info(d)); ++ proc.stderr.setEncoding('utf8').on('data', (d) => this.logger.error(d)); + } ++ + proc.on('message', (event) => { -+ if (event && event.type === '__$console') { -+ const severity = (this.log)[event.severity] ? event.severity : 'info'; -+ (this.log)[severity]('Extension host', event.arguments); -+ } -+ if (event && event.type === 'VSCODE_EXTHOST_DISCONNECTED') { -+ this.setOffline(); ++ switch (event && event.type) { ++ case '__$console': ++ const severity = (this.logger)[event.severity] || 'info'; ++ (this.logger)[severity]('console', field('arguments', event.arguments)); ++ break; ++ case 'VSCODE_EXTHOST_DISCONNECTED': ++ this.logger.trace('Going offline'); ++ this.setOffline(); ++ break; ++ case 'VSCODE_EXTHOST_IPC_READY': ++ this.logger.trace('Got ready message'); ++ this.sendInitMessage(buffer); ++ break; ++ default: ++ this.logger.error('Unexpected message', field("event", event)); ++ break; + } + }); + -+ const listen = (message: IExtHostReadyMessage) => { -+ if (message.type === 'VSCODE_EXTHOST_IPC_READY') { -+ proc.removeListener('message', listen); -+ this.sendInitMessage(buffer); -+ } -+ }; -+ -+ return proc.on('message', listen); ++ this.logger.trace('Waiting for handshake...'); ++ return proc; + } +} diff --git a/src/vs/server/node/insights.ts b/src/vs/server/node/insights.ts @@ -2463,15 +2478,17 @@ index 0000000000000000000000000000000000000000..3d428a57d31f29c40f9c3ce45f715b44 +}; diff --git a/src/vs/server/node/protocol.ts b/src/vs/server/node/protocol.ts new file mode 100644 -index 0000000000000000000000000000000000000000..3c74512192aec6220216bc8563b3127b9cfd5fbf +index 0000000000000000000000000000000000000000..523fcd3186d92799bc50e33a72832bd443b2945b --- /dev/null +++ b/src/vs/server/node/protocol.ts -@@ -0,0 +1,73 @@ +@@ -0,0 +1,80 @@ ++import { field } from '@coder/logger'; +import * as net from 'net'; +import { VSBuffer } from 'vs/base/common/buffer'; +import { PersistentProtocol } from 'vs/base/parts/ipc/common/ipc.net'; +import { NodeSocket, WebSocketNodeSocket } from 'vs/base/parts/ipc/node/ipc.net'; +import { AuthRequest, ConnectionTypeRequest, HandshakeMessage } from 'vs/platform/remote/common/remoteAgentConnection'; ++import { logger } from 'vs/server/node/logger'; + +export interface SocketOptions { + readonly reconnectionToken: string; @@ -2499,16 +2516,21 @@ index 0000000000000000000000000000000000000000..3c74512192aec6220216bc8563b3127b + * Perform a handshake to get a connection request. + */ + public handshake(): Promise { ++ logger.trace('Protocol handshake', field('token', this.options.reconnectionToken)); + return new Promise((resolve, reject) => { + const handler = this.onControlMessage((rawMessage) => { + try { -+ const message = JSON.parse(rawMessage.toString()); ++ const raw = rawMessage.toString(); ++ logger.trace('Protocol message', field('token', this.options.reconnectionToken), field('message', raw)); ++ const message = JSON.parse(raw); + switch (message.type) { -+ case 'auth': return this.authenticate(message); ++ case 'auth': ++ return this.authenticate(message); + case 'connectionType': + handler.dispose(); + return resolve(message); -+ default: throw new Error('Unrecognized message type'); ++ default: ++ throw new Error('Unrecognized message type'); + } + } catch (error) { + handler.dispose(); @@ -2521,7 +2543,7 @@ index 0000000000000000000000000000000000000000..3c74512192aec6220216bc8563b3127b + /** + * TODO: This ignores the authentication process entirely for now. + */ -+ private authenticate(_message: AuthRequest): void { ++ private authenticate(_?: AuthRequest): void { + this.sendMessage({ type: 'sign', data: '' }); + } + @@ -2542,10 +2564,11 @@ index 0000000000000000000000000000000000000000..3c74512192aec6220216bc8563b3127b +} diff --git a/src/vs/server/node/server.ts b/src/vs/server/node/server.ts new file mode 100644 -index 0000000000000000000000000000000000000000..a1289865858f405f93d3d396f41c6a0aadffd5e5 +index 0000000000000000000000000000000000000000..45a7bf62a6c07d8771b0257e7c98fae095109eb1 --- /dev/null +++ b/src/vs/server/node/server.ts -@@ -0,0 +1,286 @@ +@@ -0,0 +1,291 @@ ++import { field } from '@coder/logger'; +import * as fs from 'fs'; +import * as net from 'net'; +import * as path from 'path'; @@ -2709,6 +2732,7 @@ index 0000000000000000000000000000000000000000..a1289865858f405f93d3d396f41c6a0a + ); + } + ++ logger.debug('New connection', field('token', token)); + protocol.sendMessage(await ok()); + + let connection: Connection; @@ -2727,12 +2751,14 @@ index 0000000000000000000000000000000000000000..a1289865858f405f93d3d396f41c6a0a + connection = new ExtensionHostConnection( + message.args ? message.args.language : 'en', + protocol, buffer, token, -+ this.services.get(ILogService) as ILogService, + this.services.get(IEnvironmentService) as INativeEnvironmentService, + ); + } + connections.set(token, connection); -+ connection.onClose(() => connections.delete(token)); ++ connection.onClose(() => { ++ logger.debug('Connection closed', field('token', token)); ++ connections.delete(token); ++ }); + this.disposeOldOfflineConnections(connections); + break; + case ConnectionType.Tunnel: return protocol.tunnel(); @@ -2744,6 +2770,7 @@ index 0000000000000000000000000000000000000000..a1289865858f405f93d3d396f41c6a0a + const offline = Array.from(connections.values()) + .filter((connection) => typeof connection.offline !== 'undefined'); + for (let i = 0, max = offline.length - this.maxExtraOfflineConnections; i < max; ++i) { ++ logger.debug('Disposing offline connection', field("token", offline[i].token)); + offline[i].dispose(); + } + } From 0b9af6ef67253e4db7eb128a78d6676097a59cb8 Mon Sep 17 00:00:00 2001 From: Asher Date: Thu, 29 Oct 2020 18:54:18 -0500 Subject: [PATCH 113/247] Initiate connection handshake from server This way the connection can be initiated by either side. It looks like sometimes the initial message from the client is lost (it never makes it into the onControlMessage callback) but I'm still not sure why or if that is preventable. Also added a timeout on the server end to clean things up in case the client never responds. --- ci/dev/vscode.patch | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/ci/dev/vscode.patch b/ci/dev/vscode.patch index f1eaef04a..531bffe2a 100644 --- a/ci/dev/vscode.patch +++ b/ci/dev/vscode.patch @@ -2478,10 +2478,10 @@ index 0000000000000000000000000000000000000000..3d428a57d31f29c40f9c3ce45f715b44 +}; diff --git a/src/vs/server/node/protocol.ts b/src/vs/server/node/protocol.ts new file mode 100644 -index 0000000000000000000000000000000000000000..523fcd3186d92799bc50e33a72832bd443b2945b +index 0000000000000000000000000000000000000000..0d9310038c0ca378579652d89bc8ac84924213db --- /dev/null +++ b/src/vs/server/node/protocol.ts -@@ -0,0 +1,80 @@ +@@ -0,0 +1,91 @@ +import { field } from '@coder/logger'; +import * as net from 'net'; +import { VSBuffer } from 'vs/base/common/buffer'; @@ -2518,6 +2518,11 @@ index 0000000000000000000000000000000000000000..523fcd3186d92799bc50e33a72832bd4 + public handshake(): Promise { + logger.trace('Protocol handshake', field('token', this.options.reconnectionToken)); + return new Promise((resolve, reject) => { ++ const timeout = setTimeout(() => { ++ logger.error('Handshake timed out', field('token', this.options.reconnectionToken)); ++ reject(new Error("timed out")); ++ }, 10000); // Matches the client timeout. ++ + const handler = this.onControlMessage((rawMessage) => { + try { + const raw = rawMessage.toString(); @@ -2528,15 +2533,21 @@ index 0000000000000000000000000000000000000000..523fcd3186d92799bc50e33a72832bd4 + return this.authenticate(message); + case 'connectionType': + handler.dispose(); ++ clearTimeout(timeout); + return resolve(message); + default: + throw new Error('Unrecognized message type'); + } + } catch (error) { + handler.dispose(); ++ clearTimeout(timeout); + reject(error); + } + }); ++ ++ // Kick off the handshake in case we missed the client's opening shot. ++ // TODO: Investigate why that message seems to get lost. ++ this.authenticate(); + }); + } + From 07e7c38ea2ba601c11d1c4f40380e00e629855e3 Mon Sep 17 00:00:00 2001 From: Asher Date: Fri, 30 Oct 2020 13:38:13 -0500 Subject: [PATCH 114/247] Immediately pause web socket This will buffer any data sent to it until something is ready to listen on it. --- src/node/http.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/node/http.ts b/src/node/http.ts index c616c8837..c3635e073 100644 --- a/src/node/http.ts +++ b/src/node/http.ts @@ -738,6 +738,8 @@ export class HttpServer { } private onUpgrade = async (request: http.IncomingMessage, socket: net.Socket, head: Buffer): Promise => { + socket.pause() + try { this.heart.beat() socket.on("error", () => socket.destroy()) From 9ad7d0b7a3e78ff6cb111a263f20e078da140ef3 Mon Sep 17 00:00:00 2001 From: Asher Date: Fri, 30 Oct 2020 14:50:06 -0500 Subject: [PATCH 115/247] Fix potential 500 when loading in parallel --- src/node/app/vscode.ts | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/src/node/app/vscode.ts b/src/node/app/vscode.ts index ed4f714e5..18c9d8a90 100644 --- a/src/node/app/vscode.ts +++ b/src/node/app/vscode.ts @@ -50,12 +50,15 @@ export class VscodeHttpProvider extends HttpProvider { logger.debug("setting up vs code...") return new Promise((resolve, reject) => { - vscode.once("message", (message: VscodeMessage) => { - logger.debug("got message from vs code", field("message", message)) - return message.type === "options" && message.id === id - ? resolve(message.options) - : reject(new Error("Unexpected response during initialization")) - }) + const onMessage = (message: VscodeMessage) => { + // There can be parallel initializations so wait for the right ID. + if (message.type === "options" && message.id === id) { + logger.trace("got message from vs code", field("message", message)) + vscode.off("message", onMessage) + resolve(message.options) + } + } + vscode.on("message", onMessage) vscode.once("error", reject) vscode.once("exit", (code) => reject(new Error(`VS Code exited unexpectedly with code ${code}`))) this.send({ type: "init", id, options }, vscode) @@ -77,7 +80,7 @@ export class VscodeHttpProvider extends HttpProvider { this._vscode = new Promise((resolve, reject) => { vscode.once("message", (message: VscodeMessage) => { - logger.debug("got message from vs code", field("message", message)) + logger.trace("got message from vs code", field("message", message)) return message.type === "ready" ? resolve(vscode) : reject(new Error("Unexpected response waiting for ready response")) From e07a59174599f1c39f04196d275c702795387be9 Mon Sep 17 00:00:00 2001 From: Asher Date: Mon, 2 Nov 2020 16:47:09 -0600 Subject: [PATCH 116/247] Catch cloud agent download failure - See #2251 and #2229. --- ci/build/npm-postinstall.sh | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/ci/build/npm-postinstall.sh b/ci/build/npm-postinstall.sh index bd7922d5c..cede01c4c 100755 --- a/ci/build/npm-postinstall.sh +++ b/ci/build/npm-postinstall.sh @@ -25,8 +25,11 @@ main() { esac OS="$(uname | tr '[:upper:]' '[:lower:]')" - curl -fsSL "https://storage.googleapis.com/coder-cloud-releases/agent/latest/$OS/cloud-agent" -o ./lib/coder-cloud-agent - chmod +x ./lib/coder-cloud-agent + if curl -fsSL "https://storage.googleapis.com/coder-cloud-releases/agent/latest/$OS/cloud-agent" -o ./lib/coder-cloud-agent ; then + chmod +x ./lib/coder-cloud-agent + else + echo "Failed to download cloud agent; --link will not work" + fi if ! vscode_yarn; then echo "You may not have the required dependencies to build the native modules." From 6fbbb1047f70af136e96065f73038d8bef12575b Mon Sep 17 00:00:00 2001 From: Asher Date: Mon, 2 Nov 2020 17:17:09 -0600 Subject: [PATCH 117/247] fmt --- ci/build/npm-postinstall.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ci/build/npm-postinstall.sh b/ci/build/npm-postinstall.sh index cede01c4c..743b549ff 100755 --- a/ci/build/npm-postinstall.sh +++ b/ci/build/npm-postinstall.sh @@ -25,7 +25,7 @@ main() { esac OS="$(uname | tr '[:upper:]' '[:lower:]')" - if curl -fsSL "https://storage.googleapis.com/coder-cloud-releases/agent/latest/$OS/cloud-agent" -o ./lib/coder-cloud-agent ; then + if curl -fsSL "https://storage.googleapis.com/coder-cloud-releases/agent/latest/$OS/cloud-agent" -o ./lib/coder-cloud-agent; then chmod +x ./lib/coder-cloud-agent else echo "Failed to download cloud agent; --link will not work" From 9bde62fbd611a7a91c5f327fa43e0d06f1379169 Mon Sep 17 00:00:00 2001 From: Asher Date: Mon, 2 Nov 2020 17:17:25 -0600 Subject: [PATCH 118/247] v3.6.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 6cb9dd17a..75d2e0c9b 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "code-server", "license": "MIT", - "version": "3.6.1", + "version": "3.6.2", "description": "Run VS Code on a remote server.", "homepage": "https://github.com/cdr/code-server", "bugs": { From 31306f7fddda269a35da2beef78468a92725cb24 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Fri, 30 Oct 2020 04:48:51 -0400 Subject: [PATCH 119/247] docs: Add iPad self signed certificate documentation Closes #1816 Closes #1566 --- ci/dev/fmt.sh | 1 + doc/FAQ.md | 5 +++++ doc/ipad.md | 48 ++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 54 insertions(+) create mode 100644 doc/ipad.md diff --git a/ci/dev/fmt.sh b/ci/dev/fmt.sh index d3bd41915..47911aadf 100755 --- a/ci/dev/fmt.sh +++ b/ci/dev/fmt.sh @@ -26,6 +26,7 @@ main() { doctoc --title '# Install' doc/install.md > /dev/null doctoc --title '# npm Install Requirements' doc/npm.md > /dev/null doctoc --title '# Contributing' doc/CONTRIBUTING.md > /dev/null + doctoc --title '# iPad' doc/ipad.md > /dev/null if [[ ${CI-} && $(git ls-files --other --modified --exclude-standard) ]]; then echo "Files need generation or are formatted incorrectly:" diff --git a/doc/FAQ.md b/doc/FAQ.md index 5d1407d14..25e7a13d1 100644 --- a/doc/FAQ.md +++ b/doc/FAQ.md @@ -3,6 +3,7 @@ # FAQ - [Questions?](#questions) +- [iPad Status?](#ipad-status) - [How can I reuse my VS Code configuration?](#how-can-i-reuse-my-vs-code-configuration) - [Differences compared to VS Code?](#differences-compared-to-vs-code) - [How can I request a missing extension?](#how-can-i-request-a-missing-extension) @@ -33,6 +34,10 @@ Please file all questions and support requests at https://github.com/cdr/code-server/discussions. +## iPad Status? + +Please see [./ipad.md](./ipad.md). + ## How can I reuse my VS Code configuration? The very popular [Settings Sync](https://marketplace.visualstudio.com/items?itemName=Shan.code-settings-sync) extension works. diff --git a/doc/ipad.md b/doc/ipad.md new file mode 100644 index 000000000..e190000e6 --- /dev/null +++ b/doc/ipad.md @@ -0,0 +1,48 @@ + + +# iPad + +- [iPad](#ipad) + - [How to access code-server with a self signed certificate on iPad](#how-to-access-code-server-with-a-self-signed-certificate-on-ipad) + + + +# iPad + +## How to access code-server with a self signed certificate on iPad + +Accessing a self signed certificate on iPad isn't as easy as accepting through all +the security warnings. Safari will prevent WebSocket connections unless the certificate +is installed as a profile on the device. + +The below assumes you are using the self signed certificate that code-server +generates for you. If not, that's fine but you'll have to make sure your certificate +abides by the following guidelines from Apple: https://support.apple.com/en-us/HT210176 + +**note**: Another undocumented requirement we noticed is that the certificate has to have `basicConstraints=CA:true`. + +The following instructions assume you have code-server installed and running +with a self signed certificate. If not, please first go through [./guide.md](./guide.md)! + +**warning**: Your iPad must access code-server via a domain name. It could be local +DNS like `mymacbookpro.local` but it must be a domain name. Otherwise Safari will +refuse to allow WebSockets to connect. + +1. Your certificate **must** have a subject alt name that matches the hostname + at which you will access code-server from your iPad. You can pass this to code-server + so that it generates the certificate correctly with `--cert-host`. +2. Share your self signed certificate with the iPad. + - code-server will print the location of the certificate it has generated in the logs. + +``` +[2020-10-30T08:55:45.139Z] info - Using generated certificate and key for HTTPS: ~/.local/share/code-server/mymbp_local.crt +``` + +- You can mail it to yourself or if you have a Mac, it's easiest to just Airdrop to the iPad. + +3. When opening the `*.crt` file, you'll be prompted to go into settings to install. +4. Go to `Settings -> General -> Profile`, select the profile and then hit `Install`. + - It should say the profile is verified. +5. Go to `Settings -> About -> Certificate Trust Settings` and enable full trust for + the certificate. +6. Now you can access code-server! 🍻 From c07296cce0e448803491aa72a1ceeb2219eee387 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Fri, 30 Oct 2020 06:08:42 -0400 Subject: [PATCH 120/247] docs: Add known issues to iPad docs and add more links to iPad docs Closes #1816 --- doc/FAQ.md | 10 ---------- doc/guide.md | 3 +-- doc/ipad.md | 12 ++++++++++-- 3 files changed, 11 insertions(+), 14 deletions(-) diff --git a/doc/FAQ.md b/doc/FAQ.md index 25e7a13d1..1a6a217d6 100644 --- a/doc/FAQ.md +++ b/doc/FAQ.md @@ -22,7 +22,6 @@ - [Heartbeat File](#heartbeat-file) - [Healthz endpoint](#healthz-endpoint) - [How does the config file work?](#how-does-the-config-file-work) -- [Blank screen on iPad?](#blank-screen-on-ipad) - [Isn't an install script piped into sh insecure?](#isnt-an-install-script-piped-into-sh-insecure) - [How do I make my keyboard shortcuts work?](#how-do-i-make-my-keyboard-shortcuts-work) - [Differences compared to Theia?](#differences-compared-to-theia) @@ -287,15 +286,6 @@ The `--config` flag or `$CODE_SERVER_CONFIG` can be used to change the config fi The default location also respects `$XDG_CONFIG_HOME`. -## Blank screen on iPad? - -Unfortunately at the moment self signed certificates cause a blank screen on iPadOS - -There does seem to be a way to get it to work if you create your own CA and create a -certificate using the CA and then import the CA onto your iPad. - -See [#1566](https://github.com/cdr/code-server/issues/1566#issuecomment-623159434). - ## Isn't an install script piped into sh insecure? Please give diff --git a/doc/guide.md b/doc/guide.md index 8782f57f8..ce17a3614 100644 --- a/doc/guide.md +++ b/doc/guide.md @@ -251,8 +251,7 @@ Visit `https://` to access `code-server`. Congratulations! ### Self Signed Certificate -**note:** Self signed certificates do not work with iPad and will cause a blank page. You'll -have to use [Let's Encrypt](#lets-encrypt) instead. See the [FAQ](./FAQ.md#blank-screen-on-ipad). +**note:** Self signed certificates do not work with iPad normally. See [./ipad.md](./ipad.md) for details. Recommended reading: https://security.stackexchange.com/a/8112. diff --git a/doc/ipad.md b/doc/ipad.md index e190000e6..1bda0bd44 100644 --- a/doc/ipad.md +++ b/doc/ipad.md @@ -3,13 +3,21 @@ # iPad - [iPad](#ipad) - - [How to access code-server with a self signed certificate on iPad](#how-to-access-code-server-with-a-self-signed-certificate-on-ipad) + - [Known Issues](#known-issues) + - [How to access code-server with a self signed certificate on iPad?](#how-to-access-code-server-with-a-self-signed-certificate-on-ipad) # iPad -## How to access code-server with a self signed certificate on iPad +## Known Issues + +- Getting self signed certificates certificates to work is involved, see below. +- Keyboard may disappear sometimes [#1313](https://github.com/cdr/code-server/issues/1313), [#979](https://github.com/cdr/code-server/issues/979) +- Trackpad scrolling does not work [#1455](https://github.com/cdr/code-server/issues/1455) +- See [issues tagged with the iPad label](https://github.com/cdr/code-server/issues?q=is%3Aopen+is%3Aissue+label%3AiPad) for more. + +## How to access code-server with a self signed certificate on iPad? Accessing a self signed certificate on iPad isn't as easy as accepting through all the security warnings. Safari will prevent WebSocket connections unless the certificate From 1067507c41810a24eccc02ee7645b762d25b2a0e Mon Sep 17 00:00:00 2001 From: Asher Date: Wed, 28 Oct 2020 11:29:43 -0500 Subject: [PATCH 121/247] Proxy to 0.0.0.0 instead of localhost --- src/node/proxy.ts | 4 ++-- src/node/routes/proxy.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/node/proxy.ts b/src/node/proxy.ts index 4c861d0fe..a95badad9 100644 --- a/src/node/proxy.ts +++ b/src/node/proxy.ts @@ -80,7 +80,7 @@ router.all("*", (req, res, next) => { proxy.web(req, res, { ignorePath: true, - target: `http://127.0.0.1:${port}${req.originalUrl}`, + target: `http://0.0.0.0:${port}${req.originalUrl}`, }) }) @@ -95,6 +95,6 @@ router.ws("*", (socket, head, req, next) => { proxy.ws(req, socket, head, { ignorePath: true, - target: `http://127.0.0.1:${port}${req.originalUrl}`, + target: `http://0.0.0.0:${port}${req.originalUrl}`, }) }) diff --git a/src/node/routes/proxy.ts b/src/node/routes/proxy.ts index 8c83827a6..29aa999ae 100644 --- a/src/node/routes/proxy.ts +++ b/src/node/routes/proxy.ts @@ -9,9 +9,9 @@ export const router = Router() const getProxyTarget = (req: Request, rewrite: boolean): string => { if (rewrite) { const query = qs.stringify(req.query) - return `http://127.0.0.1:${req.params.port}/${req.params[0] || ""}${query ? `?${query}` : ""}` + return `http://0.0.0.0:${req.params.port}/${req.params[0] || ""}${query ? `?${query}` : ""}` } - return `http://127.0.0.1:${req.params.port}/${req.originalUrl}` + return `http://0.0.0.0:${req.params.port}/${req.originalUrl}` } router.all("/(:port)(/*)?", (req, res) => { From 8a9e61defbe45eebc55facf6e083ad6dc03c1dca Mon Sep 17 00:00:00 2001 From: Asher Date: Tue, 3 Nov 2020 14:26:25 -0600 Subject: [PATCH 122/247] Use Addr interface everywhere and loop over arg sources --- src/node/cli.ts | 30 +++++++++++++++++------------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/src/node/cli.ts b/src/node/cli.ts index a5757aa6b..45aed3040 100644 --- a/src/node/cli.ts +++ b/src/node/cli.ts @@ -414,9 +414,9 @@ export async function setDefaults(cliArgs: Args, configArgs?: ConfigArgs): Promi args.auth = AuthType.Password } - const [host, port] = bindAddrFromAllSources(args, configArgs || { _: [] }) - args.host = host - args.port = port + const addr = bindAddrFromAllSources(args, configArgs || { _: [] }) + args.host = addr.host + args.port = addr.port // If we're being exposed to the cloud, we listen on a random address and // disable auth. @@ -512,12 +512,15 @@ export async function readConfigFile(configPath?: string): Promise { } } -function parseBindAddr(bindAddr: string): [string, number] { +function parseBindAddr(bindAddr: string): Addr { const u = new URL(`http://${bindAddr}`) - // With the http scheme 80 will be dropped so assume it's 80 if missing. This - // means --bind-addr without a port will default to 80 as well and not - // the code-server default. - return [u.hostname, u.port ? parseInt(u.port, 10) : 80] + return { + host: u.hostname, + // With the http scheme 80 will be dropped so assume it's 80 if missing. + // This means --bind-addr without a port will default to 80 as well + // and not the code-server default. + port: u.port ? parseInt(u.port, 10) : 80, + } } interface Addr { @@ -528,7 +531,7 @@ interface Addr { function bindAddrFromArgs(addr: Addr, args: Args): Addr { addr = { ...addr } if (args["bind-addr"]) { - ;[addr.host, addr.port] = parseBindAddr(args["bind-addr"]) + addr = parseBindAddr(args["bind-addr"]) } if (args.host) { addr.host = args.host @@ -543,16 +546,17 @@ function bindAddrFromArgs(addr: Addr, args: Args): Addr { return addr } -function bindAddrFromAllSources(cliArgs: Args, configArgs: Args): [string, number] { +function bindAddrFromAllSources(...argsConfig: Args[]): Addr { let addr: Addr = { host: "localhost", port: 8080, } - addr = bindAddrFromArgs(addr, configArgs) - addr = bindAddrFromArgs(addr, cliArgs) + for (const args of argsConfig) { + addr = bindAddrFromArgs(addr, args) + } - return [addr.host, addr.port] + return addr } async function copyOldMacOSDataDir(): Promise { From 3a074fd84460cab04d740bf4651da671fd0f4fac Mon Sep 17 00:00:00 2001 From: Asher Date: Tue, 3 Nov 2020 14:30:34 -0600 Subject: [PATCH 123/247] Skip unnecessary auth type check when using --link --- src/node/cli.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/node/cli.ts b/src/node/cli.ts index 45aed3040..fcb74d805 100644 --- a/src/node/cli.ts +++ b/src/node/cli.ts @@ -425,10 +425,7 @@ export async function setDefaults(cliArgs: Args, configArgs?: ConfigArgs): Promi args.port = 0 args.socket = undefined args.cert = undefined - - if (args.auth !== AuthType.None) { - args.auth = AuthType.None - } + args.auth = AuthType.None } if (args.cert && !args.cert.value) { From f4e58553187e085fbee1fb3bfc3ed86ecf9633c6 Mon Sep 17 00:00:00 2001 From: Asher Date: Tue, 3 Nov 2020 14:31:32 -0600 Subject: [PATCH 124/247] Simplify update request --- src/node/update.ts | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/src/node/update.ts b/src/node/update.ts index 42baa1848..2959b874c 100644 --- a/src/node/update.ts +++ b/src/node/update.ts @@ -105,24 +105,23 @@ export class UpdateProvider { logger.debug("Making request", field("uri", uri)) const httpx = uri.startsWith("https") ? https : http const client = httpx.get(uri, { headers: { "User-Agent": "code-server" } }, (response) => { - if ( - response.statusCode && - response.statusCode >= 300 && - response.statusCode < 400 && - response.headers.location - ) { + if (!response.statusCode || response.statusCode < 200 || response.statusCode >= 400) { + return reject(new Error(`${uri}: ${response.statusCode || "500"}`)) + } + + if (response.statusCode >= 300) { ++redirects if (redirects > maxRedirects) { + response.destroy() return reject(new Error("reached max redirects")) } + if (!response.headers.location) { + return reject(new Error("received redirect with no location header")) + } response.destroy() return request(url.resolve(uri, response.headers.location)) } - if (!response.statusCode || response.statusCode < 200 || response.statusCode >= 400) { - return reject(new Error(`${uri}: ${response.statusCode || "500"}`)) - } - resolve(response) }) client.on("error", reject) From c72c53f64de627801ae92e9911638031011a8d57 Mon Sep 17 00:00:00 2001 From: Asher Date: Tue, 3 Nov 2020 14:36:27 -0600 Subject: [PATCH 125/247] Fix not being able to dispose vscode after failed disposal --- src/node/vscode.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/node/vscode.ts b/src/node/vscode.ts index 5cf4e4b76..9d935e663 100644 --- a/src/node/vscode.ts +++ b/src/node/vscode.ts @@ -22,8 +22,8 @@ export class VscodeProvider { if (this._vscode) { const vscode = await this._vscode vscode.removeAllListeners() - this._vscode = undefined vscode.kill() + this._vscode = undefined } } From c10450c4c5479af9e458c903fffe3f2ee19f53dd Mon Sep 17 00:00:00 2001 From: Asher Date: Tue, 3 Nov 2020 14:40:06 -0600 Subject: [PATCH 126/247] Move isFile into util That allows its use in entry.ts as well. --- src/node/entry.ts | 18 ++++-------------- src/node/util.ts | 9 +++++++++ src/node/vscode.ts | 11 +---------- 3 files changed, 14 insertions(+), 24 deletions(-) diff --git a/src/node/entry.ts b/src/node/entry.ts index df218a52d..82e04f5d9 100644 --- a/src/node/entry.ts +++ b/src/node/entry.ts @@ -1,6 +1,5 @@ import { field, logger } from "@coder/logger" import * as cp from "child_process" -import { promises as fs } from "fs" import http from "http" import * as path from "path" import { CliMessage, OpenCommandPipeArgs } from "../../lib/vscode/src/vs/server/ipc" @@ -19,7 +18,7 @@ import { import { coderCloudBind } from "./coder-cloud" import { commit, version } from "./constants" import { register } from "./routes" -import { humanPath, open } from "./util" +import { humanPath, isFile, open } from "./util" import { ipcMain, WrapperProcess } from "./wrapper" export const runVsCodeCli = (args: DefaultedArgs): void => { @@ -55,21 +54,12 @@ export const openInExistingInstance = async (args: DefaultedArgs, socketPath: st forceNewWindow: args["new-window"], } - const isDir = async (path: string): Promise => { - try { - const st = await fs.stat(path) - return st.isDirectory() - } catch (error) { - return false - } - } - for (let i = 0; i < args._.length; i++) { const fp = path.resolve(args._[i]) - if (await isDir(fp)) { - pipeArgs.folderURIs.push(fp) - } else { + if (await isFile(fp)) { pipeArgs.fileURIs.push(fp) + } else { + pipeArgs.folderURIs.push(fp) } } diff --git a/src/node/util.ts b/src/node/util.ts index 75122fe76..349c8edfb 100644 --- a/src/node/util.ts +++ b/src/node/util.ts @@ -261,3 +261,12 @@ export function canConnect(path: string): Promise { }) }) } + +export const isFile = async (path: string): Promise => { + try { + const stat = await fs.stat(path) + return stat.isFile() + } catch (error) { + return false + } +} diff --git a/src/node/vscode.ts b/src/node/vscode.ts index 9d935e663..6e050de2f 100644 --- a/src/node/vscode.ts +++ b/src/node/vscode.ts @@ -1,12 +1,12 @@ import { field, logger } from "@coder/logger" import * as cp from "child_process" -import * as fs from "fs-extra" import * as net from "net" import * as path from "path" import * as ipc from "../../lib/vscode/src/vs/server/ipc" import { arrayify, generateUuid } from "../common/util" import { rootPath } from "./constants" import { settings } from "./settings" +import { isFile } from "./util" export class VscodeProvider { public readonly serverRootPath: string @@ -123,15 +123,6 @@ export class VscodeProvider { private async getFirstPath( startPaths: Array<{ url?: string | string[] | ipc.Query | ipc.Query[]; workspace?: boolean } | undefined>, ): Promise { - const isFile = async (path: string): Promise => { - try { - const stat = await fs.stat(path) - return stat.isFile() - } catch (error) { - logger.warn(error.message) - return false - } - } for (let i = 0; i < startPaths.length; ++i) { const startPath = startPaths[i] const url = arrayify(startPath && startPath.url).find((p) => !!p) From e243f6e36952cbe4d655cdbd77765b701feccd9f Mon Sep 17 00:00:00 2001 From: Asher Date: Tue, 3 Nov 2020 14:42:37 -0600 Subject: [PATCH 127/247] Return early when forking to reduce indentation --- src/node/vscode.ts | 46 ++++++++++++++++++++++++---------------------- 1 file changed, 24 insertions(+), 22 deletions(-) diff --git a/src/node/vscode.ts b/src/node/vscode.ts index 6e050de2f..c25b7e9b0 100644 --- a/src/node/vscode.ts +++ b/src/node/vscode.ts @@ -74,30 +74,32 @@ export class VscodeProvider { } private fork(): Promise { - if (!this._vscode) { - logger.debug("forking vs code...") - const vscode = cp.fork(path.join(this.serverRootPath, "fork")) - vscode.on("error", (error) => { - logger.error(error.message) - this._vscode = undefined - }) - vscode.on("exit", (code) => { - logger.error(`VS Code exited unexpectedly with code ${code}`) - this._vscode = undefined - }) - - this._vscode = new Promise((resolve, reject) => { - vscode.once("message", (message: ipc.VscodeMessage) => { - logger.debug("got message from vs code", field("message", message)) - return message.type === "ready" - ? resolve(vscode) - : reject(new Error("Unexpected response waiting for ready response")) - }) - vscode.once("error", reject) - vscode.once("exit", (code) => reject(new Error(`VS Code exited unexpectedly with code ${code}`))) - }) + if (this._vscode) { + return this._vscode } + logger.debug("forking vs code...") + const vscode = cp.fork(path.join(this.serverRootPath, "fork")) + vscode.on("error", (error) => { + logger.error(error.message) + this._vscode = undefined + }) + vscode.on("exit", (code) => { + logger.error(`VS Code exited unexpectedly with code ${code}`) + this._vscode = undefined + }) + + this._vscode = new Promise((resolve, reject) => { + vscode.once("message", (message: ipc.VscodeMessage) => { + logger.debug("got message from vs code", field("message", message)) + return message.type === "ready" + ? resolve(vscode) + : reject(new Error("Unexpected response waiting for ready response")) + }) + vscode.once("error", reject) + vscode.once("exit", (code) => reject(new Error(`VS Code exited unexpectedly with code ${code}`))) + }) + return this._vscode } From 03e00131120263c38786410d48ff99f9a2144133 Mon Sep 17 00:00:00 2001 From: Asher Date: Tue, 3 Nov 2020 14:54:27 -0600 Subject: [PATCH 128/247] Unbind error/exit events once handshakes resolve --- src/node/vscode.ts | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/node/vscode.ts b/src/node/vscode.ts index c25b7e9b0..5cf826a16 100644 --- a/src/node/vscode.ts +++ b/src/node/vscode.ts @@ -51,14 +51,20 @@ export class VscodeProvider { logger.debug("setting up vs code...") return new Promise((resolve, reject) => { + const onExit = (code: number | null) => reject(new Error(`VS Code exited unexpectedly with code ${code}`)) + vscode.once("message", (message: ipc.VscodeMessage) => { logger.debug("got message from vs code", field("message", message)) + vscode.off("error", reject) + vscode.off("exit", onExit) return message.type === "options" && message.id === id ? resolve(message.options) : reject(new Error("Unexpected response during initialization")) }) + vscode.once("error", reject) - vscode.once("exit", (code) => reject(new Error(`VS Code exited unexpectedly with code ${code}`))) + vscode.once("exit", onExit) + this.send( { type: "init", @@ -90,14 +96,19 @@ export class VscodeProvider { }) this._vscode = new Promise((resolve, reject) => { + const onExit = (code: number | null) => reject(new Error(`VS Code exited unexpectedly with code ${code}`)) + vscode.once("message", (message: ipc.VscodeMessage) => { logger.debug("got message from vs code", field("message", message)) + vscode.off("error", reject) + vscode.off("exit", onExit) return message.type === "ready" ? resolve(vscode) : reject(new Error("Unexpected response waiting for ready response")) }) + vscode.once("error", reject) - vscode.once("exit", (code) => reject(new Error(`VS Code exited unexpectedly with code ${code}`))) + vscode.once("exit", onExit) }) return this._vscode From 8066da12fe17f4d0f158ce5243e65ac0a0d4b189 Mon Sep 17 00:00:00 2001 From: Asher Date: Tue, 3 Nov 2020 15:37:16 -0600 Subject: [PATCH 129/247] Remove unused Locals interface --- src/node/http.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/node/http.ts b/src/node/http.ts index 960669100..27164c0fb 100644 --- a/src/node/http.ts +++ b/src/node/http.ts @@ -9,13 +9,8 @@ import { HttpCode, HttpError } from "../common/http" import { normalize, Options } from "../common/util" import { AuthType } from "./cli" import { commit, rootPath } from "./constants" -import { Heart } from "./heart" import { hash } from "./util" -export interface Locals { - heart: Heart -} - /** * Replace common variable strings in HTML templates. */ From ddbac8dd78819b76f6fcc385f301ecf725fcec13 Mon Sep 17 00:00:00 2001 From: Jon Ayers Date: Tue, 3 Nov 2020 23:23:41 +0000 Subject: [PATCH 130/247] Update README Alpha section to remove reference to typeform --- README.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index ee0b8700b..aa5673938 100644 --- a/README.md +++ b/README.md @@ -35,12 +35,17 @@ We also have an in-depth [setup and configuration](./doc/guide.md) guide. ### Alpha Program 🐣 -We're working on a cloud platform that makes deploying and managing code-server easier. Consider [joining our alpha program](https://codercom.typeform.com/to/U4IKyv0W) if you don't want to worry about +We're working on a cloud platform that makes deploying and managing code-server easier. Consider running code-server with our experimental flag `--link` if you don't want to worry about - TLS - Authentication - Port Forwarding +```bash +$ code-server --link +Proxying code-server to Coder Cloud, you can access your IDE at https://valmar-jon.cdr.co +``` + ## FAQ See [./doc/FAQ.md](./doc/FAQ.md). From 1eebde56ab6618252dfabcd778715fc685a9d885 Mon Sep 17 00:00:00 2001 From: Jon Ayers Date: Tue, 3 Nov 2020 18:03:28 -0600 Subject: [PATCH 131/247] Specify that Coder Alpha requires v3.6.2 (#2270) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index aa5673938..0e7750039 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,7 @@ We also have an in-depth [setup and configuration](./doc/guide.md) guide. ### Alpha Program 🐣 -We're working on a cloud platform that makes deploying and managing code-server easier. Consider running code-server with our experimental flag `--link` if you don't want to worry about +We're working on a cloud platform that makes deploying and managing code-server easier. Consider [updating to 3.6.2](https://github.com/cdr/code-server/releases/tag/v3.6.2) and running code-server with our experimental flag `--link` if you don't want to worry about - TLS - Authentication From 75b93f9dc520ae8bc9256d3c6b02a6dcb8deebdc Mon Sep 17 00:00:00 2001 From: Asher Date: Wed, 4 Nov 2020 17:07:03 -0600 Subject: [PATCH 132/247] Fix bind address priority Broke when converting to a loop. --- src/node/cli.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/node/cli.ts b/src/node/cli.ts index fcb74d805..7a5b8d611 100644 --- a/src/node/cli.ts +++ b/src/node/cli.ts @@ -414,7 +414,7 @@ export async function setDefaults(cliArgs: Args, configArgs?: ConfigArgs): Promi args.auth = AuthType.Password } - const addr = bindAddrFromAllSources(args, configArgs || { _: [] }) + const addr = bindAddrFromAllSources(configArgs || { _: [] }, cliArgs) args.host = addr.host args.port = addr.port From e2c35facdbfd926ff624adf8ec3c07fe43692afd Mon Sep 17 00:00:00 2001 From: Asher Date: Tue, 3 Nov 2020 15:47:20 -0600 Subject: [PATCH 133/247] Remove invalid comment on maybeProxy It no longer handles authentication. --- src/node/proxy.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/node/proxy.ts b/src/node/proxy.ts index a95badad9..d53de6927 100644 --- a/src/node/proxy.ts +++ b/src/node/proxy.ts @@ -25,8 +25,6 @@ export const router = Router() * * For example if `coder.com` is specified `8080.coder.com` will be proxied * but `8080.test.coder.com` and `test.8080.coder.com` will not. - * - * Throw an error if proxying but the user isn't authenticated. */ const maybeProxy = (req: Request): string | undefined => { // Split into parts. From a653b93ce210e1e160fbe906a777d77ca5b893bb Mon Sep 17 00:00:00 2001 From: Asher Date: Tue, 3 Nov 2020 16:11:28 -0600 Subject: [PATCH 134/247] Include protocol on printed address This makes it clickable from the terminal. --- src/node/app.ts | 4 ++-- src/node/entry.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/node/app.ts b/src/node/app.ts index 5fde27675..171a7c4d0 100644 --- a/src/node/app.ts +++ b/src/node/app.ts @@ -45,7 +45,7 @@ export const createApp = async (args: DefaultedArgs): Promise<[Express, http.Ser } /** - * Get the address of a server as a string (protocol not included) while + * Get the address of a server as a string (protocol *is* included) while * ensuring there is one (will throw if there isn't). */ export const ensureAddress = (server: http.Server): string => { @@ -54,7 +54,7 @@ export const ensureAddress = (server: http.Server): string => { throw new Error("server has no address") } if (typeof addr !== "string") { - return `${addr.address}:${addr.port}` + return `http://${addr.address}:${addr.port}` } return addr } diff --git a/src/node/entry.ts b/src/node/entry.ts index 82e04f5d9..3cca62a5b 100644 --- a/src/node/entry.ts +++ b/src/node/entry.ts @@ -141,7 +141,7 @@ const main = async (args: DefaultedArgs): Promise => { if (args.link) { try { - await coderCloudBind(serverAddress, args.link.value) + await coderCloudBind(serverAddress.replace(/^https?:\/\//, ""), args.link.value) logger.info(" - Connected to cloud agent") } catch (err) { logger.error(err.message) From c5ce365482e1f10385605ef66b04838cdcf2818a Mon Sep 17 00:00:00 2001 From: Asher Date: Tue, 3 Nov 2020 16:13:41 -0600 Subject: [PATCH 135/247] Use query variable to force update check --- src/node/routes/update.ts | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/src/node/routes/update.ts b/src/node/routes/update.ts index ea7479fe0..b4fbc197e 100644 --- a/src/node/routes/update.ts +++ b/src/node/routes/update.ts @@ -12,19 +12,8 @@ router.use((req, _, next) => { next() }) -router.get("/", async (_, res) => { - const update = await provider.getUpdate() - res.json({ - checked: update.checked, - latest: update.version, - current: version, - isLatest: provider.isLatestVersion(update), - }) -}) - -// This route will force a check. -router.get("/check", async (_, res) => { - const update = await provider.getUpdate(true) +router.get("/", async (req, res) => { + const update = await provider.getUpdate(req.query.force === "true") res.json({ checked: update.checked, latest: update.version, From e5c8e0aad1c6fd11fb3039d7b7b77968caae0198 Mon Sep 17 00:00:00 2001 From: Asher Date: Tue, 3 Nov 2020 16:14:24 -0600 Subject: [PATCH 136/247] Remove useless || --- src/node/routes/vscode.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/node/routes/vscode.ts b/src/node/routes/vscode.ts index adef232fd..cec3a8321 100644 --- a/src/node/routes/vscode.ts +++ b/src/node/routes/vscode.ts @@ -14,7 +14,7 @@ const vscode = new VscodeProvider() router.get("/", async (req, res) => { if (!authenticated(req)) { return redirect(req, res, "login", { - to: req.baseUrl || "/", + to: req.baseUrl, }) } From 210fc049c40865a09f0ec747cd9d51b12208dd3c Mon Sep 17 00:00:00 2001 From: Asher Date: Tue, 3 Nov 2020 16:30:45 -0600 Subject: [PATCH 137/247] Document VS Code endpoints --- src/node/routes/static.ts | 3 +++ src/node/routes/vscode.ts | 10 ++++++++++ 2 files changed, 13 insertions(+) diff --git a/src/node/routes/static.ts b/src/node/routes/static.ts index 0678c2313..e073219e4 100644 --- a/src/node/routes/static.ts +++ b/src/node/routes/static.ts @@ -32,6 +32,9 @@ router.get("/(:commit)(/*)?", async (req, res) => { res.header("Cache-Control", "public, max-age=31536000") } + /** + * Used by VS Code to load extensions into the web worker. + */ const tar = Array.isArray(req.query.tar) ? req.query.tar[0] : req.query.tar if (typeof tar === "string") { let stream: Readable = tarFs.pack(pathToFsPath(tar)) diff --git a/src/node/routes/vscode.ts b/src/node/routes/vscode.ts index cec3a8321..e7842a297 100644 --- a/src/node/routes/vscode.ts +++ b/src/node/routes/vscode.ts @@ -71,6 +71,9 @@ router.ws("/", async (socket, _, req) => { await vscode.sendWebsocket(socket, req.query) }) +/** + * TODO: Might currently be unused. + */ router.get("/resource(/*)?", async (req, res) => { ensureAuthenticated(req) if (typeof req.query.path === "string") { @@ -79,6 +82,9 @@ router.get("/resource(/*)?", async (req, res) => { } }) +/** + * Used by VS Code to load files. + */ router.get("/vscode-remote-resource(/*)?", async (req, res) => { ensureAuthenticated(req) if (typeof req.query.path === "string") { @@ -87,6 +93,10 @@ router.get("/vscode-remote-resource(/*)?", async (req, res) => { } }) +/** + * VS Code webviews use these paths to load files and to load webview assets + * like HTML and JavaScript. + */ router.get("/webview/*", async (req, res) => { ensureAuthenticated(req) res.set("Content-Type", getMediaMime(req.path)) From 476379a77e02f11f5261b1ec42cad67b1624a393 Mon Sep 17 00:00:00 2001 From: Asher Date: Tue, 3 Nov 2020 16:44:08 -0600 Subject: [PATCH 138/247] Fix cookie domain Had double Domain= --- src/node/http.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/node/http.ts b/src/node/http.ts index 27164c0fb..071ccd97e 100644 --- a/src/node/http.ts +++ b/src/node/http.ts @@ -130,7 +130,7 @@ export const getCookieDomain = (host: string, proxyDomains: string[]): string | }) logger.debug("got cookie doman", field("host", host)) - return host ? `Domain=${host}` : undefined + return host || undefined } declare module "express" { From 34225e2bdf6f10a7a9b3b5fc25932ec8b8a8e17d Mon Sep 17 00:00:00 2001 From: Asher Date: Tue, 3 Nov 2020 16:45:03 -0600 Subject: [PATCH 139/247] Use ensureAuthenticated as middleware --- src/node/http.ts | 43 ++++++++++++++++++++++++++------------- src/node/proxy.ts | 4 ++-- src/node/routes/proxy.ts | 4 ++-- src/node/routes/update.ts | 7 +------ src/node/routes/vscode.ts | 16 ++++++--------- 5 files changed, 40 insertions(+), 34 deletions(-) diff --git a/src/node/http.ts b/src/node/http.ts index 071ccd97e..42896f26b 100644 --- a/src/node/http.ts +++ b/src/node/http.ts @@ -34,12 +34,15 @@ export const replaceTemplates = ( } /** - * Throw an error if not authorized. + * Throw an error if not authorized. Call `next` if provided. */ -export const ensureAuthenticated = (req: express.Request): void => { +export const ensureAuthenticated = (req: express.Request, _?: express.Response, next?: express.NextFunction): void => { if (!authenticated(req)) { throw new HttpError("Unauthorized", HttpCode.Unauthorized) } + if (next) { + next() + } } /** @@ -136,20 +139,32 @@ export const getCookieDomain = (host: string, proxyDomains: string[]): string | declare module "express" { function Router(options?: express.RouterOptions): express.Router & WithWebsocketMethod - type WebsocketRequestHandler = ( - socket: net.Socket, - head: Buffer, - req: express.Request, + type WebSocketRequestHandler = ( + req: express.Request & WithWebSocket, + res: express.Response, next: express.NextFunction, ) => void | Promise - type WebsocketMethod = (route: expressCore.PathParams, ...handlers: WebsocketRequestHandler[]) => T + type WebSocketMethod = (route: expressCore.PathParams, ...handlers: WebSocketRequestHandler[]) => T + + interface WithWebSocket { + ws: net.Socket + head: Buffer + } interface WithWebsocketMethod { - ws: WebsocketMethod + ws: WebSocketMethod } } +interface WebsocketRequest extends express.Request, express.WithWebSocket { + _ws_handled: boolean +} + +function isWebSocketRequest(req: express.Request): req is WebsocketRequest { + return !!(req as WebsocketRequest).ws +} + export const handleUpgrade = (app: express.Express, server: http.Server): void => { server.on("upgrade", (req, socket, head) => { socket.on("error", () => socket.destroy()) @@ -193,15 +208,15 @@ function patchRouter(): void { // Inject the `ws` method. ;(express.Router as any).ws = function ws( route: expressCore.PathParams, - ...handlers: express.WebsocketRequestHandler[] + ...handlers: express.WebSocketRequestHandler[] ) { originalGet.apply(this, [ route, ...handlers.map((handler) => { - const wrapped: express.Handler = (req, _, next) => { - if ((req as any).ws) { - ;(req as any)._ws_handled = true - Promise.resolve(handler((req as any).ws, (req as any).head, req, next)).catch(next) + const wrapped: express.Handler = (req, res, next) => { + if (isWebSocketRequest(req)) { + req._ws_handled = true + Promise.resolve(handler(req, res, next)).catch(next) } else { next() } @@ -218,7 +233,7 @@ function patchRouter(): void { route, ...handlers.map((handler) => { const wrapped: express.Handler = (req, res, next) => { - if (!(req as any).ws) { + if (!isWebSocketRequest(req)) { Promise.resolve(handler(req, res, next)).catch(next) } else { next() diff --git a/src/node/proxy.ts b/src/node/proxy.ts index d53de6927..bfc6af5b3 100644 --- a/src/node/proxy.ts +++ b/src/node/proxy.ts @@ -82,7 +82,7 @@ router.all("*", (req, res, next) => { }) }) -router.ws("*", (socket, head, req, next) => { +router.ws("*", (req, _, next) => { const port = maybeProxy(req) if (!port) { return next() @@ -91,7 +91,7 @@ router.ws("*", (socket, head, req, next) => { // Must be authenticated to use the proxy. ensureAuthenticated(req) - proxy.ws(req, socket, head, { + proxy.ws(req, req.ws, req.head, { ignorePath: true, target: `http://0.0.0.0:${port}${req.originalUrl}`, }) diff --git a/src/node/routes/proxy.ts b/src/node/routes/proxy.ts index 29aa999ae..59db92d97 100644 --- a/src/node/routes/proxy.ts +++ b/src/node/routes/proxy.ts @@ -35,8 +35,8 @@ router.all("/(:port)(/*)?", (req, res) => { }) }) -router.ws("/(:port)(/*)?", (socket, head, req) => { - proxy.ws(req, socket, head, { +router.ws("/(:port)(/*)?", (req) => { + proxy.ws(req, req.ws, req.head, { ignorePath: true, target: getProxyTarget(req, true), }) diff --git a/src/node/routes/update.ts b/src/node/routes/update.ts index b4fbc197e..ac1ddc413 100644 --- a/src/node/routes/update.ts +++ b/src/node/routes/update.ts @@ -7,12 +7,7 @@ export const router = Router() const provider = new UpdateProvider() -router.use((req, _, next) => { - ensureAuthenticated(req) - next() -}) - -router.get("/", async (req, res) => { +router.get("/", ensureAuthenticated, async (req, res) => { const update = await provider.getUpdate(req.query.force === "true") res.json({ checked: update.checked, diff --git a/src/node/routes/vscode.ts b/src/node/routes/vscode.ts index e7842a297..c936571c5 100644 --- a/src/node/routes/vscode.ts +++ b/src/node/routes/vscode.ts @@ -53,14 +53,13 @@ router.get("/", async (req, res) => { ) }) -router.ws("/", async (socket, _, req) => { - ensureAuthenticated(req) +router.ws("/", ensureAuthenticated, async (req) => { const magic = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" const reply = crypto .createHash("sha1") .update(req.headers["sec-websocket-key"] + magic) .digest("base64") - socket.write( + req.ws.write( [ "HTTP/1.1 101 Switching Protocols", "Upgrade: websocket", @@ -68,14 +67,13 @@ router.ws("/", async (socket, _, req) => { `Sec-WebSocket-Accept: ${reply}`, ].join("\r\n") + "\r\n\r\n", ) - await vscode.sendWebsocket(socket, req.query) + await vscode.sendWebsocket(req.ws, req.query) }) /** * TODO: Might currently be unused. */ -router.get("/resource(/*)?", async (req, res) => { - ensureAuthenticated(req) +router.get("/resource(/*)?", ensureAuthenticated, async (req, res) => { if (typeof req.query.path === "string") { res.set("Content-Type", getMediaMime(req.query.path)) res.send(await fs.readFile(pathToFsPath(req.query.path))) @@ -85,8 +83,7 @@ router.get("/resource(/*)?", async (req, res) => { /** * Used by VS Code to load files. */ -router.get("/vscode-remote-resource(/*)?", async (req, res) => { - ensureAuthenticated(req) +router.get("/vscode-remote-resource(/*)?", ensureAuthenticated, async (req, res) => { if (typeof req.query.path === "string") { res.set("Content-Type", getMediaMime(req.query.path)) res.send(await fs.readFile(pathToFsPath(req.query.path))) @@ -97,8 +94,7 @@ router.get("/vscode-remote-resource(/*)?", async (req, res) => { * VS Code webviews use these paths to load files and to load webview assets * like HTML and JavaScript. */ -router.get("/webview/*", async (req, res) => { - ensureAuthenticated(req) +router.get("/webview/*", ensureAuthenticated, async (req, res) => { res.set("Content-Type", getMediaMime(req.path)) if (/^vscode-resource/.test(req.params[0])) { return res.send(await fs.readFile(req.params[0].replace(/^vscode-resource(\/file)?/, ""))) From 396af238421458ace5ca9fb0218fd9d02257fdae Mon Sep 17 00:00:00 2001 From: Asher Date: Tue, 3 Nov 2020 17:14:04 -0600 Subject: [PATCH 140/247] Kill VS Code when process exits This is to ensure it doesn't hang around. --- src/node/entry.ts | 4 ---- src/node/vscode.ts | 2 ++ 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/node/entry.ts b/src/node/entry.ts index 3cca62a5b..c192158b6 100644 --- a/src/node/entry.ts +++ b/src/node/entry.ts @@ -102,10 +102,6 @@ const main = async (args: DefaultedArgs): Promise => { throw new Error("Please pass in a password via the config file or $PASSWORD") } - ipcMain.onDispose(() => { - // TODO: register disposables - }) - const [app, server] = await createApp(args) const serverAddress = ensureAddress(server) await register(app, server, args) diff --git a/src/node/vscode.ts b/src/node/vscode.ts index 5cf826a16..aed005f9f 100644 --- a/src/node/vscode.ts +++ b/src/node/vscode.ts @@ -7,6 +7,7 @@ import { arrayify, generateUuid } from "../common/util" import { rootPath } from "./constants" import { settings } from "./settings" import { isFile } from "./util" +import { ipcMain } from "./wrapper" export class VscodeProvider { public readonly serverRootPath: string @@ -16,6 +17,7 @@ export class VscodeProvider { public constructor() { this.vsRootPath = path.resolve(rootPath, "lib/vscode") this.serverRootPath = path.join(this.vsRootPath, "out/vs/server") + ipcMain.onDispose(() => this.dispose()) } public async dispose(): Promise { From 8252c372af84d8f5d2de40674af5eeda96e387e6 Mon Sep 17 00:00:00 2001 From: Asher Date: Wed, 4 Nov 2020 16:49:01 -0600 Subject: [PATCH 141/247] Provide a way to tell when event handlers are finished This lets us actually wait for disposal before a graceful exit. --- src/common/emitter.ts | 25 ++++++++++++++++++++++--- src/common/types.ts | 1 - src/node/wrapper.ts | 7 ++++--- 3 files changed, 26 insertions(+), 7 deletions(-) delete mode 100644 src/common/types.ts diff --git a/src/common/emitter.ts b/src/common/emitter.ts index 7a1ebf668..353ce851e 100644 --- a/src/common/emitter.ts +++ b/src/common/emitter.ts @@ -1,4 +1,10 @@ -import { Callback } from "./types" +import { logger } from "@coder/logger" + +/** + * Event emitter callback. Called with the emitted value and a promise that + * resolves when all emitters have finished. + */ +export type Callback> = (t: T, p: Promise) => R export interface Disposable { dispose(): void @@ -32,8 +38,21 @@ export class Emitter { /** * Emit an event with a value. */ - public emit(value: T): void { - this.listeners.forEach((cb) => cb(value)) + public async emit(value: T): Promise { + let resolve: () => void + const promise = new Promise((r) => (resolve = r)) + + await Promise.all( + this.listeners.map(async (cb) => { + try { + await cb(value, promise) + } catch (error) { + logger.error(error.message) + } + }), + ) + + resolve!() } public dispose(): void { diff --git a/src/common/types.ts b/src/common/types.ts deleted file mode 100644 index a8a0e4c1b..000000000 --- a/src/common/types.ts +++ /dev/null @@ -1 +0,0 @@ -export type Callback = (t: T) => R diff --git a/src/node/wrapper.ts b/src/node/wrapper.ts index cce841901..2e8c51cd0 100644 --- a/src/node/wrapper.ts +++ b/src/node/wrapper.ts @@ -39,13 +39,14 @@ export class IpcMain { process.on("SIGTERM", () => this._onDispose.emit("SIGTERM")) process.on("exit", () => this._onDispose.emit(undefined)) - this.onDispose((signal) => { + this.onDispose((signal, wait) => { // Remove listeners to avoid possibly triggering disposal again. process.removeAllListeners() - // Let any other handlers run first then exit. + // Try waiting for other handlers run first then exit. logger.debug(`${parentPid ? "inner process" : "wrapper"} ${process.pid} disposing`, field("code", signal)) - setTimeout(() => this.exit(0), 0) + wait.then(() => this.exit(0)) + setTimeout(() => this.exit(0), 5000) }) // Kill the inner process if the parent dies. This is for the case where the From 9e09c1f92b27b4da5c38e56d282f62f646405358 Mon Sep 17 00:00:00 2001 From: Asher Date: Thu, 5 Nov 2020 11:36:27 -0600 Subject: [PATCH 142/247] Upgrade to Express 5 Now async routes are handled! --- package.json | 2 +- src/node/http.ts | 46 +++++++++++------------------------ src/node/routes/index.ts | 4 ---- yarn.lock | 52 ++++++++++++++++++++++++++++++---------- 4 files changed, 54 insertions(+), 50 deletions(-) diff --git a/package.json b/package.json index 187052f8d..6459e6a0f 100644 --- a/package.json +++ b/package.json @@ -72,7 +72,7 @@ "body-parser": "^1.19.0", "cookie-parser": "^1.4.5", "env-paths": "^2.2.0", - "express": "^4.17.1", + "express": "^5.0.0-alpha.8", "fs-extra": "^9.0.1", "http-proxy": "^1.18.0", "httpolyglot": "^0.1.2", diff --git a/src/node/http.ts b/src/node/http.ts index 42896f26b..71b938a25 100644 --- a/src/node/http.ts +++ b/src/node/http.ts @@ -191,22 +191,19 @@ export const handleUpgrade = (app: express.Express, server: http.Server): void = } /** - * Patch Express routers to handle web sockets and async routes (since we have - * to patch `get` anyway). + * Patch Express routers to handle web sockets. * - * Not using express-ws since the ws-wrapped sockets don't work with the proxy - * and wildcards don't work correctly. + * Not using express-ws since the ws-wrapped sockets don't work with the proxy. */ function patchRouter(): void { - // Apparently this all works because Router is also the prototype assigned to - // the routers it returns. + // This works because Router is also the prototype assigned to the routers it + // returns. - // Store these since the original methods will be overridden. - const originalGet = (express.Router as any).get - const originalPost = (express.Router as any).post + // Store this since the original method will be overridden. + const originalGet = (express.Router as any).prototype.get // Inject the `ws` method. - ;(express.Router as any).ws = function ws( + ;(express.Router as any).prototype.ws = function ws( route: expressCore.PathParams, ...handlers: express.WebSocketRequestHandler[] ) { @@ -216,10 +213,9 @@ function patchRouter(): void { const wrapped: express.Handler = (req, res, next) => { if (isWebSocketRequest(req)) { req._ws_handled = true - Promise.resolve(handler(req, res, next)).catch(next) - } else { - next() + return handler(req, res, next) } + next() } return wrapped }), @@ -227,30 +223,16 @@ function patchRouter(): void { return this } // Overwrite `get` so we can distinguish between websocket and non-websocket - // routes. While we're at it handle async responses. - ;(express.Router as any).get = function get(route: expressCore.PathParams, ...handlers: express.Handler[]) { + // routes. + ;(express.Router as any).prototype.get = function get(route: expressCore.PathParams, ...handlers: express.Handler[]) { originalGet.apply(this, [ route, ...handlers.map((handler) => { const wrapped: express.Handler = (req, res, next) => { if (!isWebSocketRequest(req)) { - Promise.resolve(handler(req, res, next)).catch(next) - } else { - next() + return handler(req, res, next) } - } - return wrapped - }), - ]) - return this - } - // Handle async responses for `post` as well since we're in here anyway. - ;(express.Router as any).post = function post(route: expressCore.PathParams, ...handlers: express.Handler[]) { - originalPost.apply(this, [ - route, - ...handlers.map((handler) => { - const wrapped: express.Handler = (req, res, next) => { - Promise.resolve(handler(req, res, next)).catch(next) + next() } return wrapped }), @@ -259,5 +241,5 @@ function patchRouter(): void { } } -// This needs to happen before anything uses the router. +// This needs to happen before anything creates a router. patchRouter() diff --git a/src/node/routes/index.ts b/src/node/routes/index.ts index 8c541d555..910f5b690 100644 --- a/src/node/routes/index.ts +++ b/src/node/routes/index.ts @@ -55,10 +55,6 @@ export const register = async (app: Express, server: http.Server, args: Defaulte app.use(bodyParser.json()) app.use(bodyParser.urlencoded({ extended: true })) - server.on("upgrade", () => { - heart.beat() - }) - app.use(async (req, res, next) => { heart.beat() diff --git a/yarn.lock b/yarn.lock index 1d98caab6..d96b2e6ee 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1421,10 +1421,10 @@ array-equal@^1.0.0: resolved "https://registry.yarnpkg.com/array-equal/-/array-equal-1.0.0.tgz#8c2a5ef2472fd9ea742b04c77a75093ba2757c93" integrity sha1-jCpe8kcv2ep0KwTHenUJO6J1fJM= -array-flatten@1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" - integrity sha1-ml9pkFGx5wczKPKgCJaLZOopVdI= +array-flatten@2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-2.1.1.tgz#426bb9da84090c1838d812c8150af20a8331e296" + integrity sha1-Qmu52oQJDBg42BLIFQryCoMx4pY= array-includes@^3.1.1: version "3.1.1" @@ -2585,6 +2585,13 @@ debug@2.6.9, debug@^2.1.3, debug@^2.2.0, debug@^2.3.3, debug@^2.6.9: dependencies: ms "2.0.0" +debug@3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261" + integrity sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g== + dependencies: + ms "2.0.0" + debug@4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/debug/-/debug-4.1.1.tgz#3b72260255109c6b589cee050f1d516139664791" @@ -3209,19 +3216,19 @@ expand-brackets@^2.1.4: snapdragon "^0.8.1" to-regex "^3.0.1" -express@^4.17.1: - version "4.17.1" - resolved "https://registry.yarnpkg.com/express/-/express-4.17.1.tgz#4491fc38605cf51f8629d39c2b5d026f98a4c134" - integrity sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g== +express@^5.0.0-alpha.8: + version "5.0.0-alpha.8" + resolved "https://registry.yarnpkg.com/express/-/express-5.0.0-alpha.8.tgz#b9dd3a568eab791e3391db47f9e6ab91e61b13fe" + integrity sha512-PL8wTLgaNOiq7GpXt187/yWHkrNSfbr4H0yy+V0fpqJt5wpUzBi9DprAkwGKBFOqWHylJ8EyPy34V5u9YArfng== dependencies: accepts "~1.3.7" - array-flatten "1.1.1" + array-flatten "2.1.1" body-parser "1.19.0" content-disposition "0.5.3" content-type "~1.0.4" cookie "0.4.0" cookie-signature "1.0.6" - debug "2.6.9" + debug "3.1.0" depd "~1.1.2" encodeurl "~1.0.2" escape-html "~1.0.3" @@ -3232,10 +3239,11 @@ express@^4.17.1: methods "~1.1.2" on-finished "~2.3.0" parseurl "~1.3.3" - path-to-regexp "0.1.7" + path-is-absolute "1.0.1" proxy-addr "~2.0.5" qs "6.7.0" range-parser "~1.2.1" + router "2.0.0-alpha.1" safe-buffer "5.1.2" send "0.17.1" serve-static "1.14.1" @@ -5489,7 +5497,7 @@ parse5@5.1.0: resolved "https://registry.yarnpkg.com/parse5/-/parse5-5.1.0.tgz#c59341c9723f414c452975564c7c00a68d58acd2" integrity sha512-fxNG2sQjHvlVAYmzBZS9YlDp6PTSSDwa98vkD4QgVDDCAo84z5X1t5XyJQ62ImdLXx5NdIIfihey6xpum9/gRQ== -parseurl@~1.3.3: +parseurl@~1.3.2, parseurl@~1.3.3: version "1.3.3" resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== @@ -5519,7 +5527,7 @@ path-exists@^4.0.0: resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== -path-is-absolute@^1.0.0: +path-is-absolute@1.0.1, path-is-absolute@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18= @@ -6619,6 +6627,19 @@ rotating-file-stream@^2.1.1: resolved "https://registry.yarnpkg.com/rotating-file-stream/-/rotating-file-stream-2.1.3.tgz#4b3cc8f56ae70b3e30ccdb4ee6b14d95e66b02bb" integrity sha512-zZ4Tkngxispo7DgiTqX0s4ChLtM3qET6iYsDA9tmgDEqJ3BFgRq/ZotsKEDAYQt9pAn9JwwqT27CSwQt3CTxNg== +router@2.0.0-alpha.1: + version "2.0.0-alpha.1" + resolved "https://registry.yarnpkg.com/router/-/router-2.0.0-alpha.1.tgz#9188213b972215e03ef830e0ac77837870085f6d" + integrity sha512-fz/T/qLkJM6RTtbqGqA1+uZ88ejqJoPyKeJAeXPYjebA7HzV/UyflH4gXWqW/Y6SERnp4kDwNARjqy6se3PcOw== + dependencies: + array-flatten "2.1.1" + debug "3.1.0" + methods "~1.1.2" + parseurl "~1.3.2" + path-to-regexp "0.1.7" + setprototypeof "1.1.0" + utils-merge "1.0.1" + run-parallel@^1.1.9: version "1.1.9" resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.1.9.tgz#c9dd3a7cf9f4b2c4b6244e173a6ed866e61dd679" @@ -6736,6 +6757,11 @@ setimmediate@^1.0.4: resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285" integrity sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU= +setprototypeof@1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.0.tgz#d0bd85536887b6fe7c0d818cb962d9d91c54e656" + integrity sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ== + setprototypeof@1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.1.tgz#7e95acb24aa92f5885e0abef5ba131330d4ae683" From 7b2752a62cd770d411aa9abb30b2082efc312dba Mon Sep 17 00:00:00 2001 From: Asher Date: Thu, 5 Nov 2020 12:58:37 -0600 Subject: [PATCH 143/247] Move websocket routes into a separate app This is mostly so we don't have to do any wacky patching but it also makes it so we don't have to keep checking if the request is a web socket request every time we add middleware. --- src/node/app.ts | 9 ++-- src/node/entry.ts | 4 +- src/node/http.ts | 110 -------------------------------------- src/node/proxy.ts | 5 +- src/node/routes/index.ts | 52 ++++++++++++++---- src/node/routes/proxy.ts | 5 +- src/node/routes/vscode.ts | 37 +++++++------ src/node/wsRouter.ts | 57 ++++++++++++++++++++ 8 files changed, 134 insertions(+), 145 deletions(-) create mode 100644 src/node/wsRouter.ts diff --git a/src/node/app.ts b/src/node/app.ts index 171a7c4d0..448ec9660 100644 --- a/src/node/app.ts +++ b/src/node/app.ts @@ -4,12 +4,12 @@ import { promises as fs } from "fs" import http from "http" import * as httpolyglot from "httpolyglot" import { DefaultedArgs } from "./cli" -import { handleUpgrade } from "./http" +import { handleUpgrade } from "./wsRouter" /** * Create an Express app and an HTTP/S server to serve it. */ -export const createApp = async (args: DefaultedArgs): Promise<[Express, http.Server]> => { +export const createApp = async (args: DefaultedArgs): Promise<[Express, Express, http.Server]> => { const app = express() const server = args.cert @@ -39,9 +39,10 @@ export const createApp = async (args: DefaultedArgs): Promise<[Express, http.Ser } }) - handleUpgrade(app, server) + const wsApp = express() + handleUpgrade(wsApp, server) - return [app, server] + return [app, wsApp, server] } /** diff --git a/src/node/entry.ts b/src/node/entry.ts index c192158b6..431aa8f49 100644 --- a/src/node/entry.ts +++ b/src/node/entry.ts @@ -102,9 +102,9 @@ const main = async (args: DefaultedArgs): Promise => { throw new Error("Please pass in a password via the config file or $PASSWORD") } - const [app, server] = await createApp(args) + const [app, wsApp, server] = await createApp(args) const serverAddress = ensureAddress(server) - await register(app, server, args) + await register(app, wsApp, server, args) logger.info(`Using config file ${humanPath(args.config)}`) logger.info(`HTTP server listening on ${serverAddress} ${args.link ? "(randomized by --link)" : ""}`) diff --git a/src/node/http.ts b/src/node/http.ts index 71b938a25..f259d1037 100644 --- a/src/node/http.ts +++ b/src/node/http.ts @@ -1,8 +1,6 @@ import { field, logger } from "@coder/logger" import * as express from "express" import * as expressCore from "express-serve-static-core" -import * as http from "http" -import * as net from "net" import qs from "qs" import safeCompare from "safe-compare" import { HttpCode, HttpError } from "../common/http" @@ -135,111 +133,3 @@ export const getCookieDomain = (host: string, proxyDomains: string[]): string | logger.debug("got cookie doman", field("host", host)) return host || undefined } - -declare module "express" { - function Router(options?: express.RouterOptions): express.Router & WithWebsocketMethod - - type WebSocketRequestHandler = ( - req: express.Request & WithWebSocket, - res: express.Response, - next: express.NextFunction, - ) => void | Promise - - type WebSocketMethod = (route: expressCore.PathParams, ...handlers: WebSocketRequestHandler[]) => T - - interface WithWebSocket { - ws: net.Socket - head: Buffer - } - - interface WithWebsocketMethod { - ws: WebSocketMethod - } -} - -interface WebsocketRequest extends express.Request, express.WithWebSocket { - _ws_handled: boolean -} - -function isWebSocketRequest(req: express.Request): req is WebsocketRequest { - return !!(req as WebsocketRequest).ws -} - -export const handleUpgrade = (app: express.Express, server: http.Server): void => { - server.on("upgrade", (req, socket, head) => { - socket.on("error", () => socket.destroy()) - - req.ws = socket - req.head = head - req._ws_handled = false - - const res = new http.ServerResponse(req) - res.writeHead = function writeHead(statusCode: number) { - if (statusCode > 200) { - socket.destroy(new Error(`${statusCode}`)) - } - return res - } - - // Send the request off to be handled by Express. - ;(app as any).handle(req, res, () => { - if (!req._ws_handled) { - socket.destroy(new Error("Not found")) - } - }) - }) -} - -/** - * Patch Express routers to handle web sockets. - * - * Not using express-ws since the ws-wrapped sockets don't work with the proxy. - */ -function patchRouter(): void { - // This works because Router is also the prototype assigned to the routers it - // returns. - - // Store this since the original method will be overridden. - const originalGet = (express.Router as any).prototype.get - - // Inject the `ws` method. - ;(express.Router as any).prototype.ws = function ws( - route: expressCore.PathParams, - ...handlers: express.WebSocketRequestHandler[] - ) { - originalGet.apply(this, [ - route, - ...handlers.map((handler) => { - const wrapped: express.Handler = (req, res, next) => { - if (isWebSocketRequest(req)) { - req._ws_handled = true - return handler(req, res, next) - } - next() - } - return wrapped - }), - ]) - return this - } - // Overwrite `get` so we can distinguish between websocket and non-websocket - // routes. - ;(express.Router as any).prototype.get = function get(route: expressCore.PathParams, ...handlers: express.Handler[]) { - originalGet.apply(this, [ - route, - ...handlers.map((handler) => { - const wrapped: express.Handler = (req, res, next) => { - if (!isWebSocketRequest(req)) { - return handler(req, res, next) - } - next() - } - return wrapped - }), - ]) - return this - } -} - -// This needs to happen before anything creates a router. -patchRouter() diff --git a/src/node/proxy.ts b/src/node/proxy.ts index bfc6af5b3..4343d3346 100644 --- a/src/node/proxy.ts +++ b/src/node/proxy.ts @@ -2,6 +2,7 @@ import { Request, Router } from "express" import proxyServer from "http-proxy" import { HttpCode, HttpError } from "../common/http" import { authenticated, ensureAuthenticated } from "./http" +import { Router as WsRouter } from "./wsRouter" export const proxy = proxyServer.createProxyServer({}) proxy.on("error", (error, _, res) => { @@ -82,7 +83,9 @@ router.all("*", (req, res, next) => { }) }) -router.ws("*", (req, _, next) => { +export const wsRouter = WsRouter() + +wsRouter.ws("*", (req, _, next) => { const port = maybeProxy(req) if (!port) { return next() diff --git a/src/node/routes/index.ts b/src/node/routes/index.ts index 910f5b690..8e5d3c181 100644 --- a/src/node/routes/index.ts +++ b/src/node/routes/index.ts @@ -1,7 +1,7 @@ import { logger } from "@coder/logger" import bodyParser from "body-parser" import cookieParser from "cookie-parser" -import { ErrorRequestHandler, Express } from "express" +import * as express from "express" import { promises as fs } from "fs" import http from "http" import * as path from "path" @@ -15,6 +15,7 @@ import { replaceTemplates } from "../http" import { loadPlugins } from "../plugin" import * as domainProxy from "../proxy" import { getMediaMime, paths } from "../util" +import { WebsocketRequest } from "../wsRouter" import * as health from "./health" import * as login from "./login" import * as proxy from "./proxy" @@ -36,7 +37,12 @@ declare global { /** * Register all routes and middleware. */ -export const register = async (app: Express, server: http.Server, args: DefaultedArgs): Promise => { +export const register = async ( + app: express.Express, + wsApp: express.Express, + server: http.Server, + args: DefaultedArgs, +): Promise => { const heart = new Heart(path.join(paths.data, "heartbeat"), async () => { return new Promise((resolve, reject) => { server.getConnections((error, count) => { @@ -50,14 +56,28 @@ export const register = async (app: Express, server: http.Server, args: Defaulte }) app.disable("x-powered-by") + wsApp.disable("x-powered-by") app.use(cookieParser()) + wsApp.use(cookieParser()) + app.use(bodyParser.json()) app.use(bodyParser.urlencoded({ extended: true })) - app.use(async (req, res, next) => { + const common: express.RequestHandler = (req, _, next) => { heart.beat() + // Add common variables routes can use. + req.args = args + req.heart = heart + + next() + } + + app.use(common) + wsApp.use(common) + + app.use(async (req, res, next) => { // If we're handling TLS ensure all requests are redirected to HTTPS. // TODO: This does *NOT* work if you have a base path since to specify the // protocol we need to specify the whole path. @@ -72,23 +92,28 @@ export const register = async (app: Express, server: http.Server, args: Defaulte return res.send(await fs.readFile(resourcePath)) } - // Add common variables routes can use. - req.args = args - req.heart = heart - - return next() + next() }) app.use("/", domainProxy.router) + wsApp.use("/", domainProxy.wsRouter.router) + app.use("/", vscode.router) + wsApp.use("/", vscode.wsRouter.router) + app.use("/vscode", vscode.router) + wsApp.use("/vscode", vscode.wsRouter.router) + app.use("/healthz", health.router) + if (args.auth === AuthType.Password) { app.use("/login", login.router) } + app.use("/proxy", proxy.router) + wsApp.use("/proxy", proxy.wsRouter.router) + app.use("/static", _static.router) app.use("/update", update.router) - app.use("/vscode", vscode.router) await loadPlugins(app, args) @@ -96,7 +121,7 @@ export const register = async (app: Express, server: http.Server, args: Defaulte throw new HttpError("Not Found", HttpCode.NotFound) }) - const errorHandler: ErrorRequestHandler = async (err, req, res, next) => { + const errorHandler: express.ErrorRequestHandler = async (err, req, res, next) => { const resourcePath = path.resolve(rootPath, "src/browser/pages/error.html") res.set("Content-Type", getMediaMime(resourcePath)) try { @@ -117,4 +142,11 @@ export const register = async (app: Express, server: http.Server, args: Defaulte } app.use(errorHandler) + + const wsErrorHandler: express.ErrorRequestHandler = async (err, req) => { + logger.error(`${err.message} ${err.stack}`) + ;(req as WebsocketRequest).ws.destroy(err) + } + + wsApp.use(wsErrorHandler) } diff --git a/src/node/routes/proxy.ts b/src/node/routes/proxy.ts index 59db92d97..ff6f4067f 100644 --- a/src/node/routes/proxy.ts +++ b/src/node/routes/proxy.ts @@ -3,6 +3,7 @@ import qs from "qs" import { HttpCode, HttpError } from "../../common/http" import { authenticated, redirect } from "../http" import { proxy } from "../proxy" +import { Router as WsRouter } from "../wsRouter" export const router = Router() @@ -35,7 +36,9 @@ router.all("/(:port)(/*)?", (req, res) => { }) }) -router.ws("/(:port)(/*)?", (req) => { +export const wsRouter = WsRouter() + +wsRouter.ws("/(:port)(/*)?", (req) => { proxy.ws(req, req.ws, req.head, { ignorePath: true, target: getProxyTarget(req, true), diff --git a/src/node/routes/vscode.ts b/src/node/routes/vscode.ts index c936571c5..db2dc2071 100644 --- a/src/node/routes/vscode.ts +++ b/src/node/routes/vscode.ts @@ -6,6 +6,7 @@ import { commit, rootPath, version } from "../constants" import { authenticated, ensureAuthenticated, redirect, replaceTemplates } from "../http" import { getMediaMime, pathToFsPath } from "../util" import { VscodeProvider } from "../vscode" +import { Router as WsRouter } from "../wsRouter" export const router = Router() @@ -53,23 +54,6 @@ router.get("/", async (req, res) => { ) }) -router.ws("/", ensureAuthenticated, async (req) => { - const magic = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" - const reply = crypto - .createHash("sha1") - .update(req.headers["sec-websocket-key"] + magic) - .digest("base64") - req.ws.write( - [ - "HTTP/1.1 101 Switching Protocols", - "Upgrade: websocket", - "Connection: Upgrade", - `Sec-WebSocket-Accept: ${reply}`, - ].join("\r\n") + "\r\n\r\n", - ) - await vscode.sendWebsocket(req.ws, req.query) -}) - /** * TODO: Might currently be unused. */ @@ -103,3 +87,22 @@ router.get("/webview/*", ensureAuthenticated, async (req, res) => { await fs.readFile(path.join(vscode.vsRootPath, "out/vs/workbench/contrib/webview/browser/pre", req.params[0])), ) }) + +export const wsRouter = WsRouter() + +wsRouter.ws("/", ensureAuthenticated, async (req) => { + const magic = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" + const reply = crypto + .createHash("sha1") + .update(req.headers["sec-websocket-key"] + magic) + .digest("base64") + req.ws.write( + [ + "HTTP/1.1 101 Switching Protocols", + "Upgrade: websocket", + "Connection: Upgrade", + `Sec-WebSocket-Accept: ${reply}`, + ].join("\r\n") + "\r\n\r\n", + ) + await vscode.sendWebsocket(req.ws, req.query) +}) diff --git a/src/node/wsRouter.ts b/src/node/wsRouter.ts new file mode 100644 index 000000000..1a057f0fa --- /dev/null +++ b/src/node/wsRouter.ts @@ -0,0 +1,57 @@ +import * as express from "express" +import * as expressCore from "express-serve-static-core" +import * as http from "http" +import * as net from "net" + +export const handleUpgrade = (app: express.Express, server: http.Server): void => { + server.on("upgrade", (req, socket, head) => { + socket.on("error", () => socket.destroy()) + + req.ws = socket + req.head = head + req._ws_handled = false + + // Send the request off to be handled by Express. + ;(app as any).handle(req, new http.ServerResponse(req), () => { + if (!req._ws_handled) { + socket.destroy(new Error("Not found")) + } + }) + }) +} + +export interface WebsocketRequest extends express.Request { + ws: net.Socket + head: Buffer +} + +interface InternalWebsocketRequest extends WebsocketRequest { + _ws_handled: boolean +} + +export type WebSocketHandler = ( + req: WebsocketRequest, + res: express.Response, + next: express.NextFunction, +) => void | Promise + +export class WebsocketRouter { + public readonly router = express.Router() + + public ws(route: expressCore.PathParams, ...handlers: WebSocketHandler[]): void { + this.router.get( + route, + ...handlers.map((handler) => { + const wrapped: express.Handler = (req, res, next) => { + ;(req as InternalWebsocketRequest)._ws_handled = true + return handler(req as WebsocketRequest, res, next) + } + return wrapped + }), + ) + } +} + +export function Router(): WebsocketRouter { + return new WebsocketRouter() +} From 3f1750cf83ac23ef390e997cb039eb3ec30d2f58 Mon Sep 17 00:00:00 2001 From: Asher Date: Thu, 5 Nov 2020 14:34:57 -0600 Subject: [PATCH 144/247] Fix destroying response in update again I added another reject that doesn't destroy the response. --- src/node/update.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/node/update.ts b/src/node/update.ts index 2959b874c..13ac73c37 100644 --- a/src/node/update.ts +++ b/src/node/update.ts @@ -111,14 +111,13 @@ export class UpdateProvider { if (response.statusCode >= 300) { ++redirects + response.destroy() if (redirects > maxRedirects) { - response.destroy() return reject(new Error("reached max redirects")) } if (!response.headers.location) { return reject(new Error("received redirect with no location header")) } - response.destroy() return request(url.resolve(uri, response.headers.location)) } From cb991a9143137287321ae48861620565630103d3 Mon Sep 17 00:00:00 2001 From: Asher Date: Thu, 5 Nov 2020 15:19:15 -0600 Subject: [PATCH 145/247] Handle errors for JSON requests Previously it would have just given them the error HTML. --- src/node/routes/index.ts | 28 +++++++++++++++++----------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/src/node/routes/index.ts b/src/node/routes/index.ts index 8e5d3c181..4643aa133 100644 --- a/src/node/routes/index.ts +++ b/src/node/routes/index.ts @@ -121,23 +121,29 @@ export const register = async ( throw new HttpError("Not Found", HttpCode.NotFound) }) - const errorHandler: express.ErrorRequestHandler = async (err, req, res, next) => { - const resourcePath = path.resolve(rootPath, "src/browser/pages/error.html") - res.set("Content-Type", getMediaMime(resourcePath)) - try { + const errorHandler: express.ErrorRequestHandler = async (err, req, res) => { + if (err.code === "ENOENT" || err.code === "EISDIR") { + err.status = HttpCode.NotFound + } + + const status = err.status ?? err.statusCode ?? 500 + res.status(status) + + if (req.accepts("application/json")) { + res.json({ + error: err.message, + ...(err.details || {}), + }) + } else { + const resourcePath = path.resolve(rootPath, "src/browser/pages/error.html") + res.set("Content-Type", getMediaMime(resourcePath)) const content = await fs.readFile(resourcePath, "utf8") - if (err.code === "ENOENT" || err.code === "EISDIR") { - err.status = HttpCode.NotFound - } - const status = err.status ?? err.statusCode ?? 500 - res.status(status).send( + res.send( replaceTemplates(req, content) .replace(/{{ERROR_TITLE}}/g, status) .replace(/{{ERROR_HEADER}}/g, status) .replace(/{{ERROR_BODY}}/g, err.message), ) - } catch (error) { - next(error) } } From f6c4434191eb529aaa0f86e98365b490bb81a0d8 Mon Sep 17 00:00:00 2001 From: Asher Date: Thu, 5 Nov 2020 16:42:58 -0600 Subject: [PATCH 146/247] Tweak proxy fallthrough behavior It will now redirect all HTML requests. Also it avoids req.accepts since that's always truthy. --- src/node/proxy.ts | 41 ++++++++++++++++++++--------------------- 1 file changed, 20 insertions(+), 21 deletions(-) diff --git a/src/node/proxy.ts b/src/node/proxy.ts index 4343d3346..d59df1d37 100644 --- a/src/node/proxy.ts +++ b/src/node/proxy.ts @@ -1,7 +1,7 @@ import { Request, Router } from "express" import proxyServer from "http-proxy" import { HttpCode, HttpError } from "../common/http" -import { authenticated, ensureAuthenticated } from "./http" +import { authenticated, ensureAuthenticated, redirect } from "./http" import { Router as WsRouter } from "./wsRouter" export const proxy = proxyServer.createProxyServer({}) @@ -44,25 +44,6 @@ const maybeProxy = (req: Request): string | undefined => { return port } -/** - * Determine if the user is browsing /, /login, or static assets and if so fall - * through to allow the redirect and login flow. - */ -const shouldFallThrough = (req: Request): boolean => { - // See if it looks like a request for the root or login HTML. - if (req.accepts("text/html")) { - if ( - (req.path === "/" && req.method === "GET") || - (/\/login\/?/.test(req.path) && (req.method === "GET" || req.method === "POST")) - ) { - return true - } - } - - // See if it looks like a request for a static asset. - return req.path.startsWith("/static/") && req.method === "GET" -} - router.all("*", (req, res, next) => { const port = maybeProxy(req) if (!port) { @@ -71,9 +52,27 @@ router.all("*", (req, res, next) => { // Must be authenticated to use the proxy. if (!authenticated(req)) { - if (shouldFallThrough(req)) { + // Let the assets through since they're used on the login page. + if (req.path.startsWith("/static/") && req.method === "GET") { return next() } + + // Assume anything that explicitly accepts text/html is a user browsing a + // page (as opposed to an xhr request). Don't use `req.accepts()` since + // *every* request that I've seen (in Firefox and Chromium at least) + // includes `*/*` making it always truthy. + if (typeof req.headers.accepts === "string" && req.headers.accepts.split(",").includes("text/html")) { + // Let the login through. + if (/\/login\/?/.test(req.path)) { + return next() + } + // Redirect all other pages to the login. + return redirect(req, res, "login", { + to: req.path, + }) + } + + // Everything else gets an unauthorized message. throw new HttpError("Unauthorized", HttpCode.Unauthorized) } From f7076247f96210ea0049897582a0c8b50bfdfea1 Mon Sep 17 00:00:00 2001 From: Asher Date: Thu, 5 Nov 2020 16:45:58 -0600 Subject: [PATCH 147/247] Move domain proxy to routes This matches better with the other routes. Also add a missing authentication check to the path proxy web socket. --- src/node/proxy.ts | 88 +--------------------- src/node/routes/domainProxy.ts | 87 +++++++++++++++++++++ src/node/routes/index.ts | 4 +- src/node/routes/{proxy.ts => pathProxy.ts} | 4 +- 4 files changed, 93 insertions(+), 90 deletions(-) create mode 100644 src/node/routes/domainProxy.ts rename src/node/routes/{proxy.ts => pathProxy.ts} (90%) diff --git a/src/node/proxy.ts b/src/node/proxy.ts index d59df1d37..da430f5b3 100644 --- a/src/node/proxy.ts +++ b/src/node/proxy.ts @@ -1,10 +1,8 @@ -import { Request, Router } from "express" import proxyServer from "http-proxy" -import { HttpCode, HttpError } from "../common/http" -import { authenticated, ensureAuthenticated, redirect } from "./http" -import { Router as WsRouter } from "./wsRouter" +import { HttpCode } from "../common/http" export const proxy = proxyServer.createProxyServer({}) + proxy.on("error", (error, _, res) => { res.writeHead(HttpCode.ServerError) res.end(error.message) @@ -16,85 +14,3 @@ proxy.on("proxyRes", (res, req) => { res.headers.location = (req as any).base + res.headers.location } }) - -export const router = Router() - -/** - * Return the port if the request should be proxied. Anything that ends in a - * proxy domain and has a *single* subdomain should be proxied. Anything else - * should return `undefined` and will be handled as normal. - * - * For example if `coder.com` is specified `8080.coder.com` will be proxied - * but `8080.test.coder.com` and `test.8080.coder.com` will not. - */ -const maybeProxy = (req: Request): string | undefined => { - // Split into parts. - const host = req.headers.host || "" - const idx = host.indexOf(":") - const domain = idx !== -1 ? host.substring(0, idx) : host - const parts = domain.split(".") - - // There must be an exact match. - const port = parts.shift() - const proxyDomain = parts.join(".") - if (!port || !req.args["proxy-domain"].includes(proxyDomain)) { - return undefined - } - - return port -} - -router.all("*", (req, res, next) => { - const port = maybeProxy(req) - if (!port) { - return next() - } - - // Must be authenticated to use the proxy. - if (!authenticated(req)) { - // Let the assets through since they're used on the login page. - if (req.path.startsWith("/static/") && req.method === "GET") { - return next() - } - - // Assume anything that explicitly accepts text/html is a user browsing a - // page (as opposed to an xhr request). Don't use `req.accepts()` since - // *every* request that I've seen (in Firefox and Chromium at least) - // includes `*/*` making it always truthy. - if (typeof req.headers.accepts === "string" && req.headers.accepts.split(",").includes("text/html")) { - // Let the login through. - if (/\/login\/?/.test(req.path)) { - return next() - } - // Redirect all other pages to the login. - return redirect(req, res, "login", { - to: req.path, - }) - } - - // Everything else gets an unauthorized message. - throw new HttpError("Unauthorized", HttpCode.Unauthorized) - } - - proxy.web(req, res, { - ignorePath: true, - target: `http://0.0.0.0:${port}${req.originalUrl}`, - }) -}) - -export const wsRouter = WsRouter() - -wsRouter.ws("*", (req, _, next) => { - const port = maybeProxy(req) - if (!port) { - return next() - } - - // Must be authenticated to use the proxy. - ensureAuthenticated(req) - - proxy.ws(req, req.ws, req.head, { - ignorePath: true, - target: `http://0.0.0.0:${port}${req.originalUrl}`, - }) -}) diff --git a/src/node/routes/domainProxy.ts b/src/node/routes/domainProxy.ts new file mode 100644 index 000000000..ac249b809 --- /dev/null +++ b/src/node/routes/domainProxy.ts @@ -0,0 +1,87 @@ +import { Request, Router } from "express" +import { HttpCode, HttpError } from "../../common/http" +import { authenticated, ensureAuthenticated, redirect } from "../http" +import { proxy } from "../proxy" +import { Router as WsRouter } from "../wsRouter" + +export const router = Router() + +/** + * Return the port if the request should be proxied. Anything that ends in a + * proxy domain and has a *single* subdomain should be proxied. Anything else + * should return `undefined` and will be handled as normal. + * + * For example if `coder.com` is specified `8080.coder.com` will be proxied + * but `8080.test.coder.com` and `test.8080.coder.com` will not. + */ +const maybeProxy = (req: Request): string | undefined => { + // Split into parts. + const host = req.headers.host || "" + const idx = host.indexOf(":") + const domain = idx !== -1 ? host.substring(0, idx) : host + const parts = domain.split(".") + + // There must be an exact match. + const port = parts.shift() + const proxyDomain = parts.join(".") + if (!port || !req.args["proxy-domain"].includes(proxyDomain)) { + return undefined + } + + return port +} + +router.all("*", (req, res, next) => { + const port = maybeProxy(req) + if (!port) { + return next() + } + + // Must be authenticated to use the proxy. + if (!authenticated(req)) { + // Let the assets through since they're used on the login page. + if (req.path.startsWith("/static/") && req.method === "GET") { + return next() + } + + // Assume anything that explicitly accepts text/html is a user browsing a + // page (as opposed to an xhr request). Don't use `req.accepts()` since + // *every* request that I've seen (in Firefox and Chromium at least) + // includes `*/*` making it always truthy. + if (typeof req.headers.accepts === "string" && req.headers.accepts.split(",").includes("text/html")) { + // Let the login through. + if (/\/login\/?/.test(req.path)) { + return next() + } + // Redirect all other pages to the login. + return redirect(req, res, "login", { + to: req.path, + }) + } + + // Everything else gets an unauthorized message. + throw new HttpError("Unauthorized", HttpCode.Unauthorized) + } + + proxy.web(req, res, { + ignorePath: true, + target: `http://0.0.0.0:${port}${req.originalUrl}`, + }) +}) + +export const wsRouter = WsRouter() + +wsRouter.ws("*", (req, _, next) => { + const port = maybeProxy(req) + if (!port) { + return next() + } + + // Must be authenticated to use the proxy. + ensureAuthenticated(req) + + proxy.ws(req, req.ws, req.head, { + ignorePath: true, + target: `http://0.0.0.0:${port}${req.originalUrl}`, + }) +}) diff --git a/src/node/routes/index.ts b/src/node/routes/index.ts index 4643aa133..afb24f156 100644 --- a/src/node/routes/index.ts +++ b/src/node/routes/index.ts @@ -13,12 +13,12 @@ import { rootPath } from "../constants" import { Heart } from "../heart" import { replaceTemplates } from "../http" import { loadPlugins } from "../plugin" -import * as domainProxy from "../proxy" import { getMediaMime, paths } from "../util" import { WebsocketRequest } from "../wsRouter" +import * as domainProxy from "./domainProxy" import * as health from "./health" import * as login from "./login" -import * as proxy from "./proxy" +import * as proxy from "./pathProxy" // static is a reserved keyword. import * as _static from "./static" import * as update from "./update" diff --git a/src/node/routes/proxy.ts b/src/node/routes/pathProxy.ts similarity index 90% rename from src/node/routes/proxy.ts rename to src/node/routes/pathProxy.ts index ff6f4067f..d21d08eb8 100644 --- a/src/node/routes/proxy.ts +++ b/src/node/routes/pathProxy.ts @@ -1,7 +1,7 @@ import { Request, Router } from "express" import qs from "qs" import { HttpCode, HttpError } from "../../common/http" -import { authenticated, redirect } from "../http" +import { authenticated, ensureAuthenticated, redirect } from "../http" import { proxy } from "../proxy" import { Router as WsRouter } from "../wsRouter" @@ -38,7 +38,7 @@ router.all("/(:port)(/*)?", (req, res) => { export const wsRouter = WsRouter() -wsRouter.ws("/(:port)(/*)?", (req) => { +wsRouter.ws("/(:port)(/*)?", ensureAuthenticated, (req) => { proxy.ws(req, req.ws, req.head, { ignorePath: true, target: getProxyTarget(req, true), From 959497067c32e265661f1d70c864e793b0e96084 Mon Sep 17 00:00:00 2001 From: Asher Date: Thu, 5 Nov 2020 17:07:51 -0600 Subject: [PATCH 148/247] Document HttpError Also type the status. --- src/common/http.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/common/http.ts b/src/common/http.ts index 5279bf44f..c08c8673b 100644 --- a/src/common/http.ts +++ b/src/common/http.ts @@ -8,8 +8,12 @@ export enum HttpCode { ServerError = 500, } +/** + * Represents an error with a message and an HTTP status code. This code will be + * used in the HTTP response. + */ export class HttpError extends Error { - public constructor(message: string, public readonly status: number, public readonly details?: object) { + public constructor(message: string, public readonly status: HttpCode, public readonly details?: object) { super(message) this.name = this.constructor.name } From aa2cfa2c17ebc91e87f2a0df442cecff7fe55e6a Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Thu, 29 Oct 2020 23:17:28 -0400 Subject: [PATCH 149/247] typings/plugin.d.ts: Create --- typings/plugin.d.ts | 110 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 110 insertions(+) create mode 100644 typings/plugin.d.ts diff --git a/typings/plugin.d.ts b/typings/plugin.d.ts new file mode 100644 index 000000000..92c3acada --- /dev/null +++ b/typings/plugin.d.ts @@ -0,0 +1,110 @@ +/** + * This file describes the code-server plugin API for adding new applications. + */ +import { Logger } from "@coder/logger" +import * as express from "express" + +/** + * Overlay + * + * The homepage of code-server will launch into VS Code. However, there will be an overlay + * button that when clicked, will show all available applications with their names, + * icons and provider plugins. When one clicks on an app's icon, they will be directed + * to // to access the application. + */ + +/** + * Plugins + * + * Plugins are just node modules. + * + * code-server uses $CS_PLUGIN_PATH to find plugins. Each subdirectory in + * $CS_PLUGIN_PATH with a package.json where the engine is code-server is + * a valid plugin. + * + * e.g. CS_PLUGIN_PATH=/tmp/nhooyr:/tmp/ash will cause code-server to search + * /tmp/nhooyr and then /tmp/ash for plugins. + * + * CS_PLUGIN_PATH defaults to + * ~/.local/share/code-server/plugins:/usr/share/code-server/plugins + * if unset. + * + * code-server also uses $CS_PLUGIN to find plugins. + * + * e.g. CS_PLUGIN=/tmp/will:/tmp/teffen will cause code-server to load + * /tmp/will and /tmp/teffen as plugins. + * + * Built in plugins are also loaded from __dirname/../plugins + * + * Priority is $CS_PLUGIN, $CS_PLUGIN_PATH and then the builtin plugins. + * After the search is complete, plugins will be required in first found order and + * initialized. See the Plugin interface for details. + * + * There is also a /api/applications endpoint to allow programmatic access to all + * available applications. It could be used to create a custom application dashboard + * for example. + */ + +/** + * Your plugin module must implement this interface. + * + * The plugin's name, description and version are fetched from its module's package.json + * + * The plugin's router will be mounted at / + * + * If two plugins are found with the exact same name, then code-server will + * use the last one and emit a warning. + */ +export interface Plugin { + /** + * init is called so that the plugin may initialize itself with the config. + */ + init(config: PluginConfig): void + + /** + * Returns the plugin's router. + */ + router(): express.Router + + /** + * code-server uses this to collect the list of applications that + * the plugin can currently provide. + * It is called when /api/applications is hit or the overlay needs to + * refresh the list of applications + * + * Ensure this is as fast as possible. + */ + applications(): Application[] | Promise +} + +/** + * PluginConfig contains the configuration required for initializing + * a plugin. + */ +export interface PluginConfig { + /** + * All plugin logs should be logged via this logger. + */ + readonly logger: Logger +} + +/** + * Application represents a user accessible application. + * + * When the user clicks on the icon in the overlay, they will be + * redirected to // + * where the application should be accessible. + * + * If the app's name is the same as the plugin's name then + * / will be used instead. + */ +export interface Application { + readonly name: string + readonly version: string + + /** + * The path at which the icon for this application can be accessed. + * /// + */ + readonly iconPath: string +} From 481df70622e260b8628af76c70933db419ac4f33 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Thu, 29 Oct 2020 23:18:07 -0400 Subject: [PATCH 150/247] ci/dev/test.sh: Pass through args --- ci/dev/test.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ci/dev/test.sh b/ci/dev/test.sh index 031bacf99..983b2f292 100755 --- a/ci/dev/test.sh +++ b/ci/dev/test.sh @@ -4,7 +4,7 @@ set -euo pipefail main() { cd "$(dirname "$0")/../.." - mocha -r ts-node/register ./test/*.test.ts + mocha -r ts-node/register ./test/*.test.ts "$@" } main "$@" From e08a55d44a1067c54cc845efd54cc31271c3ae08 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Fri, 30 Oct 2020 03:18:45 -0400 Subject: [PATCH 151/247] src/node/plugin.ts: Implement new plugin API --- src/node/plugin.ts | 239 ++++++++++++++++++++++++++------------- src/node/routes/index.ts | 6 +- 2 files changed, 166 insertions(+), 79 deletions(-) diff --git a/src/node/plugin.ts b/src/node/plugin.ts index 20c19d3e7..2ae29b967 100644 --- a/src/node/plugin.ts +++ b/src/node/plugin.ts @@ -1,92 +1,177 @@ -import { field, logger } from "@coder/logger" -import { Express } from "express" -import * as fs from "fs" import * as path from "path" -import * as util from "util" -import { Args } from "./cli" -import { paths } from "./util" +import * as util from "./util" +import * as pluginapi from "../../typings/plugin" +import * as fs from "fs" +import * as semver from "semver" +import { version } from "./constants" +const fsp = fs.promises +import { Logger, field } from "@coder/logger" +import * as express from "express" -/* eslint-disable @typescript-eslint/no-var-requires */ +// These fields are populated from the plugin's package.json. +interface Plugin extends pluginapi.Plugin { + name: string + version: string + description: string +} -export type Activate = (app: Express, args: Args) => void - -/** - * Plugins must implement this interface. - */ -export interface Plugin { - activate: Activate +interface Application extends pluginapi.Application { + plugin: Plugin } /** - * Intercept imports so we can inject code-server when the plugin tries to - * import it. + * PluginAPI implements the plugin API described in typings/plugin.d.ts + * Please see that file for details. */ -const originalLoad = require("module")._load -// eslint-disable-next-line @typescript-eslint/no-explicit-any -require("module")._load = function (request: string, parent: object, isMain: boolean): any { - return originalLoad.apply(this, [request.replace(/^code-server/, path.resolve(__dirname, "../..")), parent, isMain]) -} +export class PluginAPI { + private readonly plugins = new Array() + private readonly logger: Logger -/** - * Load a plugin and run its activation function. - */ -const loadPlugin = async (pluginPath: string, app: Express, args: Args): Promise => { - try { - const plugin: Plugin = require(pluginPath) - plugin.activate(app, args) - - const packageJson = require(path.join(pluginPath, "package.json")) - logger.debug( - "Loaded plugin", - field("name", packageJson.name || path.basename(pluginPath)), - field("path", pluginPath), - field("version", packageJson.version || "n/a"), - ) - } catch (error) { - logger.error(error.message) + public constructor( + logger: Logger, + /** + * These correspond to $CS_PLUGIN_PATH and $CS_PLUGIN respectively. + */ + private readonly csPlugin = "", + private readonly csPluginPath = `${path.join(util.paths.data, "plugins")}:/usr/share/code-server/plugins`, + ){ + this.logger = logger.named("pluginapi") } -} -/** - * Load all plugins in the specified directory. - */ -const _loadPlugins = async (pluginDir: string, app: Express, args: Args): Promise => { - try { - const files = await util.promisify(fs.readdir)(pluginDir, { - withFileTypes: true, - }) - await Promise.all(files.map((file) => loadPlugin(path.join(pluginDir, file.name), app, args))) - } catch (error) { - if (error.code !== "ENOENT") { - logger.warn(error.message) + /** + * applications grabs the full list of applications from + * all loaded plugins. + */ + public async applications(): Promise { + const apps = new Array() + for (let p of this.plugins) { + const pluginApps = await p.applications() + + // TODO prevent duplicates + // Add plugin key to each app. + apps.push( + ...pluginApps.map((app) => { + return { ...app, plugin: p } + }), + ) + } + return apps + } + + /** + * mount mounts all plugin routers onto r. + */ + public mount(r: express.Router): void { + for (let p of this.plugins) { + r.use(`/${p.name}`, p.router()) } } + + /** + * loadPlugins loads all plugins based on this.csPluginPath + * and this.csPlugin. + */ + public async loadPlugins(): Promise { + // Built-in plugins. + await this._loadPlugins(path.join(__dirname, "../../plugins")) + + for (let dir of this.csPluginPath.split(":")) { + if (!dir) { + continue + } + await this._loadPlugins(dir) + } + + for (let dir of this.csPlugin.split(":")) { + if (!dir) { + continue + } + await this.loadPlugin(dir) + } + } + + private async _loadPlugins(dir: string): Promise { + try { + const entries = await fsp.readdir(dir, { withFileTypes: true }) + for (let ent of entries) { + if (!ent.isDirectory()) { + continue + } + await this.loadPlugin(path.join(dir, ent.name)) + } + } catch (err) { + if (err.code !== "ENOENT") { + this.logger.warn(`failed to load plugins from ${q(dir)}: ${err.message}`) + } + } + } + + private async loadPlugin(dir: string): Promise { + try { + const str = await fsp.readFile(path.join(dir, "package.json"), { + encoding: "utf8", + }) + const packageJSON: PackageJSON = JSON.parse(str) + const p = this._loadPlugin(dir, packageJSON) + // TODO prevent duplicates + this.plugins.push(p) + } catch (err) { + if (err.code !== "ENOENT") { + this.logger.warn(`failed to load plugin: ${err.message}`) + } + } + } + + private _loadPlugin(dir: string, packageJSON: PackageJSON): Plugin { + const logger = this.logger.named(packageJSON.name) + logger.debug("loading plugin", + field("plugin_dir", dir), + field("package_json", packageJSON), + ) + + if (!semver.satisfies(version, packageJSON.engines["code-server"])) { + throw new Error(`plugin range ${q(packageJSON.engines["code-server"])} incompatible` + + ` with code-server version ${version}`) + } + if (!packageJSON.name) { + throw new Error("plugin missing name") + } + if (!packageJSON.version) { + throw new Error("plugin missing version") + } + if (!packageJSON.description) { + throw new Error("plugin missing description") + } + + const p = { + name: packageJSON.name, + version: packageJSON.version, + description: packageJSON.description, + ...require(dir), + } as Plugin + + p.init({ + logger: logger, + }) + + logger.debug("loaded") + + return p + } } -/** - * Load all plugins from the `plugins` directory, directories specified by - * `CS_PLUGIN_PATH` (colon-separated), and individual plugins specified by - * `CS_PLUGIN` (also colon-separated). - */ -export const loadPlugins = async (app: Express, args: Args): Promise => { - const pluginPath = process.env.CS_PLUGIN_PATH || `${path.join(paths.data, "plugins")}:/usr/share/code-server/plugins` - const plugin = process.env.CS_PLUGIN || "" - await Promise.all([ - // Built-in plugins. - _loadPlugins(path.resolve(__dirname, "../../plugins"), app, args), - // User-added plugins. - ...pluginPath - .split(":") - .filter((p) => !!p) - .map((dir) => _loadPlugins(path.resolve(dir), app, args)), - // Individual plugins so you don't have to symlink or move them into a - // directory specifically for plugins. This lets you load plugins that are - // on the same level as other directories that are not plugins (if you tried - // to use CS_PLUGIN_PATH code-server would try to load those other - // directories as plugins). Intended for development. - ...plugin - .split(":") - .filter((p) => !!p) - .map((dir) => loadPlugin(path.resolve(dir), app, args)), - ]) +interface PackageJSON { + name: string + version: string + description: string + engines: { + "code-server": string + } +} + +function q(s: string): string { + if (s === undefined) { + s = "undefined" + } + return JSON.stringify(s) } diff --git a/src/node/routes/index.ts b/src/node/routes/index.ts index afb24f156..5824475d9 100644 --- a/src/node/routes/index.ts +++ b/src/node/routes/index.ts @@ -12,7 +12,7 @@ import { AuthType, DefaultedArgs } from "../cli" import { rootPath } from "../constants" import { Heart } from "../heart" import { replaceTemplates } from "../http" -import { loadPlugins } from "../plugin" +import { PluginAPI } from "../plugin" import { getMediaMime, paths } from "../util" import { WebsocketRequest } from "../wsRouter" import * as domainProxy from "./domainProxy" @@ -115,7 +115,9 @@ export const register = async ( app.use("/static", _static.router) app.use("/update", update.router) - await loadPlugins(app, args) + const papi = new PluginAPI(logger, process.env.CS_PLUGIN, process.env.CS_PLUGIN_PATH) + await papi.loadPlugins() + papi.mount(app) app.use(() => { throw new HttpError("Not Found", HttpCode.NotFound) From bea185b8b2cf3487e4dc63c95d822bca76d3a030 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Fri, 30 Oct 2020 03:18:59 -0400 Subject: [PATCH 152/247] plugin: Add basic loading test Will work on testing overlay next. --- test/plugin.test.ts | 31 +++ test/test-plugin/.gitignore | 1 + test/test-plugin/package.json | 20 ++ test/test-plugin/public/icon.svg | 1 + test/test-plugin/src/index.ts | 23 ++ test/test-plugin/tsconfig.json | 69 +++++ test/test-plugin/yarn.lock | 435 +++++++++++++++++++++++++++++++ 7 files changed, 580 insertions(+) create mode 100644 test/plugin.test.ts create mode 100644 test/test-plugin/.gitignore create mode 100644 test/test-plugin/package.json create mode 100644 test/test-plugin/public/icon.svg create mode 100644 test/test-plugin/src/index.ts create mode 100644 test/test-plugin/tsconfig.json create mode 100644 test/test-plugin/yarn.lock diff --git a/test/plugin.test.ts b/test/plugin.test.ts new file mode 100644 index 000000000..05a72028a --- /dev/null +++ b/test/plugin.test.ts @@ -0,0 +1,31 @@ +import { describe } from "mocha" +import { PluginAPI } from "../src/node/plugin" +import { logger } from "@coder/logger" +import * as path from "path" +import * as assert from "assert" + +/** + * Use $LOG_LEVEL=debug to see debug logs. + */ +describe("plugin", () => { + it("loads", async () => { + const papi = new PluginAPI(logger, path.resolve(__dirname, "test-plugin") + ":meow") + await papi.loadPlugins() + + // We remove the function fields from the application's plugins. + const apps = JSON.parse(JSON.stringify(await papi.applications())) + + assert.deepEqual([ + { + name: "goland", + version: "4.0.0", + iconPath: "icon.svg", + plugin: { + name: "test-plugin", + version: "1.0.0", + description: "Fake plugin for testing code-server's plugin API", + }, + }, + ], apps) + }) +}) diff --git a/test/test-plugin/.gitignore b/test/test-plugin/.gitignore new file mode 100644 index 000000000..1fcb1529f --- /dev/null +++ b/test/test-plugin/.gitignore @@ -0,0 +1 @@ +out diff --git a/test/test-plugin/package.json b/test/test-plugin/package.json new file mode 100644 index 000000000..ccdeabb56 --- /dev/null +++ b/test/test-plugin/package.json @@ -0,0 +1,20 @@ +{ + "private": true, + "name": "test-plugin", + "version": "1.0.0", + "description": "Fake plugin for testing code-server's plugin API", + "engines": { + "code-server": "^3.6.0" + }, + "main": "out/index.js", + "devDependencies": { + "@types/express": "^4.17.8", + "typescript": "^4.0.5" + }, + "scripts": { + "build": "tsc" + }, + "dependencies": { + "express": "^4.17.1" + } +} diff --git a/test/test-plugin/public/icon.svg b/test/test-plugin/public/icon.svg new file mode 100644 index 000000000..25b9cf047 --- /dev/null +++ b/test/test-plugin/public/icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/test/test-plugin/src/index.ts b/test/test-plugin/src/index.ts new file mode 100644 index 000000000..b601cb3c0 --- /dev/null +++ b/test/test-plugin/src/index.ts @@ -0,0 +1,23 @@ +import * as pluginapi from "../../../typings/plugin" +import * as express from "express" +import * as path from "path"; + +export function init(config: pluginapi.PluginConfig) { + config.logger.debug("test-plugin loaded!") +} + +export function router(): express.Router { + const r = express.Router() + r.get("/goland/icon.svg", (req, res) => { + res.sendFile(path.resolve(__dirname, "../public/icon.svg")) + }) + return r +} + +export function applications(): pluginapi.Application[] { + return [{ + name: "goland", + version: "4.0.0", + iconPath: "icon.svg", + }] +} diff --git a/test/test-plugin/tsconfig.json b/test/test-plugin/tsconfig.json new file mode 100644 index 000000000..86e4897bb --- /dev/null +++ b/test/test-plugin/tsconfig.json @@ -0,0 +1,69 @@ +{ + "compilerOptions": { + /* Visit https://aka.ms/tsconfig.json to read more about this file */ + + /* Basic Options */ + // "incremental": true, /* Enable incremental compilation */ + "target": "es5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */ + "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */ + // "lib": [], /* Specify library files to be included in the compilation. */ + // "allowJs": true, /* Allow javascript files to be compiled. */ + // "checkJs": true, /* Report errors in .js files. */ + // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ + // "declaration": true, /* Generates corresponding '.d.ts' file. */ + // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ + // "sourceMap": true, /* Generates corresponding '.map' file. */ + // "outFile": "./", /* Concatenate and emit output to single file. */ + "outDir": "./out", /* Redirect output structure to the directory. */ + // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ + // "composite": true, /* Enable project compilation */ + // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ + // "removeComments": true, /* Do not emit comments to output. */ + // "noEmit": true, /* Do not emit outputs. */ + // "importHelpers": true, /* Import emit helpers from 'tslib'. */ + // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ + // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ + + /* Strict Type-Checking Options */ + "strict": true, /* Enable all strict type-checking options. */ + // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ + // "strictNullChecks": true, /* Enable strict null checks. */ + // "strictFunctionTypes": true, /* Enable strict checking of function types. */ + // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ + // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ + // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ + // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ + + /* Additional Checks */ + // "noUnusedLocals": true, /* Report errors on unused locals. */ + // "noUnusedParameters": true, /* Report errors on unused parameters. */ + // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ + // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ + + /* Module Resolution Options */ + // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ + // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ + // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ + // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ + // "typeRoots": [], /* List of folders to include type definitions from. */ + // "types": [], /* Type declaration files to be included in compilation. */ + // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ + "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ + // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ + // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ + + /* Source Map Options */ + // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ + // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ + // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ + // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ + + /* Experimental Options */ + // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ + // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ + + /* Advanced Options */ + "skipLibCheck": true, /* Skip type checking of declaration files. */ + "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ + } +} diff --git a/test/test-plugin/yarn.lock b/test/test-plugin/yarn.lock new file mode 100644 index 000000000..c77db2f7e --- /dev/null +++ b/test/test-plugin/yarn.lock @@ -0,0 +1,435 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@types/body-parser@*": + version "1.19.0" + resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.0.tgz#0685b3c47eb3006ffed117cdd55164b61f80538f" + integrity sha512-W98JrE0j2K78swW4ukqMleo8R7h/pFETjM2DQ90MF6XK2i4LO4W3gQ71Lt4w3bfm2EvVSyWHplECvB5sK22yFQ== + dependencies: + "@types/connect" "*" + "@types/node" "*" + +"@types/connect@*": + version "3.4.33" + resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.33.tgz#31610c901eca573b8713c3330abc6e6b9f588546" + integrity sha512-2+FrkXY4zllzTNfJth7jOqEHC+enpLeGslEhpnTAkg21GkRrWV4SsAtqchtT4YS9/nODBU2/ZfsBY2X4J/dX7A== + dependencies: + "@types/node" "*" + +"@types/express-serve-static-core@*": + version "4.17.13" + resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.17.13.tgz#d9af025e925fc8b089be37423b8d1eac781be084" + integrity sha512-RgDi5a4nuzam073lRGKTUIaL3eF2+H7LJvJ8eUnCI0wA6SNjXc44DCmWNiTLs/AZ7QlsFWZiw/gTG3nSQGL0fA== + dependencies: + "@types/node" "*" + "@types/qs" "*" + "@types/range-parser" "*" + +"@types/express@^4.17.8": + version "4.17.8" + resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.8.tgz#3df4293293317e61c60137d273a2e96cd8d5f27a" + integrity sha512-wLhcKh3PMlyA2cNAB9sjM1BntnhPMiM0JOBwPBqttjHev2428MLEB4AYVN+d8s2iyCVZac+o41Pflm/ZH5vLXQ== + dependencies: + "@types/body-parser" "*" + "@types/express-serve-static-core" "*" + "@types/qs" "*" + "@types/serve-static" "*" + +"@types/mime@*": + version "2.0.3" + resolved "https://registry.yarnpkg.com/@types/mime/-/mime-2.0.3.tgz#c893b73721db73699943bfc3653b1deb7faa4a3a" + integrity sha512-Jus9s4CDbqwocc5pOAnh8ShfrnMcPHuJYzVcSUU7lrh8Ni5HuIqX3oilL86p3dlTrk0LzHRCgA/GQ7uNCw6l2Q== + +"@types/node@*": + version "14.14.6" + resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.6.tgz#146d3da57b3c636cc0d1769396ce1cfa8991147f" + integrity sha512-6QlRuqsQ/Ox/aJEQWBEJG7A9+u7oSYl3mem/K8IzxXG/kAGbV1YPD9Bg9Zw3vyxC/YP+zONKwy8hGkSt1jxFMw== + +"@types/qs@*": + version "6.9.5" + resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.5.tgz#434711bdd49eb5ee69d90c1d67c354a9a8ecb18b" + integrity sha512-/JHkVHtx/REVG0VVToGRGH2+23hsYLHdyG+GrvoUGlGAd0ErauXDyvHtRI/7H7mzLm+tBCKA7pfcpkQ1lf58iQ== + +"@types/range-parser@*": + version "1.2.3" + resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.3.tgz#7ee330ba7caafb98090bece86a5ee44115904c2c" + integrity sha512-ewFXqrQHlFsgc09MK5jP5iR7vumV/BYayNC6PgJO2LPe8vrnNFyjQjSppfEngITi0qvfKtzFvgKymGheFM9UOA== + +"@types/serve-static@*": + version "1.13.6" + resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.13.6.tgz#866b1b8dec41c36e28c7be40ac725b88be43c5c1" + integrity sha512-nuRJmv7jW7VmCVTn+IgYDkkbbDGyIINOeu/G0d74X3lm6E5KfMeQPJhxIt1ayQeQB3cSxvYs1RA/wipYoFB4EA== + dependencies: + "@types/mime" "*" + "@types/node" "*" + +accepts@~1.3.7: + version "1.3.7" + resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.7.tgz#531bc726517a3b2b41f850021c6cc15eaab507cd" + integrity sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA== + dependencies: + mime-types "~2.1.24" + negotiator "0.6.2" + +array-flatten@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" + integrity sha1-ml9pkFGx5wczKPKgCJaLZOopVdI= + +body-parser@1.19.0: + version "1.19.0" + resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.19.0.tgz#96b2709e57c9c4e09a6fd66a8fd979844f69f08a" + integrity sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw== + dependencies: + bytes "3.1.0" + content-type "~1.0.4" + debug "2.6.9" + depd "~1.1.2" + http-errors "1.7.2" + iconv-lite "0.4.24" + on-finished "~2.3.0" + qs "6.7.0" + raw-body "2.4.0" + type-is "~1.6.17" + +bytes@3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.0.tgz#f6cf7933a360e0588fa9fde85651cdc7f805d1f6" + integrity sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg== + +content-disposition@0.5.3: + version "0.5.3" + resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.3.tgz#e130caf7e7279087c5616c2007d0485698984fbd" + integrity sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g== + dependencies: + safe-buffer "5.1.2" + +content-type@~1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b" + integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA== + +cookie-signature@1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" + integrity sha1-4wOogrNCzD7oylE6eZmXNNqzriw= + +cookie@0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.0.tgz#beb437e7022b3b6d49019d088665303ebe9c14ba" + integrity sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg== + +debug@2.6.9: + version "2.6.9" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" + integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== + dependencies: + ms "2.0.0" + +depd@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" + integrity sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak= + +destroy@~1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80" + integrity sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA= + +ee-first@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" + integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0= + +encodeurl@~1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" + integrity sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k= + +escape-html@~1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" + integrity sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg= + +etag@~1.8.1: + version "1.8.1" + resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" + integrity sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc= + +express@^4.17.1: + version "4.17.1" + resolved "https://registry.yarnpkg.com/express/-/express-4.17.1.tgz#4491fc38605cf51f8629d39c2b5d026f98a4c134" + integrity sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g== + dependencies: + accepts "~1.3.7" + array-flatten "1.1.1" + body-parser "1.19.0" + content-disposition "0.5.3" + content-type "~1.0.4" + cookie "0.4.0" + cookie-signature "1.0.6" + debug "2.6.9" + depd "~1.1.2" + encodeurl "~1.0.2" + escape-html "~1.0.3" + etag "~1.8.1" + finalhandler "~1.1.2" + fresh "0.5.2" + merge-descriptors "1.0.1" + methods "~1.1.2" + on-finished "~2.3.0" + parseurl "~1.3.3" + path-to-regexp "0.1.7" + proxy-addr "~2.0.5" + qs "6.7.0" + range-parser "~1.2.1" + safe-buffer "5.1.2" + send "0.17.1" + serve-static "1.14.1" + setprototypeof "1.1.1" + statuses "~1.5.0" + type-is "~1.6.18" + utils-merge "1.0.1" + vary "~1.1.2" + +finalhandler@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.1.2.tgz#b7e7d000ffd11938d0fdb053506f6ebabe9f587d" + integrity sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA== + dependencies: + debug "2.6.9" + encodeurl "~1.0.2" + escape-html "~1.0.3" + on-finished "~2.3.0" + parseurl "~1.3.3" + statuses "~1.5.0" + unpipe "~1.0.0" + +forwarded@~0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.1.2.tgz#98c23dab1175657b8c0573e8ceccd91b0ff18c84" + integrity sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ= + +fresh@0.5.2: + version "0.5.2" + resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" + integrity sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac= + +http-errors@1.7.2: + version "1.7.2" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.2.tgz#4f5029cf13239f31036e5b2e55292bcfbcc85c8f" + integrity sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg== + dependencies: + depd "~1.1.2" + inherits "2.0.3" + setprototypeof "1.1.1" + statuses ">= 1.5.0 < 2" + toidentifier "1.0.0" + +http-errors@~1.7.2: + version "1.7.3" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.3.tgz#6c619e4f9c60308c38519498c14fbb10aacebb06" + integrity sha512-ZTTX0MWrsQ2ZAhA1cejAwDLycFsd7I7nVtnkT3Ol0aqodaKW+0CTZDQ1uBv5whptCnc8e8HeRRJxRs0kmm/Qfw== + dependencies: + depd "~1.1.2" + inherits "2.0.4" + setprototypeof "1.1.1" + statuses ">= 1.5.0 < 2" + toidentifier "1.0.0" + +iconv-lite@0.4.24: + version "0.4.24" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" + integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== + dependencies: + safer-buffer ">= 2.1.2 < 3" + +inherits@2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" + integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4= + +inherits@2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" + integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== + +ipaddr.js@1.9.1: + version "1.9.1" + resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3" + integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g== + +media-typer@0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" + integrity sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g= + +merge-descriptors@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" + integrity sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E= + +methods@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" + integrity sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4= + +mime-db@1.44.0: + version "1.44.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.44.0.tgz#fa11c5eb0aca1334b4233cb4d52f10c5a6272f92" + integrity sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg== + +mime-types@~2.1.24: + version "2.1.27" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.27.tgz#47949f98e279ea53119f5722e0f34e529bec009f" + integrity sha512-JIhqnCasI9yD+SsmkquHBxTSEuZdQX5BuQnS2Vc7puQQQ+8yiP5AY5uWhpdv4YL4VM5c6iliiYWPgJ/nJQLp7w== + dependencies: + mime-db "1.44.0" + +mime@1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" + integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== + +ms@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" + integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g= + +ms@2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.1.tgz#30a5864eb3ebb0a66f2ebe6d727af06a09d86e0a" + integrity sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg== + +negotiator@0.6.2: + version "0.6.2" + resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.2.tgz#feacf7ccf525a77ae9634436a64883ffeca346fb" + integrity sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw== + +on-finished@~2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947" + integrity sha1-IPEzZIGwg811M3mSoWlxqi2QaUc= + dependencies: + ee-first "1.1.1" + +parseurl@~1.3.3: + version "1.3.3" + resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" + integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== + +path-to-regexp@0.1.7: + version "0.1.7" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" + integrity sha1-32BBeABfUi8V60SQ5yR6G/qmf4w= + +proxy-addr@~2.0.5: + version "2.0.6" + resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.6.tgz#fdc2336505447d3f2f2c638ed272caf614bbb2bf" + integrity sha512-dh/frvCBVmSsDYzw6n926jv974gddhkFPfiN8hPOi30Wax25QZyZEGveluCgliBnqmuM+UJmBErbAUFIoDbjOw== + dependencies: + forwarded "~0.1.2" + ipaddr.js "1.9.1" + +qs@6.7.0: + version "6.7.0" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.7.0.tgz#41dc1a015e3d581f1621776be31afb2876a9b1bc" + integrity sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ== + +range-parser@~1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" + integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== + +raw-body@2.4.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.4.0.tgz#a1ce6fb9c9bc356ca52e89256ab59059e13d0332" + integrity sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q== + dependencies: + bytes "3.1.0" + http-errors "1.7.2" + iconv-lite "0.4.24" + unpipe "1.0.0" + +safe-buffer@5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" + integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== + +"safer-buffer@>= 2.1.2 < 3": + version "2.1.2" + resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" + integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== + +send@0.17.1: + version "0.17.1" + resolved "https://registry.yarnpkg.com/send/-/send-0.17.1.tgz#c1d8b059f7900f7466dd4938bdc44e11ddb376c8" + integrity sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg== + dependencies: + debug "2.6.9" + depd "~1.1.2" + destroy "~1.0.4" + encodeurl "~1.0.2" + escape-html "~1.0.3" + etag "~1.8.1" + fresh "0.5.2" + http-errors "~1.7.2" + mime "1.6.0" + ms "2.1.1" + on-finished "~2.3.0" + range-parser "~1.2.1" + statuses "~1.5.0" + +serve-static@1.14.1: + version "1.14.1" + resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.14.1.tgz#666e636dc4f010f7ef29970a88a674320898b2f9" + integrity sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg== + dependencies: + encodeurl "~1.0.2" + escape-html "~1.0.3" + parseurl "~1.3.3" + send "0.17.1" + +setprototypeof@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.1.tgz#7e95acb24aa92f5885e0abef5ba131330d4ae683" + integrity sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw== + +"statuses@>= 1.5.0 < 2", statuses@~1.5.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" + integrity sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow= + +toidentifier@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.0.tgz#7e1be3470f1e77948bc43d94a3c8f4d7752ba553" + integrity sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw== + +type-is@~1.6.17, type-is@~1.6.18: + version "1.6.18" + resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" + integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g== + dependencies: + media-typer "0.3.0" + mime-types "~2.1.24" + +typescript@^4.0.5: + version "4.0.5" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.0.5.tgz#ae9dddfd1069f1cb5beb3ef3b2170dd7c1332389" + integrity sha512-ywmr/VrTVCmNTJ6iV2LwIrfG1P+lv6luD8sUJs+2eI9NLGigaN+nUQc13iHqisq7bra9lnmUSYqbJvegraBOPQ== + +unpipe@1.0.0, unpipe@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" + integrity sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw= + +utils-merge@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" + integrity sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM= + +vary@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" + integrity sha1-IpnwLG3tMNSllhsLn3RSShj2NPw= From 82e8a00a0d9dff6bcb4ca754ded9dbe1f42407cd Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Fri, 30 Oct 2020 03:26:30 -0400 Subject: [PATCH 153/247] Fix CI --- src/node/plugin.ts | 36 ++++++++++++++++------------------ test/plugin.test.ts | 31 ++++++++++++++++------------- test/test-plugin/src/index.ts | 16 ++++++++------- test/test-plugin/tsconfig.json | 14 ++++++------- 4 files changed, 50 insertions(+), 47 deletions(-) diff --git a/src/node/plugin.ts b/src/node/plugin.ts index 2ae29b967..ab2af5d66 100644 --- a/src/node/plugin.ts +++ b/src/node/plugin.ts @@ -1,12 +1,12 @@ -import * as path from "path" -import * as util from "./util" -import * as pluginapi from "../../typings/plugin" -import * as fs from "fs" -import * as semver from "semver" -import { version } from "./constants" -const fsp = fs.promises import { Logger, field } from "@coder/logger" import * as express from "express" +import * as fs from "fs" +import * as path from "path" +import * as semver from "semver" +import * as pluginapi from "../../typings/plugin" +import { version } from "./constants" +import * as util from "./util" +const fsp = fs.promises // These fields are populated from the plugin's package.json. interface Plugin extends pluginapi.Plugin { @@ -34,7 +34,7 @@ export class PluginAPI { */ private readonly csPlugin = "", private readonly csPluginPath = `${path.join(util.paths.data, "plugins")}:/usr/share/code-server/plugins`, - ){ + ) { this.logger = logger.named("pluginapi") } @@ -44,7 +44,7 @@ export class PluginAPI { */ public async applications(): Promise { const apps = new Array() - for (let p of this.plugins) { + for (const p of this.plugins) { const pluginApps = await p.applications() // TODO prevent duplicates @@ -62,7 +62,7 @@ export class PluginAPI { * mount mounts all plugin routers onto r. */ public mount(r: express.Router): void { - for (let p of this.plugins) { + for (const p of this.plugins) { r.use(`/${p.name}`, p.router()) } } @@ -75,14 +75,14 @@ export class PluginAPI { // Built-in plugins. await this._loadPlugins(path.join(__dirname, "../../plugins")) - for (let dir of this.csPluginPath.split(":")) { + for (const dir of this.csPluginPath.split(":")) { if (!dir) { continue } await this._loadPlugins(dir) } - for (let dir of this.csPlugin.split(":")) { + for (const dir of this.csPlugin.split(":")) { if (!dir) { continue } @@ -93,7 +93,7 @@ export class PluginAPI { private async _loadPlugins(dir: string): Promise { try { const entries = await fsp.readdir(dir, { withFileTypes: true }) - for (let ent of entries) { + for (const ent of entries) { if (!ent.isDirectory()) { continue } @@ -124,14 +124,12 @@ export class PluginAPI { private _loadPlugin(dir: string, packageJSON: PackageJSON): Plugin { const logger = this.logger.named(packageJSON.name) - logger.debug("loading plugin", - field("plugin_dir", dir), - field("package_json", packageJSON), - ) + logger.debug("loading plugin", field("plugin_dir", dir), field("package_json", packageJSON)) if (!semver.satisfies(version, packageJSON.engines["code-server"])) { - throw new Error(`plugin range ${q(packageJSON.engines["code-server"])} incompatible` + - ` with code-server version ${version}`) + throw new Error( + `plugin range ${q(packageJSON.engines["code-server"])} incompatible` + ` with code-server version ${version}`, + ) } if (!packageJSON.name) { throw new Error("plugin missing name") diff --git a/test/plugin.test.ts b/test/plugin.test.ts index 05a72028a..a77b1cf43 100644 --- a/test/plugin.test.ts +++ b/test/plugin.test.ts @@ -1,8 +1,8 @@ -import { describe } from "mocha" -import { PluginAPI } from "../src/node/plugin" import { logger } from "@coder/logger" -import * as path from "path" import * as assert from "assert" +import { describe } from "mocha" +import * as path from "path" +import { PluginAPI } from "../src/node/plugin" /** * Use $LOG_LEVEL=debug to see debug logs. @@ -15,17 +15,20 @@ describe("plugin", () => { // We remove the function fields from the application's plugins. const apps = JSON.parse(JSON.stringify(await papi.applications())) - assert.deepEqual([ - { - name: "goland", - version: "4.0.0", - iconPath: "icon.svg", - plugin: { - name: "test-plugin", - version: "1.0.0", - description: "Fake plugin for testing code-server's plugin API", + assert.deepEqual( + [ + { + name: "goland", + version: "4.0.0", + iconPath: "icon.svg", + plugin: { + name: "test-plugin", + version: "1.0.0", + description: "Fake plugin for testing code-server's plugin API", + }, }, - }, - ], apps) + ], + apps, + ) }) }) diff --git a/test/test-plugin/src/index.ts b/test/test-plugin/src/index.ts index b601cb3c0..83575533b 100644 --- a/test/test-plugin/src/index.ts +++ b/test/test-plugin/src/index.ts @@ -1,6 +1,6 @@ -import * as pluginapi from "../../../typings/plugin" import * as express from "express" -import * as path from "path"; +import * as path from "path" +import * as pluginapi from "../../../typings/plugin" export function init(config: pluginapi.PluginConfig) { config.logger.debug("test-plugin loaded!") @@ -15,9 +15,11 @@ export function router(): express.Router { } export function applications(): pluginapi.Application[] { - return [{ - name: "goland", - version: "4.0.0", - iconPath: "icon.svg", - }] + return [ + { + name: "goland", + version: "4.0.0", + iconPath: "icon.svg", + }, + ] } diff --git a/test/test-plugin/tsconfig.json b/test/test-plugin/tsconfig.json index 86e4897bb..0956ead88 100644 --- a/test/test-plugin/tsconfig.json +++ b/test/test-plugin/tsconfig.json @@ -4,8 +4,8 @@ /* Basic Options */ // "incremental": true, /* Enable incremental compilation */ - "target": "es5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */ - "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */ + "target": "es5" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */, + "module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */, // "lib": [], /* Specify library files to be included in the compilation. */ // "allowJs": true, /* Allow javascript files to be compiled. */ // "checkJs": true, /* Report errors in .js files. */ @@ -14,7 +14,7 @@ // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ // "sourceMap": true, /* Generates corresponding '.map' file. */ // "outFile": "./", /* Concatenate and emit output to single file. */ - "outDir": "./out", /* Redirect output structure to the directory. */ + "outDir": "./out" /* Redirect output structure to the directory. */, // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ // "composite": true, /* Enable project compilation */ // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ @@ -25,7 +25,7 @@ // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ /* Strict Type-Checking Options */ - "strict": true, /* Enable all strict type-checking options. */ + "strict": true /* Enable all strict type-checking options. */, // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ // "strictNullChecks": true, /* Enable strict null checks. */ // "strictFunctionTypes": true, /* Enable strict checking of function types. */ @@ -48,7 +48,7 @@ // "typeRoots": [], /* List of folders to include type definitions from. */ // "types": [], /* Type declaration files to be included in compilation. */ // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ - "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ + "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */, // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ @@ -63,7 +63,7 @@ // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ /* Advanced Options */ - "skipLibCheck": true, /* Skip type checking of declaration files. */ - "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ + "skipLibCheck": true /* Skip type checking of declaration files. */, + "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ } } From 30d2962e21468825e545235c3e272c6a7ffefe97 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Fri, 30 Oct 2020 03:37:42 -0400 Subject: [PATCH 154/247] src/node/plugin.ts: Warn on duplicate plugin and only load first --- src/node/plugin.ts | 20 +++++++++++++++++--- test/plugin.test.ts | 1 + 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/src/node/plugin.ts b/src/node/plugin.ts index ab2af5d66..ddfef7f97 100644 --- a/src/node/plugin.ts +++ b/src/node/plugin.ts @@ -8,11 +8,18 @@ import { version } from "./constants" import * as util from "./util" const fsp = fs.promises -// These fields are populated from the plugin's package.json. interface Plugin extends pluginapi.Plugin { + /** + * These fields are populated from the plugin's package.json. + */ name: string version: string description: string + + /** + * path to the node module on the disk. + */ + modulePath: string } interface Application extends pluginapi.Application { @@ -47,7 +54,6 @@ export class PluginAPI { for (const p of this.plugins) { const pluginApps = await p.applications() - // TODO prevent duplicates // Add plugin key to each app. apps.push( ...pluginApps.map((app) => { @@ -112,8 +118,15 @@ export class PluginAPI { encoding: "utf8", }) const packageJSON: PackageJSON = JSON.parse(str) + for (const p of this.plugins) { + if (p.name === packageJSON.name) { + this.logger.warn( + `ignoring duplicate plugin ${q(p.name)} at ${q(dir)}, using previously loaded ${q(p.modulePath)}`, + ) + return + } + } const p = this._loadPlugin(dir, packageJSON) - // TODO prevent duplicates this.plugins.push(p) } catch (err) { if (err.code !== "ENOENT") { @@ -145,6 +158,7 @@ export class PluginAPI { name: packageJSON.name, version: packageJSON.version, description: packageJSON.description, + modulePath: dir, ...require(dir), } as Plugin diff --git a/test/plugin.test.ts b/test/plugin.test.ts index a77b1cf43..014e07f5d 100644 --- a/test/plugin.test.ts +++ b/test/plugin.test.ts @@ -25,6 +25,7 @@ describe("plugin", () => { name: "test-plugin", version: "1.0.0", description: "Fake plugin for testing code-server's plugin API", + modulePath: path.join(__dirname, "test-plugin"), }, }, ], From ef971009d9632e333e2428fc3214dd2fa6f9ac02 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Fri, 30 Oct 2020 03:39:14 -0400 Subject: [PATCH 155/247] plugin.test.ts: Make it clear iconPath is a path --- test/plugin.test.ts | 2 +- test/test-plugin/src/index.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/test/plugin.test.ts b/test/plugin.test.ts index 014e07f5d..69c4572ee 100644 --- a/test/plugin.test.ts +++ b/test/plugin.test.ts @@ -20,7 +20,7 @@ describe("plugin", () => { { name: "goland", version: "4.0.0", - iconPath: "icon.svg", + iconPath: "/icon.svg", plugin: { name: "test-plugin", version: "1.0.0", diff --git a/test/test-plugin/src/index.ts b/test/test-plugin/src/index.ts index 83575533b..94bf73b80 100644 --- a/test/test-plugin/src/index.ts +++ b/test/test-plugin/src/index.ts @@ -19,7 +19,7 @@ export function applications(): pluginapi.Application[] { { name: "goland", version: "4.0.0", - iconPath: "icon.svg", + iconPath: "/icon.svg", }, ] } From f4d7f000331fe8152bfb95b7552c031e41fb3cd4 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Tue, 3 Nov 2020 16:21:18 -0500 Subject: [PATCH 156/247] plugin.ts: Fixes for @wbobeirne --- src/node/plugin.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/node/plugin.ts b/src/node/plugin.ts index ddfef7f97..cdd9c3d9a 100644 --- a/src/node/plugin.ts +++ b/src/node/plugin.ts @@ -150,9 +150,6 @@ export class PluginAPI { if (!packageJSON.version) { throw new Error("plugin missing version") } - if (!packageJSON.description) { - throw new Error("plugin missing description") - } const p = { name: packageJSON.name, @@ -181,7 +178,7 @@ interface PackageJSON { } } -function q(s: string): string { +function q(s: string | undefined): string { if (s === undefined) { s = "undefined" } From 75e52a37742833679711fca2b48fdf8a04fcb521 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Tue, 3 Nov 2020 16:24:06 -0500 Subject: [PATCH 157/247] plugin.ts: Fixes for @code-asher --- ci/dev/test.sh | 3 +++ src/node/plugin.ts | 19 +++++++++++++++---- test/plugin.test.ts | 3 +-- test/test-plugin/Makefile | 5 +++++ 4 files changed, 24 insertions(+), 6 deletions(-) create mode 100644 test/test-plugin/Makefile diff --git a/ci/dev/test.sh b/ci/dev/test.sh index 983b2f292..6eaa3878d 100755 --- a/ci/dev/test.sh +++ b/ci/dev/test.sh @@ -4,6 +4,9 @@ set -euo pipefail main() { cd "$(dirname "$0")/../.." + cd test/test-plugin + make -s out/index.js + cd $OLDPWD mocha -r ts-node/register ./test/*.test.ts "$@" } diff --git a/src/node/plugin.ts b/src/node/plugin.ts index cdd9c3d9a..f0dca2754 100644 --- a/src/node/plugin.ts +++ b/src/node/plugin.ts @@ -23,7 +23,10 @@ interface Plugin extends pluginapi.Plugin { } interface Application extends pluginapi.Application { - plugin: Plugin + /* + * Clone of the above without functions. + */ + plugin: Omit } /** @@ -57,7 +60,15 @@ export class PluginAPI { // Add plugin key to each app. apps.push( ...pluginApps.map((app) => { - return { ...app, plugin: p } + return { + ...app, + plugin: { + name: p.name, + version: p.version, + description: p.description, + modulePath: p.modulePath, + }, + } }), ) } @@ -74,8 +85,8 @@ export class PluginAPI { } /** - * loadPlugins loads all plugins based on this.csPluginPath - * and this.csPlugin. + * loadPlugins loads all plugins based on this.csPlugin, + * this.csPluginPath and the built in plugins. */ public async loadPlugins(): Promise { // Built-in plugins. diff --git a/test/plugin.test.ts b/test/plugin.test.ts index 69c4572ee..5836deada 100644 --- a/test/plugin.test.ts +++ b/test/plugin.test.ts @@ -12,8 +12,7 @@ describe("plugin", () => { const papi = new PluginAPI(logger, path.resolve(__dirname, "test-plugin") + ":meow") await papi.loadPlugins() - // We remove the function fields from the application's plugins. - const apps = JSON.parse(JSON.stringify(await papi.applications())) + const apps = await papi.applications() assert.deepEqual( [ diff --git a/test/test-plugin/Makefile b/test/test-plugin/Makefile new file mode 100644 index 000000000..fb66dc81a --- /dev/null +++ b/test/test-plugin/Makefile @@ -0,0 +1,5 @@ +out/index.js: src/index.ts + yarn build + +node_modules: package.json yarn.lock + yarn From 8d3a7721feaa7e319ec6182fbe4806a301b2671a Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Tue, 3 Nov 2020 16:42:18 -0500 Subject: [PATCH 158/247] plugin.d.ts: Document plugin priority correctly --- src/node/plugin.ts | 16 ++++++++-------- typings/plugin.d.ts | 22 +++++++++++----------- 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/src/node/plugin.ts b/src/node/plugin.ts index f0dca2754..a34c2027f 100644 --- a/src/node/plugin.ts +++ b/src/node/plugin.ts @@ -89,8 +89,12 @@ export class PluginAPI { * this.csPluginPath and the built in plugins. */ public async loadPlugins(): Promise { - // Built-in plugins. - await this._loadPlugins(path.join(__dirname, "../../plugins")) + for (const dir of this.csPlugin.split(":")) { + if (!dir) { + continue + } + await this.loadPlugin(dir) + } for (const dir of this.csPluginPath.split(":")) { if (!dir) { @@ -99,12 +103,8 @@ export class PluginAPI { await this._loadPlugins(dir) } - for (const dir of this.csPlugin.split(":")) { - if (!dir) { - continue - } - await this.loadPlugin(dir) - } + // Built-in plugins. + await this._loadPlugins(path.join(__dirname, "../../plugins")) } private async _loadPlugins(dir: string): Promise { diff --git a/typings/plugin.d.ts b/typings/plugin.d.ts index 92c3acada..549c15f11 100644 --- a/typings/plugin.d.ts +++ b/typings/plugin.d.ts @@ -18,7 +18,12 @@ import * as express from "express" * * Plugins are just node modules. * - * code-server uses $CS_PLUGIN_PATH to find plugins. Each subdirectory in + * 1. code-server uses $CS_PLUGIN to find plugins. + * + * e.g. CS_PLUGIN=/tmp/will:/tmp/teffen will cause code-server to load + * /tmp/will and /tmp/teffen as plugins. + * + * 2. code-server uses $CS_PLUGIN_PATH to find plugins. Each subdirectory in * $CS_PLUGIN_PATH with a package.json where the engine is code-server is * a valid plugin. * @@ -29,16 +34,14 @@ import * as express from "express" * ~/.local/share/code-server/plugins:/usr/share/code-server/plugins * if unset. * - * code-server also uses $CS_PLUGIN to find plugins. * - * e.g. CS_PLUGIN=/tmp/will:/tmp/teffen will cause code-server to load - * /tmp/will and /tmp/teffen as plugins. + * 3. Built in plugins are loaded from __dirname/../plugins * - * Built in plugins are also loaded from __dirname/../plugins + * Plugins are required as soon as they are found and then initialized. + * See the Plugin interface for details. * - * Priority is $CS_PLUGIN, $CS_PLUGIN_PATH and then the builtin plugins. - * After the search is complete, plugins will be required in first found order and - * initialized. See the Plugin interface for details. + * If two plugins are found with the exact same name, then code-server will + * use the first one and emit a warning. * * There is also a /api/applications endpoint to allow programmatic access to all * available applications. It could be used to create a custom application dashboard @@ -51,9 +54,6 @@ import * as express from "express" * The plugin's name, description and version are fetched from its module's package.json * * The plugin's router will be mounted at / - * - * If two plugins are found with the exact same name, then code-server will - * use the last one and emit a warning. */ export interface Plugin { /** From 6638daf6f05a21bf82dab86690635045443a055d Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Tue, 3 Nov 2020 17:09:28 -0500 Subject: [PATCH 159/247] plugin.d.ts: Add explicit path field and adjust types to reflect See my discussion with Will in the PR. --- typings/plugin.d.ts | 54 +++++++++++++++++++++++++++++++++++---------- 1 file changed, 42 insertions(+), 12 deletions(-) diff --git a/typings/plugin.d.ts b/typings/plugin.d.ts index 549c15f11..bc8d2ef58 100644 --- a/typings/plugin.d.ts +++ b/typings/plugin.d.ts @@ -10,7 +10,7 @@ import * as express from "express" * The homepage of code-server will launch into VS Code. However, there will be an overlay * button that when clicked, will show all available applications with their names, * icons and provider plugins. When one clicks on an app's icon, they will be directed - * to // to access the application. + * to // to access the application. */ /** @@ -51,11 +51,35 @@ import * as express from "express" /** * Your plugin module must implement this interface. * - * The plugin's name, description and version are fetched from its module's package.json - * - * The plugin's router will be mounted at / + * The plugin's router will be mounted at / */ export interface Plugin { + /** + * name is used as the plugin's unique identifier. + * No two plugins may share the same name. + * + * Fetched from package.json. + */ + name?: string + + /** + * The version for the plugin in the overlay. + * + * Fetched from package.json. + */ + version?: string + + /** + * These two are used in the overlay. + */ + displayName: string + description: string + + /** + * The path at which the plugin router is to be registered. + */ + path: string + /** * init is called so that the plugin may initialize itself with the config. */ @@ -63,6 +87,8 @@ export interface Plugin { /** * Returns the plugin's router. + * + * Mounted at / */ router(): express.Router @@ -90,21 +116,25 @@ export interface PluginConfig { /** * Application represents a user accessible application. - * - * When the user clicks on the icon in the overlay, they will be - * redirected to // - * where the application should be accessible. - * - * If the app's name is the same as the plugin's name then - * / will be used instead. */ export interface Application { readonly name: string readonly version: string + /** + * When the user clicks on the icon in the overlay, they will be + * redirected to // + * where the application should be accessible. + * + * If undefined, then / is used. + */ + readonly path?: string + + readonly description?: string + /** * The path at which the icon for this application can be accessed. - * /// + * /// */ readonly iconPath: string } From fed545e67d77e5792a0a4ee00aa4a0485ff925eb Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Tue, 3 Nov 2020 17:13:21 -0500 Subject: [PATCH 160/247] plugin.d.ts -> pluginapi.d.ts More clear. --- src/node/plugin.ts | 4 ++-- test/test-plugin/src/index.ts | 2 +- typings/{plugin.d.ts => pluginapi.d.ts} | 0 3 files changed, 3 insertions(+), 3 deletions(-) rename typings/{plugin.d.ts => pluginapi.d.ts} (100%) diff --git a/src/node/plugin.ts b/src/node/plugin.ts index a34c2027f..061523a0c 100644 --- a/src/node/plugin.ts +++ b/src/node/plugin.ts @@ -3,7 +3,7 @@ import * as express from "express" import * as fs from "fs" import * as path from "path" import * as semver from "semver" -import * as pluginapi from "../../typings/plugin" +import * as pluginapi from "../../typings/pluginapi" import { version } from "./constants" import * as util from "./util" const fsp = fs.promises @@ -30,7 +30,7 @@ interface Application extends pluginapi.Application { } /** - * PluginAPI implements the plugin API described in typings/plugin.d.ts + * PluginAPI implements the plugin API described in typings/pluginapi.d.ts * Please see that file for details. */ export class PluginAPI { diff --git a/test/test-plugin/src/index.ts b/test/test-plugin/src/index.ts index 94bf73b80..bc37d7c05 100644 --- a/test/test-plugin/src/index.ts +++ b/test/test-plugin/src/index.ts @@ -1,6 +1,6 @@ import * as express from "express" import * as path from "path" -import * as pluginapi from "../../../typings/plugin" +import * as pluginapi from "../../../typings/pluginapi" export function init(config: pluginapi.PluginConfig) { config.logger.debug("test-plugin loaded!") diff --git a/typings/plugin.d.ts b/typings/pluginapi.d.ts similarity index 100% rename from typings/plugin.d.ts rename to typings/pluginapi.d.ts From afff86ae9cd68f5e8d87e28dc9e78002489874df Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Tue, 3 Nov 2020 21:11:14 -0500 Subject: [PATCH 161/247] plugin.ts: Adjust to implement pluginapi.d.ts correctly --- src/node/plugin.ts | 33 +++++++++++++++++++++++---------- test/plugin.test.ts | 10 ++++++++-- test/test-plugin/package.json | 1 - test/test-plugin/src/index.ts | 12 +++++++++--- tsconfig.json | 3 ++- typings/pluginapi.d.ts | 7 ++++++- 6 files changed, 48 insertions(+), 18 deletions(-) diff --git a/src/node/plugin.ts b/src/node/plugin.ts index 061523a0c..8d5e552b5 100644 --- a/src/node/plugin.ts +++ b/src/node/plugin.ts @@ -10,11 +10,11 @@ const fsp = fs.promises interface Plugin extends pluginapi.Plugin { /** - * These fields are populated from the plugin's package.json. + * These fields are populated from the plugin's package.json + * and now guaranteed to exist. */ name: string version: string - description: string /** * path to the node module on the disk. @@ -34,7 +34,7 @@ interface Application extends pluginapi.Application { * Please see that file for details. */ export class PluginAPI { - private readonly plugins = new Array() + private readonly plugins = new Map() private readonly logger: Logger public constructor( @@ -54,7 +54,7 @@ export class PluginAPI { */ public async applications(): Promise { const apps = new Array() - for (const p of this.plugins) { + for (const [_, p] of this.plugins) { const pluginApps = await p.applications() // Add plugin key to each app. @@ -65,8 +65,11 @@ export class PluginAPI { plugin: { name: p.name, version: p.version, - description: p.description, modulePath: p.modulePath, + + displayName: p.displayName, + description: p.description, + path: p.path, }, } }), @@ -79,7 +82,7 @@ export class PluginAPI { * mount mounts all plugin routers onto r. */ public mount(r: express.Router): void { - for (const p of this.plugins) { + for (const [_, p] of this.plugins) { r.use(`/${p.name}`, p.router()) } } @@ -129,7 +132,7 @@ export class PluginAPI { encoding: "utf8", }) const packageJSON: PackageJSON = JSON.parse(str) - for (const p of this.plugins) { + for (const [_, p] of this.plugins) { if (p.name === packageJSON.name) { this.logger.warn( `ignoring duplicate plugin ${q(p.name)} at ${q(dir)}, using previously loaded ${q(p.modulePath)}`, @@ -138,7 +141,7 @@ export class PluginAPI { } } const p = this._loadPlugin(dir, packageJSON) - this.plugins.push(p) + this.plugins.set(p.name, p) } catch (err) { if (err.code !== "ENOENT") { this.logger.warn(`failed to load plugin: ${err.message}`) @@ -147,6 +150,8 @@ export class PluginAPI { } private _loadPlugin(dir: string, packageJSON: PackageJSON): Plugin { + dir = path.resolve(dir) + const logger = this.logger.named(packageJSON.name) logger.debug("loading plugin", field("plugin_dir", dir), field("package_json", packageJSON)) @@ -165,11 +170,20 @@ export class PluginAPI { const p = { name: packageJSON.name, version: packageJSON.version, - description: packageJSON.description, modulePath: dir, ...require(dir), } as Plugin + if (!p.displayName) { + throw new Error("plugin missing displayName") + } + if (!p.description) { + throw new Error("plugin missing description") + } + if (!p.path) { + throw new Error("plugin missing path") + } + p.init({ logger: logger, }) @@ -183,7 +197,6 @@ export class PluginAPI { interface PackageJSON { name: string version: string - description: string engines: { "code-server": string } diff --git a/test/plugin.test.ts b/test/plugin.test.ts index 5836deada..1e63fa8d8 100644 --- a/test/plugin.test.ts +++ b/test/plugin.test.ts @@ -17,14 +17,20 @@ describe("plugin", () => { assert.deepEqual( [ { - name: "goland", + name: "test app", version: "4.0.0", + + description: "my description", iconPath: "/icon.svg", + plugin: { name: "test-plugin", version: "1.0.0", - description: "Fake plugin for testing code-server's plugin API", modulePath: path.join(__dirname, "test-plugin"), + + description: "Plugin used in code-server tests.", + displayName: "Test Plugin", + path: "/test-plugin", }, }, ], diff --git a/test/test-plugin/package.json b/test/test-plugin/package.json index ccdeabb56..c1f2e6980 100644 --- a/test/test-plugin/package.json +++ b/test/test-plugin/package.json @@ -2,7 +2,6 @@ "private": true, "name": "test-plugin", "version": "1.0.0", - "description": "Fake plugin for testing code-server's plugin API", "engines": { "code-server": "^3.6.0" }, diff --git a/test/test-plugin/src/index.ts b/test/test-plugin/src/index.ts index bc37d7c05..6435592b5 100644 --- a/test/test-plugin/src/index.ts +++ b/test/test-plugin/src/index.ts @@ -1,7 +1,11 @@ import * as express from "express" -import * as path from "path" +import * as fspath from "path" import * as pluginapi from "../../../typings/pluginapi" +export const displayName = "Test Plugin" +export const path = "/test-plugin" +export const description = "Plugin used in code-server tests." + export function init(config: pluginapi.PluginConfig) { config.logger.debug("test-plugin loaded!") } @@ -9,7 +13,7 @@ export function init(config: pluginapi.PluginConfig) { export function router(): express.Router { const r = express.Router() r.get("/goland/icon.svg", (req, res) => { - res.sendFile(path.resolve(__dirname, "../public/icon.svg")) + res.sendFile(fspath.resolve(__dirname, "../public/icon.svg")) }) return r } @@ -17,9 +21,11 @@ export function router(): express.Router { export function applications(): pluginapi.Application[] { return [ { - name: "goland", + name: "test app", version: "4.0.0", iconPath: "/icon.svg", + + description: "my description", }, ] } diff --git a/tsconfig.json b/tsconfig.json index ac3a1df52..0db0b1908 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -16,7 +16,8 @@ "tsBuildInfoFile": "./.cache/tsbuildinfo", "incremental": true, "rootDir": "./src", - "typeRoots": ["./node_modules/@types", "./typings"] + "typeRoots": ["./node_modules/@types", "./typings"], + "downlevelIteration": true }, "include": ["./src/**/*.ts"] } diff --git a/typings/pluginapi.d.ts b/typings/pluginapi.d.ts index bc8d2ef58..7b61c4cef 100644 --- a/typings/pluginapi.d.ts +++ b/typings/pluginapi.d.ts @@ -70,9 +70,14 @@ export interface Plugin { version?: string /** - * These two are used in the overlay. + * Name used in the overlay. */ displayName: string + + /** + * Used in overlay. + * Should be a full sentence describing the plugin. + */ description: string /** From e03bbe31497b5581e578e03ebc5abc7a11b43580 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Tue, 3 Nov 2020 21:14:14 -0500 Subject: [PATCH 162/247] routes/apps.ts: Implement /api/applications endpoint --- src/node/routes/apps.ts | 12 ++++++++++++ src/node/routes/index.ts | 2 ++ 2 files changed, 14 insertions(+) create mode 100644 src/node/routes/apps.ts diff --git a/src/node/routes/apps.ts b/src/node/routes/apps.ts new file mode 100644 index 000000000..970bd3cb1 --- /dev/null +++ b/src/node/routes/apps.ts @@ -0,0 +1,12 @@ +import * as express from "express" +import { PluginAPI } from "../plugin" + +export function router(papi: PluginAPI): express.Router { + const router = express.Router() + + router.get("/", async (req, res) => { + res.json(await papi.applications()) + }) + + return router +} diff --git a/src/node/routes/index.ts b/src/node/routes/index.ts index 5824475d9..a39b2a6a1 100644 --- a/src/node/routes/index.ts +++ b/src/node/routes/index.ts @@ -23,6 +23,7 @@ import * as proxy from "./pathProxy" import * as _static from "./static" import * as update from "./update" import * as vscode from "./vscode" +import * as apps from "./apps" declare global { // eslint-disable-next-line @typescript-eslint/no-namespace @@ -118,6 +119,7 @@ export const register = async ( const papi = new PluginAPI(logger, process.env.CS_PLUGIN, process.env.CS_PLUGIN_PATH) await papi.loadPlugins() papi.mount(app) + app.use("/api/applications", apps.router(papi)) app.use(() => { throw new HttpError("Not Found", HttpCode.NotFound) From 139a28e0ea063120a6adc4880f93f9c4d14bbdd4 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Tue, 3 Nov 2020 21:14:19 -0500 Subject: [PATCH 163/247] plugin.ts: Describe private counterpart functions Addresses Will's comments. --- src/node/plugin.ts | 12 ++++++++++++ src/node/routes/apps.ts | 3 +++ test/plugin.test.ts | 6 +++--- test/test-plugin/src/index.ts | 4 ++-- 4 files changed, 20 insertions(+), 5 deletions(-) diff --git a/src/node/plugin.ts b/src/node/plugin.ts index 8d5e552b5..ce424770b 100644 --- a/src/node/plugin.ts +++ b/src/node/plugin.ts @@ -110,6 +110,13 @@ export class PluginAPI { await this._loadPlugins(path.join(__dirname, "../../plugins")) } + /** + * _loadPlugins is the counterpart to loadPlugins. + * + * It differs in that it loads all plugins in a single + * directory whereas loadPlugins uses all available directories + * as documented. + */ private async _loadPlugins(dir: string): Promise { try { const entries = await fsp.readdir(dir, { withFileTypes: true }) @@ -149,6 +156,11 @@ export class PluginAPI { } } + /** + * _loadPlugin is the counterpart to loadPlugin and actually + * loads the plugin now that we know there is no duplicate + * and that the package.json has been read. + */ private _loadPlugin(dir: string, packageJSON: PackageJSON): Plugin { dir = path.resolve(dir) diff --git a/src/node/routes/apps.ts b/src/node/routes/apps.ts index 970bd3cb1..c678f2fee 100644 --- a/src/node/routes/apps.ts +++ b/src/node/routes/apps.ts @@ -1,6 +1,9 @@ import * as express from "express" import { PluginAPI } from "../plugin" +/** + * Implements the /api/applications endpoint + */ export function router(papi: PluginAPI): express.Router { const router = express.Router() diff --git a/test/plugin.test.ts b/test/plugin.test.ts index 1e63fa8d8..8c419139a 100644 --- a/test/plugin.test.ts +++ b/test/plugin.test.ts @@ -17,10 +17,10 @@ describe("plugin", () => { assert.deepEqual( [ { - name: "test app", + name: "Test App", version: "4.0.0", - description: "my description", + description: "This app does XYZ.", iconPath: "/icon.svg", plugin: { @@ -28,8 +28,8 @@ describe("plugin", () => { version: "1.0.0", modulePath: path.join(__dirname, "test-plugin"), - description: "Plugin used in code-server tests.", displayName: "Test Plugin", + description: "Plugin used in code-server tests.", path: "/test-plugin", }, }, diff --git a/test/test-plugin/src/index.ts b/test/test-plugin/src/index.ts index 6435592b5..f9f316b4e 100644 --- a/test/test-plugin/src/index.ts +++ b/test/test-plugin/src/index.ts @@ -21,11 +21,11 @@ export function router(): express.Router { export function applications(): pluginapi.Application[] { return [ { - name: "test app", + name: "Test App", version: "4.0.0", iconPath: "/icon.svg", - description: "my description", + description: "This app does XYZ.", }, ] } From 687094802ec4bb88ed4b07ecdf072e3249dfc7ca Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Tue, 3 Nov 2020 21:42:21 -0500 Subject: [PATCH 164/247] plugin.ts: Make application endpoint paths absolute --- src/node/plugin.ts | 8 +++++--- test/plugin.test.ts | 5 +++-- test/test-plugin/Makefile | 3 ++- test/test-plugin/src/index.ts | 3 ++- typings/pluginapi.d.ts | 14 ++++++++------ 5 files changed, 20 insertions(+), 13 deletions(-) diff --git a/src/node/plugin.ts b/src/node/plugin.ts index ce424770b..9523782bc 100644 --- a/src/node/plugin.ts +++ b/src/node/plugin.ts @@ -60,6 +60,8 @@ export class PluginAPI { // Add plugin key to each app. apps.push( ...pluginApps.map((app) => { + app = {...app, path: path.join(p.routerPath, app.path || "")} + app = {...app, iconPath: path.join(app.path || "", app.iconPath)} return { ...app, plugin: { @@ -69,7 +71,7 @@ export class PluginAPI { displayName: p.displayName, description: p.description, - path: p.path, + routerPath: p.routerPath, }, } }), @@ -192,8 +194,8 @@ export class PluginAPI { if (!p.description) { throw new Error("plugin missing description") } - if (!p.path) { - throw new Error("plugin missing path") + if (!p.routerPath) { + throw new Error("plugin missing router path") } p.init({ diff --git a/test/plugin.test.ts b/test/plugin.test.ts index 8c419139a..bc13fc803 100644 --- a/test/plugin.test.ts +++ b/test/plugin.test.ts @@ -21,7 +21,8 @@ describe("plugin", () => { version: "4.0.0", description: "This app does XYZ.", - iconPath: "/icon.svg", + iconPath: "/test-plugin/test-app/icon.svg", + path: "/test-plugin/test-app", plugin: { name: "test-plugin", @@ -30,7 +31,7 @@ describe("plugin", () => { displayName: "Test Plugin", description: "Plugin used in code-server tests.", - path: "/test-plugin", + routerPath: "/test-plugin", }, }, ], diff --git a/test/test-plugin/Makefile b/test/test-plugin/Makefile index fb66dc81a..d01aa80a8 100644 --- a/test/test-plugin/Makefile +++ b/test/test-plugin/Makefile @@ -1,5 +1,6 @@ out/index.js: src/index.ts - yarn build + # Typescript always emits, even on errors. + yarn build || rm out/index.js node_modules: package.json yarn.lock yarn diff --git a/test/test-plugin/src/index.ts b/test/test-plugin/src/index.ts index f9f316b4e..161203319 100644 --- a/test/test-plugin/src/index.ts +++ b/test/test-plugin/src/index.ts @@ -3,7 +3,7 @@ import * as fspath from "path" import * as pluginapi from "../../../typings/pluginapi" export const displayName = "Test Plugin" -export const path = "/test-plugin" +export const routerPath = "/test-plugin" export const description = "Plugin used in code-server tests." export function init(config: pluginapi.PluginConfig) { @@ -24,6 +24,7 @@ export function applications(): pluginapi.Application[] { name: "Test App", version: "4.0.0", iconPath: "/icon.svg", + path: "/test-app", description: "This app does XYZ.", }, diff --git a/typings/pluginapi.d.ts b/typings/pluginapi.d.ts index 7b61c4cef..dbb985a58 100644 --- a/typings/pluginapi.d.ts +++ b/typings/pluginapi.d.ts @@ -45,7 +45,9 @@ import * as express from "express" * * There is also a /api/applications endpoint to allow programmatic access to all * available applications. It could be used to create a custom application dashboard - * for example. + * for example. An important difference with the API is that all application paths + * will be absolute (i.e have the plugin path prepended) so that they may be used + * directly. */ /** @@ -60,30 +62,30 @@ export interface Plugin { * * Fetched from package.json. */ - name?: string + readonly name?: string /** * The version for the plugin in the overlay. * * Fetched from package.json. */ - version?: string + readonly version?: string /** * Name used in the overlay. */ - displayName: string + readonly displayName: string /** * Used in overlay. * Should be a full sentence describing the plugin. */ - description: string + readonly description: string /** * The path at which the plugin router is to be registered. */ - path: string + readonly routerPath: string /** * init is called so that the plugin may initialize itself with the config. From 2a13d003d37d97f8650bd15f29baa45cd4fb1d5a Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Tue, 3 Nov 2020 21:45:25 -0500 Subject: [PATCH 165/247] plugin.ts: Add homepageURL to plugin and application --- src/node/plugin.ts | 4 ++++ test/plugin.test.ts | 2 ++ test/test-plugin/src/index.ts | 2 ++ typings/pluginapi.d.ts | 10 ++++++++++ 4 files changed, 18 insertions(+) diff --git a/src/node/plugin.ts b/src/node/plugin.ts index 9523782bc..368d6f7ff 100644 --- a/src/node/plugin.ts +++ b/src/node/plugin.ts @@ -72,6 +72,7 @@ export class PluginAPI { displayName: p.displayName, description: p.description, routerPath: p.routerPath, + homepageURL: p.homepageURL, }, } }), @@ -197,6 +198,9 @@ export class PluginAPI { if (!p.routerPath) { throw new Error("plugin missing router path") } + if (!p.homepageURL) { + throw new Error("plugin missing homepage") + } p.init({ logger: logger, diff --git a/test/plugin.test.ts b/test/plugin.test.ts index bc13fc803..ed040dc21 100644 --- a/test/plugin.test.ts +++ b/test/plugin.test.ts @@ -22,6 +22,7 @@ describe("plugin", () => { description: "This app does XYZ.", iconPath: "/test-plugin/test-app/icon.svg", + homepageURL: "https://example.com", path: "/test-plugin/test-app", plugin: { @@ -32,6 +33,7 @@ describe("plugin", () => { displayName: "Test Plugin", description: "Plugin used in code-server tests.", routerPath: "/test-plugin", + homepageURL: "https://example.com", }, }, ], diff --git a/test/test-plugin/src/index.ts b/test/test-plugin/src/index.ts index 161203319..2fc1ddab0 100644 --- a/test/test-plugin/src/index.ts +++ b/test/test-plugin/src/index.ts @@ -4,6 +4,7 @@ import * as pluginapi from "../../../typings/pluginapi" export const displayName = "Test Plugin" export const routerPath = "/test-plugin" +export const homepageURL = "https://example.com" export const description = "Plugin used in code-server tests." export function init(config: pluginapi.PluginConfig) { @@ -27,6 +28,7 @@ export function applications(): pluginapi.Application[] { path: "/test-app", description: "This app does XYZ.", + homepageURL: "https://example.com", }, ] } diff --git a/typings/pluginapi.d.ts b/typings/pluginapi.d.ts index dbb985a58..3ce5e5d0c 100644 --- a/typings/pluginapi.d.ts +++ b/typings/pluginapi.d.ts @@ -87,6 +87,11 @@ export interface Plugin { */ readonly routerPath: string + /** + * Link to plugin homepage. + */ + readonly homepageURL: string + /** * init is called so that the plugin may initialize itself with the config. */ @@ -144,4 +149,9 @@ export interface Application { * /// */ readonly iconPath: string + + /** + * Link to application homepage. + */ + readonly homepageURL: string } From af73b96313c4d31da917ec46cbee390c7bbb32e9 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Tue, 3 Nov 2020 21:49:10 -0500 Subject: [PATCH 166/247] routes/apps.ts: Add example output --- src/node/routes/apps.ts | 2 ++ typings/pluginapi.d.ts | 26 ++++++++++++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/src/node/routes/apps.ts b/src/node/routes/apps.ts index c678f2fee..4298fb392 100644 --- a/src/node/routes/apps.ts +++ b/src/node/routes/apps.ts @@ -3,6 +3,8 @@ import { PluginAPI } from "../plugin" /** * Implements the /api/applications endpoint + * + * See typings/pluginapi.d.ts for details. */ export function router(papi: PluginAPI): express.Router { const router = express.Router() diff --git a/typings/pluginapi.d.ts b/typings/pluginapi.d.ts index 3ce5e5d0c..94819bd3c 100644 --- a/typings/pluginapi.d.ts +++ b/typings/pluginapi.d.ts @@ -42,12 +42,38 @@ import * as express from "express" * * If two plugins are found with the exact same name, then code-server will * use the first one and emit a warning. + * + */ + +/* Programmability * * There is also a /api/applications endpoint to allow programmatic access to all * available applications. It could be used to create a custom application dashboard * for example. An important difference with the API is that all application paths * will be absolute (i.e have the plugin path prepended) so that they may be used * directly. + * + * Example output: + * + * [ + * { + * "name": "Test App", + * "version": "4.0.0", + * "iconPath": "/test-plugin/test-app/icon.svg", + * "path": "/test-plugin/test-app", + * "description": "This app does XYZ.", + * "homepageURL": "https://example.com", + * "plugin": { + * "name": "test-plugin", + * "version": "1.0.0", + * "modulePath": "/Users/nhooyr/src/cdr/code-server/test/test-plugin", + * "displayName": "Test Plugin", + * "description": "Plugin used in code-server tests.", + * "routerPath": "/test-plugin", + * "homepageURL": "https://example.com" + * } + * } + * ] */ /** From 706bc23f0489bf0fe9a4b844cc36077f52600041 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Tue, 3 Nov 2020 21:53:16 -0500 Subject: [PATCH 167/247] plugin: Fixes for CI --- ci/dev/test.sh | 2 +- src/node/plugin.ts | 10 +++++----- src/node/routes/apps.ts | 2 +- src/node/routes/index.ts | 2 +- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/ci/dev/test.sh b/ci/dev/test.sh index 6eaa3878d..9922a9c84 100755 --- a/ci/dev/test.sh +++ b/ci/dev/test.sh @@ -6,7 +6,7 @@ main() { cd test/test-plugin make -s out/index.js - cd $OLDPWD + cd "$OLDPWD" mocha -r ts-node/register ./test/*.test.ts "$@" } diff --git a/src/node/plugin.ts b/src/node/plugin.ts index 368d6f7ff..fea85710c 100644 --- a/src/node/plugin.ts +++ b/src/node/plugin.ts @@ -54,14 +54,14 @@ export class PluginAPI { */ public async applications(): Promise { const apps = new Array() - for (const [_, p] of this.plugins) { + for (const [, p] of this.plugins) { const pluginApps = await p.applications() // Add plugin key to each app. apps.push( ...pluginApps.map((app) => { - app = {...app, path: path.join(p.routerPath, app.path || "")} - app = {...app, iconPath: path.join(app.path || "", app.iconPath)} + app = { ...app, path: path.join(p.routerPath, app.path || "") } + app = { ...app, iconPath: path.join(app.path || "", app.iconPath) } return { ...app, plugin: { @@ -85,7 +85,7 @@ export class PluginAPI { * mount mounts all plugin routers onto r. */ public mount(r: express.Router): void { - for (const [_, p] of this.plugins) { + for (const [, p] of this.plugins) { r.use(`/${p.name}`, p.router()) } } @@ -142,7 +142,7 @@ export class PluginAPI { encoding: "utf8", }) const packageJSON: PackageJSON = JSON.parse(str) - for (const [_, p] of this.plugins) { + for (const [, p] of this.plugins) { if (p.name === packageJSON.name) { this.logger.warn( `ignoring duplicate plugin ${q(p.name)} at ${q(dir)}, using previously loaded ${q(p.modulePath)}`, diff --git a/src/node/routes/apps.ts b/src/node/routes/apps.ts index 4298fb392..5c8541fc9 100644 --- a/src/node/routes/apps.ts +++ b/src/node/routes/apps.ts @@ -12,6 +12,6 @@ export function router(papi: PluginAPI): express.Router { router.get("/", async (req, res) => { res.json(await papi.applications()) }) - + return router } diff --git a/src/node/routes/index.ts b/src/node/routes/index.ts index a39b2a6a1..da714eea5 100644 --- a/src/node/routes/index.ts +++ b/src/node/routes/index.ts @@ -16,6 +16,7 @@ import { PluginAPI } from "../plugin" import { getMediaMime, paths } from "../util" import { WebsocketRequest } from "../wsRouter" import * as domainProxy from "./domainProxy" +import * as apps from "./apps" import * as health from "./health" import * as login from "./login" import * as proxy from "./pathProxy" @@ -23,7 +24,6 @@ import * as proxy from "./pathProxy" import * as _static from "./static" import * as update from "./update" import * as vscode from "./vscode" -import * as apps from "./apps" declare global { // eslint-disable-next-line @typescript-eslint/no-namespace From 8a8159c683c07c8f8e854b7c176077e1b8360c39 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Wed, 4 Nov 2020 22:59:43 -0500 Subject: [PATCH 168/247] plugin: More review fixes Next commit will address Will's comments about the typings being weird. --- src/node/plugin.ts | 26 ++++++++++++++++++-------- typings/pluginapi.d.ts | 3 ++- 2 files changed, 20 insertions(+), 9 deletions(-) diff --git a/src/node/plugin.ts b/src/node/plugin.ts index fea85710c..0ac3abfc8 100644 --- a/src/node/plugin.ts +++ b/src/node/plugin.ts @@ -86,7 +86,7 @@ export class PluginAPI { */ public mount(r: express.Router): void { for (const [, p] of this.plugins) { - r.use(`/${p.name}`, p.router()) + r.use(`/${p.routerPath}`, p.router()) } } @@ -154,7 +154,7 @@ export class PluginAPI { this.plugins.set(p.name, p) } catch (err) { if (err.code !== "ENOENT") { - this.logger.warn(`failed to load plugin: ${err.message}`) + this.logger.warn(`failed to load plugin: ${err.stack}`) } } } @@ -170,17 +170,24 @@ export class PluginAPI { const logger = this.logger.named(packageJSON.name) logger.debug("loading plugin", field("plugin_dir", dir), field("package_json", packageJSON)) + if (!packageJSON.name) { + throw new Error("plugin package.json missing name") + } + if (!packageJSON.version) { + throw new Error("plugin package.json missing version") + } + if (!packageJSON.engines || !packageJSON.engines["code-server"]) { + throw new Error(`plugin package.json missing code-server range like: + "engines": { + "code-server": "^3.6.0" + } +`) + } if (!semver.satisfies(version, packageJSON.engines["code-server"])) { throw new Error( `plugin range ${q(packageJSON.engines["code-server"])} incompatible` + ` with code-server version ${version}`, ) } - if (!packageJSON.name) { - throw new Error("plugin missing name") - } - if (!packageJSON.version) { - throw new Error("plugin missing version") - } const p = { name: packageJSON.name, @@ -198,6 +205,9 @@ export class PluginAPI { if (!p.routerPath) { throw new Error("plugin missing router path") } + if (!p.routerPath.startsWith("/") || p.routerPath.length < 2) { + throw new Error(`plugin router path ${q(p.routerPath)}: invalid`) + } if (!p.homepageURL) { throw new Error("plugin missing homepage") } diff --git a/typings/pluginapi.d.ts b/typings/pluginapi.d.ts index 94819bd3c..4e3971eeb 100644 --- a/typings/pluginapi.d.ts +++ b/typings/pluginapi.d.ts @@ -45,7 +45,8 @@ import * as express from "express" * */ -/* Programmability +/** + * Programmability * * There is also a /api/applications endpoint to allow programmatic access to all * available applications. It could be used to create a custom application dashboard From 14f408a837cbdd301545d4241da4e6ecd90857cb Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Wed, 4 Nov 2020 23:10:41 -0500 Subject: [PATCH 169/247] plugin: Plugin modules now export a single top level identifier Makes typing much easier. Addresse's Will's last comment. --- src/node/plugin.ts | 7 ++++- test/test-plugin/src/index.ts | 58 ++++++++++++++++++----------------- typings/pluginapi.d.ts | 5 +-- 3 files changed, 39 insertions(+), 31 deletions(-) diff --git a/src/node/plugin.ts b/src/node/plugin.ts index 0ac3abfc8..71831f504 100644 --- a/src/node/plugin.ts +++ b/src/node/plugin.ts @@ -189,11 +189,16 @@ export class PluginAPI { ) } + const pluginModule = require(dir) + if (!pluginModule.plugin) { + throw new Error("plugin module does not export a plugin") + } + const p = { name: packageJSON.name, version: packageJSON.version, modulePath: dir, - ...require(dir), + ...pluginModule.plugin, } as Plugin if (!p.displayName) { diff --git a/test/test-plugin/src/index.ts b/test/test-plugin/src/index.ts index 2fc1ddab0..2b00c2ec5 100644 --- a/test/test-plugin/src/index.ts +++ b/test/test-plugin/src/index.ts @@ -2,33 +2,35 @@ import * as express from "express" import * as fspath from "path" import * as pluginapi from "../../../typings/pluginapi" -export const displayName = "Test Plugin" -export const routerPath = "/test-plugin" -export const homepageURL = "https://example.com" -export const description = "Plugin used in code-server tests." +export const plugin: pluginapi.Plugin = { + displayName: "Test Plugin", + routerPath: "/test-plugin", + homepageURL: "https://example.com", + description: "Plugin used in code-server tests.", -export function init(config: pluginapi.PluginConfig) { - config.logger.debug("test-plugin loaded!") -} - -export function router(): express.Router { - const r = express.Router() - r.get("/goland/icon.svg", (req, res) => { - res.sendFile(fspath.resolve(__dirname, "../public/icon.svg")) - }) - return r -} - -export function applications(): pluginapi.Application[] { - return [ - { - name: "Test App", - version: "4.0.0", - iconPath: "/icon.svg", - path: "/test-app", - - description: "This app does XYZ.", - homepageURL: "https://example.com", - }, - ] + init: (config) => { + config.logger.debug("test-plugin loaded!") + }, + + router: () => { + const r = express.Router() + r.get("/goland/icon.svg", (req, res) => { + res.sendFile(fspath.resolve(__dirname, "../public/icon.svg")) + }) + return r + }, + + applications: () => { + return [ + { + name: "Test App", + version: "4.0.0", + iconPath: "/icon.svg", + path: "/test-app", + + description: "This app does XYZ.", + homepageURL: "https://example.com", + }, + ] + }, } diff --git a/typings/pluginapi.d.ts b/typings/pluginapi.d.ts index 4e3971eeb..d0846a288 100644 --- a/typings/pluginapi.d.ts +++ b/typings/pluginapi.d.ts @@ -16,7 +16,8 @@ import * as express from "express" /** * Plugins * - * Plugins are just node modules. + * Plugins are just node modules that contain a top level export "plugin" that implements + * the Plugin interface. * * 1. code-server uses $CS_PLUGIN to find plugins. * @@ -78,7 +79,7 @@ import * as express from "express" */ /** - * Your plugin module must implement this interface. + * Your plugin module must have a top level export "plugin" that implements this interface. * * The plugin's router will be mounted at / */ From 9453f891df6283747f0b53b7dfb52524bdf346ea Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Thu, 5 Nov 2020 14:17:13 -0500 Subject: [PATCH 170/247] plugin.ts: Fix usage of routerPath in mount --- src/node/plugin.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/node/plugin.ts b/src/node/plugin.ts index 71831f504..77a4a8277 100644 --- a/src/node/plugin.ts +++ b/src/node/plugin.ts @@ -86,7 +86,7 @@ export class PluginAPI { */ public mount(r: express.Router): void { for (const [, p] of this.plugins) { - r.use(`/${p.routerPath}`, p.router()) + r.use(`${p.routerPath}`, p.router()) } } From 197a09f0c1227a47ea709042ea6c46bb6ded5227 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Fri, 6 Nov 2020 09:51:46 -0500 Subject: [PATCH 171/247] plugin: Test endpoints via supertest Unfortunately we can't use node-mocks-http to test a express.Router that has async routes. See https://github.com/howardabrams/node-mocks-http/issues/225 router will just return undefined if the executing handler is async and so the test will have no way to wait for it to complete. Thus, we have to use supertest which starts an actual HTTP server in the background and uses a HTTP client to send requests. --- package.json | 2 + test/plugin.test.ts | 66 ++++++++++++++++----------- test/test-plugin/src/index.ts | 11 +++-- yarn.lock | 85 +++++++++++++++++++++++++++++++++-- 4 files changed, 132 insertions(+), 32 deletions(-) diff --git a/package.json b/package.json index 6459e6a0f..c41b8b41c 100644 --- a/package.json +++ b/package.json @@ -43,6 +43,7 @@ "@types/safe-compare": "^1.1.0", "@types/semver": "^7.1.0", "@types/split2": "^2.1.6", + "@types/supertest": "^2.0.10", "@types/tar-fs": "^2.0.0", "@types/tar-stream": "^2.1.0", "@types/ws": "^7.2.6", @@ -59,6 +60,7 @@ "prettier": "^2.0.5", "stylelint": "^13.0.0", "stylelint-config-recommended": "^3.0.0", + "supertest": "^6.0.1", "ts-node": "^9.0.0", "typescript": "4.0.2" }, diff --git a/test/plugin.test.ts b/test/plugin.test.ts index ed040dc21..aaf8c14dc 100644 --- a/test/plugin.test.ts +++ b/test/plugin.test.ts @@ -1,43 +1,57 @@ import { logger } from "@coder/logger" -import * as assert from "assert" import { describe } from "mocha" import * as path from "path" import { PluginAPI } from "../src/node/plugin" +import * as supertest from "supertest" +import * as express from "express" +import * as apps from "../src/node/routes/apps" /** * Use $LOG_LEVEL=debug to see debug logs. */ describe("plugin", () => { - it("loads", async () => { - const papi = new PluginAPI(logger, path.resolve(__dirname, "test-plugin") + ":meow") + let papi: PluginAPI + let app: express.Application + let agent: supertest.SuperAgentTest + + before(async () => { + papi = new PluginAPI(logger, path.resolve(__dirname, "test-plugin") + ":meow") await papi.loadPlugins() - const apps = await papi.applications() + app = express.default() + papi.mount(app) - assert.deepEqual( - [ - { - name: "Test App", - version: "4.0.0", + app.use("/api/applications", apps.router(papi)) - description: "This app does XYZ.", - iconPath: "/test-plugin/test-app/icon.svg", + agent = supertest.agent(app) + }) + + it("/api/applications", async () => { + await agent.get("/api/applications").expect(200, [ + { + name: "Test App", + version: "4.0.0", + + description: "This app does XYZ.", + iconPath: "/test-plugin/test-app/icon.svg", + homepageURL: "https://example.com", + path: "/test-plugin/test-app", + + plugin: { + name: "test-plugin", + version: "1.0.0", + modulePath: path.join(__dirname, "test-plugin"), + + displayName: "Test Plugin", + description: "Plugin used in code-server tests.", + routerPath: "/test-plugin", homepageURL: "https://example.com", - path: "/test-plugin/test-app", - - plugin: { - name: "test-plugin", - version: "1.0.0", - modulePath: path.join(__dirname, "test-plugin"), - - displayName: "Test Plugin", - description: "Plugin used in code-server tests.", - routerPath: "/test-plugin", - homepageURL: "https://example.com", - }, }, - ], - apps, - ) + }, + ]) + }) + + it("/test-plugin/test-app", async () => { + await agent.get("/test-plugin/test-app").expect(200, { date: "2000-02-05T05:00:00.000Z" }) }) }) diff --git a/test/test-plugin/src/index.ts b/test/test-plugin/src/index.ts index 2b00c2ec5..9e95ffca7 100644 --- a/test/test-plugin/src/index.ts +++ b/test/test-plugin/src/index.ts @@ -8,19 +8,24 @@ export const plugin: pluginapi.Plugin = { homepageURL: "https://example.com", description: "Plugin used in code-server tests.", - init: (config) => { + init(config) { config.logger.debug("test-plugin loaded!") }, - router: () => { + router() { const r = express.Router() + r.get("/test-app", (req, res) => { + res.json({ + date: new Date("2000/02/05"), + }) + }) r.get("/goland/icon.svg", (req, res) => { res.sendFile(fspath.resolve(__dirname, "../public/icon.svg")) }) return r }, - applications: () => { + applications() { return [ { name: "Test App", diff --git a/yarn.lock b/yarn.lock index d96b2e6ee..034efecce 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1035,6 +1035,11 @@ dependencies: "@types/express" "*" +"@types/cookiejar@*": + version "2.1.2" + resolved "https://registry.yarnpkg.com/@types/cookiejar/-/cookiejar-2.1.2.tgz#66ad9331f63fe8a3d3d9d8c6e3906dd10f6446e8" + integrity sha512-t73xJJrvdTjXrn4jLS9VSGRbz0nUY3cl2DMGDU48lKl+HR9dbbjW2A9r3g40VA++mQpy6uuHg33gy7du2BKpog== + "@types/eslint-visitor-keys@^1.0.0": version "1.0.0" resolved "https://registry.yarnpkg.com/@types/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz#1ee30d79544ca84d68d4b3cdb0af4f205663dd2d" @@ -1172,6 +1177,21 @@ dependencies: "@types/node" "*" +"@types/superagent@*": + version "4.1.10" + resolved "https://registry.yarnpkg.com/@types/superagent/-/superagent-4.1.10.tgz#5e2cc721edf58f64fe9b819f326ee74803adee86" + integrity sha512-xAgkb2CMWUMCyVc/3+7iQfOEBE75NvuZeezvmixbUw3nmENf2tCnQkW5yQLTYqvXUQ+R6EXxdqKKbal2zM5V/g== + dependencies: + "@types/cookiejar" "*" + "@types/node" "*" + +"@types/supertest@^2.0.10": + version "2.0.10" + resolved "https://registry.yarnpkg.com/@types/supertest/-/supertest-2.0.10.tgz#630d79b4d82c73e043e43ff777a9ca98d457cab7" + integrity sha512-Xt8TbEyZTnD5Xulw95GLMOkmjGICrOQyJ2jqgkSjAUR3mm7pAIzSR0NFBaMcwlzVvlpCjNwbATcWWwjNiZiFrQ== + dependencies: + "@types/superagent" "*" + "@types/tar-fs@^2.0.0": version "2.0.0" resolved "https://registry.yarnpkg.com/@types/tar-fs/-/tar-fs-2.0.0.tgz#db94cb4ea1cccecafe3d1a53812807efb4bbdbc1" @@ -2182,7 +2202,7 @@ colorette@^1.2.1: resolved "https://registry.yarnpkg.com/colorette/-/colorette-1.2.1.tgz#4d0b921325c14faf92633086a536db6e89564b1b" integrity sha512-puCDz0CzydiSYOrnXpz/PKd69zRrribezjtE9yd4zvytoRc8+RY/KJPvtPFKZS3E3wP6neGyMe0vOTlHO5L3Pw== -combined-stream@^1.0.6, combined-stream@~1.0.6: +combined-stream@^1.0.6, combined-stream@^1.0.8, combined-stream@~1.0.6: version "1.0.8" resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== @@ -2204,7 +2224,7 @@ commander@^5.0.0: resolved "https://registry.yarnpkg.com/commander/-/commander-5.1.0.tgz#46abbd1652f8e059bddaef99bbdcb2ad9cf179ae" integrity sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg== -component-emitter@^1.2.1: +component-emitter@^1.2.1, component-emitter@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.3.0.tgz#16e4070fba8ae29b679f2215853ee181ab2eabc0" integrity sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg== @@ -2276,6 +2296,11 @@ cookie@0.4.0: resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.0.tgz#beb437e7022b3b6d49019d088665303ebe9c14ba" integrity sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg== +cookiejar@^2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/cookiejar/-/cookiejar-2.1.2.tgz#dd8a235530752f988f9a0844f3fc589e3111125c" + integrity sha512-Mw+adcfzPxcPeI+0WlvRrr/3lGVO0bD75SxX6811cxSh1Wbxx7xZBGK1eVtDf6si8rg2lhnUjsVLMFMfbRIuwA== + copy-descriptor@^0.1.0: version "0.1.1" resolved "https://registry.yarnpkg.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d" @@ -3351,6 +3376,11 @@ fast-levenshtein@^2.0.6, fast-levenshtein@~2.0.6: resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc= +fast-safe-stringify@^2.0.7: + version "2.0.7" + resolved "https://registry.yarnpkg.com/fast-safe-stringify/-/fast-safe-stringify-2.0.7.tgz#124aa885899261f68aedb42a7c080de9da608743" + integrity sha512-Utm6CdzT+6xsDk2m8S6uL8VHxNwI6Jub+e9NYTcAms28T84pTa25GJQV9j0CY0N1rM8hK4x6grpF2BQf+2qwVA== + fastest-levenshtein@^1.0.12: version "1.0.12" resolved "https://registry.yarnpkg.com/fastest-levenshtein/-/fastest-levenshtein-1.0.12.tgz#9990f7d3a88cc5a9ffd1f1745745251700d497e2" @@ -3493,6 +3523,15 @@ forever-agent@~0.6.1: resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" integrity sha1-+8cfDEGt6zf5bFd60e1C2P2sypE= +form-data@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-3.0.0.tgz#31b7e39c85f1355b7139ee0c647cf0de7f83c682" + integrity sha512-CKMFDglpbMi6PyN+brwB9Q/GOw0eAnsrEZDgcsH5Krhz5Od/haKHAX0NmQfha2zPPz0JpWzA7GJHGSnvCRLWsg== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.8" + mime-types "^2.1.12" + form-data@~2.3.2: version "2.3.3" resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.3.tgz#dcce52c05f644f298c6a7ab936bd724ceffbf3a6" @@ -3507,6 +3546,11 @@ format@^0.2.0: resolved "https://registry.yarnpkg.com/format/-/format-0.2.2.tgz#d6170107e9efdc4ed30c9dc39016df942b5cb58b" integrity sha1-1hcBB+nv3E7TDJ3DkBbflCtctYs= +formidable@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/formidable/-/formidable-1.2.2.tgz#bf69aea2972982675f00865342b982986f6b8dd9" + integrity sha512-V8gLm+41I/8kguQ4/o1D3RIHRmhYFG4pnNyonvua+40rqcEmT4+V71yaZ3B457xbbgCsCfjSPi65u/W6vK1U5Q== + forwarded@~0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.1.2.tgz#98c23dab1175657b8c0573e8ceccd91b0ff18c84" @@ -4807,7 +4851,7 @@ merge2@^1.2.3, merge2@^1.3.0: resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== -methods@~1.1.2: +methods@1.1.2, methods@^1.1.2, methods@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" integrity sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4= @@ -4864,6 +4908,11 @@ mime@1.6.0: resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== +mime@^2.4.6: + version "2.4.6" + resolved "https://registry.yarnpkg.com/mime/-/mime-2.4.6.tgz#e5b407c90db442f2beb5b162373d07b69affa4d1" + integrity sha512-RZKhC3EmpBchfTGBVb8fb+RL2cWyw/32lshnsETttkBAyAUXSGHxbEJWWRXc751DrIxG1q04b8QwMbAwkRPpUA== + mimic-fn@^1.0.0: version "1.2.0" resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-1.2.0.tgz#820c86a39334640e99516928bd03fca88057d022" @@ -6181,6 +6230,11 @@ qs@6.7.0: resolved "https://registry.yarnpkg.com/qs/-/qs-6.7.0.tgz#41dc1a015e3d581f1621776be31afb2876a9b1bc" integrity sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ== +qs@^6.9.4: + version "6.9.4" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.9.4.tgz#9090b290d1f91728d3c22e54843ca44aea5ab687" + integrity sha512-A1kFqHekCTM7cz0udomYUoYNWjBebHm/5wzU/XqrBRBNWectVH0QIiN+NEcZ0Dte5hvzHwbr8+XQmguPhJ6WdQ== + qs@~6.5.2: version "6.5.2" resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36" @@ -7246,6 +7300,31 @@ sugarss@^2.0.0: dependencies: postcss "^7.0.2" +superagent@6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/superagent/-/superagent-6.1.0.tgz#09f08807bc41108ef164cfb4be293cebd480f4a6" + integrity sha512-OUDHEssirmplo3F+1HWKUrUjvnQuA+nZI6i/JJBdXb5eq9IyEQwPyPpqND+SSsxf6TygpBEkUjISVRN4/VOpeg== + dependencies: + component-emitter "^1.3.0" + cookiejar "^2.1.2" + debug "^4.1.1" + fast-safe-stringify "^2.0.7" + form-data "^3.0.0" + formidable "^1.2.2" + methods "^1.1.2" + mime "^2.4.6" + qs "^6.9.4" + readable-stream "^3.6.0" + semver "^7.3.2" + +supertest@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/supertest/-/supertest-6.0.1.tgz#f6b54370de85c45d6557192c8d7df604ca2c9e18" + integrity sha512-8yDNdm+bbAN/jeDdXsRipbq9qMpVF7wRsbwLgsANHqdjPsCoecmlTuqEcLQMGpmojFBhxayZ0ckXmLXYq7e+0g== + dependencies: + methods "1.1.2" + superagent "6.1.0" + supports-color@7.1.0: version "7.1.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.1.0.tgz#68e32591df73e25ad1c4b49108a2ec507962bfd1" From 9d39c53c99eb5398ecd0df5a3ef0c0f8b3ec80c7 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Fri, 6 Nov 2020 10:09:35 -0500 Subject: [PATCH 172/247] plugin: Give test-plugin some html to test overlay --- test/plugin.test.ts | 7 ++++++- test/test-plugin/public/index.html | 10 ++++++++++ test/test-plugin/src/index.ts | 4 +--- 3 files changed, 17 insertions(+), 4 deletions(-) create mode 100644 test/test-plugin/public/index.html diff --git a/test/plugin.test.ts b/test/plugin.test.ts index aaf8c14dc..a0885916b 100644 --- a/test/plugin.test.ts +++ b/test/plugin.test.ts @@ -5,6 +5,8 @@ import { PluginAPI } from "../src/node/plugin" import * as supertest from "supertest" import * as express from "express" import * as apps from "../src/node/routes/apps" +import * as fs from "fs" +const fsp = fs.promises /** * Use $LOG_LEVEL=debug to see debug logs. @@ -52,6 +54,9 @@ describe("plugin", () => { }) it("/test-plugin/test-app", async () => { - await agent.get("/test-plugin/test-app").expect(200, { date: "2000-02-05T05:00:00.000Z" }) + const indexHTML = await fsp.readFile(path.join(__dirname, "test-plugin/public/index.html"), { + encoding: "utf8", + }) + await agent.get("/test-plugin/test-app").expect(200, indexHTML) }) }) diff --git a/test/test-plugin/public/index.html b/test/test-plugin/public/index.html new file mode 100644 index 000000000..3485f18e5 --- /dev/null +++ b/test/test-plugin/public/index.html @@ -0,0 +1,10 @@ + + + + + Test Plugin + + +

Welcome to the test plugin!

+ + diff --git a/test/test-plugin/src/index.ts b/test/test-plugin/src/index.ts index 9e95ffca7..fb1869447 100644 --- a/test/test-plugin/src/index.ts +++ b/test/test-plugin/src/index.ts @@ -15,9 +15,7 @@ export const plugin: pluginapi.Plugin = { router() { const r = express.Router() r.get("/test-app", (req, res) => { - res.json({ - date: new Date("2000/02/05"), - }) + res.sendFile(fspath.resolve(__dirname, "../public/index.html")) }) r.get("/goland/icon.svg", (req, res) => { res.sendFile(fspath.resolve(__dirname, "../public/icon.svg")) From 277211c4ce0ca66af19c0ffb80a4d64dec4099b5 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Fri, 6 Nov 2020 14:44:19 -0500 Subject: [PATCH 173/247] plugin: Make init and applications callbacks optional --- src/node/plugin.ts | 6 ++++++ typings/pluginapi.d.ts | 8 ++++++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/node/plugin.ts b/src/node/plugin.ts index 77a4a8277..899aa1111 100644 --- a/src/node/plugin.ts +++ b/src/node/plugin.ts @@ -55,6 +55,9 @@ export class PluginAPI { public async applications(): Promise { const apps = new Array() for (const [, p] of this.plugins) { + if (!p.applications) { + continue + } const pluginApps = await p.applications() // Add plugin key to each app. @@ -86,6 +89,9 @@ export class PluginAPI { */ public mount(r: express.Router): void { for (const [, p] of this.plugins) { + if (!p.router) { + continue + } r.use(`${p.routerPath}`, p.router()) } } diff --git a/typings/pluginapi.d.ts b/typings/pluginapi.d.ts index d0846a288..06ce35fb4 100644 --- a/typings/pluginapi.d.ts +++ b/typings/pluginapi.d.ts @@ -129,8 +129,10 @@ export interface Plugin { * Returns the plugin's router. * * Mounted at / + * + * If not present, the plugin provides no routes. */ - router(): express.Router + router?(): express.Router /** * code-server uses this to collect the list of applications that @@ -139,8 +141,10 @@ export interface Plugin { * refresh the list of applications * * Ensure this is as fast as possible. + * + * If not present, the plugin provides no applications. */ - applications(): Application[] | Promise + applications?(): Application[] | Promise } /** From fe399ff0fe875aed3327cbe6708572367a135f33 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Fri, 6 Nov 2020 14:46:49 -0500 Subject: [PATCH 174/247] Fix formatting --- src/node/routes/index.ts | 2 +- test/plugin.test.ts | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/node/routes/index.ts b/src/node/routes/index.ts index da714eea5..4d92c365f 100644 --- a/src/node/routes/index.ts +++ b/src/node/routes/index.ts @@ -15,8 +15,8 @@ import { replaceTemplates } from "../http" import { PluginAPI } from "../plugin" import { getMediaMime, paths } from "../util" import { WebsocketRequest } from "../wsRouter" -import * as domainProxy from "./domainProxy" import * as apps from "./apps" +import * as domainProxy from "./domainProxy" import * as health from "./health" import * as login from "./login" import * as proxy from "./pathProxy" diff --git a/test/plugin.test.ts b/test/plugin.test.ts index a0885916b..305cf041a 100644 --- a/test/plugin.test.ts +++ b/test/plugin.test.ts @@ -1,11 +1,11 @@ import { logger } from "@coder/logger" +import * as express from "express" +import * as fs from "fs" import { describe } from "mocha" import * as path from "path" -import { PluginAPI } from "../src/node/plugin" import * as supertest from "supertest" -import * as express from "express" +import { PluginAPI } from "../src/node/plugin" import * as apps from "../src/node/routes/apps" -import * as fs from "fs" const fsp = fs.promises /** From da6000b96f8666cd21e5da108e421a2d66b72e1f Mon Sep 17 00:00:00 2001 From: Ben Potter Date: Tue, 10 Nov 2020 14:00:05 -0500 Subject: [PATCH 175/247] Add Slack link to issue template (#2282) --- .github/ISSUE_TEMPLATE/config.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 9dc409dd7..2f567fce3 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -2,4 +2,7 @@ blank_issues_enabled: false contact_links: - name: Question url: https://github.com/cdr/code-server/discussions/new?category_id=22503114 - about: Ask the community for help + about: Ask the community for help on our GitHub Discussions board + - name: Chat + about: Need immediate help or just want to talk? Hop in our Slack + url: https://cdr.co/join-community From 77c2a72cf8b5f0c800a2c8d53b219b877b3b9fe0 Mon Sep 17 00:00:00 2001 From: Matthew Beckett Date: Tue, 10 Nov 2020 20:07:38 +0000 Subject: [PATCH 176/247] Bump version and update README --- charts/code-server/README.md | 10 +++------- charts/code-server/values.yaml | 2 +- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/charts/code-server/README.md b/charts/code-server/README.md index 0044909c0..f6cca84f0 100644 --- a/charts/code-server/README.md +++ b/charts/code-server/README.md @@ -1,6 +1,6 @@ # code-server -![Version: 1.0.0](https://img.shields.io/badge/Version-1.0.0-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: 3.5.0](https://img.shields.io/badge/AppVersion-3.5.0-informational?style=flat-square) +![Version: 1.0.0](https://img.shields.io/badge/Version-1.0.0-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: 3.6.2](https://img.shields.io/badge/AppVersion-3.6.2-informational?style=flat-square) [code-server](https://github.com/cdr/code-server) code-server is VS Code running on a remote server, accessible through the browser. @@ -9,7 +9,7 @@ on a remote server, accessible through the browser. ```console $ git clone https://github.com/cdr/code-server.git -$ helm install deployment/chart +$ helm install code-server/charts/code-server ``` ## Introduction @@ -27,7 +27,7 @@ package manager. To install the chart with the release name `my-release`: ```console -$ helm install --name my-release deployment/chart +$ helm install --name my-release charts/code-server ``` The command deploys code-server on the Kubernetes cluster in the default @@ -49,10 +49,6 @@ deletes the release. ## Configuration -The following table lists the configurable parameters of the nginx-ingress chart -and their default values. - - The following table lists the configurable parameters of the code-server chart and their default values. diff --git a/charts/code-server/values.yaml b/charts/code-server/values.yaml index eff180351..5cc74185d 100644 --- a/charts/code-server/values.yaml +++ b/charts/code-server/values.yaml @@ -6,7 +6,7 @@ replicaCount: 1 image: repository: codercom/code-server - tag: '3.5.0' + tag: '3.6.2' pullPolicy: Always imagePullSecrets: [] From 03aa7709cad0d041caf0e6dcf4b29f374ec3ee9b Mon Sep 17 00:00:00 2001 From: Matthew Beckett Date: Tue, 10 Nov 2020 20:09:13 +0000 Subject: [PATCH 177/247] Add maintainers --- charts/code-server/README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/charts/code-server/README.md b/charts/code-server/README.md index f6cca84f0..b9162b65d 100644 --- a/charts/code-server/README.md +++ b/charts/code-server/README.md @@ -5,6 +5,8 @@ [code-server](https://github.com/cdr/code-server) code-server is VS Code running on a remote server, accessible through the browser. +This chart is community maintained by [@Matthew-Beckett](https://github.com/Matthew-Beckett) and [@alexgorbatchev](https://github.com/alexgorbatchev) + ## TL;DR; ```console From 0e39bb9f2c31d04f0d6ec8a4da505afd82931207 Mon Sep 17 00:00:00 2001 From: Matthew Beckett Date: Tue, 10 Nov 2020 20:09:48 +0000 Subject: [PATCH 178/247] Fix trailing line breaks --- charts/code-server/README.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/charts/code-server/README.md b/charts/code-server/README.md index b9162b65d..e0a9aa63c 100644 --- a/charts/code-server/README.md +++ b/charts/code-server/README.md @@ -112,5 +112,3 @@ $ helm install --name my-release -f values.yaml deployment/chart ``` > **Tip**: You can use the default [values.yaml](values.yaml) - - From 10799aa1eca3bb92134224a609f999330c1fbb2c Mon Sep 17 00:00:00 2001 From: Matthew Beckett Date: Tue, 10 Nov 2020 20:10:50 +0000 Subject: [PATCH 179/247] Bump chart app version --- charts/code-server/Chart.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/charts/code-server/Chart.yaml b/charts/code-server/Chart.yaml index ba9281ab8..28f98f000 100644 --- a/charts/code-server/Chart.yaml +++ b/charts/code-server/Chart.yaml @@ -20,4 +20,4 @@ version: 1.0.0 # This is the version number of the application being deployed. This version number should be # incremented each time you make changes to the application. Versions are not expected to # follow Semantic Versioning. They should reflect the version the application is using. -appVersion: 3.5.0 +appVersion: 3.6.2 From aa7415a479e61c294f858ae3ecd71197f9b2415d Mon Sep 17 00:00:00 2001 From: Matthew Beckett Date: Tue, 10 Nov 2020 20:37:14 +0000 Subject: [PATCH 180/247] Update CODEOWNERS file --- .github/CODEOWNERS | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index c68d7da5e..a66dcc9ef 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1 +1,2 @@ -* @code-asher @nhooyr +charts/code-server @Matthew-Beckett @alexgorbatchev +* @code-asher @nhooyr \ No newline at end of file From 0a01338edd3a618a943567b8b95c7fce37890f9c Mon Sep 17 00:00:00 2001 From: Asher Date: Tue, 10 Nov 2020 15:46:53 -0600 Subject: [PATCH 181/247] Deduplicate child process message dance --- src/node/vscode.ts | 116 +++++++++++++++++++++++++++++---------------- 1 file changed, 74 insertions(+), 42 deletions(-) diff --git a/src/node/vscode.ts b/src/node/vscode.ts index aed005f9f..92d749447 100644 --- a/src/node/vscode.ts +++ b/src/node/vscode.ts @@ -13,6 +13,7 @@ export class VscodeProvider { public readonly serverRootPath: string public readonly vsRootPath: string private _vscode?: Promise + private timeoutInterval = 10000 // 10s, matches VS Code's timeouts. public constructor() { this.vsRootPath = path.resolve(rootPath, "lib/vscode") @@ -52,33 +53,25 @@ export class VscodeProvider { const vscode = await this.fork() logger.debug("setting up vs code...") - return new Promise((resolve, reject) => { - const onExit = (code: number | null) => reject(new Error(`VS Code exited unexpectedly with code ${code}`)) - vscode.once("message", (message: ipc.VscodeMessage) => { - logger.debug("got message from vs code", field("message", message)) - vscode.off("error", reject) - vscode.off("exit", onExit) - return message.type === "options" && message.id === id - ? resolve(message.options) - : reject(new Error("Unexpected response during initialization")) - }) - - vscode.once("error", reject) - vscode.once("exit", onExit) - - this.send( - { - type: "init", - id, - options: { - ...options, - startPath, - }, + this.send( + { + type: "init", + id, + options: { + ...options, + startPath, }, - vscode, - ) + }, + vscode, + ) + + const message = await this.onMessage(vscode, (message): message is ipc.OptionsMessage => { + // There can be parallel initializations so wait for the right ID. + return message.type === "options" && message.id === id }) + + return message.options } private fork(): Promise { @@ -88,34 +81,73 @@ export class VscodeProvider { logger.debug("forking vs code...") const vscode = cp.fork(path.join(this.serverRootPath, "fork")) - vscode.on("error", (error) => { - logger.error(error.message) + + const dispose = () => { + vscode.removeAllListeners() + vscode.kill() this._vscode = undefined + } + + vscode.on("error", (error: Error) => { + logger.error(error.message) + if (error.stack) { + logger.debug(error.stack) + } + dispose() }) + vscode.on("exit", (code) => { logger.error(`VS Code exited unexpectedly with code ${code}`) - this._vscode = undefined + dispose() }) - this._vscode = new Promise((resolve, reject) => { - const onExit = (code: number | null) => reject(new Error(`VS Code exited unexpectedly with code ${code}`)) - - vscode.once("message", (message: ipc.VscodeMessage) => { - logger.debug("got message from vs code", field("message", message)) - vscode.off("error", reject) - vscode.off("exit", onExit) - return message.type === "ready" - ? resolve(vscode) - : reject(new Error("Unexpected response waiting for ready response")) - }) - - vscode.once("error", reject) - vscode.once("exit", onExit) - }) + this._vscode = this.onMessage(vscode, (message): message is ipc.ReadyMessage => { + return message.type === "ready" + }).then(() => vscode) return this._vscode } + /** + * Listen to a single message from a process. Reject if the process errors, + * exits, or times out. + * + * `fn` is a function that determines whether the message is the one we're + * waiting for. + */ + private onMessage( + proc: cp.ChildProcess, + fn: (message: ipc.VscodeMessage) => message is T, + ): Promise { + return new Promise((resolve, _reject) => { + const reject = (error: Error) => { + clearTimeout(timeout) + _reject(error) + } + + const onExit = (code: number | null) => { + reject(new Error(`VS Code exited unexpectedly with code ${code}`)) + } + + const timeout = setTimeout(() => { + reject(new Error("timed out")) + }, this.timeoutInterval) + + proc.on("message", (message: ipc.VscodeMessage) => { + logger.debug("got message from vscode", field("message", message)) + proc.off("error", reject) + proc.off("exit", onExit) + if (fn(message)) { + clearTimeout(timeout) + resolve(message) + } + }) + + proc.once("error", reject) + proc.once("exit", onExit) + }) + } + /** * VS Code expects a raw socket. It will handle all the web socket frames. */ From de4949571cd4873116f8243a4e15d74a6b01c309 Mon Sep 17 00:00:00 2001 From: Asher Date: Tue, 10 Nov 2020 17:02:39 -0600 Subject: [PATCH 182/247] Document getFirstPath better --- src/node/vscode.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/node/vscode.ts b/src/node/vscode.ts index 92d749447..da7d5710a 100644 --- a/src/node/vscode.ts +++ b/src/node/vscode.ts @@ -165,7 +165,16 @@ export class VscodeProvider { } /** - * Choose the first non-empty path. + * Choose the first non-empty path from the provided array. + * + * Each array item consists of `url` and an optional `workspace` boolean that + * indicates whether that url is for a workspace. + * + * `url` can be a fully qualified URL or just the path portion. + * + * `url` can also be a query object to make it easier to pass in query + * variables directly but anything that isn't a string or string array is not + * valid and will be ignored. */ private async getFirstPath( startPaths: Array<{ url?: string | string[] | ipc.Query | ipc.Query[]; workspace?: boolean } | undefined>, From f706039a9d4321d79cb76ff9f813f46e7e9bb8bf Mon Sep 17 00:00:00 2001 From: Asher Date: Tue, 10 Nov 2020 17:24:07 -0600 Subject: [PATCH 183/247] Re-add TLS socket proxy --- src/node/vscode.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/node/vscode.ts b/src/node/vscode.ts index da7d5710a..9ba7b8a90 100644 --- a/src/node/vscode.ts +++ b/src/node/vscode.ts @@ -6,6 +6,7 @@ import * as ipc from "../../lib/vscode/src/vs/server/ipc" import { arrayify, generateUuid } from "../common/util" import { rootPath } from "./constants" import { settings } from "./settings" +import { SocketProxyProvider } from "./socket" import { isFile } from "./util" import { ipcMain } from "./wrapper" @@ -14,6 +15,7 @@ export class VscodeProvider { public readonly vsRootPath: string private _vscode?: Promise private timeoutInterval = 10000 // 10s, matches VS Code's timeouts. + private readonly socketProvider = new SocketProxyProvider() public constructor() { this.vsRootPath = path.resolve(rootPath, "lib/vscode") @@ -22,6 +24,7 @@ export class VscodeProvider { } public async dispose(): Promise { + this.socketProvider.stop() if (this._vscode) { const vscode = await this._vscode vscode.removeAllListeners() @@ -152,9 +155,11 @@ export class VscodeProvider { * VS Code expects a raw socket. It will handle all the web socket frames. */ public async sendWebsocket(socket: net.Socket, query: ipc.Query): Promise { - // TODO: TLS socket proxy. const vscode = await this._vscode - this.send({ type: "socket", query }, vscode, socket) + // TLS sockets cannot be transferred to child processes so we need an + // in-between. Non-TLS sockets will be returned as-is. + const socketProxy = await this.socketProvider.createProxy(socket) + this.send({ type: "socket", query }, vscode, socketProxy) } private send(message: ipc.CodeServerMessage, vscode?: cp.ChildProcess, socket?: net.Socket): void { From b8340a2ae9f9a543fe3a34c00e51fe925bf20ecd Mon Sep 17 00:00:00 2001 From: Asher Date: Tue, 10 Nov 2020 17:52:02 -0600 Subject: [PATCH 184/247] Close sockets correctly --- src/node/routes/index.ts | 2 +- src/node/wsRouter.ts | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/node/routes/index.ts b/src/node/routes/index.ts index 4d92c365f..2c54917da 100644 --- a/src/node/routes/index.ts +++ b/src/node/routes/index.ts @@ -155,7 +155,7 @@ export const register = async ( const wsErrorHandler: express.ErrorRequestHandler = async (err, req) => { logger.error(`${err.message} ${err.stack}`) - ;(req as WebsocketRequest).ws.destroy(err) + ;(req as WebsocketRequest).ws.end() } wsApp.use(wsErrorHandler) diff --git a/src/node/wsRouter.ts b/src/node/wsRouter.ts index 1a057f0fa..175f214a8 100644 --- a/src/node/wsRouter.ts +++ b/src/node/wsRouter.ts @@ -5,8 +5,6 @@ import * as net from "net" export const handleUpgrade = (app: express.Express, server: http.Server): void => { server.on("upgrade", (req, socket, head) => { - socket.on("error", () => socket.destroy()) - req.ws = socket req.head = head req._ws_handled = false @@ -14,7 +12,7 @@ export const handleUpgrade = (app: express.Express, server: http.Server): void = // Send the request off to be handled by Express. ;(app as any).handle(req, new http.ServerResponse(req), () => { if (!req._ws_handled) { - socket.destroy(new Error("Not found")) + socket.end("HTTP/1.1 404 Not Found\r\n\r\n") } }) }) From 71850e312bc981a62af80f96d64c4fef39706b62 Mon Sep 17 00:00:00 2001 From: Asher Date: Tue, 10 Nov 2020 18:14:18 -0600 Subject: [PATCH 185/247] Avoid setting ?to=/ That's the default so it's extra visual noise. --- src/node/routes/domainProxy.ts | 2 +- src/node/routes/pathProxy.ts | 7 ++++--- src/node/routes/vscode.ts | 3 ++- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/node/routes/domainProxy.ts b/src/node/routes/domainProxy.ts index ac249b809..fdffdc585 100644 --- a/src/node/routes/domainProxy.ts +++ b/src/node/routes/domainProxy.ts @@ -55,7 +55,7 @@ router.all("*", (req, res, next) => { } // Redirect all other pages to the login. return redirect(req, res, "login", { - to: req.path, + to: req.path !== "/" ? req.path : undefined, }) } diff --git a/src/node/routes/pathProxy.ts b/src/node/routes/pathProxy.ts index d21d08eb8..152402195 100644 --- a/src/node/routes/pathProxy.ts +++ b/src/node/routes/pathProxy.ts @@ -1,6 +1,7 @@ import { Request, Router } from "express" import qs from "qs" import { HttpCode, HttpError } from "../../common/http" +import { normalize } from "../../common/util" import { authenticated, ensureAuthenticated, redirect } from "../http" import { proxy } from "../proxy" import { Router as WsRouter } from "../wsRouter" @@ -17,11 +18,11 @@ const getProxyTarget = (req: Request, rewrite: boolean): string => { router.all("/(:port)(/*)?", (req, res) => { if (!authenticated(req)) { - // If visiting the root (/proxy/:port and nothing else) redirect to the - // login page. + // If visiting the root (/:port only) redirect to the login page. if (!req.params[0] || req.params[0] === "/") { + const to = normalize(`${req.baseUrl}${req.path}`) return redirect(req, res, "login", { - to: `${req.baseUrl}${req.path}` || "/", + to: to !== "/" ? to : undefined, }) } throw new HttpError("Unauthorized", HttpCode.Unauthorized) diff --git a/src/node/routes/vscode.ts b/src/node/routes/vscode.ts index db2dc2071..552001273 100644 --- a/src/node/routes/vscode.ts +++ b/src/node/routes/vscode.ts @@ -15,7 +15,8 @@ const vscode = new VscodeProvider() router.get("/", async (req, res) => { if (!authenticated(req)) { return redirect(req, res, "login", { - to: req.baseUrl, + // req.baseUrl can be blank if already at the root. + to: req.baseUrl && req.baseUrl !== "/" ? req.baseUrl : undefined, }) } From 4574593664cc4aaab167ef477389b5e88f5baa8f Mon Sep 17 00:00:00 2001 From: Asher Date: Tue, 10 Nov 2020 18:20:44 -0600 Subject: [PATCH 186/247] Refactor vscode init to use async Hopefully is a bit easier to read. --- src/node/routes/vscode.ts | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/src/node/routes/vscode.ts b/src/node/routes/vscode.ts index 552001273..9b464f61e 100644 --- a/src/node/routes/vscode.ts +++ b/src/node/routes/vscode.ts @@ -22,18 +22,14 @@ router.get("/", async (req, res) => { const [content, options] = await Promise.all([ await fs.readFile(path.join(rootPath, "src/browser/pages/vscode.html"), "utf8"), - vscode - .initialize( - { - args: req.args, - remoteAuthority: req.headers.host || "", - }, - req.query, - ) - .catch((error) => { + (async () => { + try { + return await vscode.initialize({ args: req.args, remoteAuthority: req.headers.host || "" }, req.query) + } catch (error) { const devMessage = commit === "development" ? "It might not have finished compiling." : "" throw new Error(`VS Code failed to load. ${devMessage} ${error.message}`) - }), + } + })(), ]) options.productConfiguration.codeServerVersion = version From 79478eb89f30752d96ec8726dae8d359e13d369b Mon Sep 17 00:00:00 2001 From: Asher Date: Tue, 10 Nov 2020 18:33:16 -0600 Subject: [PATCH 187/247] Clarify some points around the cookie domain Also add a check that the domain has a dot. This covers the localhost case as well, so remove that. --- src/node/http.ts | 29 +++++++++++++++++++---------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/src/node/http.ts b/src/node/http.ts index f259d1037..1aa7adb51 100644 --- a/src/node/http.ts +++ b/src/node/http.ts @@ -98,27 +98,36 @@ export const redirect = ( /** * Get the value that should be used for setting a cookie domain. This will - * allow the user to authenticate only once. This will use the highest level - * domain (e.g. `coder.com` over `test.coder.com` if both are specified). + * allow the user to authenticate once no matter what sub-domain they use to log + * in. This will use the highest level proxy domain (e.g. `coder.com` over + * `test.coder.com` if both are specified). */ export const getCookieDomain = (host: string, proxyDomains: string[]): string | undefined => { const idx = host.lastIndexOf(":") host = idx !== -1 ? host.substring(0, idx) : host + // If any of these are true we will still set cookies but without an explicit + // `Domain` attribute on the cookie. if ( - // Might be blank/missing, so there's nothing more to do. + // The host can be be blank or missing so there's nothing we can set. !host || // IP addresses can't have subdomains so there's no value in setting the - // domain for them. Assume anything with a : is ipv6 (valid domain name - // characters are alphanumeric or dashes). + // domain for them. Assume that anything with a : is ipv6 (valid domain name + // characters are alphanumeric or dashes)... host.includes(":") || - // Assume anything entirely numbers and dots is ipv4 (currently tlds + // ...and that anything entirely numbers and dots is ipv4 (currently tlds // cannot be entirely numbers). !/[^0-9.]/.test(host) || - // localhost subdomains don't seem to work at all (browser bug?). + // localhost subdomains don't seem to work at all (browser bug?). A cookie + // set at dev.localhost cannot be read by 8080.dev.localhost. host.endsWith(".localhost") || - // It might be localhost (or an IP, see above) if it's a proxy and it - // isn't setting the host header to match the access domain. - host === "localhost" + // Domains without at least one dot (technically two since domain.tld will + // become .domain.tld) are considered invalid according to the spec so don't + // set the domain for them. In my testing though localhost is the only + // problem (the browser just doesn't store the cookie at all). localhost has + // an additional problem which is that a reverse proxy might give + // code-server localhost even though the domain is really domain.tld (by + // default NGINX does this). + !host.includes(".") ) { logger.debug("no valid cookie doman", field("host", host)) return undefined From 72931edcf010c2e6ce9872f7e1ab974f999e09cb Mon Sep 17 00:00:00 2001 From: Asher Date: Thu, 12 Nov 2020 11:16:21 -0600 Subject: [PATCH 188/247] Fix cleanup after getting message from vscode --- src/node/vscode.ts | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/src/node/vscode.ts b/src/node/vscode.ts index 9ba7b8a90..6761a6d18 100644 --- a/src/node/vscode.ts +++ b/src/node/vscode.ts @@ -123,8 +123,19 @@ export class VscodeProvider { fn: (message: ipc.VscodeMessage) => message is T, ): Promise { return new Promise((resolve, _reject) => { - const reject = (error: Error) => { + const cleanup = () => { + proc.off("error", reject) + proc.off("exit", onExit) clearTimeout(timeout) + } + + const timeout = setTimeout(() => { + cleanup() + _reject(new Error("timed out")) + }, this.timeoutInterval) + + const reject = (error: Error) => { + cleanup() _reject(error) } @@ -132,16 +143,10 @@ export class VscodeProvider { reject(new Error(`VS Code exited unexpectedly with code ${code}`)) } - const timeout = setTimeout(() => { - reject(new Error("timed out")) - }, this.timeoutInterval) - proc.on("message", (message: ipc.VscodeMessage) => { logger.debug("got message from vscode", field("message", message)) - proc.off("error", reject) - proc.off("exit", onExit) if (fn(message)) { - clearTimeout(timeout) + cleanup() resolve(message) } }) From 31b67062b013ec5039c96e534e52891462edc2b1 Mon Sep 17 00:00:00 2001 From: Asher Date: Thu, 12 Nov 2020 11:17:45 -0600 Subject: [PATCH 189/247] Remove from onMessage Turns out that while Typescript can't infer the callback return type from it, Typescript can do the opposite and infer it from the callback return type. --- src/node/vscode.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/node/vscode.ts b/src/node/vscode.ts index 6761a6d18..089a3d201 100644 --- a/src/node/vscode.ts +++ b/src/node/vscode.ts @@ -69,7 +69,7 @@ export class VscodeProvider { vscode, ) - const message = await this.onMessage(vscode, (message): message is ipc.OptionsMessage => { + const message = await this.onMessage(vscode, (message): message is ipc.OptionsMessage => { // There can be parallel initializations so wait for the right ID. return message.type === "options" && message.id === id }) @@ -104,7 +104,7 @@ export class VscodeProvider { dispose() }) - this._vscode = this.onMessage(vscode, (message): message is ipc.ReadyMessage => { + this._vscode = this.onMessage(vscode, (message): message is ipc.ReadyMessage => { return message.type === "ready" }).then(() => vscode) From 5499a3d12562f4b5b15cb684f53428a8f68798e6 Mon Sep 17 00:00:00 2001 From: Asher Date: Thu, 12 Nov 2020 11:23:52 -0600 Subject: [PATCH 190/247] Use baseUrl when redirecting from domain proxy This will make the route more robust since it'll work under more than just the root. --- src/node/routes/domainProxy.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/node/routes/domainProxy.ts b/src/node/routes/domainProxy.ts index fdffdc585..25745955e 100644 --- a/src/node/routes/domainProxy.ts +++ b/src/node/routes/domainProxy.ts @@ -1,5 +1,6 @@ import { Request, Router } from "express" import { HttpCode, HttpError } from "../../common/http" +import { normalize } from "../../common/util" import { authenticated, ensureAuthenticated, redirect } from "../http" import { proxy } from "../proxy" import { Router as WsRouter } from "../wsRouter" @@ -54,8 +55,9 @@ router.all("*", (req, res, next) => { return next() } // Redirect all other pages to the login. + const to = normalize(`${req.baseUrl}${req.path}`) return redirect(req, res, "login", { - to: req.path !== "/" ? req.path : undefined, + to: to !== "/" ? to : undefined, }) } From b73ea2fea2bbc80f5f363d9f40d1687d2396b280 Mon Sep 17 00:00:00 2001 From: Asher Date: Thu, 12 Nov 2020 12:03:28 -0600 Subject: [PATCH 191/247] Unbind message handler itself after getting message Also switch `once` to `on` since we `off` them later anyway so no point in making Node do it twice. --- src/node/vscode.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/node/vscode.ts b/src/node/vscode.ts index 4d933e73e..9ab22ef78 100644 --- a/src/node/vscode.ts +++ b/src/node/vscode.ts @@ -126,6 +126,7 @@ export class VscodeProvider { const cleanup = () => { proc.off("error", reject) proc.off("exit", onExit) + proc.off("message", onMessage) clearTimeout(timeout) } @@ -143,16 +144,17 @@ export class VscodeProvider { reject(new Error(`VS Code exited unexpectedly with code ${code}`)) } - proc.on("message", (message: ipc.VscodeMessage) => { + const onMessage = (message: ipc.VscodeMessage) => { logger.trace("got message from vscode", field("message", message)) if (fn(message)) { cleanup() resolve(message) } - }) + } - proc.once("error", reject) - proc.once("exit", onExit) + proc.on("message", onMessage) + proc.on("error", reject) + proc.on("exit", onExit) }) } From 6f14b8b8dd123ae6b2b969dba3b920c5f81d3f06 Mon Sep 17 00:00:00 2001 From: Asher Date: Thu, 12 Nov 2020 12:07:45 -0600 Subject: [PATCH 192/247] Add separate handler for error Feels like it parallels better with the other handlers. --- src/node/vscode.ts | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/node/vscode.ts b/src/node/vscode.ts index 9ab22ef78..c3da8a24d 100644 --- a/src/node/vscode.ts +++ b/src/node/vscode.ts @@ -122,9 +122,9 @@ export class VscodeProvider { proc: cp.ChildProcess, fn: (message: ipc.VscodeMessage) => message is T, ): Promise { - return new Promise((resolve, _reject) => { + return new Promise((resolve, reject) => { const cleanup = () => { - proc.off("error", reject) + proc.off("error", onError) proc.off("exit", onExit) proc.off("message", onMessage) clearTimeout(timeout) @@ -132,15 +132,16 @@ export class VscodeProvider { const timeout = setTimeout(() => { cleanup() - _reject(new Error("timed out")) + reject(new Error("timed out")) }, this.timeoutInterval) - const reject = (error: Error) => { + const onError = (error: Error) => { cleanup() - _reject(error) + reject(error) } const onExit = (code: number | null) => { + cleanup() reject(new Error(`VS Code exited unexpectedly with code ${code}`)) } @@ -153,7 +154,7 @@ export class VscodeProvider { } proc.on("message", onMessage) - proc.on("error", reject) + proc.on("error", onError) proc.on("exit", onExit) }) } From 96995b78d1e8eaeb20a0c7f535b60b48b07b35cf Mon Sep 17 00:00:00 2001 From: Asher Date: Thu, 12 Nov 2020 12:29:41 -0600 Subject: [PATCH 193/247] Update cert flag test --- test/cli.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/cli.test.ts b/test/cli.test.ts index a59aac995..6b1e96c2f 100644 --- a/test/cli.test.ts +++ b/test/cli.test.ts @@ -252,9 +252,9 @@ describe("parser", () => { _: [], ...defaults, cert: { - value: path.join(tmpdir, "self-signed.cert"), + value: path.join(paths.data, "localhost.crt"), }, - "cert-key": path.join(tmpdir, "self-signed.key"), + "cert-key": path.join(paths.data, "localhost.key"), }) }) From 9889f30224b8d68e9fbbbded709dbdb408e018ed Mon Sep 17 00:00:00 2001 From: Asher Date: Thu, 12 Nov 2020 12:30:41 -0600 Subject: [PATCH 194/247] Remove unused ts-expect-error from VS Code I'm not sure why other builds are passing with this still in. --- ci/dev/vscode.patch | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/ci/dev/vscode.patch b/ci/dev/vscode.patch index ca67cb28c..9860ba965 100644 --- a/ci/dev/vscode.patch +++ b/ci/dev/vscode.patch @@ -210,6 +210,18 @@ index da4fa3e9d0443d679dfbab1000b434af2ae01afd..50f3e1144f8057883dea8b91ec2f7073 } function processLib() { +diff --git a/extensions/typescript-language-features/src/utils/platform.ts b/extensions/typescript-language-features/src/utils/platform.ts +index 2d754bf4054713f53beed030f9211b33532c1b4b..708b7e40a662e4ca93420992bf7a5af0c62ea5b2 100644 +--- a/extensions/typescript-language-features/src/utils/platform.ts ++++ b/extensions/typescript-language-features/src/utils/platform.ts +@@ -6,6 +6,6 @@ + import * as vscode from 'vscode'; + + export function isWeb(): boolean { +- // @ts-expect-error ++ // NOTE@coder: Remove unused ts-expect-error directive which causes tsc to error. + return typeof navigator !== 'undefined' && vscode.env.uiKind === vscode.UIKind.Web; + } diff --git a/package.json b/package.json index 770b44b0c1ff53d903b7680ede27715376df00f2..b27ab71647a3e7c4b6076ba4fdb8fde20fa73bb0 100644 --- a/package.json From 52ea32f4a7e45c64cef7d654794bbffd7f8debb9 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Fri, 13 Nov 2020 16:50:06 -0500 Subject: [PATCH 195/247] helm: Move chart into ci/helm-chart --- .github/CODEOWNERS | 5 +++-- {charts/code-server => ci/helm-chart}/.helmignore | 0 {charts/code-server => ci/helm-chart}/Chart.yaml | 0 {charts/code-server => ci/helm-chart}/README.md | 2 +- {charts/code-server => ci/helm-chart}/templates/NOTES.txt | 0 {charts/code-server => ci/helm-chart}/templates/_helpers.tpl | 0 .../code-server => ci/helm-chart}/templates/deployment.yaml | 0 {charts/code-server => ci/helm-chart}/templates/ingress.yaml | 0 {charts/code-server => ci/helm-chart}/templates/pvc.yaml | 0 {charts/code-server => ci/helm-chart}/templates/secrets.yaml | 0 {charts/code-server => ci/helm-chart}/templates/service.yaml | 0 .../helm-chart}/templates/serviceaccount.yaml | 0 .../helm-chart}/templates/tests/test-connection.yaml | 0 {charts/code-server => ci/helm-chart}/values.yaml | 0 14 files changed, 4 insertions(+), 3 deletions(-) rename {charts/code-server => ci/helm-chart}/.helmignore (100%) rename {charts/code-server => ci/helm-chart}/Chart.yaml (100%) rename {charts/code-server => ci/helm-chart}/README.md (98%) rename {charts/code-server => ci/helm-chart}/templates/NOTES.txt (100%) rename {charts/code-server => ci/helm-chart}/templates/_helpers.tpl (100%) rename {charts/code-server => ci/helm-chart}/templates/deployment.yaml (100%) rename {charts/code-server => ci/helm-chart}/templates/ingress.yaml (100%) rename {charts/code-server => ci/helm-chart}/templates/pvc.yaml (100%) rename {charts/code-server => ci/helm-chart}/templates/secrets.yaml (100%) rename {charts/code-server => ci/helm-chart}/templates/service.yaml (100%) rename {charts/code-server => ci/helm-chart}/templates/serviceaccount.yaml (100%) rename {charts/code-server => ci/helm-chart}/templates/tests/test-connection.yaml (100%) rename {charts/code-server => ci/helm-chart}/values.yaml (100%) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index a66dcc9ef..ee281990c 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,2 +1,3 @@ -charts/code-server @Matthew-Beckett @alexgorbatchev -* @code-asher @nhooyr \ No newline at end of file +* @code-asher @nhooyr + +ci/helm-chart @Matthew-Beckett @alexgorbatchev diff --git a/charts/code-server/.helmignore b/ci/helm-chart/.helmignore similarity index 100% rename from charts/code-server/.helmignore rename to ci/helm-chart/.helmignore diff --git a/charts/code-server/Chart.yaml b/ci/helm-chart/Chart.yaml similarity index 100% rename from charts/code-server/Chart.yaml rename to ci/helm-chart/Chart.yaml diff --git a/charts/code-server/README.md b/ci/helm-chart/README.md similarity index 98% rename from charts/code-server/README.md rename to ci/helm-chart/README.md index e0a9aa63c..cb34d274c 100644 --- a/charts/code-server/README.md +++ b/ci/helm-chart/README.md @@ -11,7 +11,7 @@ This chart is community maintained by [@Matthew-Beckett](https://github.com/Matt ```console $ git clone https://github.com/cdr/code-server.git -$ helm install code-server/charts/code-server +$ helm install code-server charts/code-server ``` ## Introduction diff --git a/charts/code-server/templates/NOTES.txt b/ci/helm-chart/templates/NOTES.txt similarity index 100% rename from charts/code-server/templates/NOTES.txt rename to ci/helm-chart/templates/NOTES.txt diff --git a/charts/code-server/templates/_helpers.tpl b/ci/helm-chart/templates/_helpers.tpl similarity index 100% rename from charts/code-server/templates/_helpers.tpl rename to ci/helm-chart/templates/_helpers.tpl diff --git a/charts/code-server/templates/deployment.yaml b/ci/helm-chart/templates/deployment.yaml similarity index 100% rename from charts/code-server/templates/deployment.yaml rename to ci/helm-chart/templates/deployment.yaml diff --git a/charts/code-server/templates/ingress.yaml b/ci/helm-chart/templates/ingress.yaml similarity index 100% rename from charts/code-server/templates/ingress.yaml rename to ci/helm-chart/templates/ingress.yaml diff --git a/charts/code-server/templates/pvc.yaml b/ci/helm-chart/templates/pvc.yaml similarity index 100% rename from charts/code-server/templates/pvc.yaml rename to ci/helm-chart/templates/pvc.yaml diff --git a/charts/code-server/templates/secrets.yaml b/ci/helm-chart/templates/secrets.yaml similarity index 100% rename from charts/code-server/templates/secrets.yaml rename to ci/helm-chart/templates/secrets.yaml diff --git a/charts/code-server/templates/service.yaml b/ci/helm-chart/templates/service.yaml similarity index 100% rename from charts/code-server/templates/service.yaml rename to ci/helm-chart/templates/service.yaml diff --git a/charts/code-server/templates/serviceaccount.yaml b/ci/helm-chart/templates/serviceaccount.yaml similarity index 100% rename from charts/code-server/templates/serviceaccount.yaml rename to ci/helm-chart/templates/serviceaccount.yaml diff --git a/charts/code-server/templates/tests/test-connection.yaml b/ci/helm-chart/templates/tests/test-connection.yaml similarity index 100% rename from charts/code-server/templates/tests/test-connection.yaml rename to ci/helm-chart/templates/tests/test-connection.yaml diff --git a/charts/code-server/values.yaml b/ci/helm-chart/values.yaml similarity index 100% rename from charts/code-server/values.yaml rename to ci/helm-chart/values.yaml From 248c2adb2e00fb72514ab55d235a55eaf0ac97e0 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Fri, 13 Nov 2020 17:08:05 -0500 Subject: [PATCH 196/247] helm: Fix README examples Not sure where --name came from? Maybe an older version of helm. Ah, it's from v2.16.7 --- ci/helm-chart/README.md | 23 +++++++++++++---------- ci/helm-chart/values.yaml | 2 +- 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/ci/helm-chart/README.md b/ci/helm-chart/README.md index cb34d274c..601596c12 100644 --- a/ci/helm-chart/README.md +++ b/ci/helm-chart/README.md @@ -10,8 +10,9 @@ This chart is community maintained by [@Matthew-Beckett](https://github.com/Matt ## TL;DR; ```console -$ git clone https://github.com/cdr/code-server.git -$ helm install code-server charts/code-server +$ git clone https://github.com/cdr/code-server +$ cd code-server +$ helm upgrade --install code-server ci/helm-chart ``` ## Introduction @@ -26,10 +27,12 @@ package manager. ## Installing the Chart -To install the chart with the release name `my-release`: +To install the chart with the release name `code-server`: ```console -$ helm install --name my-release charts/code-server +$ git clone https://github.com/cdr/code-server +$ cd code-server +$ helm upgrade --install code-server ci/helm-chart ``` The command deploys code-server on the Kubernetes cluster in the default @@ -40,10 +43,10 @@ that can be configured during installation. ## Uninstalling the Chart -To uninstall/delete the `my-release` deployment: +To uninstall/delete the `code-server` deployment: ```console -$ helm delete my-release +$ helm delete code-server ``` The command removes all the Kubernetes components associated with the chart and @@ -97,9 +100,9 @@ Specify each parameter using the `--set key=value[,key=value]` argument to `helm install`. For example, ```console -$ helm install --name my-release \ - --set persistence.enabled=false \ - deployment/chart +$ helm install code-server \ + ci/helm-chart \ + --set persistence.enabled=false ``` The above command sets the the persistence storage to false. @@ -108,7 +111,7 @@ Alternatively, a YAML file that specifies the values for the above parameters can be provided while installing the chart. For example, ```console -$ helm install --name my-release -f values.yaml deployment/chart +$ helm install code-server ci/helm-chart -f values.yaml ``` > **Tip**: You can use the default [values.yaml](values.yaml) diff --git a/ci/helm-chart/values.yaml b/ci/helm-chart/values.yaml index 5cc74185d..e71499bfb 100644 --- a/ci/helm-chart/values.yaml +++ b/ci/helm-chart/values.yaml @@ -112,7 +112,7 @@ persistence: ## # storageClass: "-" accessMode: ReadWriteOnce - size: 1Gi + size: 10Gi annotations: {} # existingClaim: "" # hostPath: /data From 9af3671c05c1f229f90f5c1170f08a4e21cc5b61 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Fri, 13 Nov 2020 17:32:14 -0500 Subject: [PATCH 197/247] helm: Add link in install.md --- ci/dev/fmt.sh | 4 +++- doc/install.md | 5 +++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/ci/dev/fmt.sh b/ci/dev/fmt.sh index 47911aadf..ce9d7518b 100755 --- a/ci/dev/fmt.sh +++ b/ci/dev/fmt.sh @@ -19,7 +19,9 @@ main() { "*.yaml" "*.yml" ) - prettier --write --loglevel=warn $(git ls-files "${prettierExts[@]}") + prettier --write --loglevel=warn $( + git ls-files "${prettierExts[@]}" | grep -v 'helm-chart' + ) doctoc --title '# FAQ' doc/FAQ.md > /dev/null doctoc --title '# Setup Guide' doc/guide.md > /dev/null diff --git a/doc/install.md b/doc/install.md index c33299cf6..90a14dfdd 100644 --- a/doc/install.md +++ b/doc/install.md @@ -12,6 +12,7 @@ - [macOS](#macos) - [Standalone Releases](#standalone-releases) - [Docker](#docker) +- [helm](#helm) @@ -192,3 +193,7 @@ Our official image supports `amd64` and `arm64`. For `arm32` support there is a popular community maintained alternative: https://hub.docker.com/r/linuxserver/code-server + +## helm + +See [the chart](../ci/helm-chart). From f4d48bc880b681714ada3dd976e1f05bb55624c4 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Fri, 13 Nov 2020 18:34:11 -0500 Subject: [PATCH 198/247] ci: Remove helm validation action in favour of helm kubeval directly --- .github/workflows/helm_validation.yaml | 14 -------------- ci/dev/lint.sh | 1 + ci/images/debian10/Dockerfile | 6 ++++++ 3 files changed, 7 insertions(+), 14 deletions(-) delete mode 100644 .github/workflows/helm_validation.yaml diff --git a/.github/workflows/helm_validation.yaml b/.github/workflows/helm_validation.yaml deleted file mode 100644 index 9ba82ec08..000000000 --- a/.github/workflows/helm_validation.yaml +++ /dev/null @@ -1,14 +0,0 @@ -name: Helm Validation - -on: [pull_request, issues] - -jobs: - validate: - runs-on: ubuntu-latest - steps: - - name: Checkout Repo - uses: actions/checkout@v2 - - name: Kubeval Helm Chart - uses: junior/kubeval-helm-chart-action@0.2.0-alpha.0 - env: - INPUT_KUBERNETES_VERSION: 1.19 diff --git a/ci/dev/lint.sh b/ci/dev/lint.sh index 5f7c549bc..7915262b4 100755 --- a/ci/dev/lint.sh +++ b/ci/dev/lint.sh @@ -8,6 +8,7 @@ main() { stylelint $(git ls-files "*.css") tsc --noEmit shellcheck -e SC2046,SC2164,SC2154,SC1091,SC1090,SC2002 $(git ls-files "*.sh") + helm kubeval ci/helm-chart } main "$@" diff --git a/ci/images/debian10/Dockerfile b/ci/images/debian10/Dockerfile index 108348b65..883bb62e5 100644 --- a/ci/images/debian10/Dockerfile +++ b/ci/images/debian10/Dockerfile @@ -45,4 +45,10 @@ ENV GO111MODULE=on RUN go get mvdan.cc/sh/v3/cmd/shfmt RUN go get github.com/goreleaser/nfpm/cmd/nfpm +RUN VERSION="$(curl -fsSL https://storage.googleapis.com/kubernetes-release/release/stable.txt)" && \ + curl -fsSL "https://storage.googleapis.com/kubernetes-release/release/$VERSION/bin/linux/amd64/kubectl" > /usr/local/bin/kubectl \ + && chmod +x /usr/local/bin/kubectl +RUN curl -fsSL https://raw.githubusercontent.com/helm/helm/master/scripts/get-helm-3 | bash +RUN helm plugin install https://github.com/instrumenta/helm-kubeval + RUN curl -fsSL https://get.docker.com | sh From 40a7c11ce3f0d9c5416eb7d54ab529b80a4847dc Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Fri, 13 Nov 2020 15:32:47 -0500 Subject: [PATCH 199/247] node/routes: Fix error handling We should always send HTML if the user agent expects it. If they do not, they should clearly indicate such via the Accept header. Closes #2297 --- src/node/routes/domainProxy.ts | 4 ++-- src/node/routes/index.ts | 18 +++++++++++------- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/src/node/routes/domainProxy.ts b/src/node/routes/domainProxy.ts index 25745955e..6b527255a 100644 --- a/src/node/routes/domainProxy.ts +++ b/src/node/routes/domainProxy.ts @@ -48,8 +48,8 @@ router.all("*", (req, res, next) => { // Assume anything that explicitly accepts text/html is a user browsing a // page (as opposed to an xhr request). Don't use `req.accepts()` since // *every* request that I've seen (in Firefox and Chromium at least) - // includes `*/*` making it always truthy. - if (typeof req.headers.accepts === "string" && req.headers.accepts.split(",").includes("text/html")) { + // includes `*/*` making it always truthy. Even for css/javascript. + if (req.headers.accept && req.headers.accept.includes("text/html")) { // Let the login through. if (/\/login\/?/.test(req.path)) { return next() diff --git a/src/node/routes/index.ts b/src/node/routes/index.ts index 2c54917da..5bb62d1ee 100644 --- a/src/node/routes/index.ts +++ b/src/node/routes/index.ts @@ -125,7 +125,7 @@ export const register = async ( throw new HttpError("Not Found", HttpCode.NotFound) }) - const errorHandler: express.ErrorRequestHandler = async (err, req, res) => { + const errorHandler: express.ErrorRequestHandler = async (err, req, res, next) => { if (err.code === "ENOENT" || err.code === "EISDIR") { err.status = HttpCode.NotFound } @@ -133,12 +133,11 @@ export const register = async ( const status = err.status ?? err.statusCode ?? 500 res.status(status) - if (req.accepts("application/json")) { - res.json({ - error: err.message, - ...(err.details || {}), - }) - } else { + // Assume anything that explicitly accepts text/html is a user browsing a + // page (as opposed to an xhr request). Don't use `req.accepts()` since + // *every* request that I've seen (in Firefox and Chromium at least) + // includes `*/*` making it always truthy. Even for css/javascript. + if (req.headers.accept && req.headers.accept.includes("text/html")) { const resourcePath = path.resolve(rootPath, "src/browser/pages/error.html") res.set("Content-Type", getMediaMime(resourcePath)) const content = await fs.readFile(resourcePath, "utf8") @@ -148,6 +147,11 @@ export const register = async ( .replace(/{{ERROR_HEADER}}/g, status) .replace(/{{ERROR_BODY}}/g, err.message), ) + } else { + res.json({ + error: err.message, + ...(err.details || {}), + }) } } From a37572d92d0e87d00e521637b3e5a3809f60490d Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Fri, 13 Nov 2020 16:28:21 -0500 Subject: [PATCH 200/247] ci: Disable no-unused-vars for function args See previous commit for failure introduced. --- .eslintrc.yaml | 3 ++ package.json | 4 +-- yarn.lock | 95 ++++++++++++++++++++++++++------------------------ 3 files changed, 55 insertions(+), 47 deletions(-) diff --git a/.eslintrc.yaml b/.eslintrc.yaml index 231c2dbca..738b0ee20 100644 --- a/.eslintrc.yaml +++ b/.eslintrc.yaml @@ -19,6 +19,9 @@ extends: - prettier/@typescript-eslint # Remove conflicts again. rules: + # Sometimes you need to add args to implement a function signature even + # if they are unused. + "@typescript-eslint/no-unused-vars": ["error", { "args": "none" }] # For overloads. no-dupe-class-members: off "@typescript-eslint/no-use-before-define": off diff --git a/package.json b/package.json index 59b23573c..1c3428f26 100644 --- a/package.json +++ b/package.json @@ -47,8 +47,8 @@ "@types/tar-fs": "^2.0.0", "@types/tar-stream": "^2.1.0", "@types/ws": "^7.2.6", - "@typescript-eslint/eslint-plugin": "^3.10.1", - "@typescript-eslint/parser": "^3.10.1", + "@typescript-eslint/eslint-plugin": "^4.7.0", + "@typescript-eslint/parser": "^4.7.0", "doctoc": "^1.4.0", "eslint": "^7.7.0", "eslint-config-prettier": "^6.0.0", diff --git a/yarn.lock b/yarn.lock index 034efecce..c2bae13fc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1040,11 +1040,6 @@ resolved "https://registry.yarnpkg.com/@types/cookiejar/-/cookiejar-2.1.2.tgz#66ad9331f63fe8a3d3d9d8c6e3906dd10f6446e8" integrity sha512-t73xJJrvdTjXrn4jLS9VSGRbz0nUY3cl2DMGDU48lKl+HR9dbbjW2A9r3g40VA++mQpy6uuHg33gy7du2BKpog== -"@types/eslint-visitor-keys@^1.0.0": - version "1.0.0" - resolved "https://registry.yarnpkg.com/@types/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz#1ee30d79544ca84d68d4b3cdb0af4f205663dd2d" - integrity sha512-OCutwjDZ4aFS6PB1UZ988C4YgwlBHJd6wCeQqaLdmadZ/7e+w79+hbMUFC1QXDNCmdyoRfAFdm0RypzwR+Qpag== - "@types/express-serve-static-core@*": version "4.17.13" resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.17.13.tgz#d9af025e925fc8b089be37423b8d1eac781be084" @@ -1219,65 +1214,75 @@ dependencies: "@types/node" "*" -"@typescript-eslint/eslint-plugin@^3.10.1": - version "3.10.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-3.10.1.tgz#7e061338a1383f59edc204c605899f93dc2e2c8f" - integrity sha512-PQg0emRtzZFWq6PxBcdxRH3QIQiyFO3WCVpRL3fgj5oQS3CDs3AeAKfv4DxNhzn8ITdNJGJ4D3Qw8eAJf3lXeQ== +"@typescript-eslint/eslint-plugin@^4.7.0": + version "4.7.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.7.0.tgz#85c9bbda00c0cb604d3c241f7bc7fb171a2d3479" + integrity sha512-li9aiSVBBd7kU5VlQlT1AqP0uWGDK6JYKUQ9cVDnOg34VNnd9t4jr0Yqc/bKxJr/tDCPDaB4KzoSFN9fgVxe/Q== dependencies: - "@typescript-eslint/experimental-utils" "3.10.1" + "@typescript-eslint/experimental-utils" "4.7.0" + "@typescript-eslint/scope-manager" "4.7.0" debug "^4.1.1" functional-red-black-tree "^1.0.1" regexpp "^3.0.0" semver "^7.3.2" tsutils "^3.17.1" -"@typescript-eslint/experimental-utils@3.10.1": - version "3.10.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-3.10.1.tgz#e179ffc81a80ebcae2ea04e0332f8b251345a686" - integrity sha512-DewqIgscDzmAfd5nOGe4zm6Bl7PKtMG2Ad0KG8CUZAHlXfAKTF9Ol5PXhiMh39yRL2ChRH1cuuUGOcVyyrhQIw== +"@typescript-eslint/experimental-utils@4.7.0": + version "4.7.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-4.7.0.tgz#8d1058c38bec3d3bbd9c898a1c32318d80faf3c5" + integrity sha512-cymzovXAiD4EF+YoHAB5Oh02MpnXjvyaOb+v+BdpY7lsJXZQN34oIETeUwVT2XfV9rSNpXaIcknDLfupO/tUoA== dependencies: "@types/json-schema" "^7.0.3" - "@typescript-eslint/types" "3.10.1" - "@typescript-eslint/typescript-estree" "3.10.1" + "@typescript-eslint/scope-manager" "4.7.0" + "@typescript-eslint/types" "4.7.0" + "@typescript-eslint/typescript-estree" "4.7.0" eslint-scope "^5.0.0" eslint-utils "^2.0.0" -"@typescript-eslint/parser@^3.10.1": - version "3.10.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-3.10.1.tgz#1883858e83e8b442627e1ac6f408925211155467" - integrity sha512-Ug1RcWcrJP02hmtaXVS3axPPTTPnZjupqhgj+NnZ6BCkwSImWk/283347+x9wN+lqOdK9Eo3vsyiyDHgsmiEJw== +"@typescript-eslint/parser@^4.7.0": + version "4.7.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-4.7.0.tgz#44bdab0f788b478178368baa65d3365fdc63da1c" + integrity sha512-+meGV8bMP1sJHBI2AFq1GeTwofcGiur8LoIr6v+rEmD9knyCqDlrQcFHR0KDDfldHIFDU/enZ53fla6ReF4wRw== dependencies: - "@types/eslint-visitor-keys" "^1.0.0" - "@typescript-eslint/experimental-utils" "3.10.1" - "@typescript-eslint/types" "3.10.1" - "@typescript-eslint/typescript-estree" "3.10.1" - eslint-visitor-keys "^1.1.0" - -"@typescript-eslint/types@3.10.1": - version "3.10.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-3.10.1.tgz#1d7463fa7c32d8a23ab508a803ca2fe26e758727" - integrity sha512-+3+FCUJIahE9q0lDi1WleYzjCwJs5hIsbugIgnbB+dSCYUxl8L6PwmsyOPFZde2hc1DlTo/xnkOgiTLSyAbHiQ== - -"@typescript-eslint/typescript-estree@3.10.1": - version "3.10.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-3.10.1.tgz#fd0061cc38add4fad45136d654408569f365b853" - integrity sha512-QbcXOuq6WYvnB3XPsZpIwztBoquEYLXh2MtwVU+kO8jgYCiv4G5xrSP/1wg4tkvrEE+esZVquIPX/dxPlePk1w== - dependencies: - "@typescript-eslint/types" "3.10.1" - "@typescript-eslint/visitor-keys" "3.10.1" + "@typescript-eslint/scope-manager" "4.7.0" + "@typescript-eslint/types" "4.7.0" + "@typescript-eslint/typescript-estree" "4.7.0" debug "^4.1.1" - glob "^7.1.6" + +"@typescript-eslint/scope-manager@4.7.0": + version "4.7.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-4.7.0.tgz#2115526085fb72723ccdc1eeae75dec7126220ed" + integrity sha512-ILITvqwDJYbcDCROj6+Ob0oCKNg3SH46iWcNcTIT9B5aiVssoTYkhKjxOMNzR1F7WSJkik4zmuqve5MdnA0DyA== + dependencies: + "@typescript-eslint/types" "4.7.0" + "@typescript-eslint/visitor-keys" "4.7.0" + +"@typescript-eslint/types@4.7.0": + version "4.7.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-4.7.0.tgz#5e95ef5c740f43d942542b35811f87b62fccca69" + integrity sha512-uLszFe0wExJc+I7q0Z/+BnP7wao/kzX0hB5vJn4LIgrfrMLgnB2UXoReV19lkJQS1a1mHWGGODSxnBx6JQC3Sg== + +"@typescript-eslint/typescript-estree@4.7.0": + version "4.7.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-4.7.0.tgz#539531167f05ba20eb0b6785567076679e29d393" + integrity sha512-5XZRQznD1MfUmxu1t8/j2Af4OxbA7EFU2rbo0No7meb46eHgGkSieFdfV6omiC/DGIBhH9H9gXn7okBbVOm8jw== + dependencies: + "@typescript-eslint/types" "4.7.0" + "@typescript-eslint/visitor-keys" "4.7.0" + debug "^4.1.1" + globby "^11.0.1" is-glob "^4.0.1" lodash "^4.17.15" semver "^7.3.2" tsutils "^3.17.1" -"@typescript-eslint/visitor-keys@3.10.1": - version "3.10.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-3.10.1.tgz#cd4274773e3eb63b2e870ac602274487ecd1e931" - integrity sha512-9JgC82AaQeglebjZMgYR5wgmfUdUc+EitGUUMW8u2nDckaeimzW+VsoLV6FoimPv2id3VQzfjwBxEMVz08ameQ== +"@typescript-eslint/visitor-keys@4.7.0": + version "4.7.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-4.7.0.tgz#6783824f22acfc49e754970ed21b88ac03b80e6f" + integrity sha512-aDJDWuCRsf1lXOtignlfiPODkzSxxop7D0rZ91L6ZuMlcMCSh0YyK+gAfo5zN/ih6WxMwhoXgJWC3cWQdaKC+A== dependencies: - eslint-visitor-keys "^1.1.0" + "@typescript-eslint/types" "4.7.0" + eslint-visitor-keys "^2.0.0" abab@^2.0.0: version "2.0.5" @@ -3675,7 +3680,7 @@ glob-to-regexp@^0.3.0: resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.3.0.tgz#8c5a1494d2066c570cc3bfe4496175acc4d502ab" integrity sha1-jFoUlNIGbFcMw7/kSWF1rMTVAqs= -glob@7.1.6, glob@^7.0.0, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6: +glob@7.1.6, glob@^7.0.0, glob@^7.1.3, glob@^7.1.4: version "7.1.6" resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6" integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA== From 79e8f3dfdbc0fe1d61a0ae387b5f0dc262464c69 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Fri, 13 Nov 2020 18:42:42 -0500 Subject: [PATCH 201/247] ci: Only use helm kubeval if installed --- ci/dev/lint.sh | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/ci/dev/lint.sh b/ci/dev/lint.sh index 7915262b4..6acae73ad 100755 --- a/ci/dev/lint.sh +++ b/ci/dev/lint.sh @@ -8,7 +8,9 @@ main() { stylelint $(git ls-files "*.css") tsc --noEmit shellcheck -e SC2046,SC2164,SC2154,SC1091,SC1090,SC2002 $(git ls-files "*.sh") - helm kubeval ci/helm-chart + if command -v helm && helm kubeval --help > /dev/null; then + helm kubeval ci/helm-chart + fi } main "$@" From 8bf1bf2c9fe8973292327978a05c06c1017603d6 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Fri, 13 Nov 2020 18:45:13 -0500 Subject: [PATCH 202/247] helm: Use upgrade --install everywhere See @sreya's review --- ci/helm-chart/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ci/helm-chart/README.md b/ci/helm-chart/README.md index 601596c12..6b6bd2b34 100644 --- a/ci/helm-chart/README.md +++ b/ci/helm-chart/README.md @@ -100,7 +100,7 @@ Specify each parameter using the `--set key=value[,key=value]` argument to `helm install`. For example, ```console -$ helm install code-server \ +$ helm upgrade --install code-server \ ci/helm-chart \ --set persistence.enabled=false ``` @@ -111,7 +111,7 @@ Alternatively, a YAML file that specifies the values for the above parameters can be provided while installing the chart. For example, ```console -$ helm install code-server ci/helm-chart -f values.yaml +$ helm upgrade --install code-server ci/helm-chart -f values.yaml ``` > **Tip**: You can use the default [values.yaml](values.yaml) From 9917da068a2d8909ece1b646a6307a073f157b08 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Mon, 16 Nov 2020 10:50:05 -0500 Subject: [PATCH 203/247] v3.7.0 --- README.md | 2 +- ci/README.md | 1 + ci/helm-chart/Chart.yaml | 2 +- ci/helm-chart/README.md | 4 ++-- ci/helm-chart/values.yaml | 2 +- doc/install.md | 14 +++++++------- package.json | 2 +- src/node/plugin.ts | 2 +- test/test-plugin/package.json | 2 +- 9 files changed, 16 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 0e7750039..77553bd62 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,7 @@ We also have an in-depth [setup and configuration](./doc/guide.md) guide. ### Alpha Program 🐣 -We're working on a cloud platform that makes deploying and managing code-server easier. Consider [updating to 3.6.2](https://github.com/cdr/code-server/releases/tag/v3.6.2) and running code-server with our experimental flag `--link` if you don't want to worry about +We're working on a cloud platform that makes deploying and managing code-server easier. Consider [updating to 3.7.0](https://github.com/cdr/code-server/releases/tag/v3.7.0) and running code-server with our experimental flag `--link` if you don't want to worry about - TLS - Authentication diff --git a/ci/README.md b/ci/README.md index 19b7ee8e8..6a8b44c5c 100644 --- a/ci/README.md +++ b/ci/README.md @@ -17,6 +17,7 @@ Make sure you have `$GITHUB_TOKEN` set and [hub](https://github.com/github/hub) 1. Update the version of code-server and make a PR. 1. Update in `package.json` 2. Update in [./doc/install.md](../doc/install.md) + 3. Update in [./ci/helm-chart/README.md](../ci/helm-chart/README.md) 2. GitHub actions will generate the `npm-package`, `release-packages` and `release-images` artifacts. 1. You do not have to wait for these. 3. Run `yarn release:github-draft` to create a GitHub draft release from the template with diff --git a/ci/helm-chart/Chart.yaml b/ci/helm-chart/Chart.yaml index 28f98f000..6b199f5d9 100644 --- a/ci/helm-chart/Chart.yaml +++ b/ci/helm-chart/Chart.yaml @@ -20,4 +20,4 @@ version: 1.0.0 # This is the version number of the application being deployed. This version number should be # incremented each time you make changes to the application. Versions are not expected to # follow Semantic Versioning. They should reflect the version the application is using. -appVersion: 3.6.2 +appVersion: 3.7.0 diff --git a/ci/helm-chart/README.md b/ci/helm-chart/README.md index 6b6bd2b34..c769048af 100644 --- a/ci/helm-chart/README.md +++ b/ci/helm-chart/README.md @@ -1,6 +1,6 @@ # code-server -![Version: 1.0.0](https://img.shields.io/badge/Version-1.0.0-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: 3.6.2](https://img.shields.io/badge/AppVersion-3.6.2-informational?style=flat-square) +![Version: 1.0.0](https://img.shields.io/badge/Version-1.0.0-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: 3.7.0](https://img.shields.io/badge/AppVersion-3.7.0-informational?style=flat-square) [code-server](https://github.com/cdr/code-server) code-server is VS Code running on a remote server, accessible through the browser. @@ -72,7 +72,7 @@ and their default values. | hostnameOverride | string | `""` | | | image.pullPolicy | string | `"Always"` | | | image.repository | string | `"codercom/code-server"` | | -| image.tag | string | `"3.5.0"` | | +| image.tag | string | `"3.7.0"` | | | imagePullSecrets | list | `[]` | | | ingress.enabled | bool | `false` | | | nameOverride | string | `""` | | diff --git a/ci/helm-chart/values.yaml b/ci/helm-chart/values.yaml index e71499bfb..718fe6628 100644 --- a/ci/helm-chart/values.yaml +++ b/ci/helm-chart/values.yaml @@ -6,7 +6,7 @@ replicaCount: 1 image: repository: codercom/code-server - tag: '3.6.2' + tag: '3.7.0' pullPolicy: Always imagePullSecrets: [] diff --git a/doc/install.md b/doc/install.md index 90a14dfdd..2fa480056 100644 --- a/doc/install.md +++ b/doc/install.md @@ -80,8 +80,8 @@ commands presented in the rest of this document. ## Debian, Ubuntu ```bash -curl -fOL https://github.com/cdr/code-server/releases/download/v3.6.1/code-server_3.6.1_amd64.deb -sudo dpkg -i code-server_3.6.1_amd64.deb +curl -fOL https://github.com/cdr/code-server/releases/download/v3.7.0/code-server_3.7.0_amd64.deb +sudo dpkg -i code-server_3.7.0_amd64.deb sudo systemctl enable --now code-server@$USER # Now visit http://127.0.0.1:8080. Your password is in ~/.config/code-server/config.yaml ``` @@ -89,8 +89,8 @@ sudo systemctl enable --now code-server@$USER ## Fedora, CentOS, RHEL, SUSE ```bash -curl -fOL https://github.com/cdr/code-server/releases/download/v3.6.1/code-server-3.6.1-amd64.rpm -sudo rpm -i code-server-3.6.1-amd64.rpm +curl -fOL https://github.com/cdr/code-server/releases/download/v3.7.0/code-server-3.7.0-amd64.rpm +sudo rpm -i code-server-3.7.0-amd64.rpm sudo systemctl enable --now code-server@$USER # Now visit http://127.0.0.1:8080. Your password is in ~/.config/code-server/config.yaml ``` @@ -159,10 +159,10 @@ Here is an example script for installing and using a standalone `code-server` re ```bash mkdir -p ~/.local/lib ~/.local/bin -curl -fL https://github.com/cdr/code-server/releases/download/v3.6.1/code-server-3.6.1-linux-amd64.tar.gz \ +curl -fL https://github.com/cdr/code-server/releases/download/v3.7.0/code-server-3.7.0-linux-amd64.tar.gz \ | tar -C ~/.local/lib -xz -mv ~/.local/lib/code-server-3.6.1-linux-amd64 ~/.local/lib/code-server-3.6.1 -ln -s ~/.local/lib/code-server-3.6.1/bin/code-server ~/.local/bin/code-server +mv ~/.local/lib/code-server-3.7.0-linux-amd64 ~/.local/lib/code-server-3.7.0 +ln -s ~/.local/lib/code-server-3.7.0/bin/code-server ~/.local/bin/code-server PATH="~/.local/bin:$PATH" code-server # Now visit http://127.0.0.1:8080. Your password is in ~/.config/code-server/config.yaml diff --git a/package.json b/package.json index 1c3428f26..e1a8740f0 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "code-server", "license": "MIT", - "version": "3.6.2", + "version": "3.7.0", "description": "Run VS Code on a remote server.", "homepage": "https://github.com/cdr/code-server", "bugs": { diff --git a/src/node/plugin.ts b/src/node/plugin.ts index 899aa1111..2c0519ac1 100644 --- a/src/node/plugin.ts +++ b/src/node/plugin.ts @@ -185,7 +185,7 @@ export class PluginAPI { if (!packageJSON.engines || !packageJSON.engines["code-server"]) { throw new Error(`plugin package.json missing code-server range like: "engines": { - "code-server": "^3.6.0" + "code-server": "^3.7.0" } `) } diff --git a/test/test-plugin/package.json b/test/test-plugin/package.json index c1f2e6980..55c474e3d 100644 --- a/test/test-plugin/package.json +++ b/test/test-plugin/package.json @@ -3,7 +3,7 @@ "name": "test-plugin", "version": "1.0.0", "engines": { - "code-server": "^3.6.0" + "code-server": "^3.7.0" }, "main": "out/index.js", "devDependencies": { From f64599b94df56cb6186dd951f4e6c054aa4c7e78 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Mon, 16 Nov 2020 13:09:57 -0500 Subject: [PATCH 204/247] ci: Update standalone build test ms-toolsai.jupyter is now a dependency of ms-python and is installed along with it. --- ci/build/test-standalone-release.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ci/build/test-standalone-release.sh b/ci/build/test-standalone-release.sh index 0344ea39f..5f5656486 100755 --- a/ci/build/test-standalone-release.sh +++ b/ci/build/test-standalone-release.sh @@ -15,7 +15,8 @@ main() { ./release-standalone/bin/code-server --extensions-dir "$EXTENSIONS_DIR" --install-extension ms-python.python local installed_extensions installed_extensions="$(./release-standalone/bin/code-server --extensions-dir "$EXTENSIONS_DIR" --list-extensions 2>&1)" - if [[ $installed_extensions != "ms-python.python" ]]; then + # We use grep as ms-python.python may have dependency extensions that change. + if ! echo "$installed_extensions" | grep -q "ms-python.python"; then echo "Unexpected output from listing extensions:" echo "$installed_extensions" exit 1 From ac09aa6ea8f12fc94f13b8421e574d14c9ec5993 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Mon, 16 Nov 2020 15:40:07 -0500 Subject: [PATCH 205/247] doc/ipad.md: Fix TOC --- doc/ipad.md | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/doc/ipad.md b/doc/ipad.md index 1bda0bd44..9c1b4ac84 100644 --- a/doc/ipad.md +++ b/doc/ipad.md @@ -2,14 +2,11 @@ # iPad -- [iPad](#ipad) - - [Known Issues](#known-issues) - - [How to access code-server with a self signed certificate on iPad?](#how-to-access-code-server-with-a-self-signed-certificate-on-ipad) +- [Known Issues](#known-issues) +- [How to access code-server with a self signed certificate on iPad?](#how-to-access-code-server-with-a-self-signed-certificate-on-ipad) -# iPad - ## Known Issues - Getting self signed certificates certificates to work is involved, see below. From 40e1f066ffba62f84f2ece40d5dfe4a3a7f05955 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Mon, 16 Nov 2020 16:56:53 -0500 Subject: [PATCH 206/247] ci: Improve release template (#2311) --- ci/build/release-github-draft.sh | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/ci/build/release-github-draft.sh b/ci/build/release-github-draft.sh index e345d04f6..d311dbb65 100755 --- a/ci/build/release-github-draft.sh +++ b/ci/build/release-github-draft.sh @@ -15,7 +15,13 @@ v$VERSION VS Code v$(vscode_version) -- Summarize changes here with references to issues +# New Features + - ⭐ Summarize new features here with references to issues + +## Bug Fixes + - ⭐ Summarize bug fixes here with references to issues + +Cheers! 🍻 EOF } From ccc519ecbd5a7d67c50c6df306bb5397e674977b Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Mon, 16 Nov 2020 16:47:05 -0500 Subject: [PATCH 207/247] ci: Pin nfpm to v1.9.0 Closes #2310 --- ci/images/centos7/Dockerfile | 2 +- ci/images/debian10/Dockerfile | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ci/images/centos7/Dockerfile b/ci/images/centos7/Dockerfile index acc6bbf6b..a37e590bb 100644 --- a/ci/images/centos7/Dockerfile +++ b/ci/images/centos7/Dockerfile @@ -27,6 +27,6 @@ ENV PATH=/usr/local/go/bin:$GOPATH/bin:$PATH # Install Go dependencies ENV GO111MODULE=on RUN go get mvdan.cc/sh/v3/cmd/shfmt -RUN go get github.com/goreleaser/nfpm/cmd/nfpm +RUN go get github.com/goreleaser/nfpm/cmd/nfpm@v1.9.0 RUN curl -fsSL https://get.docker.com | sh diff --git a/ci/images/debian10/Dockerfile b/ci/images/debian10/Dockerfile index 883bb62e5..5e4a5f859 100644 --- a/ci/images/debian10/Dockerfile +++ b/ci/images/debian10/Dockerfile @@ -43,7 +43,7 @@ ENV PATH=/usr/local/go/bin:$GOPATH/bin:$PATH # Install Go dependencies ENV GO111MODULE=on RUN go get mvdan.cc/sh/v3/cmd/shfmt -RUN go get github.com/goreleaser/nfpm/cmd/nfpm +RUN go get github.com/goreleaser/nfpm/cmd/nfpm@v1.9.0 RUN VERSION="$(curl -fsSL https://storage.googleapis.com/kubernetes-release/release/stable.txt)" && \ curl -fsSL "https://storage.googleapis.com/kubernetes-release/release/$VERSION/bin/linux/amd64/kubectl" > /usr/local/bin/kubectl \ From 2bf91ff6a6d1f29a8a93f97684177ccf5388cf34 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Mon, 16 Nov 2020 17:18:12 -0500 Subject: [PATCH 208/247] v3.7.1 --- README.md | 3 ++- ci/helm-chart/Chart.yaml | 2 +- ci/helm-chart/README.md | 4 ++-- ci/helm-chart/values.yaml | 2 +- doc/install.md | 14 +++++++------- package.json | 2 +- 6 files changed, 14 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 77553bd62..dd5532d16 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,8 @@ We also have an in-depth [setup and configuration](./doc/guide.md) guide. ### Alpha Program 🐣 -We're working on a cloud platform that makes deploying and managing code-server easier. Consider [updating to 3.7.0](https://github.com/cdr/code-server/releases/tag/v3.7.0) and running code-server with our experimental flag `--link` if you don't want to worry about +We're working on a cloud platform that makes deploying and managing code-server easier. +Consider updating to the latest version and running code-server with our experimental flag `--link` if you don't want to worry about - TLS - Authentication diff --git a/ci/helm-chart/Chart.yaml b/ci/helm-chart/Chart.yaml index 6b199f5d9..986b0395f 100644 --- a/ci/helm-chart/Chart.yaml +++ b/ci/helm-chart/Chart.yaml @@ -20,4 +20,4 @@ version: 1.0.0 # This is the version number of the application being deployed. This version number should be # incremented each time you make changes to the application. Versions are not expected to # follow Semantic Versioning. They should reflect the version the application is using. -appVersion: 3.7.0 +appVersion: 3.7.1 diff --git a/ci/helm-chart/README.md b/ci/helm-chart/README.md index c769048af..f944f48fa 100644 --- a/ci/helm-chart/README.md +++ b/ci/helm-chart/README.md @@ -1,6 +1,6 @@ # code-server -![Version: 1.0.0](https://img.shields.io/badge/Version-1.0.0-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: 3.7.0](https://img.shields.io/badge/AppVersion-3.7.0-informational?style=flat-square) +![Version: 1.0.0](https://img.shields.io/badge/Version-1.0.0-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: 3.7.1](https://img.shields.io/badge/AppVersion-3.7.1-informational?style=flat-square) [code-server](https://github.com/cdr/code-server) code-server is VS Code running on a remote server, accessible through the browser. @@ -72,7 +72,7 @@ and their default values. | hostnameOverride | string | `""` | | | image.pullPolicy | string | `"Always"` | | | image.repository | string | `"codercom/code-server"` | | -| image.tag | string | `"3.7.0"` | | +| image.tag | string | `"3.7.1"` | | | imagePullSecrets | list | `[]` | | | ingress.enabled | bool | `false` | | | nameOverride | string | `""` | | diff --git a/ci/helm-chart/values.yaml b/ci/helm-chart/values.yaml index 718fe6628..32f844697 100644 --- a/ci/helm-chart/values.yaml +++ b/ci/helm-chart/values.yaml @@ -6,7 +6,7 @@ replicaCount: 1 image: repository: codercom/code-server - tag: '3.7.0' + tag: '3.7.1' pullPolicy: Always imagePullSecrets: [] diff --git a/doc/install.md b/doc/install.md index 2fa480056..97db9dde5 100644 --- a/doc/install.md +++ b/doc/install.md @@ -80,8 +80,8 @@ commands presented in the rest of this document. ## Debian, Ubuntu ```bash -curl -fOL https://github.com/cdr/code-server/releases/download/v3.7.0/code-server_3.7.0_amd64.deb -sudo dpkg -i code-server_3.7.0_amd64.deb +curl -fOL https://github.com/cdr/code-server/releases/download/v3.7.1/code-server_3.7.1_amd64.deb +sudo dpkg -i code-server_3.7.1_amd64.deb sudo systemctl enable --now code-server@$USER # Now visit http://127.0.0.1:8080. Your password is in ~/.config/code-server/config.yaml ``` @@ -89,8 +89,8 @@ sudo systemctl enable --now code-server@$USER ## Fedora, CentOS, RHEL, SUSE ```bash -curl -fOL https://github.com/cdr/code-server/releases/download/v3.7.0/code-server-3.7.0-amd64.rpm -sudo rpm -i code-server-3.7.0-amd64.rpm +curl -fOL https://github.com/cdr/code-server/releases/download/v3.7.1/code-server-3.7.1-amd64.rpm +sudo rpm -i code-server-3.7.1-amd64.rpm sudo systemctl enable --now code-server@$USER # Now visit http://127.0.0.1:8080. Your password is in ~/.config/code-server/config.yaml ``` @@ -159,10 +159,10 @@ Here is an example script for installing and using a standalone `code-server` re ```bash mkdir -p ~/.local/lib ~/.local/bin -curl -fL https://github.com/cdr/code-server/releases/download/v3.7.0/code-server-3.7.0-linux-amd64.tar.gz \ +curl -fL https://github.com/cdr/code-server/releases/download/v3.7.1/code-server-3.7.1-linux-amd64.tar.gz \ | tar -C ~/.local/lib -xz -mv ~/.local/lib/code-server-3.7.0-linux-amd64 ~/.local/lib/code-server-3.7.0 -ln -s ~/.local/lib/code-server-3.7.0/bin/code-server ~/.local/bin/code-server +mv ~/.local/lib/code-server-3.7.1-linux-amd64 ~/.local/lib/code-server-3.7.1 +ln -s ~/.local/lib/code-server-3.7.1/bin/code-server ~/.local/bin/code-server PATH="~/.local/bin:$PATH" code-server # Now visit http://127.0.0.1:8080. Your password is in ~/.config/code-server/config.yaml diff --git a/package.json b/package.json index e1a8740f0..fe8153a0f 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "code-server", "license": "MIT", - "version": "3.7.0", + "version": "3.7.1", "description": "Run VS Code on a remote server.", "homepage": "https://github.com/cdr/code-server", "bugs": { From c6062c3d0a405be3c48ad2d3c8f69196571c88aa Mon Sep 17 00:00:00 2001 From: piousdeer <31318219+piousdeer@users.noreply.github.com> Date: Wed, 18 Nov 2020 23:41:32 +0700 Subject: [PATCH 209/247] Fix log message (#2331) --- src/node/entry.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/node/entry.ts b/src/node/entry.ts index 2774963f8..b661b1848 100644 --- a/src/node/entry.ts +++ b/src/node/entry.ts @@ -121,7 +121,7 @@ const main = async (args: DefaultedArgs): Promise => { } if (args.cert) { - logger.info(" - Using certificate for HTTPS: ${humanPath(args.cert.value)}") + logger.info(` - Using certificate for HTTPS: ${humanPath(args.cert.value)}`) } else { logger.info(" - Not serving HTTPS") } From 2a3608df533fbac4ec41e0be094614c7684319a7 Mon Sep 17 00:00:00 2001 From: Asher Date: Wed, 18 Nov 2020 12:19:08 -0600 Subject: [PATCH 210/247] Skip heartbeat on /healthz endpoint (#2333) I managed to lose this in the rewrite. Fixes #2327. --- src/node/routes/index.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/node/routes/index.ts b/src/node/routes/index.ts index 5bb62d1ee..8a073a319 100644 --- a/src/node/routes/index.ts +++ b/src/node/routes/index.ts @@ -66,7 +66,11 @@ export const register = async ( app.use(bodyParser.urlencoded({ extended: true })) const common: express.RequestHandler = (req, _, next) => { - heart.beat() + // /healthz|/healthz/ needs to be excluded otherwise health checks will make + // it look like code-server is always in use. + if (!/^\/healthz\/?$/.test(req.url)) { + heart.beat() + } // Add common variables routes can use. req.args = args From d55e06936bab67d1c364fec61dfc5b01c3ff33cc Mon Sep 17 00:00:00 2001 From: Asher Date: Wed, 18 Nov 2020 11:43:25 -0600 Subject: [PATCH 211/247] Split child and parent wrappers I think having them combined and relying on if statements was getting confusing especially if we want to add additional messages with different payloads (which will soon be the case). --- src/node/entry.ts | 13 ++- src/node/vscode.ts | 4 +- src/node/wrapper.ts | 232 ++++++++++++++++++++++++++------------------ 3 files changed, 146 insertions(+), 103 deletions(-) diff --git a/src/node/entry.ts b/src/node/entry.ts index b661b1848..8fcacc0d2 100644 --- a/src/node/entry.ts +++ b/src/node/entry.ts @@ -19,7 +19,7 @@ import { coderCloudBind } from "./coder-cloud" import { commit, version } from "./constants" import { register } from "./routes" import { humanPath, isFile, open } from "./util" -import { ipcMain, WrapperProcess } from "./wrapper" +import { isChild, wrapper } from "./wrapper" export const runVsCodeCli = (args: DefaultedArgs): void => { logger.debug("forking vs code cli...") @@ -137,7 +137,7 @@ const main = async (args: DefaultedArgs): Promise => { logger.info(" - Connected to cloud agent") } catch (err) { logger.error(err.message) - ipcMain.exit(1) + wrapper.exit(1) } } @@ -161,9 +161,9 @@ async function entry(): Promise { // There's no need to check flags like --help or to spawn in an existing // instance for the child process because these would have already happened in // the parent and the child wouldn't have been spawned. - if (ipcMain.isChild) { - await ipcMain.handshake() - ipcMain.preventExit() + if (isChild(wrapper)) { + await wrapper.handshake() + wrapper.preventExit() return main(args) } @@ -201,11 +201,10 @@ async function entry(): Promise { return openInExistingInstance(args, socketPath) } - const wrapper = new WrapperProcess(require("../../package.json").version) return wrapper.start() } entry().catch((error) => { logger.error(error.message) - ipcMain.exit(error) + wrapper.exit(error) }) diff --git a/src/node/vscode.ts b/src/node/vscode.ts index c3da8a24d..23282443e 100644 --- a/src/node/vscode.ts +++ b/src/node/vscode.ts @@ -8,7 +8,7 @@ import { rootPath } from "./constants" import { settings } from "./settings" import { SocketProxyProvider } from "./socket" import { isFile } from "./util" -import { ipcMain } from "./wrapper" +import { wrapper } from "./wrapper" export class VscodeProvider { public readonly serverRootPath: string @@ -20,7 +20,7 @@ export class VscodeProvider { public constructor() { this.vsRootPath = path.resolve(rootPath, "lib/vscode") this.serverRootPath = path.join(this.vsRootPath, "out/vs/server") - ipcMain.onDispose(() => this.dispose()) + wrapper.onDispose(() => this.dispose()) } public async dispose(): Promise { diff --git a/src/node/wrapper.ts b/src/node/wrapper.ts index 2e8c51cd0..88035efb0 100644 --- a/src/node/wrapper.ts +++ b/src/node/wrapper.ts @@ -1,4 +1,4 @@ -import { field, logger } from "@coder/logger" +import { Logger, field, logger } from "@coder/logger" import * as cp from "child_process" import * as path from "path" import * as rfs from "rotating-file-stream" @@ -14,9 +14,9 @@ interface RelaunchMessage { version: string } -export type Message = RelaunchMessage | HandshakeMessage +type Message = RelaunchMessage | HandshakeMessage -export class ProcessError extends Error { +class ProcessError extends Error { public constructor(message: string, public readonly code: number | undefined) { super(message) this.name = this.constructor.name @@ -25,16 +25,26 @@ export class ProcessError extends Error { } /** - * Allows the wrapper and inner processes to communicate. + * Wrapper around a process that tries to gracefully exit when a process exits + * and provides a way to prevent `process.exit`. */ -export class IpcMain { - private readonly _onMessage = new Emitter() - public readonly onMessage = this._onMessage.event - private readonly _onDispose = new Emitter() - public readonly onDispose = this._onDispose.event - public readonly processExit: (code?: number) => never = process.exit +abstract class Process { + /** + * Emit this to trigger a graceful exit. + */ + protected readonly _onDispose = new Emitter() - public constructor(private readonly parentPid?: number) { + /** + * Emitted when the process is about to be disposed. + */ + public readonly onDispose = this._onDispose.event + + /** + * Uniquely named logger for the process. + */ + public abstract logger: Logger + + public constructor() { process.on("SIGINT", () => this._onDispose.emit("SIGINT")) process.on("SIGTERM", () => this._onDispose.emit("SIGTERM")) process.on("exit", () => this._onDispose.emit(undefined)) @@ -43,42 +53,27 @@ export class IpcMain { // Remove listeners to avoid possibly triggering disposal again. process.removeAllListeners() - // Try waiting for other handlers run first then exit. - logger.debug(`${parentPid ? "inner process" : "wrapper"} ${process.pid} disposing`, field("code", signal)) + // Try waiting for other handlers to run first then exit. + this.logger.debug("disposing", field("code", signal)) wait.then(() => this.exit(0)) setTimeout(() => this.exit(0), 5000) }) - - // Kill the inner process if the parent dies. This is for the case where the - // parent process is forcefully terminated and cannot clean up. - if (parentPid) { - setInterval(() => { - try { - // process.kill throws an exception if the process doesn't exist. - process.kill(parentPid, 0) - } catch (_) { - // Consider this an error since it should have been able to clean up - // the child process unless it was forcefully killed. - logger.error(`parent process ${parentPid} died`) - this._onDispose.emit(undefined) - } - }, 5000) - } } /** - * Ensure we control when the process exits. + * Ensure control over when the process exits. */ public preventExit(): void { - process.exit = function (code?: number) { - logger.warn(`process.exit() was prevented: ${code || "unknown code"}.`) - } as (code?: number) => never + ;(process.exit as any) = (code?: number) => { + this.logger.warn(`process.exit() was prevented: ${code || "unknown code"}.`) + } } - public get isChild(): boolean { - return typeof this.parentPid !== "undefined" - } + private readonly processExit: (code?: number) => never = process.exit + /** + * Will always exit even if normal exit is being prevented. + */ public exit(error?: number | ProcessError): never { if (error && typeof error !== "number") { this.processExit(typeof error.code === "number" ? error.code : 1) @@ -86,47 +81,61 @@ export class IpcMain { this.processExit(error) } } +} - public handshake(child?: cp.ChildProcess): Promise { - return new Promise((resolve, reject) => { - const target = child || process +/** + * Child process that will clean up after itself if the parent goes away and can + * perform a handshake with the parent and ask it to relaunch. + */ +class ChildProcess extends Process { + public logger = logger.named(`child:${process.pid}`) + + public constructor(private readonly parentPid: number) { + super() + + // Kill the inner process if the parent dies. This is for the case where the + // parent process is forcefully terminated and cannot clean up. + setInterval(() => { + try { + // process.kill throws an exception if the process doesn't exist. + process.kill(this.parentPid, 0) + } catch (_) { + // Consider this an error since it should have been able to clean up + // the child process unless it was forcefully killed. + this.logger.error(`parent process ${parentPid} died`) + this._onDispose.emit(undefined) + } + }, 5000) + } + + /** + * Initiate the handshake and wait for a response from the parent. + */ + public handshake(): Promise { + return new Promise((resolve) => { const onMessage = (message: Message): void => { - logger.debug( - `${child ? "wrapper" : "inner process"} ${process.pid} received message from ${ - child ? child.pid : this.parentPid - }`, - field("message", message), - ) + logger.debug(`received message from ${this.parentPid}`, field("message", message)) if (message.type === "handshake") { - target.removeListener("message", onMessage) - target.on("message", (msg) => this._onMessage.emit(msg)) - // The wrapper responds once the inner process starts the handshake. - if (child) { - if (!target.send) { - throw new Error("child not spawned with IPC") - } - target.send({ type: "handshake" }) - } + process.removeListener("message", onMessage) resolve() } } - target.on("message", onMessage) - if (child) { - child.once("error", reject) - child.once("exit", (code) => { - reject(new ProcessError(`Unexpected exit with code ${code}`, code !== null ? code : undefined)) - }) - } else { - // The inner process initiates the handshake. - this.send({ type: "handshake" }) - } + // Initiate the handshake and wait for the reply. + process.on("message", onMessage) + this.send({ type: "handshake" }) }) } + /** + * Notify the parent process that it should relaunch the child. + */ public relaunch(version: string): void { this.send({ type: "relaunch", version }) } + /** + * Send a message to the parent. + */ private send(message: Message): void { if (!process.send) { throw new Error("not spawned with IPC") @@ -135,29 +144,30 @@ export class IpcMain { } } -/** - * Channel for communication between the child and parent processes. - */ -export const ipcMain = new IpcMain( - typeof process.env.CODE_SERVER_PARENT_PID !== "undefined" ? parseInt(process.env.CODE_SERVER_PARENT_PID) : undefined, -) - export interface WrapperOptions { maxMemory?: number nodeOptions?: string } /** - * Provides a way to wrap a process for the purpose of updating the running - * instance. + * Parent process wrapper that spawns the child process and performs a handshake + * with it. Will relaunch the child if it receives a SIGUSR1 or is asked to by + * the child. If the child otherwise exits the parent will also exit. */ -export class WrapperProcess { - private process?: cp.ChildProcess +export class ParentProcess extends Process { + public logger = logger.named(`parent:${process.pid}`) + + private child?: cp.ChildProcess private started?: Promise private readonly logStdoutStream: rfs.RotatingFileStream private readonly logStderrStream: rfs.RotatingFileStream + protected readonly _onChildMessage = new Emitter() + protected readonly onChildMessage = this._onChildMessage.event + public constructor(private currentVersion: string, private readonly options?: WrapperOptions) { + super() + const opts = { size: "10M", maxFiles: 10, @@ -165,19 +175,19 @@ export class WrapperProcess { this.logStdoutStream = rfs.createStream(path.join(paths.data, "coder-logs", "code-server-stdout.log"), opts) this.logStderrStream = rfs.createStream(path.join(paths.data, "coder-logs", "code-server-stderr.log"), opts) - ipcMain.onDispose(() => { + this.onDispose(() => { this.disposeChild() }) - ipcMain.onMessage((message) => { + this.onChildMessage((message) => { switch (message.type) { case "relaunch": - logger.info(`Relaunching: ${this.currentVersion} -> ${message.version}`) + this.logger.info(`Relaunching: ${this.currentVersion} -> ${message.version}`) this.currentVersion = message.version this.relaunch() break default: - logger.error(`Unrecognized message ${message}`) + this.logger.error(`Unrecognized message ${message}`) break } }) @@ -185,9 +195,9 @@ export class WrapperProcess { private disposeChild(): void { this.started = undefined - if (this.process) { - this.process.removeAllListeners() - this.process.kill() + if (this.child) { + this.child.removeAllListeners() + this.child.kill() } } @@ -196,16 +206,16 @@ export class WrapperProcess { try { await this.start() } catch (error) { - logger.error(error.message) - ipcMain.exit(typeof error.code === "number" ? error.code : 1) + this.logger.error(error.message) + this.exit(typeof error.code === "number" ? error.code : 1) } } public start(): Promise { // If we have a process then we've already bound this. - if (!this.process) { + if (!this.child) { process.on("SIGUSR1", async () => { - logger.info("Received SIGUSR1; hotswapping") + this.logger.info("Received SIGUSR1; hotswapping") this.relaunch() }) } @@ -217,7 +227,7 @@ export class WrapperProcess { private async _start(): Promise { const child = this.spawn() - this.process = child + this.child = child // Log both to stdout and to the log directory. if (child.stdout) { @@ -229,13 +239,13 @@ export class WrapperProcess { child.stderr.pipe(process.stderr) } - logger.debug(`spawned inner process ${child.pid}`) + this.logger.debug(`spawned inner process ${child.pid}`) - await ipcMain.handshake(child) + await this.handshake(child) child.once("exit", (code) => { - logger.debug(`inner process ${child.pid} exited unexpectedly`) - ipcMain.exit(code || 0) + this.logger.debug(`inner process ${child.pid} exited unexpectedly`) + this.exit(code || 0) }) } @@ -256,18 +266,52 @@ export class WrapperProcess { stdio: ["ipc"], }) } + + /** + * Wait for a handshake from the child then reply. + */ + private handshake(child: cp.ChildProcess): Promise { + return new Promise((resolve, reject) => { + const onMessage = (message: Message): void => { + logger.debug(`received message from ${child.pid}`, field("message", message)) + if (message.type === "handshake") { + child.removeListener("message", onMessage) + child.on("message", (msg) => this._onChildMessage.emit(msg)) + child.send({ type: "handshake" }) + resolve() + } + } + child.on("message", onMessage) + child.once("error", reject) + child.once("exit", (code) => { + reject(new ProcessError(`Unexpected exit with code ${code}`, code !== null ? code : undefined)) + }) + }) + } +} + +/** + * Process wrapper. + */ +export const wrapper = + typeof process.env.CODE_SERVER_PARENT_PID !== "undefined" + ? new ChildProcess(parseInt(process.env.CODE_SERVER_PARENT_PID)) + : new ParentProcess(require("../../package.json").version) + +export function isChild(proc: ChildProcess | ParentProcess): proc is ChildProcess { + return proc instanceof ChildProcess } // It's possible that the pipe has closed (for example if you run code-server // --version | head -1). Assume that means we're done. if (!process.stdout.isTTY) { - process.stdout.on("error", () => ipcMain.exit()) + process.stdout.on("error", () => wrapper.exit()) } // Don't let uncaught exceptions crash the process. process.on("uncaughtException", (error) => { - logger.error(`Uncaught exception: ${error.message}`) + wrapper.logger.error(`Uncaught exception: ${error.message}`) if (typeof error.stack !== "undefined") { - logger.error(error.stack) + wrapper.logger.error(error.stack) } }) From 247c4ec77623048e0a08a2e785f78e5972d591de Mon Sep 17 00:00:00 2001 From: Asher Date: Wed, 18 Nov 2020 12:05:24 -0600 Subject: [PATCH 212/247] Move onMessage so it can be used in the wrappers --- src/node/vscode.ts | 66 +++++------------------------ src/node/wrapper.ts | 101 +++++++++++++++++++++++++++++++------------- 2 files changed, 81 insertions(+), 86 deletions(-) diff --git a/src/node/vscode.ts b/src/node/vscode.ts index 23282443e..3c18cdee6 100644 --- a/src/node/vscode.ts +++ b/src/node/vscode.ts @@ -1,4 +1,4 @@ -import { field, logger } from "@coder/logger" +import { logger } from "@coder/logger" import * as cp from "child_process" import * as net from "net" import * as path from "path" @@ -8,13 +8,12 @@ import { rootPath } from "./constants" import { settings } from "./settings" import { SocketProxyProvider } from "./socket" import { isFile } from "./util" -import { wrapper } from "./wrapper" +import { onMessage, wrapper } from "./wrapper" export class VscodeProvider { public readonly serverRootPath: string public readonly vsRootPath: string private _vscode?: Promise - private timeoutInterval = 10000 // 10s, matches VS Code's timeouts. private readonly socketProvider = new SocketProxyProvider() public constructor() { @@ -69,10 +68,13 @@ export class VscodeProvider { vscode, ) - const message = await this.onMessage(vscode, (message): message is ipc.OptionsMessage => { - // There can be parallel initializations so wait for the right ID. - return message.type === "options" && message.id === id - }) + const message = await onMessage( + vscode, + (message): message is ipc.OptionsMessage => { + // There can be parallel initializations so wait for the right ID. + return message.type === "options" && message.id === id + }, + ) return message.options } @@ -104,61 +106,13 @@ export class VscodeProvider { dispose() }) - this._vscode = this.onMessage(vscode, (message): message is ipc.ReadyMessage => { + this._vscode = onMessage(vscode, (message): message is ipc.ReadyMessage => { return message.type === "ready" }).then(() => vscode) return this._vscode } - /** - * Listen to a single message from a process. Reject if the process errors, - * exits, or times out. - * - * `fn` is a function that determines whether the message is the one we're - * waiting for. - */ - private onMessage( - proc: cp.ChildProcess, - fn: (message: ipc.VscodeMessage) => message is T, - ): Promise { - return new Promise((resolve, reject) => { - const cleanup = () => { - proc.off("error", onError) - proc.off("exit", onExit) - proc.off("message", onMessage) - clearTimeout(timeout) - } - - const timeout = setTimeout(() => { - cleanup() - reject(new Error("timed out")) - }, this.timeoutInterval) - - const onError = (error: Error) => { - cleanup() - reject(error) - } - - const onExit = (code: number | null) => { - cleanup() - reject(new Error(`VS Code exited unexpectedly with code ${code}`)) - } - - const onMessage = (message: ipc.VscodeMessage) => { - logger.trace("got message from vscode", field("message", message)) - if (fn(message)) { - cleanup() - resolve(message) - } - } - - proc.on("message", onMessage) - proc.on("error", onError) - proc.on("exit", onExit) - }) - } - /** * VS Code expects a raw socket. It will handle all the web socket frames. */ diff --git a/src/node/wrapper.ts b/src/node/wrapper.ts index 88035efb0..5933725e6 100644 --- a/src/node/wrapper.ts +++ b/src/node/wrapper.ts @@ -5,6 +5,59 @@ import * as rfs from "rotating-file-stream" import { Emitter } from "../common/emitter" import { paths } from "./util" +const timeoutInterval = 10000 // 10s, matches VS Code's timeouts. + +/** + * Listen to a single message from a process. Reject if the process errors, + * exits, or times out. + * + * `fn` is a function that determines whether the message is the one we're + * waiting for. + */ +export function onMessage( + proc: cp.ChildProcess | NodeJS.Process, + fn: (message: M) => message is T, + customLogger?: Logger, +): Promise { + return new Promise((resolve, reject) => { + const cleanup = () => { + proc.off("error", onError) + proc.off("exit", onExit) + proc.off("message", onMessage) + clearTimeout(timeout) + } + + const timeout = setTimeout(() => { + cleanup() + reject(new Error("timed out")) + }, timeoutInterval) + + const onError = (error: Error) => { + cleanup() + reject(error) + } + + const onExit = (code: number) => { + cleanup() + reject(new Error(`exited unexpectedly with code ${code}`)) + } + + const onMessage = (message: M) => { + ;(customLogger || logger).trace("got message", field("message", message)) + if (fn(message)) { + cleanup() + resolve(message) + } + } + + proc.on("message", onMessage) + // NodeJS.Process doesn't have `error` but binding anyway shouldn't break + // anything. It does have `exit` but the types aren't working. + ;(proc as cp.ChildProcess).on("error", onError) + ;(proc as cp.ChildProcess).on("exit", onExit) + }) +} + interface HandshakeMessage { type: "handshake" } @@ -111,19 +164,15 @@ class ChildProcess extends Process { /** * Initiate the handshake and wait for a response from the parent. */ - public handshake(): Promise { - return new Promise((resolve) => { - const onMessage = (message: Message): void => { - logger.debug(`received message from ${this.parentPid}`, field("message", message)) - if (message.type === "handshake") { - process.removeListener("message", onMessage) - resolve() - } - } - // Initiate the handshake and wait for the reply. - process.on("message", onMessage) - this.send({ type: "handshake" }) - }) + public async handshake(): Promise { + this.send({ type: "handshake" }) + await onMessage( + process, + (message): message is HandshakeMessage => { + return message.type === "handshake" + }, + this.logger, + ) } /** @@ -270,23 +319,15 @@ export class ParentProcess extends Process { /** * Wait for a handshake from the child then reply. */ - private handshake(child: cp.ChildProcess): Promise { - return new Promise((resolve, reject) => { - const onMessage = (message: Message): void => { - logger.debug(`received message from ${child.pid}`, field("message", message)) - if (message.type === "handshake") { - child.removeListener("message", onMessage) - child.on("message", (msg) => this._onChildMessage.emit(msg)) - child.send({ type: "handshake" }) - resolve() - } - } - child.on("message", onMessage) - child.once("error", reject) - child.once("exit", (code) => { - reject(new ProcessError(`Unexpected exit with code ${code}`, code !== null ? code : undefined)) - }) - }) + private async handshake(child: cp.ChildProcess): Promise { + await onMessage( + child, + (message): message is HandshakeMessage => { + return message.type === "handshake" + }, + this.logger, + ) + child.send({ type: "handshake" }) } } From 016daf2fdd5056b29b6f80ab9fbeac55b218e2de Mon Sep 17 00:00:00 2001 From: Asher Date: Wed, 18 Nov 2020 13:01:46 -0600 Subject: [PATCH 213/247] Parse arguments once Fixes #2316. --- src/node/entry.ts | 17 +++++++------ src/node/wrapper.ts | 61 ++++++++++++++++++++++++++++++--------------- 2 files changed, 51 insertions(+), 27 deletions(-) diff --git a/src/node/entry.ts b/src/node/entry.ts index 8fcacc0d2..9aa69a65f 100644 --- a/src/node/entry.ts +++ b/src/node/entry.ts @@ -154,19 +154,22 @@ const main = async (args: DefaultedArgs): Promise => { } async function entry(): Promise { - const cliArgs = parse(process.argv.slice(2)) - const configArgs = await readConfigFile(cliArgs.config) - const args = await setDefaults(cliArgs, configArgs) - // There's no need to check flags like --help or to spawn in an existing // instance for the child process because these would have already happened in - // the parent and the child wouldn't have been spawned. + // the parent and the child wouldn't have been spawned. We also get the + // arguments from the parent so we don't have to parse twice and to account + // for environment manipulation (like how PASSWORD gets removed to avoid + // leaking to child processes). if (isChild(wrapper)) { - await wrapper.handshake() + const args = await wrapper.handshake() wrapper.preventExit() return main(args) } + const cliArgs = parse(process.argv.slice(2)) + const configArgs = await readConfigFile(cliArgs.config) + const args = await setDefaults(cliArgs, configArgs) + if (args.help) { console.log("code-server", version, commit) console.log("") @@ -201,7 +204,7 @@ async function entry(): Promise { return openInExistingInstance(args, socketPath) } - return wrapper.start() + return wrapper.start(args) } entry().catch((error) => { diff --git a/src/node/wrapper.ts b/src/node/wrapper.ts index 5933725e6..b63d60579 100644 --- a/src/node/wrapper.ts +++ b/src/node/wrapper.ts @@ -1,8 +1,9 @@ -import { Logger, field, logger } from "@coder/logger" +import { field, Logger, logger } from "@coder/logger" import * as cp from "child_process" import * as path from "path" import * as rfs from "rotating-file-stream" import { Emitter } from "../common/emitter" +import { DefaultedArgs } from "./cli" import { paths } from "./util" const timeoutInterval = 10000 // 10s, matches VS Code's timeouts. @@ -58,7 +59,12 @@ export function onMessage( }) } -interface HandshakeMessage { +interface ParentHandshakeMessage { + type: "handshake" + args: DefaultedArgs +} + +interface ChildHandshakeMessage { type: "handshake" } @@ -67,7 +73,8 @@ interface RelaunchMessage { version: string } -type Message = RelaunchMessage | HandshakeMessage +type ChildMessage = RelaunchMessage | ChildHandshakeMessage +type ParentMessage = ParentHandshakeMessage class ProcessError extends Error { public constructor(message: string, public readonly code: number | undefined) { @@ -164,15 +171,16 @@ class ChildProcess extends Process { /** * Initiate the handshake and wait for a response from the parent. */ - public async handshake(): Promise { + public async handshake(): Promise { this.send({ type: "handshake" }) - await onMessage( + const message = await onMessage( process, - (message): message is HandshakeMessage => { + (message): message is ParentHandshakeMessage => { return message.type === "handshake" }, this.logger, ) + return message.args } /** @@ -185,7 +193,7 @@ class ChildProcess extends Process { /** * Send a message to the parent. */ - private send(message: Message): void { + private send(message: ChildMessage): void { if (!process.send) { throw new Error("not spawned with IPC") } @@ -211,12 +219,19 @@ export class ParentProcess extends Process { private readonly logStdoutStream: rfs.RotatingFileStream private readonly logStderrStream: rfs.RotatingFileStream - protected readonly _onChildMessage = new Emitter() + protected readonly _onChildMessage = new Emitter() protected readonly onChildMessage = this._onChildMessage.event + private args?: DefaultedArgs + public constructor(private currentVersion: string, private readonly options?: WrapperOptions) { super() + process.on("SIGUSR1", async () => { + this.logger.info("Received SIGUSR1; hotswapping") + this.relaunch() + }) + const opts = { size: "10M", maxFiles: 10, @@ -253,21 +268,17 @@ export class ParentProcess extends Process { private async relaunch(): Promise { this.disposeChild() try { - await this.start() + this.started = this._start() + await this.started } catch (error) { this.logger.error(error.message) this.exit(typeof error.code === "number" ? error.code : 1) } } - public start(): Promise { - // If we have a process then we've already bound this. - if (!this.child) { - process.on("SIGUSR1", async () => { - this.logger.info("Received SIGUSR1; hotswapping") - this.relaunch() - }) - } + public start(args: DefaultedArgs): Promise { + // Store for relaunches. + this.args = args if (!this.started) { this.started = this._start() } @@ -320,14 +331,24 @@ export class ParentProcess extends Process { * Wait for a handshake from the child then reply. */ private async handshake(child: cp.ChildProcess): Promise { - await onMessage( + if (!this.args) { + throw new Error("started without args") + } + await onMessage( child, - (message): message is HandshakeMessage => { + (message): message is ChildHandshakeMessage => { return message.type === "handshake" }, this.logger, ) - child.send({ type: "handshake" }) + this.send(child, { type: "handshake", args: this.args }) + } + + /** + * Send a message to the child. + */ + private send(child: cp.ChildProcess, message: ParentMessage): void { + child.send(message) } } From 95ef6dbf2f9eaf999406878d195e3318721c1f3c Mon Sep 17 00:00:00 2001 From: Asher Date: Wed, 18 Nov 2020 13:08:57 -0600 Subject: [PATCH 214/247] Remove unused wrapper options Also move our memory default to the beginning of NODE_OPTIONS so it can be overidden. The version of the flag with dashes seems to be the more correct one now so use that instead of underscores. Related: #2113. --- src/node/wrapper.ts | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/src/node/wrapper.ts b/src/node/wrapper.ts index b63d60579..f6f84e2bd 100644 --- a/src/node/wrapper.ts +++ b/src/node/wrapper.ts @@ -201,11 +201,6 @@ class ChildProcess extends Process { } } -export interface WrapperOptions { - maxMemory?: number - nodeOptions?: string -} - /** * Parent process wrapper that spawns the child process and performs a handshake * with it. Will relaunch the child if it receives a SIGUSR1 or is asked to by @@ -224,7 +219,7 @@ export class ParentProcess extends Process { private args?: DefaultedArgs - public constructor(private currentVersion: string, private readonly options?: WrapperOptions) { + public constructor(private currentVersion: string) { super() process.on("SIGUSR1", async () => { @@ -310,18 +305,12 @@ export class ParentProcess extends Process { } private spawn(): cp.ChildProcess { - // Flags to pass along to the Node binary. - let nodeOptions = `${process.env.NODE_OPTIONS || ""} ${(this.options && this.options.nodeOptions) || ""}` - if (!/max_old_space_size=(\d+)/g.exec(nodeOptions)) { - nodeOptions += ` --max_old_space_size=${(this.options && this.options.maxMemory) || 2048}` - } - // Use spawn (instead of fork) to use the new binary in case it was updated. return cp.spawn(process.argv[0], process.argv.slice(1), { env: { ...process.env, CODE_SERVER_PARENT_PID: process.pid.toString(), - NODE_OPTIONS: nodeOptions, + NODE_OPTIONS: `--max-old-space-size=2048 ${process.env.NODE_OPTIONS || ""}`, }, stdio: ["ipc"], }) From 624cd9d44fe47538015875b46fe2902d54d58df4 Mon Sep 17 00:00:00 2001 From: Asher Date: Wed, 18 Nov 2020 16:50:04 -0600 Subject: [PATCH 215/247] Fix webview 404s An extra slash caused a 404 (was /webview//vscode-resource). --- ci/dev/vscode.patch | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ci/dev/vscode.patch b/ci/dev/vscode.patch index 9860ba965..83c8d9949 100644 --- a/ci/dev/vscode.patch +++ b/ci/dev/vscode.patch @@ -430,7 +430,7 @@ index 2c64061da7b01aef0bfe3cec851da232ca9461c8..c0ef8faedd406c38bf9c55bbbdbbb060 // Do nothing. If we can't read the file we have no // language pack config. diff --git a/src/vs/code/browser/workbench/workbench.ts b/src/vs/code/browser/workbench/workbench.ts -index 0ef8b9dc81419b53b27cf111fb206d72ba56bada..75d2ab3276049115829a38b8b7afee44bb748c2a 100644 +index 0ef8b9dc81419b53b27cf111fb206d72ba56bada..62a79602a831bca0dc62ad57dc10a9375f8b9cdb 100644 --- a/src/vs/code/browser/workbench/workbench.ts +++ b/src/vs/code/browser/workbench/workbench.ts @@ -17,6 +17,7 @@ import { isStandalone } from 'vs/base/browser/browser'; @@ -468,7 +468,7 @@ index 0ef8b9dc81419b53b27cf111fb206d72ba56bada..75d2ab3276049115829a38b8b7afee44 - const config: IWorkbenchConstructionOptions & { folderUri?: UriComponents, workspaceUri?: UriComponents } = JSON.parse(configElementAttribute); + const config: IWorkbenchConstructionOptions & { folderUri?: UriComponents, workspaceUri?: UriComponents } = { -+ webviewEndpoint: `${window.location.origin}${window.location.pathname.replace(/\/+$/, '')}/webview/`, ++ webviewEndpoint: `${window.location.origin}${window.location.pathname.replace(/\/+$/, '')}/webview`, + ...JSON.parse(configElementAttribute), + }; + From 182791319a6f387b448d0e43c405525f6dce268d Mon Sep 17 00:00:00 2001 From: Asher Date: Wed, 18 Nov 2020 17:15:14 -0600 Subject: [PATCH 216/247] Fix tar authentication It was checking the request path but for tars the path is in the query variable so the request path is irrelevant. --- src/node/routes/static.ts | 42 +++++++++++++++++++-------------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/src/node/routes/static.ts b/src/node/routes/static.ts index e073219e4..cac4dd8cd 100644 --- a/src/node/routes/static.ts +++ b/src/node/routes/static.ts @@ -7,13 +7,33 @@ import * as tarFs from "tar-fs" import * as zlib from "zlib" import { HttpCode, HttpError } from "../../common/http" import { rootPath } from "../constants" -import { authenticated, replaceTemplates } from "../http" +import { authenticated, ensureAuthenticated, replaceTemplates } from "../http" import { getMediaMime, pathToFsPath } from "../util" export const router = Router() // The commit is for caching. router.get("/(:commit)(/*)?", async (req, res) => { + // Used by VS Code to load extensions into the web worker. + const tar = Array.isArray(req.query.tar) ? req.query.tar[0] : req.query.tar + if (typeof tar === "string") { + ensureAuthenticated(req) + let stream: Readable = tarFs.pack(pathToFsPath(tar)) + if (req.headers["accept-encoding"] && req.headers["accept-encoding"].includes("gzip")) { + logger.debug("gzipping tar", field("path", tar)) + const compress = zlib.createGzip() + stream.pipe(compress) + stream.on("error", (error) => compress.destroy(error)) + stream.on("close", () => compress.end()) + stream = compress + res.header("content-encoding", "gzip") + } + res.set("Content-Type", "application/x-tar") + stream.on("close", () => res.end()) + return stream.pipe(res) + } + + // If not a tar use the remainder of the path to load the resource. if (!req.params[0]) { throw new HttpError("Not Found", HttpCode.NotFound) } @@ -32,26 +52,6 @@ router.get("/(:commit)(/*)?", async (req, res) => { res.header("Cache-Control", "public, max-age=31536000") } - /** - * Used by VS Code to load extensions into the web worker. - */ - const tar = Array.isArray(req.query.tar) ? req.query.tar[0] : req.query.tar - if (typeof tar === "string") { - let stream: Readable = tarFs.pack(pathToFsPath(tar)) - if (req.headers["accept-encoding"] && req.headers["accept-encoding"].includes("gzip")) { - logger.debug("gzipping tar", field("path", resourcePath)) - const compress = zlib.createGzip() - stream.pipe(compress) - stream.on("error", (error) => compress.destroy(error)) - stream.on("close", () => compress.end()) - stream = compress - res.header("content-encoding", "gzip") - } - res.set("Content-Type", "application/x-tar") - stream.on("close", () => res.end()) - return stream.pipe(res) - } - res.set("Content-Type", getMediaMime(resourcePath)) if (resourcePath.endsWith("manifest.json")) { From f79bb210ec6e78c135db7b006ae447f37be4ca7e Mon Sep 17 00:00:00 2001 From: Jacky <29943110+jw-12138@users.noreply.github.com> Date: Thu, 29 Oct 2020 10:07:43 +0800 Subject: [PATCH 217/247] login.css: Fix button styling on iOS --- src/browser/pages/login.css | 16 +++++++++++++--- src/browser/pages/login.html | 3 ++- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/src/browser/pages/login.css b/src/browser/pages/login.css index 43d03e7ee..4a66567c5 100644 --- a/src/browser/pages/login.css +++ b/src/browser/pages/login.css @@ -26,7 +26,7 @@ body { border: 1px solid #ddd; box-sizing: border-box; color: black; - flex: 1; + width: 100%; padding: 16px; } @@ -34,6 +34,16 @@ body { display: none; } -.login-form > .field > .submit { - margin-left: 20px; +.login-form > .field > input[type="submit"] { + position: absolute; + left: -9999px; +} + +.login-form > .field > input[type="submit"]:focus + label { + border: 2px solid #000; +} + +.login-form > .field > .submit{ + margin-left: 20px; + flex-shrink: 0; } diff --git a/src/browser/pages/login.html b/src/browser/pages/login.html index fc772f392..dcfb8f635 100644 --- a/src/browser/pages/login.html +++ b/src/browser/pages/login.html @@ -38,7 +38,8 @@ name="password" autocomplete="current-password" /> - + + {{ERROR}} From 30100caf0c444fc32d87bf447d6e28dcf5e28eda Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Thu, 19 Nov 2020 10:41:37 -0500 Subject: [PATCH 218/247] Revert "login.css: Fix button styling on iOS" This reverts commit f79bb210ec6e78c135db7b006ae447f37be4ca7e. --- src/browser/pages/login.css | 14 ++------------ src/browser/pages/login.html | 3 +-- 2 files changed, 3 insertions(+), 14 deletions(-) diff --git a/src/browser/pages/login.css b/src/browser/pages/login.css index 4a66567c5..43d03e7ee 100644 --- a/src/browser/pages/login.css +++ b/src/browser/pages/login.css @@ -26,7 +26,7 @@ body { border: 1px solid #ddd; box-sizing: border-box; color: black; - width: 100%; + flex: 1; padding: 16px; } @@ -34,16 +34,6 @@ body { display: none; } -.login-form > .field > input[type="submit"] { - position: absolute; - left: -9999px; -} - -.login-form > .field > input[type="submit"]:focus + label { - border: 2px solid #000; -} - -.login-form > .field > .submit{ +.login-form > .field > .submit { margin-left: 20px; - flex-shrink: 0; } diff --git a/src/browser/pages/login.html b/src/browser/pages/login.html index dcfb8f635..fc772f392 100644 --- a/src/browser/pages/login.html +++ b/src/browser/pages/login.html @@ -38,8 +38,7 @@ name="password" autocomplete="current-password" /> - - + {{ERROR}} From 2dc7863ec394aea3a51c0c7a526bedcf846c563e Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Thu, 19 Nov 2020 10:42:14 -0500 Subject: [PATCH 219/247] login.css: Disable webkit appearance for input elements Not sure why Safari does these things... Closes #2247 --- src/browser/pages/login.css | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/browser/pages/login.css b/src/browser/pages/login.css index 43d03e7ee..f0586ee81 100644 --- a/src/browser/pages/login.css +++ b/src/browser/pages/login.css @@ -37,3 +37,7 @@ body { .login-form > .field > .submit { margin-left: 20px; } + +input { + -webkit-appearance: none; +} From 72caafe8b04e1cbc123d44d757fdc97640e4e6c4 Mon Sep 17 00:00:00 2001 From: Asher Date: Thu, 19 Nov 2020 10:18:15 -0600 Subject: [PATCH 220/247] Fix service worker not loading (#2335) I removed this under the impression the default was to allow it anywhere but that's not the case. Since the service worker was already registered in my browser I never got the error during testing. --- src/node/routes/static.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/node/routes/static.ts b/src/node/routes/static.ts index cac4dd8cd..22bdd8d24 100644 --- a/src/node/routes/static.ts +++ b/src/node/routes/static.ts @@ -52,6 +52,11 @@ router.get("/(:commit)(/*)?", async (req, res) => { res.header("Cache-Control", "public, max-age=31536000") } + // Without this the default is to use the directory the script loaded from. + if (req.headers["service-worker"]) { + res.header("service-worker-allowed", "/") + } + res.set("Content-Type", getMediaMime(resourcePath)) if (resourcePath.endsWith("manifest.json")) { From e28c9ab287f2e1802c0d6ef28e950404e591d615 Mon Sep 17 00:00:00 2001 From: Asher Date: Fri, 6 Nov 2020 22:47:51 -0600 Subject: [PATCH 221/247] Update VS Code to 1.51.1 --- ci/dev/vscode.patch | 186 ++++++++++++++++++++++---------------------- lib/vscode | 2 +- 2 files changed, 92 insertions(+), 96 deletions(-) diff --git a/ci/dev/vscode.patch b/ci/dev/vscode.patch index 83c8d9949..7cd67f3cd 100644 --- a/ci/dev/vscode.patch +++ b/ci/dev/vscode.patch @@ -12,12 +12,12 @@ index b7f5b58c8ede171be547c56b61ce76f79a3accc3..856fbd8c67460fe099d7fbee1475e906 coverage/ diff --git a/.yarnrc b/.yarnrc deleted file mode 100644 -index 3c6eccfb102f2084d16395d70d65f05a91b6d47b..0000000000000000000000000000000000000000 +index d97527dab46aa4e7aa2df386bda3a8b4f93fcb80..0000000000000000000000000000000000000000 --- a/.yarnrc +++ /dev/null @@ -1,3 +0,0 @@ --disturl "https://atom.io/download/electron" --target "9.2.1" +-disturl "https://electronjs.org/headers" +-target "9.3.3" -runtime "electron" diff --git a/build/gulpfile.reh.js b/build/gulpfile.reh.js index 5f367d1f0777d2cb46ad47e376337900733981b5..ba74af1d61a00ce42020418126e62879397f57bf 100644 @@ -66,10 +66,10 @@ index 64397034461b1661f82007c141cbf4c039a3b722..c53dccf4dc0a99122ed96cf10c2eb632 \ No newline at end of file +console.log(nodePath); diff --git a/build/lib/util.ts b/build/lib/util.ts -index 035c7e95ea3006bb3dabd68bbf54db80de4aaaf2..4ff8dcfe6b21a0ec8064ebc7bb05506b8f1faa91 100644 +index c0a0d9619d736c6558b0b91e6c7537c1a06cc947..48853bc6201a602cadbef47a8f46281be93421e9 100644 --- a/build/lib/util.ts +++ b/build/lib/util.ts -@@ -322,6 +322,7 @@ export function streamToPromise(stream: NodeJS.ReadWriteStream): Promise { +@@ -336,6 +336,7 @@ export function streamToPromise(stream: NodeJS.ReadWriteStream): Promise { } export function getElectronVersion(): string { @@ -223,7 +223,7 @@ index 2d754bf4054713f53beed030f9211b33532c1b4b..708b7e40a662e4ca93420992bf7a5af0 return typeof navigator !== 'undefined' && vscode.env.uiKind === vscode.UIKind.Web; } diff --git a/package.json b/package.json -index 770b44b0c1ff53d903b7680ede27715376df00f2..b27ab71647a3e7c4b6076ba4fdb8fde20fa73bb0 100644 +index 28f8a69a2a91f9cb9f4dbd73ed3e689b2b3afe84..b5f5b10004d3e36092a30f685938a606b333c465 100644 --- a/package.json +++ b/package.json @@ -46,7 +46,11 @@ @@ -236,17 +236,17 @@ index 770b44b0c1ff53d903b7680ede27715376df00f2..b27ab71647a3e7c4b6076ba4fdb8fde2 + "@coder/node-browser": "^1.0.8", + "@coder/requirefs": "^1.1.5", "applicationinsights": "1.0.8", - "chokidar": "3.4.2", + "chokidar": "3.4.3", "graceful-fs": "4.2.3", @@ -60,6 +64,7 @@ "native-keymap": "2.2.0", "native-watchdog": "1.3.0", "node-pty": "0.10.0-beta17", + "rimraf": "^2.2.8", - "semver-umd": "^5.5.7", "spdlog": "^0.11.1", "sudo-prompt": "9.1.1", -@@ -160,7 +165,6 @@ + "tas-client-umd": "0.1.2", +@@ -161,7 +166,6 @@ "pump": "^1.0.1", "queue": "3.0.6", "rcedit": "^1.1.0", @@ -254,7 +254,7 @@ index 770b44b0c1ff53d903b7680ede27715376df00f2..b27ab71647a3e7c4b6076ba4fdb8fde2 "sinon": "^1.17.2", "source-map": "^0.4.4", "style-loader": "^1.0.0", -@@ -192,5 +196,8 @@ +@@ -193,5 +197,8 @@ "windows-foreground-love": "0.2.0", "windows-mutex": "0.3.0", "windows-process-tree": "0.2.4" @@ -264,7 +264,7 @@ index 770b44b0c1ff53d903b7680ede27715376df00f2..b27ab71647a3e7c4b6076ba4fdb8fde2 } } diff --git a/product.json b/product.json -index ecfb44dd74e09fc2ff1e902bea9396c2046fb9e6..026f78c698df50cfd1c2debb9823aacdcb4f7c71 100644 +index 7cab6d1b9f3b84bfc703856e93773a293fd198cf..31d3d5a943192eee791e1121415b436dc1ed3822 100644 --- a/product.json +++ b/product.json @@ -20,7 +20,7 @@ @@ -334,15 +334,13 @@ index 3361d83be5b7c3d08bdbfbe6947942a4695882c6..69ead8484e042bbad7075659f8e47f07 // Native environment diff --git a/src/vs/base/common/processes.ts b/src/vs/base/common/processes.ts -index c52f7b3774f399d3fa161682316b20d807072806..08a87fa970f159f84691c5068cf5e38f0926015c 100644 +index 17895a8510bca40924524dc107c33305c4783c45..ba019b43084e3998ab399108968c3c765a79eb32 100644 --- a/src/vs/base/common/processes.ts +++ b/src/vs/base/common/processes.ts -@@ -110,7 +110,8 @@ export function sanitizeProcessEnvironment(env: IProcessEnvironment, ...preserve - /^ELECTRON_.+$/, - /^GOOGLE_API_KEY$/, +@@ -112,6 +112,7 @@ export function sanitizeProcessEnvironment(env: IProcessEnvironment, ...preserve /^VSCODE_.+$/, -- /^SNAP(|_.*)$/ -+ /^SNAP(|_.*)$/, + /^SNAP(|_.*)$/, + /^GDK_PIXBUF_.+$/, + /^CODE_SERVER_.+$/, ]; const envKeys = Object.keys(env); @@ -529,7 +527,7 @@ index 0ef8b9dc81419b53b27cf111fb206d72ba56bada..62a79602a831bca0dc62ad57dc10a937 // If no workspace is provided through the URL, check for config attribute from server if (!foundWorkspace) { diff --git a/src/vs/platform/environment/common/argv.ts b/src/vs/platform/environment/common/argv.ts -index 2ac99a2120ec461703c2ff85da8b5379125c5d96..77450df65524b0daed61309614941195f5294c1d 100644 +index 409bb7e1960c9c06485a6f6d7f39b2efce451d56..f27b651c49ea3fc92b03e31eb64c1cf27c7e4433 100644 --- a/src/vs/platform/environment/common/argv.ts +++ b/src/vs/platform/environment/common/argv.ts @@ -7,6 +7,8 @@ @@ -542,10 +540,10 @@ index 2ac99a2120ec461703c2ff85da8b5379125c5d96..77450df65524b0daed61309614941195 'folder-uri'?: string[]; // undefined or array of 1 or more 'file-uri'?: string[]; // undefined or array of 1 or more diff --git a/src/vs/platform/environment/common/environment.ts b/src/vs/platform/environment/common/environment.ts -index eebf675762d57ddd7072b3e4182a9864c94ec81c..5c18cd4420c5f85724148bb7df14c62dfe0b34bf 100644 +index 21b4d719cec1a724bbad407aeec38db9eb8d6f5a..edf46f097bf11bfb8883d38d38ee78b735f35b3f 100644 --- a/src/vs/platform/environment/common/environment.ts +++ b/src/vs/platform/environment/common/environment.ts -@@ -111,6 +111,8 @@ export interface INativeEnvironmentService extends IEnvironmentService { +@@ -122,6 +122,8 @@ export interface INativeEnvironmentService extends IEnvironmentService { extensionsPath?: string; extensionsDownloadPath: string; builtinExtensionsPath: string; @@ -555,7 +553,7 @@ index eebf675762d57ddd7072b3e4182a9864c94ec81c..5c18cd4420c5f85724148bb7df14c62d // --- Smoke test support driverHandle?: string; diff --git a/src/vs/platform/environment/node/argv.ts b/src/vs/platform/environment/node/argv.ts -index 662547909e57ba556c10177a29beac775cc86c80..eeaba21664ed5d797d1f08f0bd77ee075354ffe5 100644 +index 149e6ffb41a82f1a69cf37f105a31872ad4af8b4..ed99aab42b31bc2ab804391b6e3f4c7ff67d9259 100644 --- a/src/vs/platform/environment/node/argv.ts +++ b/src/vs/platform/environment/node/argv.ts @@ -54,6 +54,8 @@ export const OPTIONS: OptionDescriptions> = { @@ -567,16 +565,16 @@ index 662547909e57ba556c10177a29beac775cc86c80..eeaba21664ed5d797d1f08f0bd77ee07 'list-extensions': { type: 'boolean', cat: 'e', description: localize('listExtensions', "List the installed extensions.") }, 'show-versions': { type: 'boolean', cat: 'e', description: localize('showVersions', "Show versions of installed extensions, when using --list-extension.") }, 'category': { type: 'string', cat: 'e', description: localize('category', "Filters installed extensions by provided category, when using --list-extension.") }, -@@ -316,4 +318,3 @@ export function buildHelpMessage(productName: string, executableName: string, ve +@@ -318,4 +320,3 @@ export function buildHelpMessage(productName: string, executableName: string, ve export function buildVersionMessage(version: string | undefined, commit: string | undefined): string { return `${version || localize('unknownVersion', "Unknown version")}\n${commit || localize('unknownCommit', "Unknown commit")}\n${process.arch}`; } - diff --git a/src/vs/platform/environment/node/environmentService.ts b/src/vs/platform/environment/node/environmentService.ts -index 16057c43a0807fb894a94d7d658e7d24c9db173a..d8c40c94dce474f441acbbf114e53694e3b101c5 100644 +index 80f68fb1decfd1c4fa1bcc30840900240df83f76..d4478b0000a511af11647876a536b8147163f9f8 100644 --- a/src/vs/platform/environment/node/environmentService.ts +++ b/src/vs/platform/environment/node/environmentService.ts -@@ -145,6 +145,13 @@ export class NativeEnvironmentService implements INativeEnvironmentService { +@@ -138,6 +138,13 @@ export class NativeEnvironmentService implements INativeEnvironmentService { return resources.joinPath(this.userHome, product.dataFolderName, 'extensions').fsPath; } @@ -591,10 +589,10 @@ index 16057c43a0807fb894a94d7d658e7d24c9db173a..d8c40c94dce474f441acbbf114e53694 get extensionDevelopmentLocationURI(): URI[] | undefined { const s = this._args.extensionDevelopmentPath; diff --git a/src/vs/platform/extensionManagement/node/extensionsScanner.ts b/src/vs/platform/extensionManagement/node/extensionsScanner.ts -index e7342348d46cbcafa1b301ca1373ce01c057e70f..408867d2d3b503b46363c5a853e047d396bb7e0a 100644 +index aee65f8eddbfbce3e42362be9590c98d46f2ace5..dc891fba7c7af3ace02b0091ef858bea59e754c6 100644 --- a/src/vs/platform/extensionManagement/node/extensionsScanner.ts +++ b/src/vs/platform/extensionManagement/node/extensionsScanner.ts -@@ -90,7 +90,7 @@ export class ExtensionsScanner extends Disposable { +@@ -91,7 +91,7 @@ export class ExtensionsScanner extends Disposable { } async scanAllUserExtensions(): Promise { @@ -603,7 +601,7 @@ index e7342348d46cbcafa1b301ca1373ce01c057e70f..408867d2d3b503b46363c5a853e047d3 } async extractUserExtension(identifierWithVersion: ExtensionIdentifierWithVersion, zipPath: string, token: CancellationToken): Promise { -@@ -235,7 +235,13 @@ export class ExtensionsScanner extends Disposable { +@@ -236,7 +236,13 @@ export class ExtensionsScanner extends Disposable { private async scanExtensionsInDir(dir: string, type: ExtensionType): Promise { const limiter = new Limiter(10); @@ -618,7 +616,7 @@ index e7342348d46cbcafa1b301ca1373ce01c057e70f..408867d2d3b503b46363c5a853e047d3 const extensions = await Promise.all(extensionsFolders.map(extensionFolder => limiter.queue(() => this.scanExtension(extensionFolder, dir, type)))); return extensions.filter(e => e && e.identifier); } -@@ -265,7 +271,7 @@ export class ExtensionsScanner extends Disposable { +@@ -266,7 +272,7 @@ export class ExtensionsScanner extends Disposable { } private async scanDefaultSystemExtensions(): Promise { @@ -627,7 +625,7 @@ index e7342348d46cbcafa1b301ca1373ce01c057e70f..408867d2d3b503b46363c5a853e047d3 this.logService.trace('Scanned system extensions:', result.length); return result; } -@@ -369,4 +375,9 @@ export class ExtensionsScanner extends Disposable { +@@ -370,4 +376,9 @@ export class ExtensionsScanner extends Disposable { } }); } @@ -638,7 +636,7 @@ index e7342348d46cbcafa1b301ca1373ce01c057e70f..408867d2d3b503b46363c5a853e047d3 + } } diff --git a/src/vs/platform/product/common/product.ts b/src/vs/platform/product/common/product.ts -index 798faa74ae825de7449b74609ed649912531ec0f..487abd285a42407f669ce5f5396423644f822a83 100644 +index 2bea85740cb3e00c955ec0f7aa46d5f9bb8d5dc8..c0953d7b73178fc4a7b030246a5281609c3dfce6 100644 --- a/src/vs/platform/product/common/product.ts +++ b/src/vs/platform/product/common/product.ts @@ -37,6 +37,12 @@ if (isWeb || typeof require === 'undefined' || typeof require.__$__nodeRequire ! @@ -655,7 +653,7 @@ index 798faa74ae825de7449b74609ed649912531ec0f..487abd285a42407f669ce5f539642364 // Native (non-sandboxed) diff --git a/src/vs/platform/product/common/productService.ts b/src/vs/platform/product/common/productService.ts -index 53dc899d48d85d27042600962b27ab97fb68951f..4792fee15e308e7427bb24591b19bcdc97eff600 100644 +index 333e5b24b05c96e8d44e9025b7a777e6989de9e7..b13572327a6e91592eedea9bcb1e580397f5c224 100644 --- a/src/vs/platform/product/common/productService.ts +++ b/src/vs/platform/product/common/productService.ts @@ -32,6 +32,8 @@ export type ConfigurationSyncStore = { @@ -689,10 +687,10 @@ index 3715cbb8e6ee41c3d9b5090918d243b723ae2d00..c65de8ad37e727d66da97a8f8b170cbc - - diff --git a/src/vs/platform/remote/common/remoteAgentConnection.ts b/src/vs/platform/remote/common/remoteAgentConnection.ts -index 18d3d04fd20335975293e37b3b641120dd92da20..4e49f9d63623da6c84624144765f76ec127ea526 100644 +index fdd5890c69f72025b94913380f0d226226e8c8fb..e084236526b38c1144d47b8b3000b367c3207fe8 100644 --- a/src/vs/platform/remote/common/remoteAgentConnection.ts +++ b/src/vs/platform/remote/common/remoteAgentConnection.ts -@@ -92,7 +92,7 @@ async function connectToRemoteExtensionHostAgent(options: ISimpleConnectionOptio +@@ -93,7 +93,7 @@ async function connectToRemoteExtensionHostAgent(options: ISimpleConnectionOptio options.socketFactory.connect( options.host, options.port, @@ -2902,7 +2900,7 @@ index 0000000000000000000000000000000000000000..fa47e993b46802f1a26457649e9e8bc4 + return path.split("/").map((p) => encodeURIComponent(p)).join("/"); +}; diff --git a/src/vs/workbench/api/browser/extensionHost.contribution.ts b/src/vs/workbench/api/browser/extensionHost.contribution.ts -index 9e264fb33b9a282e3a5284bcd857e17a664107a7..a23a44a781cd1f9b7d432d79a46707c93f4008e7 100644 +index a4df8523631563a498c9ab6e51105074616a481a..f03da094e9080544102bbd3f037a71b348e5bd83 100644 --- a/src/vs/workbench/api/browser/extensionHost.contribution.ts +++ b/src/vs/workbench/api/browser/extensionHost.contribution.ts @@ -61,6 +61,7 @@ import './mainThreadComments'; @@ -2914,10 +2912,10 @@ index 9e264fb33b9a282e3a5284bcd857e17a664107a7..a23a44a781cd1f9b7d432d79a46707c9 import './mainThreadAuthentication'; import './mainThreadTimeline'; diff --git a/src/vs/workbench/api/browser/mainThreadStorage.ts b/src/vs/workbench/api/browser/mainThreadStorage.ts -index 7bc3904963bed2925f3640b6bd929347159dd3cf..c6db2368ae9eaca61889efcf3c49763c01ff7459 100644 +index 57abf0e86a5edeeb2bc497af5e140ec13d9b5810..704d0f9ae19d436a7207ff735aabc289c422dd1e 100644 --- a/src/vs/workbench/api/browser/mainThreadStorage.ts +++ b/src/vs/workbench/api/browser/mainThreadStorage.ts -@@ -58,11 +58,11 @@ export class MainThreadStorage implements MainThreadStorageShape { +@@ -62,11 +62,11 @@ export class MainThreadStorage implements MainThreadStorageShape { return JSON.parse(jsonValue); } @@ -2932,7 +2930,7 @@ index 7bc3904963bed2925f3640b6bd929347159dd3cf..c6db2368ae9eaca61889efcf3c49763c return Promise.reject(err); } diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts -index 2a0576b68f943f63c010dd496e094311bdc149f0..357c63f0fec08ddfb06b3579460fe1566fa5d852 100644 +index 284c6aff854a747d1202c34581a1419c35e9654f..f0173d80103ca91b5eab144a10935bc0990119c9 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -68,6 +68,7 @@ import { IURITransformerService } from 'vs/workbench/api/common/extHostUriTransf @@ -2960,10 +2958,10 @@ index 2a0576b68f943f63c010dd496e094311bdc149f0..357c63f0fec08ddfb06b3579460fe156 rpcProtocol.set(ExtHostContext.ExtHostWindow, extHostWindow); diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts -index 3728f5602dffd0fd4b0cf326c5fa7d6d7c49c53e..2521acff0e692e97b72deef758ce41b4cd54a724 100644 +index 77ef6577821399b150407e980c8fd35e9d005ca6..264e3361accec20e4e1eaae10ae8ca05e47b1fae 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts -@@ -807,6 +807,17 @@ export interface MainThreadLabelServiceShape extends IDisposable { +@@ -816,6 +816,17 @@ export interface MainThreadLabelServiceShape extends IDisposable { $unregisterResourceLabelFormatter(handle: number): void; } @@ -2981,7 +2979,7 @@ index 3728f5602dffd0fd4b0cf326c5fa7d6d7c49c53e..2521acff0e692e97b72deef758ce41b4 export interface MainThreadSearchShape extends IDisposable { $registerFileSearchProvider(handle: number, scheme: string): void; $registerTextSearchProvider(handle: number, scheme: string): void; -@@ -1784,6 +1795,7 @@ export const MainContext = { +@@ -1796,6 +1807,7 @@ export const MainContext = { MainThreadWindow: createMainId('MainThreadWindow'), MainThreadLabelService: createMainId('MainThreadLabelService'), MainThreadNotebook: createMainId('MainThreadNotebook'), @@ -2989,7 +2987,7 @@ index 3728f5602dffd0fd4b0cf326c5fa7d6d7c49c53e..2521acff0e692e97b72deef758ce41b4 MainThreadTheming: createMainId('MainThreadTheming'), MainThreadTunnelService: createMainId('MainThreadTunnelService'), MainThreadTimeline: createMainId('MainThreadTimeline') -@@ -1826,6 +1838,7 @@ export const ExtHostContext = { +@@ -1838,6 +1850,7 @@ export const ExtHostContext = { ExtHostOutputService: createMainId('ExtHostOutputService'), ExtHosLabelService: createMainId('ExtHostLabelService'), ExtHostNotebook: createMainId('ExtHostNotebook'), @@ -2998,7 +2996,7 @@ index 3728f5602dffd0fd4b0cf326c5fa7d6d7c49c53e..2521acff0e692e97b72deef758ce41b4 ExtHostTunnelService: createMainId('ExtHostTunnelService'), ExtHostAuthentication: createMainId('ExtHostAuthentication'), diff --git a/src/vs/workbench/api/common/extHostExtensionService.ts b/src/vs/workbench/api/common/extHostExtensionService.ts -index 311b529e5adb46014466bf1852aef05a20b7a724..bc1bbd0196e4baf089c2bc7e0b08ecd771232b5e 100644 +index 328b9327207e4f2068bfab6cf374c622d8c5fc69..38963843095c9116011665027f46d3fb85c30ff8 100644 --- a/src/vs/workbench/api/common/extHostExtensionService.ts +++ b/src/vs/workbench/api/common/extHostExtensionService.ts @@ -31,6 +31,7 @@ import { IExtHostInitDataService } from 'vs/workbench/api/common/extHostInitData @@ -3042,7 +3040,7 @@ index 311b529e5adb46014466bf1852aef05a20b7a724..bc1bbd0196e4baf089c2bc7e0b08ecd7 this._loadExtensionContext(extensionDescription) ]).then(values => { return AbstractExtHostExtensionService._callActivate(this._logService, extensionDescription.identifier, values[0], values[1], activationTimesBuilder); -@@ -753,7 +757,7 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme +@@ -754,7 +758,7 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme protected abstract _beforeAlmostReadyToRunExtensions(): Promise; protected abstract _getEntryPoint(extensionDescription: IExtensionDescription): string | undefined; @@ -3070,7 +3068,7 @@ index b3c89e51cfc25a53293a352a2a8ad50d5f26d595..e21abe4e13bc25a5b72f556bbfb61085 registerSingleton(IExtHostTunnelService, ExtHostTunnelService); +registerSingleton(IExtHostNodeProxy, class extends NotImplementedProxy(String(IExtHostNodeProxy)) { whenReady = Promise.resolve(); }); diff --git a/src/vs/workbench/api/node/extHostCLIServer.ts b/src/vs/workbench/api/node/extHostCLIServer.ts -index 7cae126cc0f804273850933468690e0f9f10a5b8..08c2aa5cdae3f3d06bb08b7055dc7e7def260132 100644 +index b3857616f7006127c423dcef7020ae4653da5ff6..1c1b80a2767bf77f30ca5bfee715c337120d3625 100644 --- a/src/vs/workbench/api/node/extHostCLIServer.ts +++ b/src/vs/workbench/api/node/extHostCLIServer.ts @@ -11,6 +11,8 @@ import { IWindowOpenable, IOpenWindowOptions } from 'vs/platform/windows/common/ @@ -3082,10 +3080,10 @@ index 7cae126cc0f804273850933468690e0f9f10a5b8..08c2aa5cdae3f3d06bb08b7055dc7e7d export interface OpenCommandPipeArgs { type: 'open'; -@@ -54,6 +56,11 @@ export class CLIServer { - private async setup(): Promise { - this._ipcHandlePath = generateRandomPipeName(); +@@ -58,6 +60,11 @@ export class CLIServerBase { + } + private async setup(): Promise { + // NOTE@coder: Write this out so we can get the most recent path. + fs.promises.writeFile(join(tmpdir(), "vscode-ipc"), this._ipcHandlePath).catch((error) => { + this.logService.error(error); @@ -3112,18 +3110,18 @@ index 3843fdec386edc09a1d361b63de892a04e0070ed..8aac4df527857e964798362a69f5591b registerSingleton(ILogService, ExtHostLogService); +registerSingleton(IExtHostNodeProxy, ExtHostNodeProxy); diff --git a/src/vs/workbench/api/worker/extHostExtensionService.ts b/src/vs/workbench/api/worker/extHostExtensionService.ts -index a6a149083719d7479268e24eb5339f6cbf93e655..360888dc7dff9437f6c85f7a2043ad9e7c4daf21 100644 +index 021af6e0f8983c492f9cdd048ba2dcae7640bc1d..814dd0ff2fa7737e07833d8092c8f48953c73c47 100644 --- a/src/vs/workbench/api/worker/extHostExtensionService.ts +++ b/src/vs/workbench/api/worker/extHostExtensionService.ts -@@ -10,6 +10,7 @@ import { URI } from 'vs/base/common/uri'; - import { RequireInterceptor } from 'vs/workbench/api/common/extHostRequireInterceptor'; +@@ -11,6 +11,7 @@ import { RequireInterceptor } from 'vs/workbench/api/common/extHostRequireInterc import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { ExtensionRuntime } from 'vs/workbench/api/common/extHostTypes'; + import { timeout } from 'vs/base/common/async'; +import { loadCommonJSModule } from 'vs/server/browser/worker'; class WorkerRequireInterceptor extends RequireInterceptor { -@@ -44,10 +45,15 @@ export class ExtHostExtensionService extends AbstractExtHostExtensionService { +@@ -46,10 +47,15 @@ export class ExtHostExtensionService extends AbstractExtHostExtensionService { } protected _getEntryPoint(extensionDescription: IExtensionDescription): string | undefined { @@ -3157,18 +3155,18 @@ index ced2d815834e40a1543e80516472799075980733..dfcae73e8a042307600c67f163aa00ba .monaco-workbench .activitybar > .content > .home-bar > .home-bar-icon-badge { diff --git a/src/vs/workbench/browser/web.main.ts b/src/vs/workbench/browser/web.main.ts -index 416938f141228faafc95dc765be7d5fbaf610a7e..25ff7f7c881458fbb7b5bd588704930b52f439e8 100644 +index 80544aab34c12bb42a36519885e9872ef2b24158..17b56856a0b3fd936dbc094ff39797d5b8ccaadf 100644 --- a/src/vs/workbench/browser/web.main.ts +++ b/src/vs/workbench/browser/web.main.ts -@@ -42,6 +42,7 @@ import { FileLogService } from 'vs/platform/log/common/fileLogService'; +@@ -43,6 +43,7 @@ import { FileLogService } from 'vs/platform/log/common/fileLogService'; import { toLocalISOString } from 'vs/base/common/date'; import { isWorkspaceToOpen, isFolderToOpen } from 'vs/platform/windows/common/windows'; import { getWorkspaceIdentifier } from 'vs/workbench/services/workspaces/browser/workspaces'; +import { initialize } from 'vs/server/browser/client'; import { coalesce } from 'vs/base/common/arrays'; import { InMemoryFileSystemProvider } from 'vs/platform/files/common/inMemoryFilesystemProvider'; - import { WebResourceIdentityService, IResourceIdentityService } from 'vs/platform/resource/common/resourceIdentityService'; -@@ -94,6 +95,8 @@ class BrowserMain extends Disposable { + import { ICommandService } from 'vs/platform/commands/common/commands'; +@@ -101,6 +102,8 @@ class BrowserMain extends Disposable { // Startup const instantiationService = workbench.startup(); @@ -3200,7 +3198,7 @@ index 94e7e7a4bac154c45078a1b5034e50634a7a43af..8164200dcef1efbc65b50eef9c270af3 this._dirnameKey.set(value ? dirname(value).fsPath : null); this._pathKey.set(value ? value.fsPath : null); diff --git a/src/vs/workbench/contrib/scm/browser/media/scm.css b/src/vs/workbench/contrib/scm/browser/media/scm.css -index ac44ad3bae428def66e22fe9cc1c54648d429f6b..faa63023c4c586b51fa3c2a48ff3641b9cb0e145 100644 +index 74f6922e98b4bb6a7fb100f5aac015afe9fc171b..3243a97c2d378013d96ffbe87e9df6dd4a66776d 100644 --- a/src/vs/workbench/contrib/scm/browser/media/scm.css +++ b/src/vs/workbench/contrib/scm/browser/media/scm.css @@ -149,9 +149,11 @@ @@ -3219,10 +3217,10 @@ index ac44ad3bae428def66e22fe9cc1c54648d429f6b..faa63023c4c586b51fa3c2a48ff3641b .scm-view .monaco-list .monaco-list-row .resource-group > .actions, .scm-view .monaco-list .monaco-list-row .resource > .name > .monaco-icon-label > .actions { diff --git a/src/vs/workbench/electron-sandbox/sandbox.simpleservices.ts b/src/vs/workbench/electron-sandbox/sandbox.simpleservices.ts -index d57009a5a251ac5815c04e4f76bacef3e518a575..e825a8975449e95cf40504057d56579c12918a58 100644 +index ed4f26407391bd62219a9f8245a5cd63a7cb7488..92f26d1b082f80475cf76409a4569e948e9e0bd9 100644 --- a/src/vs/workbench/electron-sandbox/sandbox.simpleservices.ts +++ b/src/vs/workbench/electron-sandbox/sandbox.simpleservices.ts -@@ -127,6 +127,8 @@ export class SimpleNativeWorkbenchEnvironmentService implements INativeWorkbench +@@ -130,6 +130,8 @@ export class SimpleNativeWorkbenchEnvironmentService implements INativeWorkbench extensionsPath?: string | undefined; extensionsDownloadPath: string = undefined!; builtinExtensionsPath: string = undefined!; @@ -3230,12 +3228,12 @@ index d57009a5a251ac5815c04e4f76bacef3e518a575..e825a8975449e95cf40504057d56579c + extraBuiltinExtensionPaths: string[] = undefined!; driverHandle?: string | undefined; - driverVerbose = false; + diff --git a/src/vs/workbench/services/dialogs/browser/dialogService.ts b/src/vs/workbench/services/dialogs/browser/dialogService.ts -index 1360c248eb7ff937c92d08bbf30d2b76ea606dc0..adccf8b88d62381c3ec484df40c6d63142ec9ef5 100644 +index 85d83f37da179a1e39266cf72a02e971f590308e..0659738b36df1747c9afcabf8d9abf26c890990b 100644 --- a/src/vs/workbench/services/dialogs/browser/dialogService.ts +++ b/src/vs/workbench/services/dialogs/browser/dialogService.ts -@@ -124,11 +124,12 @@ export class DialogService implements IDialogService { +@@ -125,11 +125,12 @@ export class DialogService implements IDialogService { async about(): Promise { const detailString = (useAgo: boolean): string => { return nls.localize('aboutDetail', @@ -3251,10 +3249,10 @@ index 1360c248eb7ff937c92d08bbf30d2b76ea606dc0..adccf8b88d62381c3ec484df40c6d631 }; diff --git a/src/vs/workbench/services/environment/browser/environmentService.ts b/src/vs/workbench/services/environment/browser/environmentService.ts -index 8d5e0fb796661c63bf46a377607977f4b1f0b2ad..9f5da5da9435626419be5447577d20dcb408e78d 100644 +index a8d43045ecc8cbe04b3f8440cff16d42aadbcad0..cd589c6f75eccbeefbf364d426ac882396b26fb4 100644 --- a/src/vs/workbench/services/environment/browser/environmentService.ts +++ b/src/vs/workbench/services/environment/browser/environmentService.ts -@@ -112,8 +112,18 @@ export class BrowserWorkbenchEnvironmentService implements IWorkbenchEnvironment +@@ -119,8 +119,18 @@ export class BrowserWorkbenchEnvironmentService implements IWorkbenchEnvironment @memoize get logFile(): URI { return joinPath(this.options.logsPath, 'window.log'); } @@ -3274,7 +3272,7 @@ index 8d5e0fb796661c63bf46a377607977f4b1f0b2ad..9f5da5da9435626419be5447577d20dc @memoize get settingsResource(): URI { return joinPath(this.userRoamingDataHome, 'settings.json'); } -@@ -275,7 +285,12 @@ export class BrowserWorkbenchEnvironmentService implements IWorkbenchEnvironment +@@ -301,7 +311,12 @@ export class BrowserWorkbenchEnvironmentService implements IWorkbenchEnvironment extensionHostDebugEnvironment.params.port = parseInt(value); break; case 'enableProposedApi': @@ -3288,11 +3286,11 @@ index 8d5e0fb796661c63bf46a377607977f4b1f0b2ad..9f5da5da9435626419be5447577d20dc break; } } -diff --git a/src/vs/workbench/services/extensionManagement/common/extensionEnablementService.ts b/src/vs/workbench/services/extensionManagement/common/extensionEnablementService.ts -index 6e7e8c5c9ed18d3a7e9f6d5d886adf0f49038f23..f4b17da7d718a2b60db7a44f936e7d0e93ffff6f 100644 ---- a/src/vs/workbench/services/extensionManagement/common/extensionEnablementService.ts -+++ b/src/vs/workbench/services/extensionManagement/common/extensionEnablementService.ts -@@ -205,7 +205,7 @@ export class ExtensionEnablementService extends Disposable implements IWorkbench +diff --git a/src/vs/workbench/services/extensionManagement/browser/extensionEnablementService.ts b/src/vs/workbench/services/extensionManagement/browser/extensionEnablementService.ts +index 50d4d812b76f09435fcff8148aac4ceeaeb30873..faacf88fcef119f9f959739656d64a84c8f64cbf 100644 +--- a/src/vs/workbench/services/extensionManagement/browser/extensionEnablementService.ts ++++ b/src/vs/workbench/services/extensionManagement/browser/extensionEnablementService.ts +@@ -221,7 +221,7 @@ export class ExtensionEnablementService extends Disposable implements IWorkbench } } } @@ -3302,39 +3300,37 @@ index 6e7e8c5c9ed18d3a7e9f6d5d886adf0f49038f23..f4b17da7d718a2b60db7a44f936e7d0e return false; } diff --git a/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts b/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts -index a982b3ecc58c5a2f3a92be7b8cca3a1cacbb7d47..97f9bfcf0e679be683b1b09cd569149e7962f5ad 100644 +index de7e301d3f0c67ce662827f61427a5a7b3616b9f..877ea8e11e6e6d34b9a8fe16287af309e569285e 100644 --- a/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts +++ b/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts -@@ -211,8 +211,11 @@ export class ExtensionManagementService extends Disposable implements IExtension - } +@@ -251,7 +251,9 @@ export class ExtensionManagementService extends Disposable implements IWorkbench // Install Language pack on all servers -+ // NOTE@coder: It does not appear language packs can be installed on the web -+ // extension management server at this time. Filter out the web to fix this. if (isLanguagePackExtension(manifest)) { -- return Promise.all(this.servers.map(server => server.extensionManagementService.installFromGallery(gallery))).then(([local]) => local); -+ const servers = this.servers.filter(s => s !== this.extensionManagementServerService.webExtensionManagementServer); -+ return Promise.all(servers.map(server => server.extensionManagementService.installFromGallery(gallery))).then(([local]) => local); - } - - // 1. Install on preferred location -@@ -245,6 +248,11 @@ export class ExtensionManagementService extends Disposable implements IExtension - return this.extensionManagementServerService.webExtensionManagementServer.extensionManagementService.installFromGallery(gallery); +- servers.push(...this.servers); ++ // NOTE@coder: It does not appear language packs can be installed on the web ++ // extension management server at this time. Filter out the web to fix this. ++ servers.push(...this.servers.filter(s => s !== this.extensionManagementServerService.webExtensionManagementServer)); + } else { + const server = this.getExtensionManagementServerToInstall(manifest); + if (server) { +@@ -320,6 +322,11 @@ export class ExtensionManagementService extends Disposable implements IWorkbench + return this.extensionManagementServerService.webExtensionManagementServer; } + // NOTE@coder: Fall back to installing on the remote server. + if (this.extensionManagementServerService.remoteExtensionManagementServer) { -+ return this.extensionManagementServerService.remoteExtensionManagementServer.extensionManagementService.installFromGallery(gallery); ++ return this.extensionManagementServerService.remoteExtensionManagementServer; + } + - if (this.extensionManagementServerService.remoteExtensionManagementServer) { - const error = new Error(localize('cannot be installed', "Cannot install '{0}' because this extension has defined that it cannot run on the remote server.", gallery.displayName || gallery.name)); - error.name = INSTALL_ERROR_NOT_SUPPORTED; + return undefined; + } + diff --git a/src/vs/workbench/services/extensions/browser/extensionService.ts b/src/vs/workbench/services/extensions/browser/extensionService.ts -index 5eaec3499a3bd87ee4026c26a4b0e8c706978859..829514442fe60e2999378af14fd38c71ee92d2b9 100644 +index 1dff19bf177eff24f722b748b79835a653241c4d..0f59ad290c82cc4c9d09c565c1018cc275ca0249 100644 --- a/src/vs/workbench/services/extensions/browser/extensionService.ts +++ b/src/vs/workbench/services/extensions/browser/extensionService.ts -@@ -161,8 +161,10 @@ export class ExtensionService extends AbstractExtensionService implements IExten +@@ -177,8 +177,10 @@ export class ExtensionService extends AbstractExtensionService implements IExten this._remoteAgentService.getEnvironment(), this._remoteAgentService.scanExtensions() ]); @@ -3361,11 +3357,11 @@ index 65e532ee58dfc06ed944846d01b885cb8f260ebc..0b6282fde7ad03c7ea9872a777cbf487 export function getExtensionKind(manifest: IExtensionManifest, productService: IProductService, configurationService: IConfigurationService): ExtensionKind[] { diff --git a/src/vs/workbench/services/extensions/node/extensionHostProcessSetup.ts b/src/vs/workbench/services/extensions/node/extensionHostProcessSetup.ts -index 4eb8204bf5ad61d4d292dad5c2490c25fbff497a..85f6006c1f125da283b2ba615bad2805fa3598fc 100644 +index e39d131fe7b1dd4bd1093fedb8faba8e1fe969e8..94f2f1d7c4a0b3cb46eaaffe1181b3abbf997d7f 100644 --- a/src/vs/workbench/services/extensions/node/extensionHostProcessSetup.ts +++ b/src/vs/workbench/services/extensions/node/extensionHostProcessSetup.ts @@ -16,7 +16,7 @@ import { IInitData } from 'vs/workbench/api/common/extHost.protocol'; - import { MessageType, createMessageOfType, isMessageOfType, IExtHostSocketMessage, IExtHostReadyMessage, IExtHostReduceGraceTimeMessage } from 'vs/workbench/services/extensions/common/extensionHostProtocol'; + import { MessageType, createMessageOfType, isMessageOfType, IExtHostSocketMessage, IExtHostReadyMessage, IExtHostReduceGraceTimeMessage, ExtensionHostExitCode } from 'vs/workbench/services/extensions/common/extensionHostProtocol'; import { ExtensionHostMain, IExitFn } from 'vs/workbench/services/extensions/common/extensionHostMain'; import { VSBuffer } from 'vs/base/common/buffer'; -import { IURITransformer, URITransformer, IRawURITransformer } from 'vs/base/common/uriIpc'; @@ -3461,7 +3457,7 @@ index d7aefde89c74bc6096d6e66c45368c8582594efa..9758f3bb96b48603251336e6a64e270e } diff --git a/src/vs/workbench/workbench.web.main.ts b/src/vs/workbench/workbench.web.main.ts -index 59208b87022c74342489288c1a3c89937aa6d37e..f49153db796e59a5aed0cc56ea5d01c0f10f963e 100644 +index 509f8ac8ce3a689386e439302a53c27e4fdfcef7..2bf9a737bd0dbfa1e604acfc890be45823f02ebe 100644 --- a/src/vs/workbench/workbench.web.main.ts +++ b/src/vs/workbench/workbench.web.main.ts @@ -35,7 +35,8 @@ import 'vs/workbench/services/textfile/browser/browserTextFileService'; @@ -3475,7 +3471,7 @@ index 59208b87022c74342489288c1a3c89937aa6d37e..f49153db796e59a5aed0cc56ea5d01c0 import 'vs/workbench/services/credentials/browser/credentialsService'; import 'vs/workbench/services/url/browser/urlService'; diff --git a/yarn.lock b/yarn.lock -index a38db6751b5bbe5949afeb4c29921e1cb88913ac..c78f065a5587d81427aaf951cbfbc5509e2096d5 100644 +index ff358cb6a10984868ed5a5aed5729ac6eb8ebeb7..69668d95ecad219da26ccc4d837913b9324a0e28 100644 --- a/yarn.lock +++ b/yarn.lock @@ -140,6 +140,23 @@ @@ -3502,7 +3498,7 @@ index a38db6751b5bbe5949afeb4c29921e1cb88913ac..c78f065a5587d81427aaf951cbfbc550 "@electron/get@^1.0.1": version "1.7.2" resolved "https://registry.yarnpkg.com/@electron/get/-/get-1.7.2.tgz#286436a9fb56ff1a1fcdf0e80131fd65f4d1e0fd" -@@ -5383,6 +5400,13 @@ jsprim@^1.2.2: +@@ -5403,6 +5420,13 @@ jsprim@^1.2.2: json-schema "0.2.3" verror "1.10.0" @@ -3516,7 +3512,7 @@ index a38db6751b5bbe5949afeb4c29921e1cb88913ac..c78f065a5587d81427aaf951cbfbc550 just-debounce@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/just-debounce/-/just-debounce-1.0.0.tgz#87fccfaeffc0b68cd19d55f6722943f929ea35ea" -@@ -5963,26 +5987,11 @@ minimatch@0.3: +@@ -5983,26 +6007,11 @@ minimatch@0.3: dependencies: brace-expansion "^1.1.7" @@ -3544,7 +3540,7 @@ index a38db6751b5bbe5949afeb4c29921e1cb88913ac..c78f065a5587d81427aaf951cbfbc550 minipass@^2.2.1, minipass@^2.3.3: version "2.3.3" resolved "https://registry.yarnpkg.com/minipass/-/minipass-2.3.3.tgz#a7dcc8b7b833f5d368759cce544dccb55f50f233" -@@ -6724,6 +6733,11 @@ p-try@^2.0.0: +@@ -6744,6 +6753,11 @@ p-try@^2.0.0: resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.0.0.tgz#85080bb87c64688fa47996fe8f7dfbe8211760b1" integrity sha512-hMp0onDKIajHfIkdRk3P4CdCmErkYAxxDtP3Wx/4nZ3aGlau2VKh3mZpcuFkH27WQkL/3WBCPOktzA9ZOAnMQQ== diff --git a/lib/vscode b/lib/vscode index 93c2f0fbf..e5a624b78 160000 --- a/lib/vscode +++ b/lib/vscode @@ -1 +1 @@ -Subproject commit 93c2f0fbf16c5a4b10e4d5f89737d9c2c25488a3 +Subproject commit e5a624b788d92b8d34d1392e4c4d9789406efe8f From 4d276b88c0b2725ae6a262dbaa3e2f73876f8283 Mon Sep 17 00:00:00 2001 From: Asher Date: Fri, 6 Nov 2020 22:49:05 -0600 Subject: [PATCH 222/247] Add new logger service The telemetry service depends on this now. I had to move it into invokeFunction and use accessor.get otherwise getLogger on the service was undefined. I also had to move some the extension management service because it depends on the moved telemetry service. I moved a few other services as well to better match VS Code (sharedProcessMain.ts). I swapped some this.services.get with accessor.get since that seems to be the correct method although for these other services either method seems to work. --- ci/dev/vscode.patch | 69 +++++++++++++++++++++++++-------------------- 1 file changed, 39 insertions(+), 30 deletions(-) diff --git a/ci/dev/vscode.patch b/ci/dev/vscode.patch index 7cd67f3cd..0fd51f4f2 100644 --- a/ci/dev/vscode.patch +++ b/ci/dev/vscode.patch @@ -2585,10 +2585,10 @@ index 0000000000000000000000000000000000000000..0d9310038c0ca378579652d89bc8ac84 +} diff --git a/src/vs/server/node/server.ts b/src/vs/server/node/server.ts new file mode 100644 -index 0000000000000000000000000000000000000000..45a7bf62a6c07d8771b0257e7c98fae095109eb1 +index 0000000000000000000000000000000000000000..8424965d9c79d34e5513e4cfe543718521ad82c7 --- /dev/null +++ b/src/vs/server/node/server.ts -@@ -0,0 +1,291 @@ +@@ -0,0 +1,300 @@ +import { field } from '@coder/logger'; +import * as fs from 'fs'; +import * as net from 'net'; @@ -2618,8 +2618,9 @@ index 0000000000000000000000000000000000000000..45a7bf62a6c07d8771b0257e7c98fae0 +import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; +import { ILocalizationsService } from 'vs/platform/localizations/common/localizations'; +import { LocalizationsService } from 'vs/platform/localizations/node/localizations'; -+import { getLogLevel, ILogService } from 'vs/platform/log/common/log'; ++import { getLogLevel, ILoggerService, ILogService } from 'vs/platform/log/common/log'; +import { LoggerChannel } from 'vs/platform/log/common/logIpc'; ++import { LoggerService } from 'vs/platform/log/node/loggerService'; +import { SpdLogService } from 'vs/platform/log/node/spdlogService'; +import product from 'vs/platform/product/common/product'; +import { IProductService } from 'vs/platform/product/common/productService'; @@ -2630,8 +2631,9 @@ index 0000000000000000000000000000000000000000..45a7bf62a6c07d8771b0257e7c98fae0 +import { RequestService } from 'vs/platform/request/node/requestService'; +import ErrorTelemetry from 'vs/platform/telemetry/browser/errorTelemetry'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; ++import { TelemetryLogAppender } from 'vs/platform/telemetry/common/telemetryLogAppender'; +import { TelemetryService } from 'vs/platform/telemetry/common/telemetryService'; -+import { combinedAppender, LogAppender, NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtils'; ++import { combinedAppender, NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtils'; +import { AppInsightsAppender } from 'vs/platform/telemetry/node/appInsightsAppender'; +import { resolveCommonProperties } from 'vs/platform/telemetry/node/commonProperties'; +import { INodeProxyService, NodeProxyChannel } from 'vs/server/common/nodeProxy'; @@ -2820,6 +2822,7 @@ index 0000000000000000000000000000000000000000..45a7bf62a6c07d8771b0257e7c98fae0 + this.services.set(ILogService, logService); + this.services.set(IEnvironmentService, environmentService); + this.services.set(INativeEnvironmentService, environmentService); ++ this.services.set(ILoggerService, new SyncDescriptor(LoggerService)); + + const configurationService = new ConfigurationService(environmentService.settingsResource, fileService); + await configurationService.initialize(); @@ -2828,45 +2831,51 @@ index 0000000000000000000000000000000000000000..45a7bf62a6c07d8771b0257e7c98fae0 + this.services.set(IRequestService, new SyncDescriptor(RequestService)); + this.services.set(IFileService, fileService); + this.services.set(IProductService, { _serviceBrand: undefined, ...product }); -+ this.services.set(IExtensionGalleryService, new SyncDescriptor(ExtensionGalleryService)); -+ this.services.set(IExtensionManagementService, new SyncDescriptor(ExtensionManagementService)); + -+ if (!environmentService.disableTelemetry) { -+ this.services.set(ITelemetryService, new TelemetryService({ -+ appender: combinedAppender( -+ new AppInsightsAppender('code-server', null, () => new TelemetryClient() as any, logService), -+ new LogAppender(logService), -+ ), -+ sendErrorTelemetry: true, -+ commonProperties: resolveCommonProperties( -+ product.commit, product.version, await getMachineId(), -+ [], environmentService.installSourcePath, 'code-server', -+ ), -+ piiPaths, -+ }, configurationService)); -+ } else { -+ this.services.set(ITelemetryService, NullTelemetryService); -+ } ++ const machineId = await getMachineId(); + + await new Promise((resolve) => { + const instantiationService = new InstantiationService(this.services); -+ this.services.set(ILocalizationsService, instantiationService.createInstance(LocalizationsService)); -+ this.services.set(INodeProxyService, instantiationService.createInstance(NodeProxyService)); + -+ instantiationService.invokeFunction(() => { ++ instantiationService.invokeFunction((accessor) => { + instantiationService.createInstance(LogsDataCleaner); -+ const telemetryService = this.services.get(ITelemetryService) as ITelemetryService; ++ ++ let telemetryService: ITelemetryService; ++ if (!environmentService.disableTelemetry) { ++ telemetryService = new TelemetryService({ ++ appender: combinedAppender( ++ new AppInsightsAppender('code-server', null, () => new TelemetryClient() as any), ++ new TelemetryLogAppender(accessor.get(ILoggerService), environmentService) ++ ), ++ sendErrorTelemetry: true, ++ commonProperties: resolveCommonProperties( ++ product.commit, product.version, machineId, ++ [], environmentService.installSourcePath, 'code-server', ++ ), ++ piiPaths, ++ }, configurationService); ++ } else { ++ telemetryService = NullTelemetryService; ++ } ++ ++ this.services.set(ITelemetryService, telemetryService); ++ ++ this.services.set(IExtensionManagementService, new SyncDescriptor(ExtensionManagementService)); ++ this.services.set(IExtensionGalleryService, new SyncDescriptor(ExtensionGalleryService)); ++ this.services.set(ILocalizationsService, new SyncDescriptor(LocalizationsService)); ++ this.services.set(INodeProxyService, new SyncDescriptor(NodeProxyService)); ++ + this.ipc.registerChannel('extensions', new ExtensionManagementChannel( -+ this.services.get(IExtensionManagementService) as IExtensionManagementService, ++ accessor.get(IExtensionManagementService), + (context) => getUriTransformer(context.remoteAuthority), + )); + this.ipc.registerChannel('remoteextensionsenvironment', new ExtensionEnvironmentChannel( + environmentService, logService, telemetryService, '', + )); -+ this.ipc.registerChannel('request', new RequestChannel(this.services.get(IRequestService) as IRequestService)); ++ this.ipc.registerChannel('request', new RequestChannel(accessor.get(IRequestService))); + this.ipc.registerChannel('telemetry', new TelemetryChannel(telemetryService)); -+ this.ipc.registerChannel('nodeProxy', new NodeProxyChannel(this.services.get(INodeProxyService) as INodeProxyService)); -+ this.ipc.registerChannel('localizations', >createChannelReceiver(this.services.get(ILocalizationsService) as ILocalizationsService)); ++ this.ipc.registerChannel('nodeProxy', new NodeProxyChannel(accessor.get(INodeProxyService))); ++ this.ipc.registerChannel('localizations', >createChannelReceiver(accessor.get(ILocalizationsService))); + this.ipc.registerChannel(REMOTE_FILE_SYSTEM_CHANNEL_NAME, new FileProviderChannel(environmentService, logService)); + resolve(new ErrorTelemetry(telemetryService)); + }); From 431137da454a68b758fb0b86a8a7e1fb3371337c Mon Sep 17 00:00:00 2001 From: Asher Date: Fri, 13 Nov 2020 11:31:01 -0600 Subject: [PATCH 223/247] Add new (unimplemented) terminal service --- ci/dev/vscode.patch | 89 ++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 84 insertions(+), 5 deletions(-) diff --git a/ci/dev/vscode.patch b/ci/dev/vscode.patch index 0fd51f4f2..65f4a3b51 100644 --- a/ci/dev/vscode.patch +++ b/ci/dev/vscode.patch @@ -1466,10 +1466,10 @@ index 0000000000000000000000000000000000000000..6ce56bec114a6d8daf5dd3ded945ea78 +} diff --git a/src/vs/server/node/channel.ts b/src/vs/server/node/channel.ts new file mode 100644 -index 0000000000000000000000000000000000000000..609c4d1cb43f52f92906b901c14c790f4536468f +index 0000000000000000000000000000000000000000..6fb1ada50628d3826a493c6e1b58f27a8be428bb --- /dev/null +++ b/src/vs/server/node/channel.ts -@@ -0,0 +1,360 @@ +@@ -0,0 +1,437 @@ +import { Server } from '@coder/node-browser'; +import * as path from 'path'; +import { VSBuffer } from 'vs/base/common/buffer'; @@ -1494,6 +1494,8 @@ index 0000000000000000000000000000000000000000..609c4d1cb43f52f92906b901c14c790f +import { getTranslations } from 'vs/server/node/nls'; +import { getUriTransformer } from 'vs/server/node/util'; +import { IFileChangeDto } from 'vs/workbench/api/common/extHost.protocol'; ++import * as terminal from 'vs/workbench/contrib/terminal/common/remoteTerminalChannel'; ++import { ITerminalLaunchError } from 'vs/workbench/contrib/terminal/common/terminal'; +import { ExtensionScanner, ExtensionScannerInput } from 'vs/workbench/services/extensions/node/extensionPoints'; + +/** @@ -1830,6 +1832,81 @@ index 0000000000000000000000000000000000000000..609c4d1cb43f52f92906b901c14c790f + this._$onMessage.fire(message); + } +} ++ ++export class TerminalProviderChannel implements IServerChannel, IDisposable { ++ public listen(_: RemoteAgentConnectionContext, event: string, args?: any): Event { ++ switch (event) { ++ case '$onTerminalProcessEvent': return this.onTerminalProcessEvent(args); ++ } ++ ++ throw new Error(`Invalid listen '${event}'`); ++ } ++ ++ private onTerminalProcessEvent(args: terminal.IOnTerminalProcessEventArguments): Event { ++ throw new Error('not implemented'); ++ } ++ ++ public call(_: unknown, command: string, args?: any): Promise { ++ switch (command) { ++ case '$createTerminalProcess': return this.createTerminalProcess(args); ++ case '$startTerminalProcess': return this.startTerminalProcess(args); ++ case '$sendInputToTerminalProcess': return this.sendInputToTerminalProcess(args); ++ case '$shutdownTerminalProcess': return this.shutdownTerminalProcess(args); ++ case '$resizeTerminalProcess': return this.resizeTerminalProcess(args); ++ case '$getTerminalInitialCwd': return this.getTerminalInitialCwd(args); ++ case '$getTerminalCwd': return this.getTerminalCwd(args); ++ case '$sendCommandResultToTerminalProcess': return this.sendCommandResultToTerminalProcess(args); ++ case '$orphanQuestionReply': return this.orphanQuestionReply(args[0]); ++ case '$listTerminals': return this.listTerminals(args[0]); ++ } ++ ++ throw new Error(`Invalid call '${command}'`); ++ } ++ ++ public dispose(): void { ++ // Nothing yet. ++ } ++ ++ private async createTerminalProcess(args: terminal.ICreateTerminalProcessArguments): Promise { ++ throw new Error(`not implemented`); ++ } ++ ++ private async startTerminalProcess(args: terminal.IStartTerminalProcessArguments): Promise { ++ throw new Error('not implemented'); ++ } ++ ++ private async sendInputToTerminalProcess(args: terminal.ISendInputToTerminalProcessArguments): Promise { ++ throw new Error('not implemented'); ++ } ++ ++ private async shutdownTerminalProcess(args: terminal.IShutdownTerminalProcessArguments): Promise { ++ throw new Error('not implemented'); ++ } ++ ++ private async resizeTerminalProcess(args: terminal.IResizeTerminalProcessArguments): Promise { ++ throw new Error('not implemented'); ++ } ++ ++ private async getTerminalInitialCwd(args: terminal.IGetTerminalInitialCwdArguments): Promise { ++ throw new Error('not implemented'); ++ } ++ ++ private async getTerminalCwd(args: terminal.IGetTerminalCwdArguments): Promise { ++ throw new Error('not implemented'); ++ } ++ ++ private async sendCommandResultToTerminalProcess(args: terminal.ISendCommandResultToTerminalProcessArguments): Promise { ++ throw new Error('not implemented'); ++ } ++ ++ private async orphanQuestionReply(args: terminal.IOrphanQuestionReplyArgs): Promise { ++ throw new Error('not implemented'); ++ } ++ ++ private async listTerminals(args: terminal.IListTerminalsArgs): Promise { ++ throw new Error('not implemented'); ++ } ++} diff --git a/src/vs/server/node/connection.ts b/src/vs/server/node/connection.ts new file mode 100644 index 0000000000000000000000000000000000000000..93062cadc627c61e0829c27a72894b81e6a0e039 @@ -2585,10 +2662,10 @@ index 0000000000000000000000000000000000000000..0d9310038c0ca378579652d89bc8ac84 +} diff --git a/src/vs/server/node/server.ts b/src/vs/server/node/server.ts new file mode 100644 -index 0000000000000000000000000000000000000000..8424965d9c79d34e5513e4cfe543718521ad82c7 +index 0000000000000000000000000000000000000000..ebd3fbdf7554c63d23ad6bd0e51e0a35a94509dd --- /dev/null +++ b/src/vs/server/node/server.ts -@@ -0,0 +1,300 @@ +@@ -0,0 +1,302 @@ +import { field } from '@coder/logger'; +import * as fs from 'fs'; +import * as net from 'net'; @@ -2639,13 +2716,14 @@ index 0000000000000000000000000000000000000000..8424965d9c79d34e5513e4cfe5437185 +import { INodeProxyService, NodeProxyChannel } from 'vs/server/common/nodeProxy'; +import { TelemetryChannel } from 'vs/server/common/telemetry'; +import { Query, VscodeOptions, WorkbenchOptions } from 'vs/server/ipc'; -+import { ExtensionEnvironmentChannel, FileProviderChannel, NodeProxyService } from 'vs/server/node/channel'; ++import { ExtensionEnvironmentChannel, FileProviderChannel, NodeProxyService, TerminalProviderChannel } from 'vs/server/node/channel'; +import { Connection, ExtensionHostConnection, ManagementConnection } from 'vs/server/node/connection'; +import { TelemetryClient } from 'vs/server/node/insights'; +import { logger } from 'vs/server/node/logger'; +import { getLocaleFromConfig, getNlsConfiguration } from 'vs/server/node/nls'; +import { Protocol } from 'vs/server/node/protocol'; +import { getUriTransformer } from 'vs/server/node/util'; ++import { REMOTE_TERMINAL_CHANNEL_NAME } from 'vs/workbench/contrib/terminal/common/remoteTerminalChannel'; +import { REMOTE_FILE_SYSTEM_CHANNEL_NAME } from "vs/workbench/services/remote/common/remoteAgentFileSystemChannel"; +import { RemoteExtensionLogFileName } from 'vs/workbench/services/remote/common/remoteAgentService'; + @@ -2877,6 +2955,7 @@ index 0000000000000000000000000000000000000000..8424965d9c79d34e5513e4cfe5437185 + this.ipc.registerChannel('nodeProxy', new NodeProxyChannel(accessor.get(INodeProxyService))); + this.ipc.registerChannel('localizations', >createChannelReceiver(accessor.get(ILocalizationsService))); + this.ipc.registerChannel(REMOTE_FILE_SYSTEM_CHANNEL_NAME, new FileProviderChannel(environmentService, logService)); ++ this.ipc.registerChannel(REMOTE_TERMINAL_CHANNEL_NAME, new TerminalProviderChannel()); + resolve(new ErrorTelemetry(telemetryService)); + }); + }); From 3f7b91e2e2004263b5db704826b749009e5bc751 Mon Sep 17 00:00:00 2001 From: Asher Date: Tue, 17 Nov 2020 13:26:07 -0600 Subject: [PATCH 224/247] Implement most of remote terminal service It works, at least, but there are still some missing parts. --- ci/dev/vscode.patch | 385 +++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 364 insertions(+), 21 deletions(-) diff --git a/ci/dev/vscode.patch b/ci/dev/vscode.patch index 65f4a3b51..d8a5f2c05 100644 --- a/ci/dev/vscode.patch +++ b/ci/dev/vscode.patch @@ -1466,17 +1466,20 @@ index 0000000000000000000000000000000000000000..6ce56bec114a6d8daf5dd3ded945ea78 +} diff --git a/src/vs/server/node/channel.ts b/src/vs/server/node/channel.ts new file mode 100644 -index 0000000000000000000000000000000000000000..6fb1ada50628d3826a493c6e1b58f27a8be428bb +index 0000000000000000000000000000000000000000..91a932b613c473cd13dfddbde2942aeebf4bb84c --- /dev/null +++ b/src/vs/server/node/channel.ts -@@ -0,0 +1,437 @@ +@@ -0,0 +1,780 @@ ++import { field, logger } from '@coder/logger'; +import { Server } from '@coder/node-browser'; ++import * as os from 'os'; +import * as path from 'path'; +import { VSBuffer } from 'vs/base/common/buffer'; +import { CancellationTokenSource } from 'vs/base/common/cancellation'; +import { Emitter, Event } from 'vs/base/common/event'; +import { IDisposable } from 'vs/base/common/lifecycle'; -+import { OS } from 'vs/base/common/platform'; ++import * as platform from 'vs/base/common/platform'; ++import * as resources from 'vs/base/common/resources'; +import { ReadableStreamEventPayload } from 'vs/base/common/stream'; +import { URI, UriComponents } from 'vs/base/common/uri'; +import { transformOutgoingURIs } from 'vs/base/common/uriIpc'; @@ -1494,8 +1497,17 @@ index 0000000000000000000000000000000000000000..6fb1ada50628d3826a493c6e1b58f27a +import { getTranslations } from 'vs/server/node/nls'; +import { getUriTransformer } from 'vs/server/node/util'; +import { IFileChangeDto } from 'vs/workbench/api/common/extHost.protocol'; ++import { IEnvironmentVariableCollection } from 'vs/workbench/contrib/terminal/common/environmentVariable'; ++import { MergedEnvironmentVariableCollection } from 'vs/workbench/contrib/terminal/common/environmentVariableCollection'; ++import { deserializeEnvironmentVariableCollection } from 'vs/workbench/contrib/terminal/common/environmentVariableShared'; +import * as terminal from 'vs/workbench/contrib/terminal/common/remoteTerminalChannel'; -+import { ITerminalLaunchError } from 'vs/workbench/contrib/terminal/common/terminal'; ++import { IShellLaunchConfig, ITerminalEnvironment, ITerminalLaunchError } from 'vs/workbench/contrib/terminal/common/terminal'; ++import { TerminalDataBufferer } from 'vs/workbench/contrib/terminal/common/terminalDataBuffering'; ++import * as terminalEnvironment from 'vs/workbench/contrib/terminal/common/terminalEnvironment'; ++import { getSystemShell } from 'vs/workbench/contrib/terminal/node/terminal'; ++import { getMainProcessParentEnv } from 'vs/workbench/contrib/terminal/node/terminalEnvironment'; ++import { TerminalProcess } from 'vs/workbench/contrib/terminal/node/terminalProcess'; ++import { AbstractVariableResolverService } from 'vs/workbench/services/configurationResolver/common/variableResolver'; +import { ExtensionScanner, ExtensionScannerInput } from 'vs/workbench/services/extensions/node/extensionPoints'; + +/** @@ -1724,7 +1736,7 @@ index 0000000000000000000000000000000000000000..6fb1ada50628d3826a493c6e1b58f27a + globalStorageHome: this.environment.globalStorageHome, + workspaceStorageHome: this.environment.workspaceStorageHome, + userHome: this.environment.userHome, -+ os: OS, ++ os: platform.OS, + }; + } + @@ -1833,7 +1845,180 @@ index 0000000000000000000000000000000000000000..6fb1ada50628d3826a493c6e1b58f27a + } +} + ++class VariableResolverService extends AbstractVariableResolverService { ++ constructor(folders: terminal.IWorkspaceFolderData[], env: platform.IProcessEnvironment) { ++ super({ ++ getFolderUri: (name: string): URI | undefined => { ++ const folder = folders.find((f) => f.name === name); ++ return folder && URI.revive(folder.uri); ++ }, ++ getWorkspaceFolderCount: (): number => { ++ return folders.length; ++ }, ++ getConfigurationValue: (uri: URI, section: string): string | undefined => { ++ throw new Error("not implemented"); ++ }, ++ getExecPath: (): string | undefined => { ++ return env['VSCODE_EXEC_PATH']; ++ }, ++ getFilePath: (): string | undefined => { ++ throw new Error("not implemented"); ++ }, ++ getSelectedText: (): string | undefined => { ++ throw new Error("not implemented"); ++ }, ++ getLineNumber: (): string | undefined => { ++ throw new Error("not implemented"); ++ } ++ }, undefined, env); ++ } ++} ++ ++class Terminal { ++ private readonly process: TerminalProcess; ++ private _pid: number = -1; ++ private _title: string = ""; ++ public readonly workspaceId: string; ++ public readonly workspaceName: string; ++ ++ private readonly _onDispose = new Emitter(); ++ public get onDispose(): Event { return this._onDispose.event; } ++ ++ private buffering = false; ++ private readonly _onEvent = new Emitter({ ++ // Don't bind to data until something is listening. ++ onFirstListenerAdd: () => { ++ logger.debug('Terminal bound', field('id', this.id)); ++ if (!this.buffering) { ++ this.buffering = true; ++ this.bufferer.startBuffering(this.id, this.process.onProcessData); ++ } ++ }, ++ }); ++ ++ public get onEvent(): Event { return this._onEvent.event; } ++ ++ // Buffer to reduce the number of messages going to the renderer. ++ private readonly bufferer = new TerminalDataBufferer((_, data) => { ++ this._onEvent.fire({ ++ type: 'data', ++ data, ++ }); ++ }); ++ ++ public get pid(): number { ++ return this._pid; ++ } ++ ++ public get title(): string { ++ return this._title; ++ } ++ ++ public constructor( ++ public readonly id: number, ++ config: IShellLaunchConfig & { cwd: string }, ++ args: terminal.ICreateTerminalProcessArguments, ++ env: platform.IProcessEnvironment, ++ logService: ILogService, ++ ) { ++ this.workspaceId = args.workspaceId; ++ this.workspaceName = args.workspaceName; ++ ++ this.process = new TerminalProcess( ++ config, ++ config.cwd, ++ args.cols, ++ args.rows, ++ env, ++ process.env as platform.IProcessEnvironment, // Environment used for `findExecutable`. ++ false, // windowsEnableConpty: boolean, ++ logService, ++ ); ++ ++ // The current pid and title aren't exposed so they have to be tracked. ++ this.process.onProcessReady((event) => { ++ this._pid = event.pid; ++ this._onEvent.fire({ ++ type: 'ready', ++ pid: event.pid, ++ cwd: event.cwd, ++ }); ++ }); ++ ++ this.process.onProcessTitleChanged((title) => { ++ this._title = title; ++ this._onEvent.fire({ ++ type: 'titleChanged', ++ title, ++ }); ++ }); ++ ++ this.process.onProcessExit((exitCode) => { ++ logger.debug('Terminal exited', field('id', this.id), field('code', exitCode)); ++ this._onEvent.fire({ ++ type: 'exit', ++ exitCode, ++ }); ++ this.dispose(); ++ }); ++ ++ // TODO: replay event ++ // type: 'replay'; ++ // events: ReplayEntry[]; ++ ++ // TODO: exec command event ++ // type: 'execCommand'; ++ // reqId: number; ++ // commandId: string; ++ // commandArgs: any[]; ++ ++ // TODO: orphan question event ++ // type: 'orphan?'; ++ } ++ ++ public dispose() { ++ this._onEvent.dispose(); ++ this.bufferer.dispose(); ++ this.process.dispose(); ++ this._onDispose.fire(); ++ this._onDispose.dispose(); ++ } ++ ++ public shutdown(immediate: boolean): void { ++ return this.process.shutdown(immediate); ++ } ++ ++ public getCwd(): Promise { ++ return this.process.getCwd(); ++ } ++ ++ public getInitialCwd(): Promise { ++ return this.process.getInitialCwd(); ++ } ++ ++ public start(): Promise { ++ return this.process.start(); ++ } ++ ++ public input(data: string): void { ++ return this.process.input(data); ++ } ++ ++ public resize(cols: number, rows: number): void { ++ return this.process.resize(cols, rows); ++ } ++} ++ ++// References: - ../../workbench/api/node/extHostTerminalService.ts ++// - ../../workbench/contrib/terminal/browser/terminalProcessManager.ts +export class TerminalProviderChannel implements IServerChannel, IDisposable { ++ private readonly terminals = new Map(); ++ private id = 0; ++ ++ public constructor (private readonly logService: ILogService) { ++ ++ } ++ + public listen(_: RemoteAgentConnectionContext, event: string, args?: any): Event { + switch (event) { + case '$onTerminalProcessEvent': return this.onTerminalProcessEvent(args); @@ -1843,12 +2028,12 @@ index 0000000000000000000000000000000000000000..6fb1ada50628d3826a493c6e1b58f27a + } + + private onTerminalProcessEvent(args: terminal.IOnTerminalProcessEventArguments): Event { -+ throw new Error('not implemented'); ++ return this.getTerminal(args.id).onEvent; + } + -+ public call(_: unknown, command: string, args?: any): Promise { ++ public call(context: RemoteAgentConnectionContext, command: string, args?: any): Promise { + switch (command) { -+ case '$createTerminalProcess': return this.createTerminalProcess(args); ++ case '$createTerminalProcess': return this.createTerminalProcess(context.remoteAuthority, args); + case '$startTerminalProcess': return this.startTerminalProcess(args); + case '$sendInputToTerminalProcess': return this.sendInputToTerminalProcess(args); + case '$shutdownTerminalProcess': return this.shutdownTerminalProcess(args); @@ -1864,35 +2049,182 @@ index 0000000000000000000000000000000000000000..6fb1ada50628d3826a493c6e1b58f27a + } + + public dispose(): void { -+ // Nothing yet. ++ this.terminals.forEach((t) => t.dispose()); + } + -+ private async createTerminalProcess(args: terminal.ICreateTerminalProcessArguments): Promise { -+ throw new Error(`not implemented`); ++ private async createTerminalProcess(remoteAuthority: string, args: terminal.ICreateTerminalProcessArguments): Promise { ++ const terminalId = this.id++; ++ logger.debug('Creating terminal', field('id', terminalId), field("terminals", this.terminals.size)); ++ ++ const shellLaunchConfig: IShellLaunchConfig = { ++ name: args.shellLaunchConfig.name, ++ executable: args.shellLaunchConfig.executable, ++ args: args.shellLaunchConfig.args, ++ cwd: this.transform(remoteAuthority, args.shellLaunchConfig.cwd), ++ env: args.shellLaunchConfig.env, ++ }; ++ ++ // TODO: is this supposed to be the *last* workspace? ++ ++ const activeWorkspaceUri = this.transform(remoteAuthority, args.activeWorkspaceFolder?.uri); ++ const activeWorkspace = activeWorkspaceUri && args.activeWorkspaceFolder ? { ++ ...args.activeWorkspaceFolder, ++ uri: activeWorkspaceUri, ++ toResource: (relativePath: string) => resources.joinPath(activeWorkspaceUri, relativePath), ++ } : undefined; ++ ++ const resolverService = new VariableResolverService(args.workspaceFolders, process.env as platform.IProcessEnvironment); ++ const resolver = terminalEnvironment.createVariableResolver(activeWorkspace, resolverService); ++ ++ const getDefaultShellAndArgs = (): { executable: string; args: string[] | string } => { ++ if (shellLaunchConfig.executable) { ++ const executable = resolverService.resolve(activeWorkspace, shellLaunchConfig.executable); ++ let resolvedArgs: string[] | string = []; ++ if (shellLaunchConfig.args && Array.isArray(shellLaunchConfig.args)) { ++ for (const arg of shellLaunchConfig.args) { ++ resolvedArgs.push(resolverService.resolve(activeWorkspace, arg)); ++ } ++ } else if (shellLaunchConfig.args) { ++ resolvedArgs = resolverService.resolve(activeWorkspace, shellLaunchConfig.args); ++ } ++ return { executable, args: resolvedArgs }; ++ } ++ ++ const executable = terminalEnvironment.getDefaultShell( ++ (key) => args.configuration[key], ++ args.isWorkspaceShellAllowed, ++ getSystemShell(platform.platform), ++ process.env.hasOwnProperty('PROCESSOR_ARCHITEW6432'), ++ process.env.windir, ++ resolver, ++ this.logService, ++ false, // useAutomationShell ++ ); ++ ++ const resolvedArgs = terminalEnvironment.getDefaultShellArgs( ++ (key) => args.configuration[key], ++ args.isWorkspaceShellAllowed, ++ false, // useAutomationShell ++ resolver, ++ this.logService, ++ ); ++ ++ return { executable, args: resolvedArgs }; ++ }; ++ ++ const getInitialCwd = (): string => { ++ return terminalEnvironment.getCwd( ++ shellLaunchConfig, ++ os.homedir(), ++ resolver, ++ activeWorkspaceUri, ++ args.configuration['terminal.integrated.cwd'], ++ this.logService, ++ ); ++ }; ++ ++ // Use a separate var so Typescript recognizes these properties are no ++ // longer undefined. ++ const resolvedShellLaunchConfig = { ++ ...shellLaunchConfig, ++ ...getDefaultShellAndArgs(), ++ cwd: getInitialCwd(), ++ }; ++ ++ logger.debug('Resolved shell launch configuration', field('id', terminalId)); ++ ++ // Use instead of `terminal.integrated.env.${platform}` to make types work. ++ const getEnvFromConfig = (): terminal.ISingleTerminalConfiguration => { ++ if (platform.isWindows) { ++ return args.configuration['terminal.integrated.env.windows']; ++ } else if (platform.isMacintosh) { ++ return args.configuration['terminal.integrated.env.osx']; ++ } ++ return args.configuration['terminal.integrated.env.linux']; ++ }; ++ ++ const getNonInheritedEnv = async (): Promise => { ++ const env = await getMainProcessParentEnv(); ++ env.VSCODE_IPC_HOOK_CLI = process.env['VSCODE_IPC_HOOK_CLI']!; ++ return env; ++ }; ++ ++ const env = terminalEnvironment.createTerminalEnvironment( ++ shellLaunchConfig, ++ getEnvFromConfig(), ++ resolver, ++ args.isWorkspaceShellAllowed, ++ product.version, ++ args.configuration['terminal.integrated.detectLocale'], ++ args.configuration['terminal.integrated.inheritEnv'] !== false ++ ? process.env as platform.IProcessEnvironment ++ : await getNonInheritedEnv() ++ ); ++ ++ // Apply extension environment variable collections to the environment. ++ if (!shellLaunchConfig.strictEnv) { ++ // They come in an array and in serialized format. ++ const envVariableCollections = new Map(); ++ for (const [k, v] of args.envVariableCollections) { ++ envVariableCollections.set(k, { map: deserializeEnvironmentVariableCollection(v) }); ++ } ++ const mergedCollection = new MergedEnvironmentVariableCollection(envVariableCollections); ++ mergedCollection.applyToProcessEnvironment(env); ++ } ++ ++ logger.debug('Resolved terminal environment', field('id', terminalId)); ++ ++ const terminal = new Terminal(terminalId, resolvedShellLaunchConfig, args, env, this.logService); ++ this.terminals.set(terminalId, terminal); ++ logger.debug('Created terminal', field('id', terminalId)); ++ terminal.onDispose(() => this.terminals.delete(terminalId)); ++ ++ return { ++ terminalId, ++ resolvedShellLaunchConfig, ++ }; ++ } ++ ++ private transform(remoteAuthority: string, uri: UriComponents | undefined): URI | undefined ++ private transform(remoteAuthority: string, uri: string | UriComponents | undefined): string | URI | undefined ++ private transform(remoteAuthority: string, uri: string | UriComponents | undefined): string | URI | undefined { ++ if (typeof uri === 'string') { ++ return uri; ++ } ++ const transformer = getUriTransformer(remoteAuthority); ++ return uri ? URI.revive(transformer.transformIncoming(uri)) : uri; ++ } ++ ++ private getTerminal(id: number): Terminal { ++ const terminal = this.terminals.get(id); ++ if (!terminal) { ++ throw new Error(`terminal with id ${id} does not exist`); ++ } ++ return terminal; + } + + private async startTerminalProcess(args: terminal.IStartTerminalProcessArguments): Promise { -+ throw new Error('not implemented'); ++ return this.getTerminal(args.id).start(); + } + + private async sendInputToTerminalProcess(args: terminal.ISendInputToTerminalProcessArguments): Promise { -+ throw new Error('not implemented'); ++ return this.getTerminal(args.id).input(args.data); + } + + private async shutdownTerminalProcess(args: terminal.IShutdownTerminalProcessArguments): Promise { -+ throw new Error('not implemented'); ++ return this.getTerminal(args.id).shutdown(args.immediate); + } + + private async resizeTerminalProcess(args: terminal.IResizeTerminalProcessArguments): Promise { -+ throw new Error('not implemented'); ++ return this.getTerminal(args.id).resize(args.cols, args.rows); + } + + private async getTerminalInitialCwd(args: terminal.IGetTerminalInitialCwdArguments): Promise { -+ throw new Error('not implemented'); ++ return this.getTerminal(args.id).getInitialCwd(); + } + + private async getTerminalCwd(args: terminal.IGetTerminalCwdArguments): Promise { -+ throw new Error('not implemented'); ++ return this.getTerminal(args.id).getCwd(); + } + + private async sendCommandResultToTerminalProcess(args: terminal.ISendCommandResultToTerminalProcessArguments): Promise { @@ -1903,8 +2235,19 @@ index 0000000000000000000000000000000000000000..6fb1ada50628d3826a493c6e1b58f27a + throw new Error('not implemented'); + } + -+ private async listTerminals(args: terminal.IListTerminalsArgs): Promise { -+ throw new Error('not implemented'); ++ private async listTerminals(_: terminal.IListTerminalsArgs): Promise { ++ // TODO: args.isInitialization ++ return Promise.all(Array.from(this.terminals).map(async ([id, terminal]) => { ++ const cwd = await terminal.getCwd(); ++ return { ++ id, ++ pid: terminal.pid, ++ title: terminal.title, ++ cwd, ++ workspaceId: "0", ++ workspaceName: "test", ++ }; ++ })); + } +} diff --git a/src/vs/server/node/connection.ts b/src/vs/server/node/connection.ts @@ -2662,7 +3005,7 @@ index 0000000000000000000000000000000000000000..0d9310038c0ca378579652d89bc8ac84 +} diff --git a/src/vs/server/node/server.ts b/src/vs/server/node/server.ts new file mode 100644 -index 0000000000000000000000000000000000000000..ebd3fbdf7554c63d23ad6bd0e51e0a35a94509dd +index 0000000000000000000000000000000000000000..c10a5a3a6771a94b2cbcb699bb1261051c71e08b --- /dev/null +++ b/src/vs/server/node/server.ts @@ -0,0 +1,302 @@ @@ -2955,7 +3298,7 @@ index 0000000000000000000000000000000000000000..ebd3fbdf7554c63d23ad6bd0e51e0a35 + this.ipc.registerChannel('nodeProxy', new NodeProxyChannel(accessor.get(INodeProxyService))); + this.ipc.registerChannel('localizations', >createChannelReceiver(accessor.get(ILocalizationsService))); + this.ipc.registerChannel(REMOTE_FILE_SYSTEM_CHANNEL_NAME, new FileProviderChannel(environmentService, logService)); -+ this.ipc.registerChannel(REMOTE_TERMINAL_CHANNEL_NAME, new TerminalProviderChannel()); ++ this.ipc.registerChannel(REMOTE_TERMINAL_CHANNEL_NAME, new TerminalProviderChannel(logService)); + resolve(new ErrorTelemetry(telemetryService)); + }); + }); From 4de251116264052867427bcd732fd5af2f942901 Mon Sep 17 00:00:00 2001 From: Asher Date: Tue, 17 Nov 2020 17:03:15 -0600 Subject: [PATCH 225/247] Implement terminal replay event --- ci/dev/vscode.patch | 71 ++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 63 insertions(+), 8 deletions(-) diff --git a/ci/dev/vscode.patch b/ci/dev/vscode.patch index d8a5f2c05..49058f4f4 100644 --- a/ci/dev/vscode.patch +++ b/ci/dev/vscode.patch @@ -1466,10 +1466,10 @@ index 0000000000000000000000000000000000000000..6ce56bec114a6d8daf5dd3ded945ea78 +} diff --git a/src/vs/server/node/channel.ts b/src/vs/server/node/channel.ts new file mode 100644 -index 0000000000000000000000000000000000000000..91a932b613c473cd13dfddbde2942aeebf4bb84c +index 0000000000000000000000000000000000000000..cb3a45fda10a6bcbff73275b5734641b3319cc9b --- /dev/null +++ b/src/vs/server/node/channel.ts -@@ -0,0 +1,780 @@ +@@ -0,0 +1,828 @@ +import { field, logger } from '@coder/logger'; +import { Server } from '@coder/node-browser'; +import * as os from 'os'; @@ -1884,6 +1884,14 @@ index 0000000000000000000000000000000000000000..91a932b613c473cd13dfddbde2942aee + private readonly _onDispose = new Emitter(); + public get onDispose(): Event { return this._onDispose.event; } + ++ // These are replayed when a client reconnects. ++ private cols: number; ++ private rows: number; ++ private replayData: string[] = []; ++ // This is based on string length and is pretty arbitrary. ++ private readonly maxReplayData = 10000; ++ private totalReplayData = 0; ++ + private buffering = false; + private readonly _onEvent = new Emitter({ + // Don't bind to data until something is listening. @@ -1894,6 +1902,23 @@ index 0000000000000000000000000000000000000000..91a932b613c473cd13dfddbde2942aee + this.bufferer.startBuffering(this.id, this.process.onProcessData); + } + }, ++ ++ // Replay stored events. ++ onFirstListenerDidAdd: () => { ++ if (this.replayData.length === 0) { ++ return; ++ } ++ ++ logger.debug('Terminal replaying', field('id', this.id)); ++ this._onEvent.fire({ ++ type: 'replay', ++ events: [{ ++ cols: this.cols, ++ rows: this.rows, ++ data: this.replayData.join(""), ++ }] ++ }); ++ } + }); + + public get onEvent(): Event { return this._onEvent.event; } @@ -1904,6 +1929,33 @@ index 0000000000000000000000000000000000000000..91a932b613c473cd13dfddbde2942aee + type: 'data', + data, + }); ++ ++ this.replayData.push(data); ++ this.totalReplayData += data.length; ++ ++ let overflow = this.totalReplayData - this.maxReplayData; ++ if (overflow <= 0) { ++ return; ++ } ++ ++ // Drop events until doing so would put us under budget. ++ let deleteCount = 0; ++ for (; deleteCount < this.replayData.length ++ && this.replayData[deleteCount].length <= overflow; ++deleteCount) { ++ overflow -= this.replayData[deleteCount].length; ++ } ++ ++ if (deleteCount > 0) { ++ this.replayData.splice(0, deleteCount); ++ } ++ ++ // Dropping any more events would put us under budget; trim the first event ++ // instead if still over budget. ++ if (overflow > 0 && this.replayData.length > 0) { ++ this.replayData[0] = this.replayData[0].substring(overflow); ++ } ++ ++ this.totalReplayData = this.replayData.reduce((p, c) => p + c.length, 0); + }); + + public get pid(): number { @@ -1924,11 +1976,14 @@ index 0000000000000000000000000000000000000000..91a932b613c473cd13dfddbde2942aee + this.workspaceId = args.workspaceId; + this.workspaceName = args.workspaceName; + ++ this.cols = args.cols; ++ this.rows = args.rows; ++ + this.process = new TerminalProcess( + config, + config.cwd, -+ args.cols, -+ args.rows, ++ this.cols, ++ this.rows, + env, + process.env as platform.IProcessEnvironment, // Environment used for `findExecutable`. + false, // windowsEnableConpty: boolean, @@ -1962,10 +2017,6 @@ index 0000000000000000000000000000000000000000..91a932b613c473cd13dfddbde2942aee + this.dispose(); + }); + -+ // TODO: replay event -+ // type: 'replay'; -+ // events: ReplayEntry[]; -+ + // TODO: exec command event + // type: 'execCommand'; + // reqId: number; @@ -1977,9 +2028,11 @@ index 0000000000000000000000000000000000000000..91a932b613c473cd13dfddbde2942aee + } + + public dispose() { ++ logger.debug('Terminal disposing', field('id', this.id)); + this._onEvent.dispose(); + this.bufferer.dispose(); + this.process.dispose(); ++ this.process.shutdown(true); + this._onDispose.fire(); + this._onDispose.dispose(); + } @@ -2005,6 +2058,8 @@ index 0000000000000000000000000000000000000000..91a932b613c473cd13dfddbde2942aee + } + + public resize(cols: number, rows: number): void { ++ this.cols = cols; ++ this.rows = rows; + return this.process.resize(cols, rows); + } +} From 8311cf5657c7264c71373f1f15e0016da9336364 Mon Sep 17 00:00:00 2001 From: Asher Date: Tue, 17 Nov 2020 17:51:17 -0600 Subject: [PATCH 226/247] Handle non-persistent terminals --- ci/dev/vscode.patch | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/ci/dev/vscode.patch b/ci/dev/vscode.patch index 49058f4f4..949bed3fe 100644 --- a/ci/dev/vscode.patch +++ b/ci/dev/vscode.patch @@ -1880,6 +1880,7 @@ index 0000000000000000000000000000000000000000..cb3a45fda10a6bcbff73275b5734641b + private _title: string = ""; + public readonly workspaceId: string; + public readonly workspaceName: string; ++ private readonly persist: boolean; + + private readonly _onDispose = new Emitter(); + public get onDispose(): Event { return this._onDispose.event; } @@ -1918,6 +1919,13 @@ index 0000000000000000000000000000000000000000..cb3a45fda10a6bcbff73275b5734641b + data: this.replayData.join(""), + }] + }); ++ }, ++ ++ onLastListenerRemove: () => { ++ logger.debug('Terminal unbound', field('id', this.id)); ++ if (!this.persist) { // Used by debug consoles. ++ this.dispose(); ++ } + } + }); + @@ -1979,6 +1987,8 @@ index 0000000000000000000000000000000000000000..cb3a45fda10a6bcbff73275b5734641b + this.cols = args.cols; + this.rows = args.rows; + ++ this.persist = args.shouldPersistTerminal; ++ + this.process = new TerminalProcess( + config, + config.cwd, From 182aca649064bb54e8d00ac02e6667ee7702d0bb Mon Sep 17 00:00:00 2001 From: Asher Date: Thu, 19 Nov 2020 10:58:04 -0600 Subject: [PATCH 227/247] Only replay terminals when detached --- ci/dev/vscode.patch | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/ci/dev/vscode.patch b/ci/dev/vscode.patch index 949bed3fe..7bf38f340 100644 --- a/ci/dev/vscode.patch +++ b/ci/dev/vscode.patch @@ -1466,10 +1466,10 @@ index 0000000000000000000000000000000000000000..6ce56bec114a6d8daf5dd3ded945ea78 +} diff --git a/src/vs/server/node/channel.ts b/src/vs/server/node/channel.ts new file mode 100644 -index 0000000000000000000000000000000000000000..cb3a45fda10a6bcbff73275b5734641b3319cc9b +index 0000000000000000000000000000000000000000..b65bf283739a2b05c82fe38a038d2a10957c0d40 --- /dev/null +++ b/src/vs/server/node/channel.ts -@@ -0,0 +1,828 @@ +@@ -0,0 +1,848 @@ +import { field, logger } from '@coder/logger'; +import { Server } from '@coder/node-browser'; +import * as os from 'os'; @@ -1893,6 +1893,7 @@ index 0000000000000000000000000000000000000000..cb3a45fda10a6bcbff73275b5734641b + private readonly maxReplayData = 10000; + private totalReplayData = 0; + ++ private detached = false; + private buffering = false; + private readonly _onEvent = new Emitter({ + // Don't bind to data until something is listening. @@ -1906,10 +1907,11 @@ index 0000000000000000000000000000000000000000..cb3a45fda10a6bcbff73275b5734641b + + // Replay stored events. + onFirstListenerDidAdd: () => { -+ if (this.replayData.length === 0) { ++ if (!this.detached) { + return; + } + ++ this.detached = false; + logger.debug('Terminal replaying', field('id', this.id)); + this._onEvent.fire({ + type: 'replay', @@ -1922,6 +1924,7 @@ index 0000000000000000000000000000000000000000..cb3a45fda10a6bcbff73275b5734641b + }, + + onLastListenerRemove: () => { ++ this.detached = true; + logger.debug('Terminal unbound', field('id', this.id)); + if (!this.persist) { // Used by debug consoles. + this.dispose(); From 1feb30a7ff18149f94ad171f42edc82a8844cfef Mon Sep 17 00:00:00 2001 From: Asher Date: Thu, 19 Nov 2020 11:11:46 -0600 Subject: [PATCH 228/247] Send back workspace ID and name in terminal list This makes it re-connect automatically. --- ci/dev/vscode.patch | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ci/dev/vscode.patch b/ci/dev/vscode.patch index 7bf38f340..00c05d9d3 100644 --- a/ci/dev/vscode.patch +++ b/ci/dev/vscode.patch @@ -1466,7 +1466,7 @@ index 0000000000000000000000000000000000000000..6ce56bec114a6d8daf5dd3ded945ea78 +} diff --git a/src/vs/server/node/channel.ts b/src/vs/server/node/channel.ts new file mode 100644 -index 0000000000000000000000000000000000000000..b65bf283739a2b05c82fe38a038d2a10957c0d40 +index 0000000000000000000000000000000000000000..40779e80aa56d6b802d39f7170c9c94a997393ef --- /dev/null +++ b/src/vs/server/node/channel.ts @@ -0,0 +1,848 @@ @@ -2312,8 +2312,8 @@ index 0000000000000000000000000000000000000000..b65bf283739a2b05c82fe38a038d2a10 + pid: terminal.pid, + title: terminal.title, + cwd, -+ workspaceId: "0", -+ workspaceName: "test", ++ workspaceId: terminal.workspaceId, ++ workspaceName: terminal.workspaceName, + }; + })); + } From a6f884000995b85cdbec3e857eae893fd59b67b5 Mon Sep 17 00:00:00 2001 From: Asher Date: Thu, 19 Nov 2020 11:21:15 -0600 Subject: [PATCH 229/247] Add timeout for disposing detached terminals --- ci/dev/vscode.patch | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/ci/dev/vscode.patch b/ci/dev/vscode.patch index 00c05d9d3..3d7633753 100644 --- a/ci/dev/vscode.patch +++ b/ci/dev/vscode.patch @@ -1466,10 +1466,10 @@ index 0000000000000000000000000000000000000000..6ce56bec114a6d8daf5dd3ded945ea78 +} diff --git a/src/vs/server/node/channel.ts b/src/vs/server/node/channel.ts new file mode 100644 -index 0000000000000000000000000000000000000000..40779e80aa56d6b802d39f7170c9c94a997393ef +index 0000000000000000000000000000000000000000..95d0d3c51e4a25a9d7d0cada90d031c79bd86380 --- /dev/null +++ b/src/vs/server/node/channel.ts -@@ -0,0 +1,848 @@ +@@ -0,0 +1,860 @@ +import { field, logger } from '@coder/logger'; +import { Server } from '@coder/node-browser'; +import * as os from 'os'; @@ -1893,7 +1893,12 @@ index 0000000000000000000000000000000000000000..40779e80aa56d6b802d39f7170c9c94a + private readonly maxReplayData = 10000; + private totalReplayData = 0; + -+ private detached = false; ++ // According to the release notes the terminals are supposed to dispose after ++ // a short timeout; in our case we'll use 48 hours so you can get them back ++ // the next day or over the weekend. ++ private disposeTimeout: NodeJS.Timeout | undefined; ++ private disposeDelay = 48 * 60 * 60 * 1000; ++ + private buffering = false; + private readonly _onEvent = new Emitter({ + // Don't bind to data until something is listening. @@ -1907,11 +1912,15 @@ index 0000000000000000000000000000000000000000..40779e80aa56d6b802d39f7170c9c94a + + // Replay stored events. + onFirstListenerDidAdd: () => { -+ if (!this.detached) { ++ // We only need to replay if the terminal is being reconnected which is ++ // true if there is a dispose timeout. ++ if (typeof this.disposeTimeout !== "undefined") { + return; + } + -+ this.detached = false; ++ clearTimeout(this.disposeTimeout); ++ this.disposeTimeout = undefined; ++ + logger.debug('Terminal replaying', field('id', this.id)); + this._onEvent.fire({ + type: 'replay', @@ -1924,10 +1933,13 @@ index 0000000000000000000000000000000000000000..40779e80aa56d6b802d39f7170c9c94a + }, + + onLastListenerRemove: () => { -+ this.detached = true; + logger.debug('Terminal unbound', field('id', this.id)); + if (!this.persist) { // Used by debug consoles. + this.dispose(); ++ } else { ++ this.disposeTimeout = setTimeout(() => { ++ this.dispose(); ++ }, this.disposeDelay); + } + } + }); From 8ffe5997965e2be5c30ef5c8ecd9717fb286a61d Mon Sep 17 00:00:00 2001 From: Asher Date: Thu, 19 Nov 2020 11:24:52 -0600 Subject: [PATCH 230/247] Add notes on unimplemented terminal events --- ci/dev/vscode.patch | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/ci/dev/vscode.patch b/ci/dev/vscode.patch index 3d7633753..17ee41304 100644 --- a/ci/dev/vscode.patch +++ b/ci/dev/vscode.patch @@ -1466,10 +1466,10 @@ index 0000000000000000000000000000000000000000..6ce56bec114a6d8daf5dd3ded945ea78 +} diff --git a/src/vs/server/node/channel.ts b/src/vs/server/node/channel.ts new file mode 100644 -index 0000000000000000000000000000000000000000..95d0d3c51e4a25a9d7d0cada90d031c79bd86380 +index 0000000000000000000000000000000000000000..7081bbf178c660803830675a4d8d596c5d0b7821 --- /dev/null +++ b/src/vs/server/node/channel.ts -@@ -0,0 +1,860 @@ +@@ -0,0 +1,869 @@ +import { field, logger } from '@coder/logger'; +import { Server } from '@coder/node-browser'; +import * as os from 'os'; @@ -2042,13 +2042,17 @@ index 0000000000000000000000000000000000000000..95d0d3c51e4a25a9d7d0cada90d031c7 + this.dispose(); + }); + -+ // TODO: exec command event ++ // TODO: I think `execCommand` must have something to do with running ++ // commands on the terminal that will do things in VS Code but we already ++ // have that functionality via a socket so I'm not sure what this is for. + // type: 'execCommand'; + // reqId: number; + // commandId: string; + // commandArgs: any[]; + -+ // TODO: orphan question event ++ // TODO: Maybe this is to ask if the terminal is currently attached to ++ // anything? But we already know that on account of whether anything is ++ // listening to our event emitter. + // type: 'orphan?'; + } + @@ -2307,16 +2311,21 @@ index 0000000000000000000000000000000000000000..95d0d3c51e4a25a9d7d0cada90d031c7 + return this.getTerminal(args.id).getCwd(); + } + -+ private async sendCommandResultToTerminalProcess(args: terminal.ISendCommandResultToTerminalProcessArguments): Promise { ++ private async sendCommandResultToTerminalProcess(_: terminal.ISendCommandResultToTerminalProcessArguments): Promise { ++ // NOTE: Not required unless we implement the `execCommand` event, see above. + throw new Error('not implemented'); + } + -+ private async orphanQuestionReply(args: terminal.IOrphanQuestionReplyArgs): Promise { ++ private async orphanQuestionReply(_: terminal.IOrphanQuestionReplyArgs): Promise { ++ // NOTE: Not required unless we implement the `orphan?` event, see above. + throw new Error('not implemented'); + } + + private async listTerminals(_: terminal.IListTerminalsArgs): Promise { -+ // TODO: args.isInitialization ++ // TODO: args.isInitialization. Maybe this is to have slightly different ++ // behavior when first listing terminals but I don't know what you'd want to ++ // do differently. Maybe it's to reset the terminal dispose timeouts or ++ // something like that, but why not do it each time you list? + return Promise.all(Array.from(this.terminals).map(async ([id, terminal]) => { + const cwd = await terminal.getCwd(); + return { From fa59156a2aa30fc9d3e4332046a0dab749f6bc7e Mon Sep 17 00:00:00 2001 From: Asher Date: Thu, 19 Nov 2020 12:26:31 -0600 Subject: [PATCH 231/247] Implement remaining resolver methods --- ci/dev/vscode.patch | 70 ++++++++++++++++++++++++++++----------------- 1 file changed, 44 insertions(+), 26 deletions(-) diff --git a/ci/dev/vscode.patch b/ci/dev/vscode.patch index 17ee41304..4ec8b2665 100644 --- a/ci/dev/vscode.patch +++ b/ci/dev/vscode.patch @@ -1466,10 +1466,10 @@ index 0000000000000000000000000000000000000000..6ce56bec114a6d8daf5dd3ded945ea78 +} diff --git a/src/vs/server/node/channel.ts b/src/vs/server/node/channel.ts new file mode 100644 -index 0000000000000000000000000000000000000000..7081bbf178c660803830675a4d8d596c5d0b7821 +index 0000000000000000000000000000000000000000..7619b02f04b6e61e86e741b09b542d86fab97e3d --- /dev/null +++ b/src/vs/server/node/channel.ts -@@ -0,0 +1,869 @@ +@@ -0,0 +1,887 @@ +import { field, logger } from '@coder/logger'; +import { Server } from '@coder/node-browser'; +import * as os from 'os'; @@ -1846,30 +1846,51 @@ index 0000000000000000000000000000000000000000..7081bbf178c660803830675a4d8d596c +} + +class VariableResolverService extends AbstractVariableResolverService { -+ constructor(folders: terminal.IWorkspaceFolderData[], env: platform.IProcessEnvironment) { ++ constructor( ++ remoteAuthority: string, ++ args: terminal.ICreateTerminalProcessArguments, ++ env: platform.IProcessEnvironment, ++ ) { + super({ + getFolderUri: (name: string): URI | undefined => { -+ const folder = folders.find((f) => f.name === name); ++ const folder = args.workspaceFolders.find((f) => f.name === name); + return folder && URI.revive(folder.uri); + }, + getWorkspaceFolderCount: (): number => { -+ return folders.length; ++ return args.workspaceFolders.length; + }, -+ getConfigurationValue: (uri: URI, section: string): string | undefined => { -+ throw new Error("not implemented"); ++ // In ../../workbench/contrib/terminal/common/remoteTerminalChannel.ts it ++ // looks like there are `config:` entries which must be for this? Not sure ++ // how/if the URI comes into play though. ++ getConfigurationValue: (_: URI, section: string): string | undefined => { ++ return args.resolvedVariables[`config:${section}`]; + }, + getExecPath: (): string | undefined => { + return env['VSCODE_EXEC_PATH']; + }, ++ // This is just a guess; this is the only file-related thing we're sent ++ // and none of these resolver methods seem to get called so I don't know ++ // how to test. + getFilePath: (): string | undefined => { -+ throw new Error("not implemented"); ++ const resource = transformIncoming(remoteAuthority, args.activeFileResource); ++ if (!resource) { ++ return undefined; ++ } ++ // See ../../editor/standalone/browser/simpleServices.ts; ++ // `BaseConfigurationResolverService` calls `getUriLabel` from there. ++ if (resource.scheme === 'file') { ++ return resource.fsPath; ++ } ++ return resource.path; + }, ++ // It looks like these are set here although they aren't on the types: ++ // ../../workbench/contrib/terminal/common/remoteTerminalChannel.ts + getSelectedText: (): string | undefined => { -+ throw new Error("not implemented"); ++ return args.resolvedVariables.selectedText; + }, + getLineNumber: (): string | undefined => { -+ throw new Error("not implemented"); -+ } ++ return args.resolvedVariables.selectedText; ++ }, + }, undefined, env); + } +} @@ -2144,20 +2165,22 @@ index 0000000000000000000000000000000000000000..7081bbf178c660803830675a4d8d596c + name: args.shellLaunchConfig.name, + executable: args.shellLaunchConfig.executable, + args: args.shellLaunchConfig.args, -+ cwd: this.transform(remoteAuthority, args.shellLaunchConfig.cwd), ++ // TODO: Should we transform if it's a string as well? The incoming ++ // transform only takes `UriComponents` so I suspect it's not necessary. ++ cwd: typeof args.shellLaunchConfig.cwd !== "string" ++ ? transformIncoming(remoteAuthority, args.shellLaunchConfig.cwd) ++ : args.shellLaunchConfig.cwd, + env: args.shellLaunchConfig.env, + }; + -+ // TODO: is this supposed to be the *last* workspace? -+ -+ const activeWorkspaceUri = this.transform(remoteAuthority, args.activeWorkspaceFolder?.uri); ++ const activeWorkspaceUri = transformIncoming(remoteAuthority, args.activeWorkspaceFolder?.uri); + const activeWorkspace = activeWorkspaceUri && args.activeWorkspaceFolder ? { + ...args.activeWorkspaceFolder, + uri: activeWorkspaceUri, + toResource: (relativePath: string) => resources.joinPath(activeWorkspaceUri, relativePath), + } : undefined; + -+ const resolverService = new VariableResolverService(args.workspaceFolders, process.env as platform.IProcessEnvironment); ++ const resolverService = new VariableResolverService(remoteAuthority, args, process.env as platform.IProcessEnvironment); + const resolver = terminalEnvironment.createVariableResolver(activeWorkspace, resolverService); + + const getDefaultShellAndArgs = (): { executable: string; args: string[] | string } => { @@ -2269,16 +2292,6 @@ index 0000000000000000000000000000000000000000..7081bbf178c660803830675a4d8d596c + }; + } + -+ private transform(remoteAuthority: string, uri: UriComponents | undefined): URI | undefined -+ private transform(remoteAuthority: string, uri: string | UriComponents | undefined): string | URI | undefined -+ private transform(remoteAuthority: string, uri: string | UriComponents | undefined): string | URI | undefined { -+ if (typeof uri === 'string') { -+ return uri; -+ } -+ const transformer = getUriTransformer(remoteAuthority); -+ return uri ? URI.revive(transformer.transformIncoming(uri)) : uri; -+ } -+ + private getTerminal(id: number): Terminal { + const terminal = this.terminals.get(id); + if (!terminal) { @@ -2339,6 +2352,11 @@ index 0000000000000000000000000000000000000000..7081bbf178c660803830675a4d8d596c + })); + } +} ++ ++function transformIncoming(remoteAuthority: string, uri: UriComponents | undefined): URI | undefined { ++ const transformer = getUriTransformer(remoteAuthority); ++ return uri ? URI.revive(transformer.transformIncoming(uri)) : uri; ++} diff --git a/src/vs/server/node/connection.ts b/src/vs/server/node/connection.ts new file mode 100644 index 0000000000000000000000000000000000000000..93062cadc627c61e0829c27a72894b81e6a0e039 From d0f6cbb02d73cc80ca42390c87bd6386b4390f00 Mon Sep 17 00:00:00 2001 From: Asher Date: Thu, 19 Nov 2020 12:42:59 -0600 Subject: [PATCH 232/247] Use resolverEnv to get exec path This is the last unused variable in the create terminal payload. --- ci/dev/vscode.patch | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/ci/dev/vscode.patch b/ci/dev/vscode.patch index 4ec8b2665..0bcec08e9 100644 --- a/ci/dev/vscode.patch +++ b/ci/dev/vscode.patch @@ -1466,10 +1466,10 @@ index 0000000000000000000000000000000000000000..6ce56bec114a6d8daf5dd3ded945ea78 +} diff --git a/src/vs/server/node/channel.ts b/src/vs/server/node/channel.ts new file mode 100644 -index 0000000000000000000000000000000000000000..7619b02f04b6e61e86e741b09b542d86fab97e3d +index 0000000000000000000000000000000000000000..769d57953e782b7a72519599274630236114b17d --- /dev/null +++ b/src/vs/server/node/channel.ts -@@ -0,0 +1,887 @@ +@@ -0,0 +1,889 @@ +import { field, logger } from '@coder/logger'; +import { Server } from '@coder/node-browser'; +import * as os from 'os'; @@ -1866,7 +1866,9 @@ index 0000000000000000000000000000000000000000..7619b02f04b6e61e86e741b09b542d86 + return args.resolvedVariables[`config:${section}`]; + }, + getExecPath: (): string | undefined => { -+ return env['VSCODE_EXEC_PATH']; ++ // Assuming that resolverEnv is just for use in the resolver and not for ++ // the terminal itself. ++ return (args.resolverEnv && args.resolverEnv['VSCODE_EXEC_PATH']) || env['VSCODE_EXEC_PATH']; + }, + // This is just a guess; this is the only file-related thing we're sent + // and none of these resolver methods seem to get called so I don't know From 42390da0978b544679fdcd0be5c4573631d52c34 Mon Sep 17 00:00:00 2001 From: Asher Date: Thu, 19 Nov 2020 15:13:46 -0600 Subject: [PATCH 233/247] Don't persist terminals for now --- ci/dev/vscode.patch | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/ci/dev/vscode.patch b/ci/dev/vscode.patch index 0bcec08e9..f6af1c227 100644 --- a/ci/dev/vscode.patch +++ b/ci/dev/vscode.patch @@ -1466,10 +1466,10 @@ index 0000000000000000000000000000000000000000..6ce56bec114a6d8daf5dd3ded945ea78 +} diff --git a/src/vs/server/node/channel.ts b/src/vs/server/node/channel.ts new file mode 100644 -index 0000000000000000000000000000000000000000..769d57953e782b7a72519599274630236114b17d +index 0000000000000000000000000000000000000000..693174ee0d21353c3a08a42fd30eaad1e95c3b9d --- /dev/null +++ b/src/vs/server/node/channel.ts -@@ -0,0 +1,889 @@ +@@ -0,0 +1,897 @@ +import { field, logger } from '@coder/logger'; +import { Server } from '@coder/node-browser'; +import * as os from 'os'; @@ -1976,6 +1976,11 @@ index 0000000000000000000000000000000000000000..769d57953e782b7a7251959927463023 + data, + }); + ++ // No need to store data if we aren't persisting. ++ if (!this.persist) { ++ return; ++ } ++ + this.replayData.push(data); + this.totalReplayData += data.length; + @@ -2025,7 +2030,10 @@ index 0000000000000000000000000000000000000000..769d57953e782b7a7251959927463023 + this.cols = args.cols; + this.rows = args.rows; + -+ this.persist = args.shouldPersistTerminal; ++ // TODO: Don't persist terminals until we make it work with things like ++ // htop, vim, etc. ++ // this.persist = args.shouldPersistTerminal; ++ this.persist = false; + + this.process = new TerminalProcess( + config, From a0db6723c1ef22b40d7ac0df5ac1bb9a92e4fe46 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Thu, 19 Nov 2020 17:28:22 -0500 Subject: [PATCH 234/247] v3.7.2 --- ci/README.md | 1 + ci/helm-chart/Chart.yaml | 4 ++-- ci/helm-chart/README.md | 4 ++-- ci/helm-chart/values.yaml | 2 +- doc/install.md | 14 +++++++------- package.json | 2 +- 6 files changed, 14 insertions(+), 13 deletions(-) diff --git a/ci/README.md b/ci/README.md index 6a8b44c5c..3448f9e35 100644 --- a/ci/README.md +++ b/ci/README.md @@ -18,6 +18,7 @@ Make sure you have `$GITHUB_TOKEN` set and [hub](https://github.com/github/hub) 1. Update in `package.json` 2. Update in [./doc/install.md](../doc/install.md) 3. Update in [./ci/helm-chart/README.md](../ci/helm-chart/README.md) + - Remember to update the chart version as well on top of appVersion in `Chart.yaml`. 2. GitHub actions will generate the `npm-package`, `release-packages` and `release-images` artifacts. 1. You do not have to wait for these. 3. Run `yarn release:github-draft` to create a GitHub draft release from the template with diff --git a/ci/helm-chart/Chart.yaml b/ci/helm-chart/Chart.yaml index 986b0395f..089e03486 100644 --- a/ci/helm-chart/Chart.yaml +++ b/ci/helm-chart/Chart.yaml @@ -15,9 +15,9 @@ type: application # This is the chart version. This version number should be incremented each time you make changes # to the chart and its templates, including the app version. # Versions are expected to follow Semantic Versioning (https://semver.org/) -version: 1.0.0 +version: 1.0.1 # This is the version number of the application being deployed. This version number should be # incremented each time you make changes to the application. Versions are not expected to # follow Semantic Versioning. They should reflect the version the application is using. -appVersion: 3.7.1 +appVersion: 3.7.2 diff --git a/ci/helm-chart/README.md b/ci/helm-chart/README.md index f944f48fa..aa6b506ad 100644 --- a/ci/helm-chart/README.md +++ b/ci/helm-chart/README.md @@ -1,6 +1,6 @@ # code-server -![Version: 1.0.0](https://img.shields.io/badge/Version-1.0.0-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: 3.7.1](https://img.shields.io/badge/AppVersion-3.7.1-informational?style=flat-square) +![Version: 1.0.0](https://img.shields.io/badge/Version-1.0.0-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: 3.7.2](https://img.shields.io/badge/AppVersion-3.7.2-informational?style=flat-square) [code-server](https://github.com/cdr/code-server) code-server is VS Code running on a remote server, accessible through the browser. @@ -72,7 +72,7 @@ and their default values. | hostnameOverride | string | `""` | | | image.pullPolicy | string | `"Always"` | | | image.repository | string | `"codercom/code-server"` | | -| image.tag | string | `"3.7.1"` | | +| image.tag | string | `"3.7.2"` | | | imagePullSecrets | list | `[]` | | | ingress.enabled | bool | `false` | | | nameOverride | string | `""` | | diff --git a/ci/helm-chart/values.yaml b/ci/helm-chart/values.yaml index 32f844697..5d2ba0299 100644 --- a/ci/helm-chart/values.yaml +++ b/ci/helm-chart/values.yaml @@ -6,7 +6,7 @@ replicaCount: 1 image: repository: codercom/code-server - tag: '3.7.1' + tag: '3.7.2' pullPolicy: Always imagePullSecrets: [] diff --git a/doc/install.md b/doc/install.md index 97db9dde5..7515db45a 100644 --- a/doc/install.md +++ b/doc/install.md @@ -80,8 +80,8 @@ commands presented in the rest of this document. ## Debian, Ubuntu ```bash -curl -fOL https://github.com/cdr/code-server/releases/download/v3.7.1/code-server_3.7.1_amd64.deb -sudo dpkg -i code-server_3.7.1_amd64.deb +curl -fOL https://github.com/cdr/code-server/releases/download/v3.7.2/code-server_3.7.2_amd64.deb +sudo dpkg -i code-server_3.7.2_amd64.deb sudo systemctl enable --now code-server@$USER # Now visit http://127.0.0.1:8080. Your password is in ~/.config/code-server/config.yaml ``` @@ -89,8 +89,8 @@ sudo systemctl enable --now code-server@$USER ## Fedora, CentOS, RHEL, SUSE ```bash -curl -fOL https://github.com/cdr/code-server/releases/download/v3.7.1/code-server-3.7.1-amd64.rpm -sudo rpm -i code-server-3.7.1-amd64.rpm +curl -fOL https://github.com/cdr/code-server/releases/download/v3.7.2/code-server-3.7.2-amd64.rpm +sudo rpm -i code-server-3.7.2-amd64.rpm sudo systemctl enable --now code-server@$USER # Now visit http://127.0.0.1:8080. Your password is in ~/.config/code-server/config.yaml ``` @@ -159,10 +159,10 @@ Here is an example script for installing and using a standalone `code-server` re ```bash mkdir -p ~/.local/lib ~/.local/bin -curl -fL https://github.com/cdr/code-server/releases/download/v3.7.1/code-server-3.7.1-linux-amd64.tar.gz \ +curl -fL https://github.com/cdr/code-server/releases/download/v3.7.2/code-server-3.7.2-linux-amd64.tar.gz \ | tar -C ~/.local/lib -xz -mv ~/.local/lib/code-server-3.7.1-linux-amd64 ~/.local/lib/code-server-3.7.1 -ln -s ~/.local/lib/code-server-3.7.1/bin/code-server ~/.local/bin/code-server +mv ~/.local/lib/code-server-3.7.2-linux-amd64 ~/.local/lib/code-server-3.7.2 +ln -s ~/.local/lib/code-server-3.7.2/bin/code-server ~/.local/bin/code-server PATH="~/.local/bin:$PATH" code-server # Now visit http://127.0.0.1:8080. Your password is in ~/.config/code-server/config.yaml diff --git a/package.json b/package.json index fe8153a0f..9f41af56f 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "code-server", "license": "MIT", - "version": "3.7.1", + "version": "3.7.2", "description": "Run VS Code on a remote server.", "homepage": "https://github.com/cdr/code-server", "bugs": { From 2e2d03371f7eda4ab0647b839d7b818c86b3b6e7 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Thu, 19 Nov 2020 18:03:12 -0500 Subject: [PATCH 235/247] ci: Fix typo in release template --- ci/build/release-github-draft.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ci/build/release-github-draft.sh b/ci/build/release-github-draft.sh index d311dbb65..71cbb714a 100755 --- a/ci/build/release-github-draft.sh +++ b/ci/build/release-github-draft.sh @@ -15,7 +15,7 @@ v$VERSION VS Code v$(vscode_version) -# New Features +## New Features - ⭐ Summarize new features here with references to issues ## Bug Fixes From 3d7fbec40f820ab6552bfa0d16c2d2efaf663b62 Mon Sep 17 00:00:00 2001 From: Asher Date: Fri, 20 Nov 2020 13:47:58 -0600 Subject: [PATCH 236/247] Use file system for settings and fix data home path It's possible that using browser storage makes more sense with settings sync, so we might want to revisit this once/if we get settings sync working. As it currently is though, browser storage just causes jank. The path was also missing a `User` at the end so I added that. This might affect the Vim extension which would have been writing to the wrong path previously but I don't believe it should affect anything else since they would have been writing to browser storage. - Fixes #2208 - Fixes #2231 - Fixes #2279 - Fixes #2274 --- ci/dev/vscode.patch | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/ci/dev/vscode.patch b/ci/dev/vscode.patch index f6af1c227..b900f53d0 100644 --- a/ci/dev/vscode.patch +++ b/ci/dev/vscode.patch @@ -3797,18 +3797,25 @@ index 85d83f37da179a1e39266cf72a02e971f590308e..0659738b36df1747c9afcabf8d9abf26 }; diff --git a/src/vs/workbench/services/environment/browser/environmentService.ts b/src/vs/workbench/services/environment/browser/environmentService.ts -index a8d43045ecc8cbe04b3f8440cff16d42aadbcad0..cd589c6f75eccbeefbf364d426ac882396b26fb4 100644 +index a8d43045ecc8cbe04b3f8440cff16d42aadbcad0..8e122c761ac7ddfee11f9dda2ac5e845b893cc28 100644 --- a/src/vs/workbench/services/environment/browser/environmentService.ts +++ b/src/vs/workbench/services/environment/browser/environmentService.ts -@@ -119,8 +119,18 @@ export class BrowserWorkbenchEnvironmentService implements IWorkbenchEnvironment +@@ -119,8 +119,25 @@ export class BrowserWorkbenchEnvironmentService implements IWorkbenchEnvironment @memoize get logFile(): URI { return joinPath(this.options.logsPath, 'window.log'); } -+ // NOTE@coder: Use the regular path for extensions that write directly to disk -+ // instead of using the VS Code API. ++ // NOTE@coder: Use the same path in // ../../../../platform/environment/node/environmentService.ts ++ // and don't use the user data scheme. This solves two problems: ++ // 1. Extensions running in the browser (like Vim) might use these paths ++ // directly instead of using the file service and most likely can't write ++ // to `/User` on disk. ++ // 2. Settings will be stored in the file system instead of in browser ++ // storage. Using browser storage makes sharing or seeding settings ++ // between browsers difficult. We may want to revisit this once/if we get ++ // settings sync. @memoize - get userRoamingDataHome(): URI { return URI.file('/User').with({ scheme: Schemas.userData }); } -+ get userRoamingDataHome(): URI { return URI.file(this.userDataPath).with({ scheme: Schemas.userData }); } ++ get userRoamingDataHome(): URI { return joinPath(URI.file(this.userDataPath).with({ scheme: Schemas.vscodeRemote }), 'User'); } + @memoize + get userDataPath(): string { + const dataPath = this.payload?.get("userDataPath"); @@ -3820,7 +3827,7 @@ index a8d43045ecc8cbe04b3f8440cff16d42aadbcad0..cd589c6f75eccbeefbf364d426ac8823 @memoize get settingsResource(): URI { return joinPath(this.userRoamingDataHome, 'settings.json'); } -@@ -301,7 +311,12 @@ export class BrowserWorkbenchEnvironmentService implements IWorkbenchEnvironment +@@ -301,7 +318,12 @@ export class BrowserWorkbenchEnvironmentService implements IWorkbenchEnvironment extensionHostDebugEnvironment.params.port = parseInt(value); break; case 'enableProposedApi': From fb835838db90e4a846dc11f1a0c214bc9c5d58b0 Mon Sep 17 00:00:00 2001 From: Asher Date: Fri, 20 Nov 2020 15:35:18 -0600 Subject: [PATCH 237/247] Remove semver-umd link This is included in the bundle now. --- src/browser/pages/vscode.html | 5 ----- src/browser/pages/vscode.ts | 1 - 2 files changed, 6 deletions(-) diff --git a/src/browser/pages/vscode.html b/src/browser/pages/vscode.html index d9305fe9d..727a1ca87 100644 --- a/src/browser/pages/vscode.html +++ b/src/browser/pages/vscode.html @@ -32,11 +32,6 @@ - - - diff --git a/src/browser/pages/vscode.ts b/src/browser/pages/vscode.ts index 2cb7973f1..d95e91741 100644 --- a/src/browser/pages/vscode.ts +++ b/src/browser/pages/vscode.ts @@ -41,7 +41,6 @@ try { "xterm-addon-search": `../node_modules/xterm-addon-search/lib/xterm-addon-search.js`, "xterm-addon-unicode11": `../node_modules/xterm-addon-unicode11/lib/xterm-addon-unicode11.js`, "xterm-addon-webgl": `../node_modules/xterm-addon-webgl/lib/xterm-addon-webgl.js`, - "semver-umd": `../node_modules/semver-umd/lib/semver-umd.js`, "tas-client-umd": `../node_modules/tas-client-umd/lib/tas-client-umd.js`, "iconv-lite-umd": `../node_modules/iconv-lite-umd/lib/iconv-lite-umd.js`, jschardet: `../node_modules/jschardet/dist/jschardet.min.js`, From 19710ab1442b914566e8825d2221d2658dd09413 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Mon, 23 Nov 2020 19:03:54 -0500 Subject: [PATCH 238/247] vscode: Update product.json The new fields are from vscodium and make the welcome page documentation links work correctly. I also renamed the distribution to "code-server" so that when you're in a browser, it now says code-server instead of Code OSS. --- ci/dev/vscode.patch | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/ci/dev/vscode.patch b/ci/dev/vscode.patch index f6af1c227..3737261ed 100644 --- a/ci/dev/vscode.patch +++ b/ci/dev/vscode.patch @@ -264,9 +264,18 @@ index 28f8a69a2a91f9cb9f4dbd73ed3e689b2b3afe84..b5f5b10004d3e36092a30f685938a606 } } diff --git a/product.json b/product.json -index 7cab6d1b9f3b84bfc703856e93773a293fd198cf..31d3d5a943192eee791e1121415b436dc1ed3822 100644 +index 7cab6d1b9f3b84bfc703856e93773a293fd198cf..6924d94f65b390f52885b1036f7e96bce0e34680 100644 --- a/product.json +++ b/product.json +@@ -1,6 +1,6 @@ + { +- "nameShort": "Code - OSS", +- "nameLong": "Code - OSS", ++ "nameShort": "code-server", ++ "nameLong": "code-server", + "applicationName": "code-oss", + "dataFolderName": ".vscode-oss", + "win32MutexName": "vscodeoss", @@ -20,7 +20,7 @@ "darwinBundleIdentifier": "com.visualstudio.code.oss", "linuxIconName": "com.visualstudio.code.oss", @@ -276,6 +285,22 @@ index 7cab6d1b9f3b84bfc703856e93773a293fd198cf..31d3d5a943192eee791e1121415b436d "urlProtocol": "code-oss", "extensionAllowedProposedApi": [ "ms-vscode.vscode-js-profile-flame", +@@ -136,5 +136,14 @@ + "publisherDisplayName": "Microsoft" + } + } +- ] ++ ], ++ ++ "//": "https://github.com/VSCodium/vscodium/pull/155/files", ++ "documentationUrl": "https://go.microsoft.com/fwlink/?LinkID=533484#vscode", ++ "keyboardShortcutsUrlMac": "https://go.microsoft.com/fwlink/?linkid=832143", ++ "keyboardShortcutsUrlLinux": "https://go.microsoft.com/fwlink/?linkid=832144", ++ "keyboardShortcutsUrlWin": "https://go.microsoft.com/fwlink/?linkid=832145", ++ "introductoryVideosUrl": "https://go.microsoft.com/fwlink/?linkid=832146", ++ "tipsAndTricksUrl": "https://go.microsoft.com/fwlink/?linkid=852118", ++ "newsletterSignupUrl": "https://www.research.net/r/vsc-newsletter" + } diff --git a/remote/.yarnrc b/remote/.yarnrc deleted file mode 100644 index c1a32ce532afa501fb19bdbcf6bcb0ec151ecd99..0000000000000000000000000000000000000000 From 5a38ab95fe0a0da3858254667335df7c1d2db57a Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Mon, 23 Nov 2020 19:06:51 -0500 Subject: [PATCH 239/247] vscode: Disable go home button See https://github.com/cdr/code-server/issues/2328 --- ci/dev/vscode.patch | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/ci/dev/vscode.patch b/ci/dev/vscode.patch index 3737261ed..abf86b4ba 100644 --- a/ci/dev/vscode.patch +++ b/ci/dev/vscode.patch @@ -3712,6 +3712,20 @@ index 021af6e0f8983c492f9cdd048ba2dcae7640bc1d..814dd0ff2fa7737e07833d8092c8f489 module = module.with({ path: ensureSuffix(module.path, '.js') }); const response = await fetch(module.toString(true)); +diff --git a/src/vs/workbench/browser/actions/navigationActions.ts b/src/vs/workbench/browser/actions/navigationActions.ts +index 7344a3a29b32f7b370b99bf0cfdc79a322195ff8..dc21396e83e2f53914447d3460c2ee1103ecb28e 100644 +--- a/src/vs/workbench/browser/actions/navigationActions.ts ++++ b/src/vs/workbench/browser/actions/navigationActions.ts +@@ -310,4 +310,8 @@ actionsRegistry.registerWorkbenchAction(SyncActionDescriptor.from(FocusNextPart, + actionsRegistry.registerWorkbenchAction(SyncActionDescriptor.from(FocusPreviousPart, { primary: KeyMod.Shift | KeyCode.F6 }), 'View: Focus Previous Part', CATEGORIES.View.value); + + const workbenchRegistry = Registry.as(WorkbenchExtensions.Workbench); +-workbenchRegistry.registerWorkbenchContribution(GoHomeContributor, LifecyclePhase.Ready); ++// See https://github.com/cdr/code-server/issues/2328 ++// workbenchRegistry.registerWorkbenchContribution(GoHomeContributor, LifecyclePhase.Ready); ++export const _1 = workbenchRegistry; ++export const _2 = GoHomeContributor; ++export const _3 = LifecyclePhase.Ready; diff --git a/src/vs/workbench/browser/parts/activitybar/media/activitybarpart.css b/src/vs/workbench/browser/parts/activitybar/media/activitybarpart.css index ced2d815834e40a1543e80516472799075980733..dfcae73e8a042307600c67f163aa00ba9e0762f4 100644 --- a/src/vs/workbench/browser/parts/activitybar/media/activitybarpart.css From 303fe2bc4eeeeb62248fdfa909cc68ecc3724bf9 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Mon, 23 Nov 2020 19:07:35 -0500 Subject: [PATCH 240/247] vscode: Customize welcome page for code-server - Title/subtitle are now code-server and VS Code version - Added a list of code-server help links --- ci/dev/vscode.patch | 67 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/ci/dev/vscode.patch b/ci/dev/vscode.patch index abf86b4ba..d5ce3da4c 100644 --- a/ci/dev/vscode.patch +++ b/ci/dev/vscode.patch @@ -3803,6 +3803,73 @@ index 74f6922e98b4bb6a7fb100f5aac015afe9fc171b..3243a97c2d378013d96ffbe87e9df6dd .scm-view .monaco-list .monaco-list-row .resource-group > .actions, .scm-view .monaco-list .monaco-list-row .resource > .name > .monaco-icon-label > .actions { +diff --git a/src/vs/workbench/contrib/welcome/page/browser/vs_code_welcome_page.ts b/src/vs/workbench/contrib/welcome/page/browser/vs_code_welcome_page.ts +index 6af6a4b7f42654ec6cc60e0ba5efd376919f3e04..3a8176951628e0b2528aae8796ba684c3ab53d9a 100644 +--- a/src/vs/workbench/contrib/welcome/page/browser/vs_code_welcome_page.ts ++++ b/src/vs/workbench/contrib/welcome/page/browser/vs_code_welcome_page.ts +@@ -4,6 +4,7 @@ + *--------------------------------------------------------------------------------------------*/ + + import { escape } from 'vs/base/common/strings'; ++import product from 'vs/platform/product/common/product'; + import { localize } from 'vs/nls'; + + export default () => ` +@@ -11,7 +12,7 @@ export default () => ` +
+
+

${escape(localize('welcomePage.vscode', "Visual Studio Code"))}

+-

${escape(localize({ key: 'welcomePage.editingEvolved', comment: ['Shown as subtitle on the Welcome page.'] }, "Editing evolved"))}

++

VS Code v${product.version}

+
+
+
+@@ -32,6 +33,19 @@ export default () => ` + +

${escape(localize('welcomePage.noRecentFolders', "No recent folders"))}

+
++
++

code-server ${escape(localize('welcomePage.help', "Help"))}

++ ++
+
+

${escape(localize('welcomePage.help', "Help"))}

+
    +diff --git a/src/vs/workbench/contrib/welcome/page/browser/welcomePage.css b/src/vs/workbench/contrib/welcome/page/browser/welcomePage.css +index 738ce140c1af76ee0017c59cc883578e966f5348..80833b7023ed5795bb3de303b54ec08d9dab9b94 100644 +--- a/src/vs/workbench/contrib/welcome/page/browser/welcomePage.css ++++ b/src/vs/workbench/contrib/welcome/page/browser/welcomePage.css +@@ -94,7 +94,7 @@ + } + + .monaco-workbench .part.editor > .content .welcomePage .splash .section { +- margin-bottom: 5em; ++ margin-bottom: 3em; + } + + .monaco-workbench .part.editor > .content .welcomePage .splash ul { +diff --git a/src/vs/workbench/contrib/welcome/page/browser/welcomePage.ts b/src/vs/workbench/contrib/welcome/page/browser/welcomePage.ts +index 4a61a79fe447e2aa238af568791bff1e0cec4d29..791b63342f476f1baba9d31b040d3ef589e3f70a 100644 +--- a/src/vs/workbench/contrib/welcome/page/browser/welcomePage.ts ++++ b/src/vs/workbench/contrib/welcome/page/browser/welcomePage.ts +@@ -328,7 +328,7 @@ class WelcomePage extends Disposable { + + const prodName = container.querySelector('.welcomePage .title .caption') as HTMLElement; + if (prodName) { +- prodName.textContent = this.productService.nameLong; ++ prodName.textContent = `code-server v${this.productService.codeServerVersion}` + } + + recentlyOpened.then(({ workspaces }) => { diff --git a/src/vs/workbench/electron-sandbox/sandbox.simpleservices.ts b/src/vs/workbench/electron-sandbox/sandbox.simpleservices.ts index ed4f26407391bd62219a9f8245a5cd63a7cb7488..92f26d1b082f80475cf76409a4569e948e9e0bd9 100644 --- a/src/vs/workbench/electron-sandbox/sandbox.simpleservices.ts From fb63c0cd220d3f768e1d8d54741beff5657fc919 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Mon, 23 Nov 2020 21:06:45 -0500 Subject: [PATCH 241/247] vscode: Show notification when upgrade is available And link to the release notes. --- ci/dev/vscode.patch | 54 ++++++++++++++++++++++++++++++++++++--- src/node/routes/update.ts | 2 +- 2 files changed, 52 insertions(+), 4 deletions(-) diff --git a/ci/dev/vscode.patch b/ci/dev/vscode.patch index 8d193980d..54676c494 100644 --- a/ci/dev/vscode.patch +++ b/ci/dev/vscode.patch @@ -769,10 +769,10 @@ index 096b9e23493539c9937940a56e555d95bbae38d9..ef37e614004f550f7b64eacd362f6894 remove(key: string, scope: StorageScope): void { diff --git a/src/vs/server/browser/client.ts b/src/vs/server/browser/client.ts new file mode 100644 -index 0000000000000000000000000000000000000000..3c0703b7174ad792a4b42841e96ee93765d71601 +index 0000000000000000000000000000000000000000..667ca961830feaf6fc5e5bb7ef2df3b8be97b176 --- /dev/null +++ b/src/vs/server/browser/client.ts -@@ -0,0 +1,189 @@ +@@ -0,0 +1,237 @@ +import { Emitter } from 'vs/base/common/event'; +import { URI } from 'vs/base/common/uri'; +import { localize } from 'vs/nls'; @@ -791,6 +791,7 @@ index 0000000000000000000000000000000000000000..3c0703b7174ad792a4b42841e96ee937 +import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; +import { Options } from 'vs/server/ipc.d'; +import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; ++import { ILogService } from 'vs/platform/log/common/log'; + +class TelemetryService extends TelemetryChannelClient { + public constructor( @@ -922,8 +923,55 @@ index 0000000000000000000000000000000000000000..3c0703b7174ad792a4b42841e96ee937 + }); + } + ++ const logService = (services.get(ILogService) as ILogService); ++ const storageService = (services.get(IStorageService) as IStorageService) ++ const getUpdate = async (): Promise => { ++ logService.debug("Checking for update..."); ++ ++ const response = await fetch("update/check", { ++ headers: { "Accept": "application/json" }, ++ }); ++ if (!response.ok) { ++ throw new Error(response.statusText); ++ } ++ const json = await response.json(); ++ if (json.error) { ++ throw new Error(json.error); ++ } ++ if (json.isLatest) { ++ return; ++ } ++ ++ const lastNoti = storageService.getNumber("csLastUpdateNotification", StorageScope.GLOBAL); ++ if (lastNoti) { ++ // Only remind them again after two days. ++ const timeout = 1000*60*24*2 ++ const threshold = lastNoti + timeout; ++ if (Date.now() < threshold) { ++ return; ++ } ++ } ++ ++ storageService.store("csLastUpdateNotification", Date.now(), StorageScope.GLOBAL); ++ (services.get(INotificationService) as INotificationService).notify({ ++ severity: Severity.Info, ++ message: `[code-server v${json.latest}](https://github.com/cdr/code-server/releases/tag/v${json.latest}) has been released!`, ++ }); ++ }; ++ ++ const updateLoop = (): void => { ++ getUpdate().catch((error) => { ++ logService.debug(`failed to check for update: ${error}`); ++ }).finally(() => { ++ // Check again every 6 hours. ++ setTimeout(updateLoop, 1000*60*6); ++ }); ++ }; ++ ++ updateLoop(); ++ + // This will be used to set the background color while VS Code loads. -+ const theme = (services.get(IStorageService) as IStorageService).get("colorThemeData", StorageScope.GLOBAL); ++ const theme = storageService.get("colorThemeData", StorageScope.GLOBAL); + if (theme) { + localStorage.setItem("colorThemeData", theme); + } diff --git a/src/node/routes/update.ts b/src/node/routes/update.ts index ac1ddc413..5c9aa181e 100644 --- a/src/node/routes/update.ts +++ b/src/node/routes/update.ts @@ -7,7 +7,7 @@ export const router = Router() const provider = new UpdateProvider() -router.get("/", ensureAuthenticated, async (req, res) => { +router.get("/check", ensureAuthenticated, async (req, res) => { const update = await provider.getUpdate(req.query.force === "true") res.json({ checked: update.checked, From f74f1721e69a722ff0a90757ec89e64ed34b4711 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Mon, 23 Nov 2020 21:07:02 -0500 Subject: [PATCH 242/247] doc: Add note on upgrading into release notes and install.md Closes #1652 Closes #2221 --- ci/build/release-github-draft.sh | 4 ++++ doc/install.md | 7 +++++++ 2 files changed, 11 insertions(+) diff --git a/ci/build/release-github-draft.sh b/ci/build/release-github-draft.sh index 71cbb714a..4e077a356 100755 --- a/ci/build/release-github-draft.sh +++ b/ci/build/release-github-draft.sh @@ -15,6 +15,10 @@ v$VERSION VS Code v$(vscode_version) +Upgrading is as easy as installing the new version over the old one. code-server +maintains all user data in \`~/.local/share/code-server\` so that it is preserved in between +installations. + ## New Features - ⭐ Summarize new features here with references to issues diff --git a/doc/install.md b/doc/install.md index 7515db45a..e18d27d01 100644 --- a/doc/install.md +++ b/doc/install.md @@ -2,6 +2,7 @@ # Install +- [Upgrading](#upgrading) - [install.sh](#installsh) - [Flags](#flags) - [Detection Reference](#detection-reference) @@ -19,6 +20,12 @@ This document demonstrates how to install `code-server` on various distros and operating systems. +## Upgrading + +When upgrading you can just install the new version over the old one. code-server +maintains all user data in `~/.local/share/code-server` so that it is preserved in between +installations. + ## install.sh We have a [script](../install.sh) to install code-server for Linux, macOS and FreeBSD. From be37821ab9dbd88ab4d78805e6e6df228c2426e7 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Mon, 23 Nov 2020 21:09:27 -0500 Subject: [PATCH 243/247] update.ts: Simplify comparison --- src/node/update.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/node/update.ts b/src/node/update.ts index 13ac73c37..a156aad65 100644 --- a/src/node/update.ts +++ b/src/node/update.ts @@ -75,7 +75,7 @@ export class UpdateProvider { public isLatestVersion(latest: Update): boolean { logger.debug("comparing versions", field("current", version), field("latest", latest.version)) try { - return latest.version === version || semver.lt(latest.version, version) + return semver.lte(latest.version, version) } catch (error) { return true } From 37c80c9bbd4a01ce0dabbd010b3b73831af9d3db Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Tue, 24 Nov 2020 12:47:58 -0500 Subject: [PATCH 244/247] vscode: Add missing semicolons See #2359 --- ci/dev/vscode.patch | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/ci/dev/vscode.patch b/ci/dev/vscode.patch index 54676c494..4f249d363 100644 --- a/ci/dev/vscode.patch +++ b/ci/dev/vscode.patch @@ -769,7 +769,7 @@ index 096b9e23493539c9937940a56e555d95bbae38d9..ef37e614004f550f7b64eacd362f6894 remove(key: string, scope: StorageScope): void { diff --git a/src/vs/server/browser/client.ts b/src/vs/server/browser/client.ts new file mode 100644 -index 0000000000000000000000000000000000000000..667ca961830feaf6fc5e5bb7ef2df3b8be97b176 +index 0000000000000000000000000000000000000000..c6eef331346ebc244a26e8b1e5919d192225b971 --- /dev/null +++ b/src/vs/server/browser/client.ts @@ -0,0 +1,237 @@ @@ -924,7 +924,7 @@ index 0000000000000000000000000000000000000000..667ca961830feaf6fc5e5bb7ef2df3b8 + } + + const logService = (services.get(ILogService) as ILogService); -+ const storageService = (services.get(IStorageService) as IStorageService) ++ const storageService = (services.get(IStorageService) as IStorageService); + const getUpdate = async (): Promise => { + logService.debug("Checking for update..."); + @@ -945,7 +945,7 @@ index 0000000000000000000000000000000000000000..667ca961830feaf6fc5e5bb7ef2df3b8 + const lastNoti = storageService.getNumber("csLastUpdateNotification", StorageScope.GLOBAL); + if (lastNoti) { + // Only remind them again after two days. -+ const timeout = 1000*60*24*2 ++ const timeout = 1000*60*24*2; + const threshold = lastNoti + timeout; + if (Date.now() < threshold) { + return; @@ -3906,7 +3906,7 @@ index 738ce140c1af76ee0017c59cc883578e966f5348..80833b7023ed5795bb3de303b54ec08d .monaco-workbench .part.editor > .content .welcomePage .splash ul { diff --git a/src/vs/workbench/contrib/welcome/page/browser/welcomePage.ts b/src/vs/workbench/contrib/welcome/page/browser/welcomePage.ts -index 4a61a79fe447e2aa238af568791bff1e0cec4d29..791b63342f476f1baba9d31b040d3ef589e3f70a 100644 +index 4a61a79fe447e2aa238af568791bff1e0cec4d29..69cc2e4331a3b04d05d79632920f5c5bbfa924e8 100644 --- a/src/vs/workbench/contrib/welcome/page/browser/welcomePage.ts +++ b/src/vs/workbench/contrib/welcome/page/browser/welcomePage.ts @@ -328,7 +328,7 @@ class WelcomePage extends Disposable { @@ -3914,7 +3914,7 @@ index 4a61a79fe447e2aa238af568791bff1e0cec4d29..791b63342f476f1baba9d31b040d3ef5 const prodName = container.querySelector('.welcomePage .title .caption') as HTMLElement; if (prodName) { - prodName.textContent = this.productService.nameLong; -+ prodName.textContent = `code-server v${this.productService.codeServerVersion}` ++ prodName.textContent = `code-server v${this.productService.codeServerVersion}`; } recentlyOpened.then(({ workspaces }) => { From def81245a425178578318f3ec80d301c4c30f6b9 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Tue, 24 Nov 2020 12:59:06 -0500 Subject: [PATCH 245/247] vscode: Check updates with absolute path In case the window location path changes. Not entirely sure if it can but best to be on the safe side. --- ci/dev/vscode.patch | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/ci/dev/vscode.patch b/ci/dev/vscode.patch index 4f249d363..df3f673ec 100644 --- a/ci/dev/vscode.patch +++ b/ci/dev/vscode.patch @@ -769,10 +769,10 @@ index 096b9e23493539c9937940a56e555d95bbae38d9..ef37e614004f550f7b64eacd362f6894 remove(key: string, scope: StorageScope): void { diff --git a/src/vs/server/browser/client.ts b/src/vs/server/browser/client.ts new file mode 100644 -index 0000000000000000000000000000000000000000..c6eef331346ebc244a26e8b1e5919d192225b971 +index 0000000000000000000000000000000000000000..385b9da491d38a9f5d10fab6e4666c84a892f49d --- /dev/null +++ b/src/vs/server/browser/client.ts -@@ -0,0 +1,237 @@ +@@ -0,0 +1,240 @@ +import { Emitter } from 'vs/base/common/event'; +import { URI } from 'vs/base/common/uri'; +import { localize } from 'vs/nls'; @@ -792,6 +792,7 @@ index 0000000000000000000000000000000000000000..c6eef331346ebc244a26e8b1e5919d19 +import { Options } from 'vs/server/ipc.d'; +import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; +import { ILogService } from 'vs/platform/log/common/log'; ++import * as path from 'vs/base/common/path'; + +class TelemetryService extends TelemetryChannelClient { + public constructor( @@ -925,10 +926,12 @@ index 0000000000000000000000000000000000000000..c6eef331346ebc244a26e8b1e5919d19 + + const logService = (services.get(ILogService) as ILogService); + const storageService = (services.get(IStorageService) as IStorageService); ++ // We set this here first in case the path changes. ++ const updateCheckEndpoint = path.join(window.location.pathname, "/update/check") + const getUpdate = async (): Promise => { + logService.debug("Checking for update..."); + -+ const response = await fetch("update/check", { ++ const response = await fetch(updateCheckEndpoint, { + headers: { "Accept": "application/json" }, + }); + if (!response.ok) { From 93fb76e4a71b1959ec2a23481ba2611200d714b2 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Tue, 24 Nov 2020 13:12:10 -0500 Subject: [PATCH 246/247] v3.7.3 --- ci/helm-chart/Chart.yaml | 4 ++-- ci/helm-chart/README.md | 4 ++-- ci/helm-chart/values.yaml | 2 +- doc/install.md | 14 +++++++------- package.json | 2 +- 5 files changed, 13 insertions(+), 13 deletions(-) diff --git a/ci/helm-chart/Chart.yaml b/ci/helm-chart/Chart.yaml index 089e03486..e4665ec49 100644 --- a/ci/helm-chart/Chart.yaml +++ b/ci/helm-chart/Chart.yaml @@ -15,9 +15,9 @@ type: application # This is the chart version. This version number should be incremented each time you make changes # to the chart and its templates, including the app version. # Versions are expected to follow Semantic Versioning (https://semver.org/) -version: 1.0.1 +version: 1.0.2 # This is the version number of the application being deployed. This version number should be # incremented each time you make changes to the application. Versions are not expected to # follow Semantic Versioning. They should reflect the version the application is using. -appVersion: 3.7.2 +appVersion: 3.7.3 diff --git a/ci/helm-chart/README.md b/ci/helm-chart/README.md index aa6b506ad..85642cf87 100644 --- a/ci/helm-chart/README.md +++ b/ci/helm-chart/README.md @@ -1,6 +1,6 @@ # code-server -![Version: 1.0.0](https://img.shields.io/badge/Version-1.0.0-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: 3.7.2](https://img.shields.io/badge/AppVersion-3.7.2-informational?style=flat-square) +![Version: 1.0.0](https://img.shields.io/badge/Version-1.0.0-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: 3.7.3](https://img.shields.io/badge/AppVersion-3.7.3-informational?style=flat-square) [code-server](https://github.com/cdr/code-server) code-server is VS Code running on a remote server, accessible through the browser. @@ -72,7 +72,7 @@ and their default values. | hostnameOverride | string | `""` | | | image.pullPolicy | string | `"Always"` | | | image.repository | string | `"codercom/code-server"` | | -| image.tag | string | `"3.7.2"` | | +| image.tag | string | `"3.7.3"` | | | imagePullSecrets | list | `[]` | | | ingress.enabled | bool | `false` | | | nameOverride | string | `""` | | diff --git a/ci/helm-chart/values.yaml b/ci/helm-chart/values.yaml index 5d2ba0299..6594ca5ed 100644 --- a/ci/helm-chart/values.yaml +++ b/ci/helm-chart/values.yaml @@ -6,7 +6,7 @@ replicaCount: 1 image: repository: codercom/code-server - tag: '3.7.2' + tag: '3.7.3' pullPolicy: Always imagePullSecrets: [] diff --git a/doc/install.md b/doc/install.md index e18d27d01..d987cea26 100644 --- a/doc/install.md +++ b/doc/install.md @@ -87,8 +87,8 @@ commands presented in the rest of this document. ## Debian, Ubuntu ```bash -curl -fOL https://github.com/cdr/code-server/releases/download/v3.7.2/code-server_3.7.2_amd64.deb -sudo dpkg -i code-server_3.7.2_amd64.deb +curl -fOL https://github.com/cdr/code-server/releases/download/v3.7.3/code-server_3.7.3_amd64.deb +sudo dpkg -i code-server_3.7.3_amd64.deb sudo systemctl enable --now code-server@$USER # Now visit http://127.0.0.1:8080. Your password is in ~/.config/code-server/config.yaml ``` @@ -96,8 +96,8 @@ sudo systemctl enable --now code-server@$USER ## Fedora, CentOS, RHEL, SUSE ```bash -curl -fOL https://github.com/cdr/code-server/releases/download/v3.7.2/code-server-3.7.2-amd64.rpm -sudo rpm -i code-server-3.7.2-amd64.rpm +curl -fOL https://github.com/cdr/code-server/releases/download/v3.7.3/code-server-3.7.3-amd64.rpm +sudo rpm -i code-server-3.7.3-amd64.rpm sudo systemctl enable --now code-server@$USER # Now visit http://127.0.0.1:8080. Your password is in ~/.config/code-server/config.yaml ``` @@ -166,10 +166,10 @@ Here is an example script for installing and using a standalone `code-server` re ```bash mkdir -p ~/.local/lib ~/.local/bin -curl -fL https://github.com/cdr/code-server/releases/download/v3.7.2/code-server-3.7.2-linux-amd64.tar.gz \ +curl -fL https://github.com/cdr/code-server/releases/download/v3.7.3/code-server-3.7.3-linux-amd64.tar.gz \ | tar -C ~/.local/lib -xz -mv ~/.local/lib/code-server-3.7.2-linux-amd64 ~/.local/lib/code-server-3.7.2 -ln -s ~/.local/lib/code-server-3.7.2/bin/code-server ~/.local/bin/code-server +mv ~/.local/lib/code-server-3.7.3-linux-amd64 ~/.local/lib/code-server-3.7.3 +ln -s ~/.local/lib/code-server-3.7.3/bin/code-server ~/.local/bin/code-server PATH="~/.local/bin:$PATH" code-server # Now visit http://127.0.0.1:8080. Your password is in ~/.config/code-server/config.yaml diff --git a/package.json b/package.json index 9f41af56f..c96242477 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "code-server", "license": "MIT", - "version": "3.7.2", + "version": "3.7.3", "description": "Run VS Code on a remote server.", "homepage": "https://github.com/cdr/code-server", "bugs": { From ae65c83cbdb6851335c9a5f01d7629434f6b0972 Mon Sep 17 00:00:00 2001 From: Asher Date: Thu, 26 Nov 2020 16:58:34 -0600 Subject: [PATCH 247/247] Fix exthost error and warn logging (#2366) Previously anything that wasn't "log" such as "warn" would end up doing `logger[logger.warn]`. Would have caught this if I hadn't used `any`... Fixes #2364. --- ci/dev/vscode.patch | 30 ++++++++++++++++++++++++------ 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/ci/dev/vscode.patch b/ci/dev/vscode.patch index df3f673ec..23dc41770 100644 --- a/ci/dev/vscode.patch +++ b/ci/dev/vscode.patch @@ -2445,10 +2445,10 @@ index 0000000000000000000000000000000000000000..693174ee0d21353c3a08a42fd30eaad1 +} diff --git a/src/vs/server/node/connection.ts b/src/vs/server/node/connection.ts new file mode 100644 -index 0000000000000000000000000000000000000000..93062cadc627c61e0829c27a72894b81e6a0e039 +index 0000000000000000000000000000000000000000..5c3caf4d12cbf9b7228699ec4fa40cb406aa6307 --- /dev/null +++ b/src/vs/server/node/connection.ts -@@ -0,0 +1,171 @@ +@@ -0,0 +1,189 @@ +import { field, Logger, logger } from '@coder/logger'; +import * as cp from 'child_process'; +import { VSBuffer } from 'vs/base/common/buffer'; @@ -2459,6 +2459,7 @@ index 0000000000000000000000000000000000000000..93062cadc627c61e0829c27a72894b81 +import { INativeEnvironmentService } from 'vs/platform/environment/common/environment'; +import { getNlsConfiguration } from 'vs/server/node/nls'; +import { Protocol } from 'vs/server/node/protocol'; ++import { IExtHostReadyMessage } from 'vs/workbench/services/extensions/common/extensionHostProtocol'; + +export abstract class Connection { + private readonly _onClose = new Emitter(); @@ -2520,6 +2521,19 @@ index 0000000000000000000000000000000000000000..93062cadc627c61e0829c27a72894b81 + } +} + ++interface DisconnectedMessage { ++ type: 'VSCODE_EXTHOST_DISCONNECTED'; ++} ++ ++interface ConsoleMessage { ++ type: '__$console'; ++ // See bootstrap-fork.js#L135. ++ severity: 'log' | 'warn' | 'error'; ++ arguments: any[]; ++} ++ ++type ExtHostMessage = DisconnectedMessage | ConsoleMessage | IExtHostReadyMessage; ++ +export class ExtensionHostConnection extends Connection { + private process?: cp.ChildProcess; + private readonly logger: Logger; @@ -2596,11 +2610,15 @@ index 0000000000000000000000000000000000000000..93062cadc627c61e0829c27a72894b81 + proc.stderr.setEncoding('utf8').on('data', (d) => this.logger.error(d)); + } + -+ proc.on('message', (event) => { -+ switch (event && event.type) { ++ proc.on('message', (event: ExtHostMessage) => { ++ switch (event.type) { + case '__$console': -+ const severity = (this.logger)[event.severity] || 'info'; -+ (this.logger)[severity]('console', field('arguments', event.arguments)); ++ const fn = this.logger[event.severity === 'log' ? 'info' : event.severity]; ++ if (fn) { ++ fn('console', field('arguments', event.arguments)); ++ } else { ++ this.logger.error('Unexpected severity', field('event', event)); ++ } + break; + case 'VSCODE_EXTHOST_DISCONNECTED': + this.logger.trace('Going offline');