Open-sourced my personal cloud-gaming scripts
This commit is contained in:
+28
@@ -0,0 +1,28 @@
|
||||
.classpath
|
||||
.project
|
||||
.settings
|
||||
.eclipse/*
|
||||
.gradle
|
||||
*.launch
|
||||
*maven-eclipse.xml
|
||||
\#*
|
||||
.\#*
|
||||
*~
|
||||
*.swp
|
||||
.idea
|
||||
*.iml
|
||||
*.ipr
|
||||
*.iws
|
||||
*.ids
|
||||
target
|
||||
build
|
||||
out
|
||||
*.class
|
||||
junit*.properties
|
||||
*.orig
|
||||
nb-*
|
||||
.DS_Store
|
||||
.metadata
|
||||
classes
|
||||
*.MF
|
||||
*.retry
|
||||
@@ -0,0 +1,99 @@
|
||||
# AWS Cloud Gaming
|
||||
This repo will automate the creation and connection to an AWS EC2 spot instance to be used for cloud gaming.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
* Running on an Ubuntu system
|
||||
* [AWS CLI (v2)](https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip) is installed
|
||||
* [Node.js](https://linuxize.com/post/how-to-install-node-js-on-ubuntu-22-04/) and NPM are installed
|
||||
* You have sudo permissions on your current system
|
||||
|
||||
## Configuration
|
||||
|
||||
### Environment Variables
|
||||
|
||||
| Name | Description | Example |
|
||||
|----------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|------------------------------------------|
|
||||
| `AWS_CLOUD_GAMING_PROFILE` | The AWS profile to use corresponding to a profile in your AWS config (usually ~/.aws/config).<br/><br/> Defaults to `personal`<br/><br/> This profile should have permissions to create the appropriate resources in AWS, including CloudFormation stacks, and EC2 instances | `AWS_CLOUD_GAMING_PROFILE=uber-sandbox` |
|
||||
| `AWS_CLOUD_GAMING_SSH_KEY` | The name of some key pair that exists in AWS and that you have locally in your `~/.ssh` directory | `AWS_CLOUD_GAMING_SSH_KEY=team-building` |
|
||||
|
||||
### CDK Variables
|
||||
Modify the following properties in the [cloud-gaming-on-ec2](cdk/bin/cloud-gaming-on-ec2.ts) stack:
|
||||
|
||||
| Parameter Name | Description |
|
||||
|----------------|---------------------------------------------------------------------|
|
||||
| `ACCOUNT_ID` | The AWS account ID you want to use |
|
||||
| `REGION` | The AWS region in which you want the resources created |
|
||||
| `VPC_ID` | The ID of the VPC you wish to deploy the instance into |
|
||||
| `SUBNET_ID` | The ID of a public subnet that you want your instance deployed into |
|
||||
|
||||
## Running the application
|
||||
To run the application, simply run the `cloud-gaming.sh` script in the root directory and follow all instructions/menu choices and the script will take care of everything else!
|
||||
|
||||
## Debugging
|
||||
The scripts output logs to `/tmp/cloud-gaming.log` for easy debugging and auditing.
|
||||
|
||||
Note that CDK specific logs are output for each CDK task (`synth`, `bootstrap`, `deploy`) in their own log files to make debugging the CDK easier:
|
||||
|
||||
* `synth` outputs logs to `/tmp/cdk-synth.log`
|
||||
* `bootstrap` outputs logs to `/tmp/cdk-bootstrap.log`
|
||||
* `deploy` outputs logs to `/tmp/cdk-deploy.log`
|
||||
|
||||
## Customizing the EC2 Instance
|
||||
|
||||
### Change the Instance type
|
||||
|
||||
To change the instance type, simply create a new stack that subclasses the [base.ts](cdk/lib/base.ts), and override the `getUserData()` and `getInstanceType()`
|
||||
methods to change the type, and customize the user data for the instance. Just make sure to add the `new` call to this stack in [cloud-gaming-on-ec2.ts](cdk/bin/cloud-gaming-on-ec2.ts)
|
||||
|
||||
### Log into instance desktop
|
||||
|
||||
You can log into the desktop of the instance via the `Manage Personal Instance` menu in the `cloud-gaming.sh` script.
|
||||
|
||||
### Managing instance state
|
||||
|
||||
All personal instance management can be achieved via the `Manage Personal Instance` menu in the `cloud-gaming.sh` script.
|
||||
This includes
|
||||
|
||||
* Checking the state of the instance (`started`, `stopped`, `terminated`, etc.)
|
||||
* Start the instance
|
||||
* Stop the instance
|
||||
|
||||
## Managing the Stream
|
||||
|
||||
There are two types of streams this project enables: Personal and Shared
|
||||
|
||||
### Personal Streams
|
||||
|
||||
A personal stream is a Steam Link stream to your personal EC2 instance. This is ideal for online multiplayer games where players all play on their own machines.
|
||||
|
||||
You can start a stream to your personal instance via the `Stream Settings` menu in the `cloud-gaming.sh` script. This will prompt to start your personal instance if it's not already started.
|
||||
|
||||
### Shared Streams
|
||||
|
||||
A shared stream is a Steam Link stream to an instance that multiple people are connecting to at once. This is ideal for couch co-op games like Super Smash Bros, Mario Kart, etc.
|
||||
where everyone needs to be on the same machine.
|
||||
|
||||
You can either be a host or a player in a shared stream.
|
||||
|
||||
Hosts host the shared stream on their EC2 instance, and players are the other players connecting to that same instance.
|
||||
|
||||
The `Stream Settings` menu in the `cloud-gaming.sh` script will guide you through the setup of either the host or player stream for a shared stream.
|
||||
|
||||
Note: A Shared stream will not overwrite your settings to connect to your personal instance. You'll just have to change back to your personal instance in Steam Link via the Gear icon -> Computers menu.
|
||||
|
||||
## Built With
|
||||
* [Bash](https://www.gnu.org/software/bash/) - Shell that all the scripts are written in
|
||||
* [Node.js](https://nodejs.org/en/) - JS runtime for CDK
|
||||
* [TypeScript](https://www.typescriptlang.org/) - CDK stacks and constructs are written in TS
|
||||
* [AWS CDK](https://aws.amazon.com/cdk/) - AWS Cloud Development Kit is IaC using familiar programming languages. It's used to define the EC2 instance
|
||||
* [AWS CLI](https://aws.amazon.com/cli/) - Used to manage the EC2 instance; e.g. get credentials for instance, start/stop/restart instance, check instance status, etc.
|
||||
* [Whiptail](https://linux.die.net/man/1/whiptail) - Used to create a TUI for the user
|
||||
* [Dialog](https://linux.die.net/man/1/dialog) - Used to display tail boxes for long-running processes like CDK deploys
|
||||
* [NICE DCV](https://aws.amazon.com/hpc/dcv/) - High-performance RDP for connecting to EC2 instance desktop
|
||||
* [Xvfb](https://www.x.org/archive/X11R7.6/doc/man/man1/Xvfb.1.xhtml) - X Virtual FrameBuffer, used to open a connection to your instance via NICE DCV in the background. Necessary to allow SteamLink connections to your instance
|
||||
* [Steam Link](https://store.steampowered.com/app/353380/Steam_Link/) - High quality, low latency stream from your machine to your EC2 instance that forwards all inputs, controllers or otherwise.
|
||||
* [Flatpak](https://flatpak.org/) - Used to install Steam Link (Ubuntu only)
|
||||
* [xdotool](https://manpages.ubuntu.com/manpages/trusty/man1/xdotool.1.html) - X11 automation tool to minimize the terminal
|
||||
* [pulsemixer](https://github.com/GeorgeFilipkin/pulsemixer) - Used to mute NICE DCV running in Xvfb so there's no echoing of sound between Steam Link and NICE DCV
|
||||
* [nc](http://netcat.sourceforge.net/) - Ping the EC2 instance on port 8443 to see when NICE DCV is running
|
||||
+230
@@ -0,0 +1,230 @@
|
||||
#!/bin/bash
|
||||
source ~/.bashrc
|
||||
source logger.sh
|
||||
source utils.sh
|
||||
|
||||
if [[ -z $AWS_CLOUD_GAMING_PROFILE ]]; then
|
||||
AWS_CLOUD_GAMING_PROFILE=personal
|
||||
fi
|
||||
|
||||
checkAwsProfile() {
|
||||
printInfo "Checking for the $AWS_CLOUD_GAMING_PROFILE AWS Profile in AWS config"
|
||||
if ! (aws --profile $AWS_CLOUD_GAMING_PROFILE sts get-caller-identity --no-cli-auto-prompt > /dev/null 2>&1); then
|
||||
printError "AWS config is missing the $AWS_CLOUD_GAMING_PROFILE profile. Add it to your ~/.aws/config file and try running this application again"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
getInstanceState() {
|
||||
aws --profile $AWS_CLOUD_GAMING_PROFILE ec2 describe-instances --instance-ids "$AWS_TEAM_BUILDING_EC2_INSTANCE_ID" --query 'Reservations[0].Instances[0].State.Name' --no-cli-auto-prompt --output text &
|
||||
}
|
||||
|
||||
showGaugeBoxForAwsCommand() {
|
||||
declare increment
|
||||
declare i
|
||||
increment=$(echo "scale=1; 100 / 30" | bc)
|
||||
i="0"
|
||||
|
||||
printInfo "$2"
|
||||
|
||||
{
|
||||
while [[ $(getInstanceState) != "$1" ]]; do
|
||||
declare percent
|
||||
percent=$(printf "%.0f" $i)
|
||||
echo -e "XXX\n$percent\n$2... \nXXX"
|
||||
i=$(echo "$i + $increment" | bc)
|
||||
sleep 5
|
||||
done
|
||||
|
||||
if [[ $(getInstanceState) != "$1" ]]; then
|
||||
printError "$4"
|
||||
echo -e "XXX\n0\n$4\nXXX"
|
||||
return 1
|
||||
else
|
||||
echo -e "XXX\n100\n$3\nXXX"
|
||||
sleep 1
|
||||
fi
|
||||
} | whiptail --title "$2..." --gauge "$2..." "$GAUGE_BOX_HEIGHT" "$GAUGE_BOX_WIDTH" 0
|
||||
|
||||
printInfo "$3"
|
||||
}
|
||||
|
||||
waitForInstanceToBecomeAvailable() {
|
||||
printInfo "Waiting for instance to become available"
|
||||
|
||||
{
|
||||
declare increment
|
||||
increment=$(echo "scale=1; 100/90" | bc)
|
||||
for ((i=0; i<=100; i=$(printf "%.0f" $(echo "scale=1; $i + $increment" | bc)))); do
|
||||
if (timeout 10s nc -vz "$AWS_TEAM_BUILDING_EC2_INSTANCE_IP" 8443); then
|
||||
break
|
||||
fi
|
||||
echo "$i"
|
||||
done
|
||||
echo 100
|
||||
sleep 1
|
||||
} | whiptail --title "Instance Startup" --gauge "Waiting for instance to become available..." "$GAUGE_BOX_HEIGHT" "$GAUGE_BOX_WIDTH" 0
|
||||
}
|
||||
|
||||
getInstanceIp() {
|
||||
printInfo "Fetching instance IP for id: $AWS_TEAM_BUILDING_EC2_INSTANCE_ID"
|
||||
aws --profile $AWS_CLOUD_GAMING_PROFILE ec2 describe-instances --instance-ids "$AWS_TEAM_BUILDING_EC2_INSTANCE_ID" --query "Reservations[0].Instances[0].PublicIpAddress" --output text --no-cli-auto-prompt
|
||||
}
|
||||
|
||||
createDcvConnectionProfileFromTemplate() {
|
||||
printInfo "Creating DCV connection profile from template"
|
||||
PASSWORD="$(aws --profile $AWS_CLOUD_GAMING_PROFILE ec2 get-password-data --instance-id "$AWS_TEAM_BUILDING_EC2_INSTANCE_ID" --priv-launch-key ~/.ssh/insights-team-building-key-pair.pem --query 'PasswordData' --output text --no-cli-auto-prompt)"
|
||||
PASSWORD=$(echo -n $PASSWORD)
|
||||
sed -i "/^host=/c\host=$AWS_TEAM_BUILDING_EC2_INSTANCE_IP" cloud_gaming_dcv_profile.dcv
|
||||
sed -i "/^password=/c\password=$PASSWORD" cloud_gaming_dcv_profile.dcv
|
||||
}
|
||||
|
||||
startInstance() {
|
||||
declare state
|
||||
declare desiredState=running
|
||||
state=$(getInstanceState)
|
||||
printInfo "Starting instance"
|
||||
|
||||
if [[ $state == "$desiredState" ]]; then
|
||||
printError "Instance is already running. Doing nothing"
|
||||
msgBox "Instance is already running."
|
||||
else
|
||||
declare instanceIp
|
||||
aws --profile $AWS_CLOUD_GAMING_PROFILE ec2 start-instances --instance-ids "$AWS_TEAM_BUILDING_EC2_INSTANCE_ID" --no-cli-auto-prompt > /dev/null 2>&1 &
|
||||
showGaugeBoxForAwsCommand "$desiredState" "Starting Your Instance" "Successfully Started Your Instance!" "Failed to start your instance!"
|
||||
printInfo "Checking to see if IP changed"
|
||||
instanceIp=$(getInstanceIp)
|
||||
if [[ $instanceIp != $AWS_TEAM_BUILDING_EC2_INSTANCE_IP ]]; then
|
||||
setConfigValue "AWS_TEAM_BUILDING_EC2_INSTANCE_IP" "$instanceIp"
|
||||
createDcvConnectionProfileFromTemplate
|
||||
fi
|
||||
|
||||
waitForInstanceToBecomeAvailable
|
||||
|
||||
fi
|
||||
}
|
||||
|
||||
stopInstance() {
|
||||
declare state
|
||||
declare desiredState=stopped
|
||||
state=$(getInstanceState)
|
||||
printInfo "Stopping instance"
|
||||
|
||||
if [[ $state == "$desiredState" ]]; then
|
||||
printError "Instance is already stopped."
|
||||
msgBox "Instance is already stopped."
|
||||
else
|
||||
aws --profile $AWS_CLOUD_GAMING_PROFILE ec2 stop-instances --instance-ids "$AWS_TEAM_BUILDING_EC2_INSTANCE_ID" --no-cli-auto-prompt > /dev/null 2>&1 &
|
||||
showGaugeBoxForAwsCommand "$desiredState" "Stopping Your Instance" "Successfully Stopped Your Instance!" "Failed to stop your instance!"
|
||||
fi
|
||||
}
|
||||
|
||||
rebootInstance() {
|
||||
declare desiredState=running
|
||||
|
||||
printInfo "Rebooting instance"
|
||||
|
||||
aws --profile $AWS_CLOUD_GAMING_PROFILE ec2 reboot-instances --instance-ids "$AWS_TEAM_BUILDING_EC2_INSTANCE_ID" --no-cli-auto-prompt > /dev/null 2>&1 &
|
||||
|
||||
if ! (showGaugeBoxForAwsCommand "$desiredState" "Restarting Your Instance" "Successfully Restarted Your Instance!"); then
|
||||
printError "Failed to restart instance. Waiting for user to manually start instance before continuing."
|
||||
msgBox "$(cat <<-EOF
|
||||
Failed to restart your instance! Please make sure your instance is started before continuing!
|
||||
|
||||
Your instance ID is $AWS_TEAM_BUILDING_EC2_INSTANCE_ID
|
||||
|
||||
Hit 'OK' Once your instance is started
|
||||
EOF
|
||||
)"
|
||||
fi
|
||||
}
|
||||
|
||||
getMyIp() {
|
||||
curl -s -L -X GET http://checkip.amazonaws.com
|
||||
}
|
||||
|
||||
deployCdk() {
|
||||
printInfo "Deploying CDK"
|
||||
|
||||
cd cdk
|
||||
|
||||
declare user
|
||||
declare localIp
|
||||
declare logFile=/tmp/cdk
|
||||
user="$(whoami)"
|
||||
localIp="$(getMyIp)"
|
||||
|
||||
{
|
||||
echo -e "XXX\n0\nRunning npm install... \nXXX"
|
||||
printInfo "Running npm install"
|
||||
npm install > /dev/null 2>&1
|
||||
echo -e "XXX\n50\nBuilding CDK... \nXXX"
|
||||
printInfo "Running npm run build"
|
||||
npm run build > /dev/null 2>&1
|
||||
echo -e "XXX\n100\nDone! \nXXX"
|
||||
sleep 1
|
||||
} | whiptail --title "Preparing CDK..." --gauge "Preparing CDK..." "$GAUGE_BOX_HEIGHT" "$GAUGE_BOX_WIDTH" 0
|
||||
|
||||
declare pid
|
||||
declare synthLogFile="${logFile}-synth.log"
|
||||
printInfo "Running CDK synth and logging to $synthLogFile"
|
||||
yes | npx cdk --no-color --require-approval never --profile $AWS_CLOUD_GAMING_PROFILE -c "user=$user" -c "localIp=$localIp" synth "TeamBuildingCloudGaming-$user" > $synthLogFile 2>&1 &
|
||||
pid=$!
|
||||
showTailBox "Synthesizing CDK" $pid $synthLogFile
|
||||
|
||||
declare bootstrapLogFile="${logFile}-bootstrap.log"
|
||||
printInfo "Bootstrapping CDK and logging to $bootstrapLogFile"
|
||||
yes | npx cdk --no-color --require-approval never --profile $AWS_CLOUD_GAMING_PROFILE -c "user=$user" -c "localIp=$localIp" bootstrap > $bootstrapLogFile 2>&1 &
|
||||
pid=$!
|
||||
showTailBox "Bootstrapping CDK" $pid $bootstrapLogFile
|
||||
|
||||
declare deployLogFile="${logFile}-deploy.log"
|
||||
printInfo "Deploying CDK and logging to $deployLogFile"
|
||||
yes | npx cdk --no-color --require-approval never --profile $AWS_CLOUD_GAMING_PROFILE -c "user=$user" -c "localIp=$localIp" deploy "TeamBuildingCloudGaming-$user" > $deployLogFile 2>&1 &
|
||||
pid=$!
|
||||
showTailBox "Deploy EC2 Instance" $pid $deployLogFile
|
||||
|
||||
unset AWS_TEAM_BUILDING_EC2_INSTANCE_ID
|
||||
unset AWS_TEAM_BUILDING_EC2_INSTANCE_IP
|
||||
|
||||
AWS_TEAM_BUILDING_EC2_INSTANCE_ID=$(cat $deployLogFile | grep InstanceId | awk '{print $NF;}')
|
||||
setConfigValue "AWS_TEAM_BUILDING_EC2_INSTANCE_ID" "$AWS_TEAM_BUILDING_EC2_INSTANCE_ID"
|
||||
|
||||
AWS_TEAM_BUILDING_EC2_INSTANCE_IP=$(getInstanceIp)
|
||||
setConfigValue "AWS_TEAM_BUILDING_EC2_INSTANCE_IP" "$AWS_TEAM_BUILDING_EC2_INSTANCE_IP"
|
||||
|
||||
cd ..
|
||||
|
||||
waitForInstanceToBecomeAvailable
|
||||
rebootInstance
|
||||
waitForInstanceToBecomeAvailable
|
||||
}
|
||||
|
||||
connectToInstanceViaDcvViewer() {
|
||||
printInfo "Connecting to instance desktop via DCV Viewer"
|
||||
|
||||
if ! (hash dcvviewer 2> /dev/null); then
|
||||
printError "dcvviewer is not installed. Cannot connect to personal instance until first time setup is run."
|
||||
msgBox "Can't connect to personal instance via DCV Viewer without first time setup. Run the deploy instance task from the instance management menu first!"
|
||||
fi
|
||||
|
||||
if (pgrep -f dcvviewer); then
|
||||
printError "DCV Viewer is already running."
|
||||
msgBox "DCV Viewer is already running"
|
||||
return 0
|
||||
fi
|
||||
|
||||
declare state
|
||||
state=$(getInstanceState)
|
||||
if [[ $state != "running" ]]; then
|
||||
if (whiptail --fb --title "Start your instance?" --yesno "In order to stream, you instance needs to be started. Would you like to start your personal instance?" "$BOX_BOX_HEIGHT" "$BOX_WIDTH"); then
|
||||
startInstance
|
||||
else
|
||||
printError "Unable to start desktop connection: Instance is not running"
|
||||
msgBox "Can't start desktop connection! Instance is not running. You can start the instance from the personal Instance Management menu."
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
dcvviewer cloud_gaming_dcv_profile.dcv --certificate-validation-policy=accept-untrusted > /dev/null 2>&1 &
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
!jest.config.js
|
||||
*.d.ts
|
||||
*.js
|
||||
node_modules
|
||||
|
||||
# CDK asset staging directory
|
||||
.cdk.staging
|
||||
cdk.out
|
||||
|
||||
bin/*.js
|
||||
lib/*.js
|
||||
@@ -0,0 +1,6 @@
|
||||
*.ts
|
||||
!*.d.ts
|
||||
|
||||
# CDK asset staging directory
|
||||
.cdk.staging
|
||||
cdk.out
|
||||
@@ -0,0 +1,51 @@
|
||||
/* tslint:disable:no-import-side-effect no-submodule-imports no-unused-expression */
|
||||
import "source-map-support/register";
|
||||
import { G4ADStack } from "../lib/g4ad";
|
||||
import {App} from "aws-cdk-lib";
|
||||
import {InstanceSize} from "aws-cdk-lib/aws-ec2";
|
||||
|
||||
const app = new App();
|
||||
|
||||
const NICE_DCV_DISPLAY_DRIVER_URL = "https://d1uj6qtbmh3dt5.cloudfront.net/Drivers/nice-dcv-virtual-display-x64-Release-34.msi";
|
||||
const NICE_DCV_SERVER_URL = "https://d1uj6qtbmh3dt5.cloudfront.net/2021.0/Servers/nice-dcv-server-x64-Release-2021.0-10242.msi";
|
||||
const VOLUME_SIZE_GIB = 150;
|
||||
const OPEN_PORTS = [3389, 8443];
|
||||
const ACCOUNT_ID = "PLACEHOLDER"
|
||||
const REGION = "us-east-1"
|
||||
const VPC_ID = 'PLACEHOLDER'
|
||||
const SUBNET_ID = 'PLACEHOLDER'
|
||||
|
||||
const user = app.node.tryGetContext("user");
|
||||
if (!user) {
|
||||
throw new Error("User is a required parameter. Specify it with `-c user=me`.");
|
||||
}
|
||||
|
||||
const localIp = app.node.tryGetContext("localIp");
|
||||
if (!localIp) {
|
||||
throw new Error("Local IP is a required parameter. Specify it with '-c localIp=XXX.XXX.XXX.XXX'.");
|
||||
}
|
||||
|
||||
const sshKeyName = process.env.AWS_CLOUD_GAMING_SSH_KEY;
|
||||
if (!sshKeyName) {
|
||||
throw new Error("SSH key name is a required parameter. Specify it by setting the environment variable 'AWS_CLOUD_GAMING_SSH_KEY'.");
|
||||
}
|
||||
|
||||
new G4ADStack(app, `TeamBuildingCloudGaming-${user}`, {
|
||||
niceDCVDisplayDriverUrl: NICE_DCV_DISPLAY_DRIVER_URL,
|
||||
niceDCVServerUrl: NICE_DCV_SERVER_URL,
|
||||
instanceSize: InstanceSize.XLARGE4,
|
||||
sshKeyName,
|
||||
volumeSizeGiB: VOLUME_SIZE_GIB,
|
||||
openPorts: OPEN_PORTS,
|
||||
allowInboundCidr: `${localIp}/32`,
|
||||
env: {
|
||||
account: ACCOUNT_ID,
|
||||
region: REGION
|
||||
},
|
||||
tags: {
|
||||
"Application": "cloud-gaming"
|
||||
},
|
||||
user,
|
||||
vpcId: VPC_ID,
|
||||
subnetId: SUBNET_ID
|
||||
});
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"app": "npx ts-node bin/cloud-gaming-on-ec2.ts"
|
||||
}
|
||||
+113
@@ -0,0 +1,113 @@
|
||||
/* tslint:disable:no-submodule-imports quotemark no-unused-expression */
|
||||
|
||||
import {
|
||||
BlockDeviceVolume,
|
||||
CfnLaunchTemplate,
|
||||
EbsDeviceVolumeType,
|
||||
Instance,
|
||||
InstanceSize,
|
||||
MachineImage,
|
||||
Peer,
|
||||
Port,
|
||||
SecurityGroup,
|
||||
Subnet, UserData,
|
||||
Vpc,
|
||||
WindowsVersion,
|
||||
InstanceType
|
||||
} from "aws-cdk-lib/aws-ec2";
|
||||
import {App, CfnOutput, Stack, StackProps} from "aws-cdk-lib";
|
||||
import {ManagedPolicy, Role, ServicePrincipal} from "aws-cdk-lib/aws-iam";
|
||||
|
||||
export interface BaseConfig extends StackProps {
|
||||
readonly instanceSize: InstanceSize;
|
||||
readonly vpcId: string;
|
||||
readonly subnetId: string;
|
||||
readonly sshKeyName: string;
|
||||
readonly volumeSizeGiB: number;
|
||||
readonly niceDCVDisplayDriverUrl: string;
|
||||
readonly niceDCVServerUrl: string;
|
||||
readonly openPorts: number[];
|
||||
readonly allowInboundCidr: string;
|
||||
readonly user: String;
|
||||
}
|
||||
|
||||
export abstract class BaseEc2Stack extends Stack {
|
||||
protected props: BaseConfig;
|
||||
|
||||
constructor(scope: App, id: string, props: BaseConfig) {
|
||||
super(scope, id, props);
|
||||
this.props = props;
|
||||
const { vpcId, subnetId, sshKeyName, volumeSizeGiB, openPorts, allowInboundCidr, user } = props;
|
||||
const vpc = Vpc.fromLookup(this, "Vpc", { vpcId });
|
||||
|
||||
const securityGroup = new SecurityGroup(this, `SecurityGroup-${user}`, {
|
||||
vpc,
|
||||
description: `Allow RDP, and NICE DCV access for ${user}`,
|
||||
securityGroupName: `InboundAccessFromRdpDcvFor${user}`
|
||||
});
|
||||
|
||||
for (const port of openPorts) {
|
||||
securityGroup.connections.allowFrom(Peer.ipv4(allowInboundCidr), Port.tcp(port));
|
||||
}
|
||||
|
||||
const role = new Role(this, `${id}S3Read-${user}`, {
|
||||
roleName: `${id}.GraphicsDriverS3Access-${user}`,
|
||||
assumedBy: new ServicePrincipal('ec2.amazonaws.com'),
|
||||
managedPolicies: [
|
||||
ManagedPolicy.fromAwsManagedPolicyName('AmazonS3ReadOnlyAccess')
|
||||
],
|
||||
});
|
||||
|
||||
const launchTemplate = new CfnLaunchTemplate(this, `TeamBuildingCloudGamingLaunchTemplate-${user}`, {
|
||||
launchTemplateData: {
|
||||
keyName: sshKeyName,
|
||||
instanceType: this.getInstanceType().toString(),
|
||||
networkInterfaces: [{
|
||||
subnetId,
|
||||
deviceIndex: 0,
|
||||
description: "ENI",
|
||||
groups: [securityGroup.securityGroupId]
|
||||
}],
|
||||
instanceMarketOptions: {
|
||||
spotOptions: {
|
||||
blockDurationMinutes: 120,
|
||||
instanceInterruptionBehavior: "stop",
|
||||
}
|
||||
}
|
||||
},
|
||||
launchTemplateName: `TeamBuildingCloudGamingInstanceLaunchTemplate-${user}/${this.getInstanceType().toString()}`,
|
||||
});
|
||||
|
||||
const ec2Instance = new Instance(this, `EC2Instance-${user}`, {
|
||||
instanceType: this.getInstanceType(),
|
||||
vpc,
|
||||
securityGroup,
|
||||
vpcSubnets: vpc.selectSubnets({ subnets: [Subnet.fromSubnetAttributes(this, 'publicSubnet', {subnetId, availabilityZone: "us-east-1a"})] }),
|
||||
keyName: sshKeyName,
|
||||
userData: this.getUserdata(),
|
||||
machineImage: MachineImage.latestWindows(WindowsVersion.WINDOWS_SERVER_2019_ENGLISH_FULL_BASE),
|
||||
blockDevices: [
|
||||
{
|
||||
deviceName: "/dev/sda1",
|
||||
volume: BlockDeviceVolume.ebs(volumeSizeGiB, {
|
||||
volumeType: EbsDeviceVolumeType.GP3,
|
||||
encrypted: true
|
||||
}),
|
||||
}
|
||||
],
|
||||
role,
|
||||
instanceName: `TeamBuildingCloudGaming-${user}/${this.getInstanceType().toString()}`
|
||||
});
|
||||
|
||||
new CfnOutput(this, `Public IP`, { value: ec2Instance.instancePublicIp });
|
||||
|
||||
new CfnOutput(this, `Credentials`, { value: `https://${this.region}.console.aws.amazon.com/ec2/v2/home?region=${this.region}#ConnectToInstance:instanceId=${ec2Instance.instanceId}` });
|
||||
new CfnOutput(this, `InstanceId`, { value: ec2Instance.instanceId });
|
||||
new CfnOutput(this, `KeyName`, { value: sshKeyName });
|
||||
new CfnOutput(this, `LaunchTemplateId`, { value: launchTemplate.launchTemplateName! });
|
||||
}
|
||||
|
||||
protected abstract getUserdata(): UserData;
|
||||
|
||||
protected abstract getInstanceType(): InstanceType;
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
import { BaseConfig, BaseEc2Stack } from "./base";
|
||||
import {App} from "aws-cdk-lib";
|
||||
import {InstanceType, UserData} from "aws-cdk-lib/aws-ec2";
|
||||
|
||||
// tslint:disable-next-line:no-empty-interface
|
||||
export interface G4ADConfig extends BaseConfig {
|
||||
|
||||
}
|
||||
|
||||
export class G4ADStack extends BaseEc2Stack {
|
||||
protected props: G4ADConfig;
|
||||
|
||||
constructor(scope: App, id: string, props: G4ADConfig) {
|
||||
super(scope, id, props);
|
||||
}
|
||||
|
||||
protected getUserdata() {
|
||||
const userData = UserData.forWindows();
|
||||
const { niceDCVDisplayDriverUrl, niceDCVServerUrl } = this.props;
|
||||
|
||||
userData.addCommands(
|
||||
`$NiceDCVDisplayDrivers = "${niceDCVDisplayDriverUrl}"`,
|
||||
`$NiceDCVServer = "${niceDCVServerUrl}"`,
|
||||
'$SteamInstallation = "https://cdn.cloudflare.steamstatic.com/client/installer/SteamSetup.exe"',
|
||||
'$MicrosoftEdgeInstallation = "https://go.microsoft.com/fwlink/?linkid=2108834&Channel=Stable&language=en"',
|
||||
`$InstallationFilesFolder = "$home\\Desktop\\InstallationFiles"`,
|
||||
`$Bucket = "ec2-amd-windows-drivers"`,
|
||||
`$KeyPrefix = "latest"`,
|
||||
`$Objects = Get-S3Object -BucketName $Bucket -KeyPrefix $KeyPrefix -Region us-east-1`,
|
||||
`foreach ($Object in $Objects) {
|
||||
$LocalFileName = $Object.Key
|
||||
if ($LocalFileName -ne '' -and $Object.Size -ne 0) {
|
||||
$LocalFilePath = Join-Path $InstallationFilesFolder $LocalFileName
|
||||
Copy-S3Object -BucketName $Bucket -Key $Object.Key -LocalFile $LocalFilePath -Region us-east-1
|
||||
Expand-Archive $LocalFilePath -DestinationPath $InstallationFilesFolder\\1_AMD_driver
|
||||
}
|
||||
}`,
|
||||
'pnputil /add-driver $home\\Desktop\\InstallationFiles\\1_AMD_Driver\\210414a-365562C-Retail_End_User.2\\packages\\Drivers\\Display\\WT6A_INF/*.inf /install',
|
||||
'Invoke-WebRequest -Uri $NiceDCVServer -OutFile $InstallationFilesFolder\\2_NICEDCV-Server.msi',
|
||||
'Invoke-WebRequest -Uri $NiceDCVDisplayDrivers -OutFile $InstallationFilesFolder\\3_NICEDCV-DisplayDriver.msi',
|
||||
'Remove-Item $InstallationFilesFolder\\latest -Recurse',
|
||||
'Set-ExecutionPolicy Bypass -Scope Process -Force; [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072; iex ((New-Object System.Net.WebClient).DownloadString(\'https://community.chocolatey.org/install.ps1\'))',
|
||||
'choco feature enable -n=allowGlobalConfirmation',
|
||||
'choco install steam-rom-manager',
|
||||
'choco install steam-client',
|
||||
'choco install microsoft-edge',
|
||||
`Start-Process msiexec.exe -Wait -ArgumentList '/I C:\\Users\\Administrator\\Desktop\\InstallationFiles\\2_NICEDCV-Server.msi /QN /L* "C:\\msilog.log"'`,
|
||||
`Start-Process msiexec.exe -Wait -ArgumentList '/I C:\\Users\\Administrator\\Desktop\\InstallationFiles\\3_NICEDCV-DisplayDriver.msi /QN /L* "C:\\msilog.log"'`,
|
||||
`'' >> $InstallationFilesFolder\\OK`
|
||||
);
|
||||
|
||||
return userData;
|
||||
}
|
||||
|
||||
protected getInstanceType() {
|
||||
return new InstanceType(`g4ad.${this.props.instanceSize}`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"name": "cloud-gaming-on-ec2-instances",
|
||||
"version": "1.0.0",
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"watch": "tsc -w",
|
||||
"cdk": "cdk"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^14.0.0",
|
||||
"aws-cdk": "^2.41.0",
|
||||
"ts-node": "^8.1.0",
|
||||
"tslint": "^6.1.3",
|
||||
"typescript": "~3.9.7"
|
||||
},
|
||||
"dependencies": {
|
||||
"aws-cdk-lib": "^2.41.0",
|
||||
"constructs": "^10.1.96",
|
||||
"source-map-support": "^0.5.16"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2018",
|
||||
"module": "commonjs",
|
||||
"lib": ["es2018"],
|
||||
"declaration": true,
|
||||
"strict": true,
|
||||
"noImplicitAny": true,
|
||||
"strictNullChecks": true,
|
||||
"noImplicitThis": true,
|
||||
"alwaysStrict": true,
|
||||
"noUnusedLocals": false,
|
||||
"noUnusedParameters": false,
|
||||
"noImplicitReturns": true,
|
||||
"noFallthroughCasesInSwitch": false,
|
||||
"inlineSourceMap": true,
|
||||
"inlineSources": true,
|
||||
"experimentalDecorators": true,
|
||||
"strictPropertyInitialization": false,
|
||||
"typeRoots": ["./node_modules/@types"]
|
||||
},
|
||||
"exclude": ["cdk.out"]
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
{
|
||||
"extends": [
|
||||
"tslint:all"
|
||||
],
|
||||
"rules": {
|
||||
"cyclomatic-complexity": false,
|
||||
"increment-decrement": false,
|
||||
"newline-before-return": false,
|
||||
"newline-per-chained-call": false,
|
||||
"no-parameter-properties": false,
|
||||
"no-parameter-reassignment": false,
|
||||
"no-implicit-dependencies": false,
|
||||
"no-unnecessary-class": ["allow-empty-class", "allow-constructor-only"],
|
||||
"file-name-casing": false,
|
||||
"object-literal-sort-keys": false,
|
||||
"max-line-length": false,
|
||||
"whitespace": false,
|
||||
"no-unused-variable": false,
|
||||
"no-var-requires": false,
|
||||
"no-console": false,
|
||||
"typedef": false,
|
||||
"unnecessary-else": false,
|
||||
"trailing-comma": false,
|
||||
"comment-format": {
|
||||
"options": [
|
||||
"check-space"
|
||||
]
|
||||
},
|
||||
"member-access": true,
|
||||
"only-arrow-functions": {
|
||||
"options": [
|
||||
"allow-declarations",
|
||||
"allow-named-functions"
|
||||
]
|
||||
},
|
||||
|
||||
"completed-docs": false,
|
||||
"no-any": false,
|
||||
"no-magic-numbers": false,
|
||||
"no-non-null-assertion": false,
|
||||
"no-null-keyword": false,
|
||||
"no-require-imports": false,
|
||||
"no-unbound-method": false,
|
||||
"no-unnecessary-qualifier": false,
|
||||
"no-use-before-declare": false,
|
||||
"no-void-expression": false,
|
||||
"prefer-function-over-method": false,
|
||||
"strict-comparisons": false,
|
||||
"strict-type-predicates": false,
|
||||
"triple-equals": {
|
||||
"options": [
|
||||
"allow-undefined-check"
|
||||
]
|
||||
},
|
||||
"interface-name": false,
|
||||
"max-classes-per-file": false,
|
||||
"member-ordering": {
|
||||
"options": {
|
||||
"order": "statics-first"
|
||||
}
|
||||
},
|
||||
"no-switch-case-fall-through": true,
|
||||
"strict-boolean-expressions": {
|
||||
"options": [
|
||||
"allow-boolean-or-undefined"
|
||||
]
|
||||
},
|
||||
"switch-default": false,
|
||||
"variable-name": {
|
||||
"options": [
|
||||
"ban-keywords",
|
||||
"check-format",
|
||||
"allow-leading-underscore",
|
||||
"allow-pascal-case"
|
||||
]
|
||||
},
|
||||
"linebreak-style": false
|
||||
}
|
||||
}
|
||||
Executable
+414
@@ -0,0 +1,414 @@
|
||||
#!/bin/bash
|
||||
source ~/.bashrc
|
||||
source aws-tools.sh
|
||||
source logger.sh
|
||||
source utils.sh
|
||||
source stream-tools.sh
|
||||
|
||||
cloudGamingLogFile=/tmp/cloud-gaming.log
|
||||
rm "$cloudGamingLogFile" > /dev/null 2>&1 &
|
||||
export KERNEL=$(uname -s)
|
||||
|
||||
if [[ -z $AWS_CLOUD_GAMING_SSH_KEY ]]; then
|
||||
printError "The AWS_CLOUD_GAMING_SSH_KEY must be defined in order to use this script." true
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ $KERNEL == "Darwin" ]]; then
|
||||
if ! (hash brew 2>/dev/null); then
|
||||
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
|
||||
fi
|
||||
fi
|
||||
|
||||
if ! (hash whiptail 2>/dev/null); then
|
||||
printWarn "whiptail is not installed. Installing now..." true
|
||||
|
||||
if [[ $KERNEL == "Linux" ]]; then
|
||||
sudo apt-get -y install whiptail
|
||||
elif [[ $KERNEL == "Darwin" ]]; then
|
||||
yes | brew install whiptail
|
||||
fi
|
||||
fi
|
||||
|
||||
createPrerequisitesMap() {
|
||||
declare mapName="prerequisites"
|
||||
declare linuxName="Linux"
|
||||
declare darwinName="Darwin"
|
||||
|
||||
put $mapName "flatpak" $linuxName
|
||||
put $mapName "xvfb-run" $linuxName
|
||||
put $mapName "xdotool" $linuxName
|
||||
put $mapName "dialog" $linuxName
|
||||
put $mapName "pulsemixer" $linuxName
|
||||
put $mapName "nc" $linuxName
|
||||
|
||||
put $mapName "mas" $darwinName
|
||||
put $mapName "python" $darwinName
|
||||
put $mapName "pulseaudio" $darwinName
|
||||
put $mapName "xdotool" $darwinName
|
||||
put $mapName "dialog" $darwinName
|
||||
put $mapName "pulsemixer" $darwinName
|
||||
put $mapName "nc" $darwinName
|
||||
}
|
||||
|
||||
verifyPrerequisites() {
|
||||
printInfo "Verifying prerequisites"
|
||||
declare -a prerequisites
|
||||
|
||||
createPrerequisitesMap
|
||||
prerequisites=($(ls $map/prerequisites/ | xargs -i basename {}))
|
||||
|
||||
printInfo "Detected kernel: $KERNEL"
|
||||
|
||||
for application in "${prerequisites[@]}"; do
|
||||
declare value
|
||||
value=$(get prerequisites "$application")
|
||||
|
||||
if [[ ${value[*]} =~ $KERNEL ]] && ! (hash $application 2>/dev/null); then
|
||||
printWarn "$application is not installed. Installing now..." true
|
||||
|
||||
if [[ $KERNEL == "Linux" ]]; then
|
||||
checkSudoPass "Installing $application requires sudo permissions."
|
||||
|
||||
if [[ $application == "xvfb-run" ]]; then
|
||||
echo "$SUDO_PASSWORD" | sudo -k -S apt-get -y install xvfb
|
||||
elif [[ $application == "nc" ]]; then
|
||||
echo "$SUDO_PASSWORD" | sudo -k -S apt-get -y install netcat
|
||||
else
|
||||
echo "$SUDO_PASSWORD" | sudo -k -S apt-get -y install $application
|
||||
fi
|
||||
elif [[ $KERNEL == "Darwin" ]]; then
|
||||
if [[ $application == "pulsemixer" ]]; then
|
||||
pip install pulsemixer
|
||||
elif [[ $application == "nc" ]]; then
|
||||
yes | brew install netcat
|
||||
else
|
||||
yes | brew install $application
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ $KERNEL == "Linux" ]]; then
|
||||
if ! (flatpak info com.valvesoftware.SteamLink > /dev/null 2>&1); then
|
||||
printWarn "SteamLink is not installed. Installing now..." true
|
||||
checkSudoPass "Installing SteamLink requires sudo permissions."
|
||||
printWarn "Installing the FlatHub repo for Flatpak if it doesn't already exist..."
|
||||
echo "$SUDO_PASSWORD" | sudo -k -S flatpak remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo
|
||||
printWarn "Installing SteamLink from FlatHub..."
|
||||
echo "$SUDO_PASSWORD" | sudo -k -S flatpak install flathub com.valvesoftware.SteamLink
|
||||
fi
|
||||
# elif [[ $KERNEL == "Darwin" ]]; then
|
||||
# TODO check if SteamLink is installed, and if not, install it via mas-cli
|
||||
fi
|
||||
|
||||
if ! (hash aws 2> /dev/null); then
|
||||
printError "The AWS CLI must be installed to use this script. Please install the applicable AWS CLI from AWS and try again" true
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! (hash npm 2> /dev/null); then
|
||||
printError "NodeJS and NPM must be installed to use this script. Please install NodeJS and NPM and try again." true
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ ! -f ~/.ssh/"$AWS_CLOUD_GAMING_SSH_KEY".pem ]]; then
|
||||
printError "In order to use this script, you need to have the ~/.ssh/$AWS_CLOUD_GAMING_SSH_KEY key. Reach out to one of the team members to acquire it via Snappass and then try again." true
|
||||
exit 1
|
||||
fi
|
||||
|
||||
checkAwsProfile
|
||||
}
|
||||
|
||||
verifyPrerequisites
|
||||
|
||||
checkInstanceStatus() {
|
||||
declare state
|
||||
state=$(getInstanceState)
|
||||
|
||||
msgBox "Current instance state: $state"
|
||||
printInfo "Checking instance state. Received state: $state"
|
||||
}
|
||||
|
||||
guideHostThroughSharedStreamSetup() {
|
||||
printInfo "Guiding user through host shared stream setup"
|
||||
|
||||
msgBox "$(cat <<-EOF
|
||||
This will guide you through getting the other players connected to your personal EC2 instance.
|
||||
|
||||
Hit 'OK' to start a desktop connection to your instance
|
||||
EOF
|
||||
)"
|
||||
|
||||
msgBox "$(cat <<-EOF
|
||||
We now need to add your user's PINs to your steam client so they can connect to your instance.
|
||||
|
||||
Hit 'OK' to start a desktop connection to your steam instance.
|
||||
EOF
|
||||
)"
|
||||
|
||||
connectToInstanceViaDcvViewer
|
||||
|
||||
printInfo "Directing user to enter PIN acquired from user's local SteamLinks"
|
||||
msgBox "$(cat <<-EOF
|
||||
On your EC2 Instance, in Steam, navigate to Steam -> Settings -> Remote Play.
|
||||
|
||||
Click the 'Pair Steam Link' button and when prompted.
|
||||
Your users should have this connection PIN ready to give to you, so for each player, provide the PIN you received from them.
|
||||
|
||||
Click 'OK' to exit the Steam settings menu when you've entered everyone's PINs and everyone confirms that they see your EC2 instance ready.
|
||||
|
||||
Tell your users they can hit 'OK'.
|
||||
|
||||
You're now ready to play!
|
||||
|
||||
Hit 'OK' to finish the shared instance hosting setup and to start your stream!
|
||||
EOF
|
||||
)"
|
||||
|
||||
stopStream
|
||||
startStream
|
||||
}
|
||||
|
||||
guideClientThroughSharedStreamSetup() {
|
||||
printInfo "Guiding user through client shared stream setup"
|
||||
|
||||
printInfo "Starting shared stream for client"
|
||||
printInfo "Starting SteamLink"
|
||||
flatpak run com.valvesoftware.SteamLink > /dev/null 2>&1 &
|
||||
|
||||
printInfo "Prompting user to fetch the connection PIN from local SteamLink"
|
||||
msgBox "$(cat <<-EOF
|
||||
We need to get a unique PIN from SteamLink that will identify this machine for connection to the host's EC2 Instance.
|
||||
|
||||
In SteamLink, do the following:
|
||||
|
||||
1. Click on the gear icon in the top right hand corner.
|
||||
2. Then click on 'Computer'
|
||||
3. Click 'Other Computer'
|
||||
|
||||
This will give you a PIN to enter into Steam on the host's EC2 instance.
|
||||
Give the host this PIN when prompted.
|
||||
|
||||
Hit 'OK' when the host says to do so.
|
||||
EOF
|
||||
)"
|
||||
|
||||
msgBox "$(cat <<-EOF
|
||||
You should see that EC2 instance highlighted as a streaming option with a big 'Start Playing' button. You're now ready to play!
|
||||
|
||||
Finished setting up the shared stream. Have fun!
|
||||
EOF
|
||||
)"
|
||||
}
|
||||
|
||||
startSharedStream() {
|
||||
if (whiptail --fb --title "Shared Stream Setup" --yesno "Are you hosting this shared stream?" --defaultno "$BOX_HEIGHT" "$BOX_WIDTH"); then
|
||||
printInfo "User is the HOST of the shared stream"
|
||||
|
||||
guideHostThroughSharedStreamSetup
|
||||
else
|
||||
printInfo "User is a CLIENT of the shared stream"
|
||||
|
||||
guideClientThroughSharedStreamSetup
|
||||
fi
|
||||
}
|
||||
|
||||
streamSettings() {
|
||||
declare choice
|
||||
choice=$(whiptail --fb --title "Stream Settings" --menu "Select an option" "$BOX_HEIGHT" "$BOX_WIDTH" 4 \
|
||||
"P" "Start stream to (p)ersonal instance" \
|
||||
"S" "Start (S)hared stream" \
|
||||
"K" "(K)ill the stream" \
|
||||
"B" "(B)ack" 3>&2 2>&1 1>&3
|
||||
)
|
||||
|
||||
case $choice in
|
||||
"P")
|
||||
startStream
|
||||
streamSettings
|
||||
;;
|
||||
"S")
|
||||
startSharedStream
|
||||
streamSettings
|
||||
;;
|
||||
"K")
|
||||
stopStream
|
||||
streamSettings
|
||||
;;
|
||||
"B")
|
||||
mainMenu
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
guideThroughSteamLink() {
|
||||
printInfo "Guiding user through SteamLink setup"
|
||||
|
||||
msgBox "$(cat <<-EOF
|
||||
Now we need to set up SteamLink.
|
||||
|
||||
First, we're going to connect to your instance via the fancy new NICE DCV viewer.
|
||||
Hit 'OK' when you're ready to log into your instance.
|
||||
EOF
|
||||
)"
|
||||
|
||||
printInfo "Starting DCV Viewer connection to instance using cloud_gaming_dcv_profile.dcv profile"
|
||||
waitForInstanceToBecomeAvailable
|
||||
dcvviewer cloud_gaming_dcv_profile.dcv --certificate-validation-policy=accept-untrusted > /dev/null 2>&1 &
|
||||
|
||||
printInfo "Directing user to log into Steam on the instance"
|
||||
msgBox "$(cat <<-EOF
|
||||
Next, we need to log into Steam. So start Steam and log into your account.
|
||||
|
||||
For ease of use, check the 'Remember my password' box so you don't have to log in manually every time your instance starts.
|
||||
|
||||
Hit 'OK' once you're logged in.
|
||||
EOF
|
||||
)"
|
||||
|
||||
msgBox "$(cat <<-EOF
|
||||
Next, we need to connect your local SteamLink to this box.
|
||||
|
||||
Hit 'OK' to start SteamLink
|
||||
EOF
|
||||
)"
|
||||
|
||||
printInfo "Starting SteamLink"
|
||||
flatpak run com.valvesoftware.SteamLink > /dev/null 2>&1 &
|
||||
|
||||
printInfo "Prompting user to fetch the connection PIN from local SteamLink"
|
||||
msgBox "$(cat <<-EOF
|
||||
Now, we need to get a unique PIN from SteamLink that will identify this machine for connection to your EC2 Instance.
|
||||
|
||||
In SteamLink, do the following:
|
||||
|
||||
1. Click on the gear icon in the top right hand corner.
|
||||
2. Then click on 'Computer'
|
||||
3. Click 'Other Computer'
|
||||
|
||||
This will give you a PIN to enter into Steam on your EC2 instance. Hit 'OK' when you're ready to continue.
|
||||
EOF
|
||||
)"
|
||||
|
||||
printInfo "Directing user to enter PIN acquired from local SteamLink"
|
||||
msgBox "$(cat <<-EOF
|
||||
On your EC2 Instance, in Steam, navigate to Steam -> Settings -> Remote Play.
|
||||
|
||||
Click the 'Pair Steam Link' button and when prompted, provide the PIN you received from SteamLink in the last step.
|
||||
Click 'OK' to exit the Steam settings menu.
|
||||
|
||||
Once you're done, your SteamLink should have the EC2 instance highlighted. Click on it and it should return you to the main menu.
|
||||
|
||||
You should see that EC2 instance highlighted as a streaming option with a big 'Start Playing' button. You're now ready to play!
|
||||
|
||||
Hit 'OK' to finish the one-time setup and return to the main menu.
|
||||
EOF
|
||||
)"
|
||||
|
||||
printInfo "Killing dcvviewer, xvfb (if running, which it shouldn't be), and steamlink"
|
||||
pkill -9 -f dcvviewer > /dev/null 2>&1 &
|
||||
pkill -9 -f xvfb > /dev/null 2>&1 &
|
||||
pkill -9 -f steamlink > /dev/null 2>&1 &
|
||||
}
|
||||
|
||||
deployInstance() {
|
||||
if (whiptail --fb --title "Deploy CDK" --yesno "This will now deploy the CDK for your cloud gaming instance. Do you wish to continue?" --defaultno "$BOX_HEIGHT" "$BOX_WIDTH"); then
|
||||
deployCdk
|
||||
fi
|
||||
|
||||
if (whiptail --fb --title "Setup" --yesno "We'll now go through the first time setup. Do you wish to continue?" "$BOX_HEIGHT" "$BOX_WIDTH"); then
|
||||
msgBox "For first time setups, ensure your terminal is full screen so you don't miss any instructions. If it's not, exit this application, enter full screen, then start again"
|
||||
|
||||
if (whiptail --fb --title "First Time Setup" --yesno "This will now perform first time setup for your cloud gaming instance. Is your terminal full screen?" --defaultno "$BOX_HEIGHT" "$BOX_WIDTH"); then
|
||||
printInfo "Running first time setup"
|
||||
msgBox "For the first run, some manual, one-time setup steps are required. When ready, hit 'OK' to continue and start the connection to your instance's desktop."
|
||||
|
||||
prepareStream
|
||||
guideThroughSteamLink
|
||||
|
||||
msgBox "Finished setting up your instance. Have fun!"
|
||||
else
|
||||
printInfo "User selected 'No' to running the first time setup. Nothing was done"
|
||||
msgBox "Nothing was done."
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
managePersonalInstance() {
|
||||
declare choice
|
||||
choice=$(whiptail --fb --title "Manage personal instance" --menu "Select an option" "$BOX_HEIGHT" "$BOX_WIDTH" 7 \
|
||||
"T" "Check instance s(t)atus" \
|
||||
"C" "(C)onnect to personal instance desktop via DCV Viewer" \
|
||||
"I" "D(i)sconnect from personal instance desktop" \
|
||||
"D" "(D)eploy a personal gaming instance" \
|
||||
"S" "(S)tart instance" \
|
||||
"K" "Stop instance" \
|
||||
"B" "(B)ack" 3>&2 2>&1 1>&3
|
||||
)
|
||||
|
||||
case $choice in
|
||||
"T")
|
||||
checkInstanceStatus
|
||||
managePersonalInstance
|
||||
;;
|
||||
"C")
|
||||
connectToInstanceViaDcvViewer
|
||||
managePersonalInstance
|
||||
;;
|
||||
"I")
|
||||
printInfo "Killing connection to instance desktop via DCV Viewer"
|
||||
pkill -9 -f dcvviewer > /dev/null 2>&1 &
|
||||
managePersonalInstance
|
||||
;;
|
||||
"D")
|
||||
deployInstance
|
||||
managePersonalInstance
|
||||
;;
|
||||
"S")
|
||||
startInstance
|
||||
managePersonalInstance
|
||||
;;
|
||||
"K")
|
||||
stopInstance
|
||||
managePersonalInstance
|
||||
;;
|
||||
"B")
|
||||
mainMenu
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
mainMenu() {
|
||||
declare choice
|
||||
choice=$(whiptail --fb --title "Team Building Cloud Gaming" --menu "Select an option" "$BOX_HEIGHT" "$BOX_WIDTH" 3 \
|
||||
"M" "(M)anage your personal instance" \
|
||||
"S" "(S)tream settings" \
|
||||
"X" "E(x)it" 3>&2 2>&1 1>&3
|
||||
)
|
||||
|
||||
case $choice in
|
||||
"M")
|
||||
managePersonalInstance
|
||||
;;
|
||||
"S")
|
||||
streamSettings
|
||||
;;
|
||||
"X")
|
||||
clear
|
||||
printInfo "Killing dcvviewer, xvfb, and steamlink"
|
||||
pkill -9 -f dcvviewer > /dev/null 2>&1 &
|
||||
pkill -9 -f xvfb > /dev/null 2>&1 &
|
||||
pkill -9 -f steamlink > /dev/null 2>&1 &
|
||||
if (whiptail --fb --title "Stop instance" --yesno "Do you wish to stop your instance before exiting?" "$BOX_HEIGHT" "$BOX_WIDTH"); then
|
||||
stopInstance
|
||||
fi
|
||||
printInfo "Exiting"
|
||||
exit 0
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
while :; do
|
||||
mainMenu
|
||||
done
|
||||
@@ -0,0 +1,12 @@
|
||||
[connect]
|
||||
host=PLACEHOLDER
|
||||
port=8443
|
||||
user=Administrator
|
||||
password=PLACEHOLDER
|
||||
weburlpath=
|
||||
|
||||
[version]
|
||||
format=1.0
|
||||
|
||||
[input]
|
||||
enable-relative-mouse=false
|
||||
@@ -0,0 +1,38 @@
|
||||
#!/bin/bash
|
||||
|
||||
red=$(tput setaf 1)
|
||||
green=$(tput setaf 2)
|
||||
gold=$(tput setaf 3)
|
||||
blue=$(tput setaf 4)
|
||||
magenta=$(tput setaf 5)
|
||||
cyan=$(tput setaf 6)
|
||||
gray=$(tput setaf 243)
|
||||
default=$(tput sgr0)
|
||||
bold=$(tput bold)
|
||||
|
||||
printError() {
|
||||
if [[ -z $2 ]]; then
|
||||
echo -e "${red}${bold}ERROR:${default}${red} $1${default}" >> $cloudGamingLogFile
|
||||
else
|
||||
echo -e "${red}${bold}ERROR:${default}${red} $1${default}"
|
||||
echo -e "${red}${bold}ERROR:${default}${red} $1${default}" >> $cloudGamingLogFile
|
||||
fi
|
||||
}
|
||||
|
||||
printWarn() {
|
||||
if [[ -z $2 ]]; then
|
||||
echo -e "${gold}${bold}WARN:${default}${gold} $1${default}" >> $cloudGamingLogFile
|
||||
else
|
||||
echo -e "${gold}${bold}WARN:${default}${gold} $1${default}"
|
||||
echo -e "${gold}${bold}WARN:${default}${gold} $1${default}" >> $cloudGamingLogFile
|
||||
fi
|
||||
}
|
||||
|
||||
printInfo() {
|
||||
if [[ -z $2 ]]; then
|
||||
echo -e "${cyan}${bold}INFO:${default}${cyan} $1${default}" >> $cloudGamingLogFile
|
||||
else
|
||||
echo -e "${cyan}${bold}INFO:${default}${cyan} $1${default}"
|
||||
echo -e "${cyan}${bold}INFO:${default}${cyan} $1${default}" >> $cloudGamingLogFile
|
||||
fi
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
#!/bin/bash
|
||||
source aws-tools.sh
|
||||
source logger.sh
|
||||
source utils.sh
|
||||
|
||||
prepareStream() {
|
||||
printInfo "Preparing stream and configuring NICE DCV Viewer"
|
||||
|
||||
checkSudoPass "Installing NICE DCV Client requires sudo permissions."
|
||||
|
||||
declare architecture
|
||||
architecture=$(uname -p)
|
||||
|
||||
printInfo "Detected architecture: $architecture"
|
||||
|
||||
{
|
||||
echo -e "XXX\n0\nInstalling NICE DCV Client... \nXXX"
|
||||
printInfo "Installing NICE DCV Viewer client"
|
||||
# if [[ $kernel == "Linux" ]]; then
|
||||
wget -o nice-dcv-viewer.deb https://d1uj6qtbmh3dt5.cloudfront.net/2022.1/Clients/nice-dcv-viewer_2022.1.4251-1_amd64.ubuntu2004.deb > /dev/null 2>&1
|
||||
echo "$SUDO_PASSWORD" | sudo -k -S sh -c "dpkg -i nice-dcv-viewer.deb" > /dev/null 2>&1
|
||||
# elif [[ $kernel == "Darwin" ]]; then
|
||||
# if [[ $architecture == "x86_64" ]]; then
|
||||
# wget -o nice-dcv-viewer.dmg https://d1uj6qtbmh3dt5.cloudfront.net/2022.1/Clients/nice-dcv-viewer-2022.1.4279.x86_64.dmg > /dev/null 2>&1
|
||||
# elif [[ $architecture == "arm64" ]]; then
|
||||
# wget -o nice-dcv-viewer.dmg https://d1uj6qtbmh3dt5.cloudfront.net/2022.1/Clients/nice-dcv-viewer-2022.1.4279.arm64.dmg > /dev/null 2>&1
|
||||
# fi
|
||||
#
|
||||
# TODO figure out how to install dcvviewer and how to run it on a Mac
|
||||
# echo "$SUDO_PASSWORD" | sudo -k -S sh -c "sudo hdiutil attach nice-dcv-viewer.dmg"
|
||||
# fi
|
||||
echo -e "XXX\n33\nCleaning up... \nXXX"
|
||||
printInfo "Removing downloaded DCV Viewer installation"
|
||||
rm nice-dcv-viewer* > /dev/null 2>&1
|
||||
echo -e "XXX\n66\nCreating Connection Profile from template... \nXXX"
|
||||
createDcvConnectionProfileFromTemplate
|
||||
echo -e "XXX\n100\nDone! \nXXX"
|
||||
sleep 1
|
||||
} | whiptail --title "Installing NICE DCV Client..." --gauge "Installing NICE DCV Client..." "$GAUGE_BOX_HEIGHT" "$GAUGE_BOX_WIDTH" 0
|
||||
}
|
||||
|
||||
startStream() {
|
||||
printInfo "Starting stream"
|
||||
if ! (hash dcvviewer 2> /dev/null); then
|
||||
printError "Unable to start stream: dcvviewer is not installed"
|
||||
msgBox "Can't stream without first time setup. Run the deploy instance task from the instance management menu first!"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if (pgrep -f dcvviewer || pgrep -f steam); then
|
||||
printError "Stream is already running."
|
||||
msgBox "Stream is already running"
|
||||
return 0
|
||||
fi
|
||||
|
||||
declare state
|
||||
state=$(getInstanceState)
|
||||
if [[ $state != "running" ]]; then
|
||||
if (whiptail --fb --title "Start your instance?" --yesno "In order to stream, you instance needs to be started. Would you like to start your personal instance?" "$BOX_BOX_HEIGHT" "$BOX_WIDTH"); then
|
||||
startInstance
|
||||
else
|
||||
printError "Unable to start stream: Instance is not running"
|
||||
msgBox "Can't stream! Instance is not running. You can start the instance from the Instance Management menu."
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
printInfo "Starting dcvviewer in background"
|
||||
xvfb-run -a dcvviewer cloud_gaming_dcv_profile.dcv --certificate-validation-policy=accept-untrusted > /dev/null 2>&1 &
|
||||
sleep 0.25
|
||||
|
||||
printInfo "Minimizing active window"
|
||||
xdotool windowminimize $(xdotool getactivewindow)
|
||||
printInfo "Muting DCV Viewer"
|
||||
pulsemixer --toggle-mute --id $(pulsemixer -l | grep dcvviewer | awk '{ print $4; }' | sed 's/,//g') > /dev/null 2>&1
|
||||
sleep 0.25
|
||||
|
||||
printInfo "Starting SteamLink"
|
||||
flatpak run com.valvesoftware.SteamLink > /dev/null 2>&1 &
|
||||
}
|
||||
|
||||
stopStream() {
|
||||
printInfo "Stopping the stream"
|
||||
pkill -9 -f dcvviewer > /dev/null 2>&1 &
|
||||
pkill -9 -f xvfb > /dev/null 2>&1 &
|
||||
pkill -9 -f steamlink > /dev/null 2>&1 &
|
||||
|
||||
printInfo "Stopped the stream"
|
||||
msgBox "Successfully killed the stream"
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
#!/bin/bash
|
||||
source logger.sh
|
||||
|
||||
TERMINAL_HEIGHT=$(tput lines)
|
||||
BOX_HEIGHT=$(printf "%.0f" "$(echo "scale=2; $TERMINAL_HEIGHT * .5" | bc)")
|
||||
GAUGE_BOX_HEIGHT=$(printf "%.0f" "$(echo "scale=2; $TERMINAL_HEIGHT * .25" | bc)")
|
||||
TERMINAL_WIDTH=$(tput cols)
|
||||
BOX_WIDTH=$(printf "%.0f" "$(echo "scale=2; $TERMINAL_WIDTH * .75" | bc)")
|
||||
GAUGE_BOX_WIDTH=$(printf "%.0f" "$(echo "scale=2; $TERMINAL_WIDTH * .5" | bc)")
|
||||
|
||||
setConfigValue() {
|
||||
printInfo "Setting bashrc environment variable: $1=$2"
|
||||
|
||||
if ( grep "$1" ~/.bashrc ); then
|
||||
sed -i "/$1=/c\export $1=$2" ~/.bashrc
|
||||
else
|
||||
echo "export $1=$2" >> ~/.bashrc
|
||||
fi
|
||||
|
||||
unset "$1"
|
||||
printf -v "$1" '%s' "$2"
|
||||
}
|
||||
|
||||
msgBox() {
|
||||
whiptail --fb --msgbox "$1" "$BOX_HEIGHT" "$BOX_WIDTH"
|
||||
}
|
||||
|
||||
showTailBox() {
|
||||
trap "kill $2 2> /dev/null" EXIT
|
||||
|
||||
while kill -0 "$2" 2> /dev/null; do
|
||||
dialog --title "$1" --exit-label "Finished" --tailbox "$3" "$BOX_HEIGHT" "$BOX_WIDTH"
|
||||
done
|
||||
|
||||
clear
|
||||
|
||||
trap - EXIT
|
||||
}
|
||||
|
||||
checkSudoPass() {
|
||||
printInfo "Prompting user for sudo password with message: $1"
|
||||
if [[ ! "$SUDO_PASSWORD" ]]; then
|
||||
SUDO_PASSWORD=$(whiptail --passwordbox "$1 Enter your sudo password" "$BOX_HEIGHT" "$BOX_WIDTH" 3>&2 2>&1 1>&3)
|
||||
fi
|
||||
}
|
||||
|
||||
createMap() {
|
||||
declare prefix
|
||||
prefix=$(basename -- "$0")
|
||||
map=$(mktemp -dt "$prefix.XXXXXXXX")
|
||||
trap "rm -rf $map" EXIT
|
||||
}
|
||||
|
||||
put() {
|
||||
declare mapName="$1"
|
||||
declare key="$2"
|
||||
declare value="$3"
|
||||
|
||||
printInfo "Adding [$key: $value] to map $mapName"
|
||||
|
||||
[[ -z $map ]] && createMap
|
||||
[[ -d "$map/$mapName" ]] || mkdir "$map/$mapName"
|
||||
|
||||
echo "$value" >> "$map/$mapName/$key"
|
||||
}
|
||||
|
||||
get() {
|
||||
declare mapName="$1"
|
||||
declare key="$2"
|
||||
|
||||
[[ -z $map ]] && createMap
|
||||
cat "$map/$mapName/$key"
|
||||
|
||||
printInfo "Fetched $map/$mapName/$key: $(cat $map/$mapName/$key)"
|
||||
}
|
||||
Reference in New Issue
Block a user