// -------------------------------------------------------------------------------------------------------------------- // // 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; } } }