diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..10e543e
--- /dev/null
+++ b/.gitignore
@@ -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
\ No newline at end of file
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..c66eadc
--- /dev/null
+++ b/README.md
@@ -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).
Defaults to `personal`
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
\ No newline at end of file
diff --git a/aws-tools.sh b/aws-tools.sh
new file mode 100644
index 0000000..4e5c0fd
--- /dev/null
+++ b/aws-tools.sh
@@ -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 &
+}
\ No newline at end of file
diff --git a/cdk/.gitignore b/cdk/.gitignore
new file mode 100644
index 0000000..e3d809f
--- /dev/null
+++ b/cdk/.gitignore
@@ -0,0 +1,11 @@
+!jest.config.js
+*.d.ts
+*.js
+node_modules
+
+# CDK asset staging directory
+.cdk.staging
+cdk.out
+
+bin/*.js
+lib/*.js
\ No newline at end of file
diff --git a/cdk/.npmignore b/cdk/.npmignore
new file mode 100644
index 0000000..c1d6d45
--- /dev/null
+++ b/cdk/.npmignore
@@ -0,0 +1,6 @@
+*.ts
+!*.d.ts
+
+# CDK asset staging directory
+.cdk.staging
+cdk.out
diff --git a/cdk/bin/cloud-gaming-on-ec2.ts b/cdk/bin/cloud-gaming-on-ec2.ts
new file mode 100644
index 0000000..5618103
--- /dev/null
+++ b/cdk/bin/cloud-gaming-on-ec2.ts
@@ -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
+});
diff --git a/cdk/cdk.json b/cdk/cdk.json
new file mode 100644
index 0000000..4232123
--- /dev/null
+++ b/cdk/cdk.json
@@ -0,0 +1,3 @@
+{
+ "app": "npx ts-node bin/cloud-gaming-on-ec2.ts"
+}
diff --git a/cdk/lib/base.ts b/cdk/lib/base.ts
new file mode 100644
index 0000000..4526bd7
--- /dev/null
+++ b/cdk/lib/base.ts
@@ -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;
+}
diff --git a/cdk/lib/g4ad.ts b/cdk/lib/g4ad.ts
new file mode 100644
index 0000000..c0473bc
--- /dev/null
+++ b/cdk/lib/g4ad.ts
@@ -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}`);
+ }
+}
diff --git a/cdk/package.json b/cdk/package.json
new file mode 100644
index 0000000..30c0ff6
--- /dev/null
+++ b/cdk/package.json
@@ -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"
+ }
+}
diff --git a/cdk/tsconfig.json b/cdk/tsconfig.json
new file mode 100644
index 0000000..ec75123
--- /dev/null
+++ b/cdk/tsconfig.json
@@ -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"]
+}
diff --git a/cdk/tslint.json b/cdk/tslint.json
new file mode 100644
index 0000000..9ffeb5c
--- /dev/null
+++ b/cdk/tslint.json
@@ -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
+ }
+}
diff --git a/cloud-gaming.sh b/cloud-gaming.sh
new file mode 100755
index 0000000..8eb6150
--- /dev/null
+++ b/cloud-gaming.sh
@@ -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
\ No newline at end of file
diff --git a/cloud_gaming_dcv_profile.dcv b/cloud_gaming_dcv_profile.dcv
new file mode 100644
index 0000000..577bc01
--- /dev/null
+++ b/cloud_gaming_dcv_profile.dcv
@@ -0,0 +1,12 @@
+[connect]
+host=PLACEHOLDER
+port=8443
+user=Administrator
+password=PLACEHOLDER
+weburlpath=
+
+[version]
+format=1.0
+
+[input]
+enable-relative-mouse=false
diff --git a/logger.sh b/logger.sh
new file mode 100644
index 0000000..f52f6d8
--- /dev/null
+++ b/logger.sh
@@ -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
+}
\ No newline at end of file
diff --git a/stream-tools.sh b/stream-tools.sh
new file mode 100644
index 0000000..3b16795
--- /dev/null
+++ b/stream-tools.sh
@@ -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"
+}
\ No newline at end of file
diff --git a/utils.sh b/utils.sh
new file mode 100644
index 0000000..5c70b5b
--- /dev/null
+++ b/utils.sh
@@ -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)"
+}
\ No newline at end of file