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