259 lines
9.1 KiB
C#
259 lines
9.1 KiB
C#
|
// 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
|