FittsLaw/Assets/GoogleVR/Scripts/Controller/Internal/Emulator/EmulatorClientSocket.cs

259 lines
9.1 KiB
C#
Raw Normal View History

2018-10-08 23:54:11 -04:00
// Copyright 2016 Google Inc. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// This class is only used in the Editor, so make sure to only compile it on that platform.
// Additionally, If this class is compiled on Android then Unity will insert the INTERNET permission
// into the manifest because of the reference to the type TCPClient. Excluding this class in the android
// build ensures that it is only included if the developer using the SDK actually uses INTERNET related services.
// This MonoBehaviour is only ever instantiated dynamically, so it is fine that it is only compiled in the Editor,
// Otherwise it would cause serialization issues.
#if UNITY_EDITOR
using UnityEngine;
using System;
using System.IO;
using System.Net.Sockets;
using System.Threading;
using proto;
/// @cond
namespace Gvr.Internal {
public enum EmulatorClientSocketConnectionState {
Disconnected = 0,
Connecting = 1,
Connected = 2,
};
class EmulatorClientSocket : MonoBehaviour {
private static readonly int kPhoneEventPort = 7003;
private const int kSocketReadTimeoutMillis = 5000;
// Minimum interval, in seconds, between attempts to reconnect the socket.
private const float kMinReconnectInterval = 1f;
private TcpClient phoneMirroringSocket;
private Thread phoneEventThread;
private volatile bool shouldStop = false;
// Flag used to limit connection state logging to initial failure and successful reconnects.
private volatile bool lastConnectionAttemptWasSuccessful = true;
private EmulatorManager phoneRemote;
public EmulatorClientSocketConnectionState connected { get; private set; }
public void Init(EmulatorManager remote) {
phoneRemote = remote;
if (EmulatorConfig.Instance.PHONE_EVENT_MODE != EmulatorConfig.Mode.OFF) {
phoneEventThread = new Thread(phoneEventSocketLoop);
phoneEventThread.IsBackground = true;
phoneEventThread.Start();
}
}
private void phoneEventSocketLoop() {
while (!shouldStop) {
long lastConnectionAttemptTime = DateTime.Now.Ticks;
try {
phoneConnect();
} catch(Exception e) {
if (lastConnectionAttemptWasSuccessful) {
Debug.LogWarningFormat("{0}\n{1}", e.Message, e.StackTrace);
// Suppress additional failures until we have successfully reconnected.
lastConnectionAttemptWasSuccessful = false;
}
}
// Wait a while in order to enforce the minimum time between connection attempts.
TimeSpan elapsed = new TimeSpan(DateTime.Now.Ticks - lastConnectionAttemptTime);
float toWait = kMinReconnectInterval - (float) elapsed.TotalSeconds;
if (toWait > 0) {
Thread.Sleep((int) (toWait * 1000));
}
}
}
private void phoneConnect() {
string addr = EmulatorConfig.Instance.PHONE_EVENT_MODE == EmulatorConfig.Mode.USB
? EmulatorConfig.USB_SERVER_IP : EmulatorConfig.WIFI_SERVER_IP;
try {
if (EmulatorConfig.Instance.PHONE_EVENT_MODE == EmulatorConfig.Mode.USB) {
setupPortForwarding(kPhoneEventPort);
}
TcpClient tcpClient = new TcpClient(addr, kPhoneEventPort);
connected = EmulatorClientSocketConnectionState.Connecting;
ProcessConnection(tcpClient);
tcpClient.Close();
} finally {
connected = EmulatorClientSocketConnectionState.Disconnected;
}
}
private void setupPortForwarding(int port) {
#if !UNITY_WEBPLAYER
string adbCommand = string.Format("adb forward tcp:{0} tcp:{0}", port);
System.Diagnostics.Process myProcess = new System.Diagnostics.Process();
string processFilename;
string processArguments;
int kExitCodeCommandNotFound;
if (Application.platform == RuntimePlatform.WindowsEditor ||
Application.platform == RuntimePlatform.WindowsPlayer) {
processFilename = "CMD.exe";
processArguments = @"/k " + adbCommand + " & exit";
// See "Common Error Lookup Tool" (https://www.microsoft.com/en-us/download/details.aspx?id=985)
// MSG_DIR_BAD_COMMAND_OR_FILE (cmdmsg.h)
kExitCodeCommandNotFound = 9009; // 0x2331
} else { // Unix
processFilename = "bash";
processArguments = string.Format("-l -c \"{0}\"", adbCommand);
// "command not found" (see http://tldp.org/LDP/abs/html/exitcodes.html)
kExitCodeCommandNotFound = 127;
}
System.Diagnostics.ProcessStartInfo myProcessStartInfo =
new System.Diagnostics.ProcessStartInfo(processFilename, processArguments);
myProcessStartInfo.UseShellExecute = false;
myProcessStartInfo.RedirectStandardError = true;
myProcessStartInfo.CreateNoWindow = true;
myProcess.StartInfo = myProcessStartInfo;
myProcess.Start();
myProcess.WaitForExit();
// Also wait for HasExited here, to avoid ExitCode access below occasionally throwing InvalidOperationException
while (!myProcess.HasExited) {
Thread.Sleep(1);
}
int exitCode = myProcess.ExitCode;
string standardError = myProcess.StandardError.ReadToEnd();
myProcess.Close();
if (exitCode == 0) {
// Port forwarding setup successfully.
return;
}
if (exitCode == kExitCodeCommandNotFound) {
// Caught by phoneEventSocketLoop.
throw new Exception(
"Android Debug Bridge (`adb`) command not found." +
"\nVerify that the Android SDK is installed and that the directory containing" +
" `adb` is included in your PATH environment variable.");
}
// Caught by phoneEventSocketLoop.
throw new Exception(
string.Format(
"Failed to setup port forwarding." +
" Exit code {0} returned by process: {1} {2}\n{3}",
exitCode, processFilename, processArguments, standardError));
#endif // !UNITY_WEBPLAYER
}
private void ProcessConnection(TcpClient tcpClient) {
byte[] buffer = new byte[4];
NetworkStream stream = tcpClient.GetStream();
stream.ReadTimeout = kSocketReadTimeoutMillis;
tcpClient.ReceiveTimeout = kSocketReadTimeoutMillis;
while (!shouldStop) {
int bytesRead = blockingRead(stream, buffer, 0, 4);
if (bytesRead < 4) {
// Caught by phoneEventSocketLoop.
throw new Exception(
"Failed to read from controller emulator app event socket." +
"\nVerify that the controller emulator app is running.");
}
int msgLen = unpack32bits(correctEndianness(buffer), 0);
byte[] dataBuffer = new byte[msgLen];
bytesRead = blockingRead(stream, dataBuffer, 0, msgLen);
if (bytesRead < msgLen) {
// Caught by phoneEventSocketLoop.
throw new Exception(
"Failed to read from controller emulator app event socket." +
"\nVerify that the controller emulator app is running.");
}
PhoneEvent proto =
PhoneEvent.CreateBuilder().MergeFrom(dataBuffer).Build();
phoneRemote.OnPhoneEvent(proto);
connected = EmulatorClientSocketConnectionState.Connected;
if (!lastConnectionAttemptWasSuccessful) {
Debug.Log("Successfully connected to controller emulator app.");
// Log first failure after after successful read from event socket.
lastConnectionAttemptWasSuccessful = true;
}
}
}
private int blockingRead(NetworkStream stream, byte[] buffer, int index,
int count) {
int bytesRead = 0;
while (!shouldStop && bytesRead < count) {
try {
int n = stream.Read(buffer, index + bytesRead, count - bytesRead);
if (n <= 0) {
// Failed to read.
return -1;
}
bytesRead += n;
} catch (IOException) {
// Read failed or timed out.
return -1;
} catch (ObjectDisposedException) {
// Socket closed.
return -1;
}
}
return bytesRead;
}
void OnDestroy() {
shouldStop = true;
if (phoneMirroringSocket != null) {
phoneMirroringSocket.Close ();
phoneMirroringSocket = null;
}
if (phoneEventThread != null) {
phoneEventThread.Join();
}
}
private int unpack32bits(byte[] array, int offset) {
int num = 0;
for (int i = 0; i < 4; i++) {
num += array [offset + i] << (i * 8);
}
return num;
}
static private byte[] correctEndianness(byte[] array) {
if (BitConverter.IsLittleEndian)
Array.Reverse(array);
return array;
}
}
}
/// @endcond
#endif // UNITY_EDITOR