using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Net; using System.Reflection; using System.Runtime.InteropServices; using System.Text; using Robust.Client.Console; using Robust.Shared.Configuration; using Robust.Shared.ContentPack; using Robust.Shared.IoC; using Robust.Shared.Localization; using Robust.Shared.Log; using Robust.Shared.Prototypes; using Robust.Shared.Utility; using Xilium.CefGlue; namespace Robust.Client.WebView.Cef { internal sealed partial class WebViewManagerCef : IWebViewManagerImpl { private static readonly string BasePath = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location!)!; private CefApp _app = default!; [Dependency] private IDependencyCollection _dependencyCollection = default!; [Dependency] private IPrototypeManager _prototypeManager = default!; [Dependency] private IGameControllerInternal _gameController = default!; [Dependency] private IResourceManagerInternal _resourceManager = default!; [Dependency] private IClientConsoleHost _consoleHost = default!; [Dependency] private IConfigurationManager _cfg = default!; [Dependency] private ILogManager _logManager = default!; [Dependency] private ILocalizationManager _localization = default!; private ISawmill _sawmill = default!; public void Initialize() { _sawmill = _logManager.GetSawmill("web.cef"); _consoleHost.RegisterCommand( "flushcookies", _localization.GetString("cmd-flushcookies-desc"), _localization.GetString("cmd-flushcookies-help"), (_, _, _) => CefCookieManager.GetGlobal(null).FlushStore(null)); #if !MACOS string subProcessName; if (OperatingSystem.IsWindows()) subProcessName = "Robust.Client.WebView.exe"; else if (OperatingSystem.IsLinux() || OperatingSystem.IsMacOS()) subProcessName = "Robust.Client.WebView"; else throw new NotSupportedException("Unsupported platform for CEF!"); var subProcessPath = Path.Combine(BasePath, subProcessName); var cefResourcesPath = LocateCefResources(); _sawmill.Debug($"Subprocess path: {subProcessPath}, resources: {cefResourcesPath}"); // System.Console.WriteLine(AppContext.GetData("NATIVE_DLL_SEARCH_DIRECTORIES")); if (cefResourcesPath == null) throw new InvalidOperationException("Unable to locate cef_resources directory!"); #endif var remoteDebugPort = _cfg.GetCVar(WCVars.WebRemoteDebugPort); var cachePath = FindAndLockCacheDirectory(); #if MACOS NativeLibrary.SetDllImportResolver(typeof(CefSettings).Assembly, (name, assembly, path) => { if (name == "libcef") { var libPath = PathHelpers.ExecutableRelativeFile("../Frameworks/Chromium Embedded Framework.framework/Chromium Embedded Framework"); return NativeLibrary.Load(libPath, assembly, path); } return 0; }); // Needed to implement CefAppProtocol on our NSApplication. NativeLibrary.Load("robust_native_webview", typeof(WebViewManagerCef).Assembly, null); #endif var settings = new CefSettings() { WindowlessRenderingEnabled = true, // So we can render to our UI controls. ExternalMessagePump = true, NoSandbox = true, // Not disabling the sandbox crashes CEF. #if !MACOS BrowserSubprocessPath = subProcessPath, LocalesDirPath = Path.Combine(cefResourcesPath, "locales"), ResourcesDirPath = cefResourcesPath, #endif RemoteDebuggingPort = remoteDebugPort, CookieableSchemesList = "usr,res", CachePath = cachePath, }; var userAgentOverride = _cfg.GetCVar(WCVars.WebUserAgentOverride); if (!string.IsNullOrEmpty(userAgentOverride)) { settings.UserAgent = userAgentOverride; } _sawmill.Info($"CEF Build Version: {CefRuntime.ChromeBuildVersion}"); _app = new RobustCefApp(_sawmill); var process = Process.GetCurrentProcess(); Environment.SetEnvironmentVariable("ROBUST_CEF_BROWSER_PROCESS_ID", process.Id.ToString()); Environment.SetEnvironmentVariable("ROBUST_CEF_BROWSER_PROCESS_MODULE", process.MainModule?.FileName ?? ""); // So these arguments look like nonsense, but it turns out CEF is just *like that*. // The first argument is literally nonsense, but it needs to be there as otherwise the second argument doesn't apply // The second argument turns off CEF's bullshit error handling, which breaks dotnet's error handling. CefRuntime.Initialize(new CefMainArgs(new string[]{"binary","--disable-in-process-stack-traces"}), settings, _app, IntPtr.Zero); _sawmill.Info($"CEF Runtime Version: {CefRuntime.ChromeRuntimeVersion}"); if (_cfg.GetCVar(WCVars.WebResProtocol)) { var handler = new ResourceSchemeFactoryHandler( this, _resourceManager, _logManager.GetSawmill("web.res")); CefRuntime.RegisterSchemeHandlerFactory("res", "", handler); } } private static string? LocateCefResources() { if (ProbeDir(BasePath, out var path)) return path; foreach (var searchDir in NativeDllSearchDirectories()) { if (ProbeDir(searchDir, out path)) return path; } return null; static bool ProbeDir(string dir, out string path) { path = Path.Combine(dir, "cef_resources"); return Directory.Exists(path); } } internal static string[] NativeDllSearchDirectories() { var sepChar = OperatingSystem.IsWindows() ? ';' : ':'; var searchDirectories = ((string)AppContext.GetData("NATIVE_DLL_SEARCH_DIRECTORIES")!) .Split(sepChar, StringSplitOptions.RemoveEmptyEntries); return searchDirectories; } public void Update() { // Calling this makes CEF do its work, without using its own update loop. CefRuntime.DoMessageLoopWork(); } public void Shutdown() { foreach (var control in _activeControls.ToArray()) { control.CloseBrowser(); } foreach (var window in _browserWindows.ToArray()) { window.Dispose(); } CefRuntime.Shutdown(); } private sealed class ResourceSchemeFactoryHandler : CefSchemeHandlerFactory { private readonly WebViewManagerCef _parent; private readonly IResourceManager _resourceManager; private readonly ISawmill _sawmill; public ResourceSchemeFactoryHandler( WebViewManagerCef parent, IResourceManager resourceManager, ISawmill sawmill) { _parent = parent; _resourceManager = resourceManager; _sawmill = sawmill; } protected override CefResourceHandler Create( CefBrowser browser, CefFrame frame, string schemeName, CefRequest request) { var uri = new Uri(request.Url); _sawmill.Debug($"HANDLING: {request.Url}"); var resPath = new ResPath(uri.AbsolutePath); if (_resourceManager.TryContentFileRead(resPath, out var stream)) { if (!_parent.TryGetResourceMimeType(resPath.Extension, out var mime)) mime = "application/octet-stream"; return new RequestResultStream(stream, mime, HttpStatusCode.OK).MakeHandler(); } var notFoundStream = new MemoryStream(); notFoundStream.Write(Encoding.UTF8.GetBytes("Not found")); notFoundStream.Position = 0; return new RequestResultStream(notFoundStream, "text/plain", HttpStatusCode.NotFound).MakeHandler(); } } } }