// --------------------------------------------------------------------------------------------------------------------
//
// This file is part of the HandBrake source code - It may be used under the terms of the GNU General Public License.
//
//
// An Implementation of IEncodeInstance that works with a remote process rather than locally in-process.
// This class is effectively just a shim.
//
// --------------------------------------------------------------------------------------------------------------------
namespace HandBrakeWPF.Instance
{
using System;
using System.ComponentModel;
using System.Diagnostics;
using System.IO;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using HandBrake.Interop.Interop.EventArgs;
using HandBrake.Interop.Interop.Interfaces;
using HandBrake.Interop.Interop.Json.Encode;
using HandBrake.Interop.Interop.Json.State;
using HandBrake.Interop.Json;
using HandBrake.Worker.Routing.Commands;
using HandBrakeWPF.Instance.Model;
using HandBrakeWPF.Model.Options;
using HandBrakeWPF.Services.Interfaces;
using HandBrakeWPF.Services.Logging.Interfaces;
using HandBrakeWPF.Utilities;
using Timer = System.Timers.Timer;
public class RemoteInstance : HttpRequestBase, IEncodeInstance, IDisposable
{
private readonly ILog logService;
private readonly IUserSettingService userSettingService;
private readonly IPortService portService;
private const double EncodePollIntervalMs = 500;
private Process workerProcess;
private Timer encodePollTimer;
private int retryCount;
private bool encodeCompleteFired;
private bool serverStarted;
public RemoteInstance(ILog logService, IUserSettingService userSettingService, IPortService portService)
{
this.logService = logService;
this.userSettingService = userSettingService;
this.portService = portService;
}
public event EventHandler EncodeCompleted;
public event EventHandler EncodeProgress;
public bool IsRemoteInstance => true;
public async void PauseEncode()
{
if (this.IsServerRunning())
{
await this.MakeHttpGetRequest("PauseEncode");
this.StopPollingProgress();
}
}
public async void ResumeEncode()
{
if (this.IsServerRunning())
{
await this.MakeHttpGetRequest("ResumeEncode");
this.MonitorEncodeProgress();
}
}
public async void StartEncode(JsonEncodeObject jobToStart)
{
if (this.IsServerRunning())
{
Thread thread1 = new Thread(() => RunEncodeInitProcess(jobToStart));
thread1.Start();
}
else
{
this.EncodeCompleted?.Invoke(sender: this, e: new EncodeCompletedEventArgs(-10));
}
}
public async void StopEncode()
{
if (this.IsServerRunning())
{
await this.MakeHttpGetRequest("StopEncode");
}
}
public JsonState GetEncodeProgress()
{
Task response = this.MakeHttpGetRequest("PollEncodeProgress");
response.Wait();
if (!response.Result.WasSuccessful)
{
return null;
}
string statusJson = response.Result?.JsonResponse;
JsonState state = JsonSerializer.Deserialize(statusJson, JsonSettings.Options);
return state;
}
public void Initialize(int verbosityLvl, bool noHardwareMode)
{
try
{
if (this.workerProcess == null || this.workerProcess.HasExited)
{
var plainTextBytes = Encoding.UTF8.GetBytes(Guid.NewGuid().ToString());
this.base64Token = Convert.ToBase64String(plainTextBytes);
this.port = this.portService.GetOpenPort(userSettingService.GetUserSetting(UserSettingConstants.ProcessIsolationPort));
this.serverUrl = string.Format("http://127.0.0.1:{0}/", this.port);
workerProcess = new Process
{
StartInfo =
{
FileName = "HandBrake.Worker.exe",
Arguments =
string.Format(" --port={0} --token={1}", port, this.base64Token),
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true
}
};
workerProcess.Exited += this.WorkerProcess_Exited;
workerProcess.OutputDataReceived += this.WorkerProcess_OutputDataReceived;
workerProcess.ErrorDataReceived += this.WorkerProcess_OutputDataReceived;
workerProcess.Start();
workerProcess.BeginOutputReadLine();
workerProcess.BeginErrorReadLine();
// Set Process Priority
switch ((ProcessPriority)this.userSettingService.GetUserSetting(UserSettingConstants.ProcessPriorityInt))
{
case ProcessPriority.High:
workerProcess.PriorityClass = ProcessPriorityClass.High;
break;
case ProcessPriority.AboveNormal:
workerProcess.PriorityClass = ProcessPriorityClass.AboveNormal;
break;
case ProcessPriority.Normal:
workerProcess.PriorityClass = ProcessPriorityClass.Normal;
break;
case ProcessPriority.Low:
workerProcess.PriorityClass = ProcessPriorityClass.Idle;
break;
default:
workerProcess.PriorityClass = ProcessPriorityClass.BelowNormal;
break;
}
int maxAllowed = userSettingService.GetUserSetting(UserSettingConstants.SimultaneousEncodes);
this.logService.LogMessage(string.Format("Remote Process started with Process ID: {0} using port: {1}. Max Allowed Instances: {2}", this.workerProcess.Id, port, maxAllowed));
}
}
catch (Exception e)
{
this.logService.LogMessage("Unable to start worker process.");
this.logService.LogMessage(e.ToString());
}
}
public void Dispose()
{
this.workerProcess?.Dispose();
}
private void WorkerProcess_OutputDataReceived(object sender, DataReceivedEventArgs e)
{
this.logService.LogMessage(e.Data);
}
private void MonitorEncodeProgress()
{
this.encodePollTimer = new Timer();
this.encodePollTimer.Interval = EncodePollIntervalMs;
this.encodePollTimer.Elapsed += (o, e) =>
{
try
{
this.PollEncodeProgress();
}
catch (Exception exc)
{
Debug.WriteLine(exc);
}
};
this.encodePollTimer.Start();
}
private void StopPollingProgress()
{
try
{
this.PollEncodeProgress(); // Get the last progress state.
}
catch (Exception exc)
{
Debug.WriteLine(exc);
}
this.encodePollTimer?.Stop();
}
private void WorkerProcess_Exited(object sender, EventArgs e)
{
this.logService.LogMessage("Worker Process Exited!");
}
private void StopServer()
{
if (this.workerProcess != null && !this.workerProcess.HasExited)
{
this.workerProcess.Kill();
}
}
private async void PollEncodeProgress()
{
if (encodeCompleteFired)
{
this.encodePollTimer?.Stop();
this.encodePollTimer?.Dispose();
return;
}
ServerResponse response = null;
try
{
if (this.retryCount > 5)
{
encodeCompleteFired = true;
this.encodePollTimer?.Stop();
this.EncodeCompleted?.Invoke(sender: this, e: new EncodeCompletedEventArgs(-11));
if (this.workerProcess != null && !this.workerProcess.HasExited)
{
this.workerProcess?.Kill();
}
return;
}
response = await this.MakeHttpGetRequest("PollEncodeProgress");
}
catch (Exception e)
{
retryCount = this.retryCount + 1;
}
if (response == null || !response.WasSuccessful)
{
retryCount = this.retryCount + 1;
return;
}
this.retryCount = 0; // Reset
string statusJson = response.JsonResponse;
JsonState state = JsonSerializer.Deserialize(statusJson, JsonSettings.Options);
TaskState taskState = state != null ? TaskState.FromRepositoryValue(state.State) : null;
if (taskState != null && (taskState == TaskState.Working || taskState == TaskState.Searching))
{
if (this.EncodeProgress != null)
{
var progressEventArgs = new EncodeProgressEventArgs(
fractionComplete: state.Working.Progress,
currentFrameRate: state.Working.Rate,
averageFrameRate: state.Working.RateAvg,
estimatedTimeLeft: TimeSpan.FromSeconds(state.Working.ETASeconds),
passId: state.Working.PassID,
pass: state.Working.Pass,
passCount: state.Working.PassCount,
stateCode: taskState.Code);
this.EncodeProgress(this, progressEventArgs);
}
}
else if (taskState != null && taskState == TaskState.WorkDone)
{
this.encodePollTimer.Stop();
encodeCompleteFired = true;
if (this.workerProcess != null && !this.workerProcess.HasExited)
{
try
{
this.workerProcess?.Kill();
}
catch (Win32Exception e)
{
Debug.WriteLine(e);
}
}
this.EncodeCompleted?.Invoke(sender: this, e: new EncodeCompletedEventArgs(state.WorkDone.Error));
this.portService.FreePort(this.port);
}
}
private void RunEncodeInitProcess(JsonEncodeObject jobToStart)
{
if (this.IsServerRunning())
{
InitCommand initCommand = new InitCommand
{
EnableDiskLogging = false,
AllowDisconnectedWorker = false,
DisableLibDvdNav = !this.userSettingService.GetUserSetting(UserSettingConstants.DisableLibDvdNav),
EnableHardwareAcceleration = true,
LogDirectory = DirectoryUtilities.GetLogDirectory(),
LogVerbosity = this.userSettingService.GetUserSetting(UserSettingConstants.Verbosity)
};
initCommand.LogFile = Path.Combine(initCommand.LogDirectory, string.Format("activity_log.worker.{0}.txt", GeneralUtilities.ProcessId));
string job = JsonSerializer.Serialize(new EncodeCommand { InitialiseCommand = initCommand, EncodeJob = jobToStart }, JsonSettings.Options);
var task = Task.Run(async () => await this.MakeHttpJsonPostRequest("StartEncode", job));
task.Wait();
this.MonitorEncodeProgress();
}
}
private bool IsServerRunning()
{
// Poll the server until it's started up. This allows us to prevent failures in upstream methods.
if (this.serverStarted)
{
return this.serverStarted;
}
int count = 0;
while (!this.serverStarted)
{
if (count > 10)
{
logService.LogMessage("Unable to connect to the HandBrake Worker instance after 10 attempts. Try disabling this option in Tools -> Preferences -> Advanced.");
return false;
}
try
{
var task = Task.Run(async () => await this.MakeHttpGetRequest("IsTokenSet"));
task.Wait(2000);
if (string.Equals(task.Result.JsonResponse, "True", StringComparison.CurrentCultureIgnoreCase))
{
this.serverStarted = true;
return true;
}
}
catch (Exception)
{
// Do nothing. We'll try again. The service isn't ready yet.
}
finally
{
count = count + 1;
}
}
return true;
}
}
}