Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions com.unity.mobile.android-logcat/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.

### Changes & Improvements:
- Unity 6.0 or later is required.
- Added a Commands window, accessible via the Tools menu in the Android Logcat window. It lets you save, organize, search and run adb commands which don't have a dedicated UI in this package, such as package management, permissions, dumpsys, settings and Meta Quest properties. Commands run asynchronously so the Editor stays responsive, support `<placeholder>` substitution, and can be imported and exported as JSON.

## [1.4.7] - 2025-12-12
### Fixes & Improvements
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -338,6 +338,9 @@ private void MenuToolsSelection(object userData, string[] options, int selected)
case ToolsContextMenu.LayoutViewer:
AndroidLogcatLayoutViewerWindow.ShowWindow();
break;
case ToolsContextMenu.Commands:
AndroidLogcatCommandsWindow.ShowWindow();
break;
case ToolsContextMenu.WindowMemory:
m_Runtime.UserSettings.ExtraWindowState.Type = ExtraWindow.Memory;
break;
Expand All @@ -363,6 +366,7 @@ private void DoToolsGUI()
contextMenu.Add(ToolsContextMenu.StacktraceUtility, "Stacktrace Utility");
if (Unsupported.IsDeveloperMode())
contextMenu.Add(ToolsContextMenu.LayoutViewer, "Experimental/Layout Viewer");
contextMenu.Add(ToolsContextMenu.Commands, "Commands");
var b = m_Runtime.UserSettings.ExtraWindowState.Type;
contextMenu.Add(ToolsContextMenu.WindowMemory, "Window/Memory", b == ExtraWindow.Memory);
contextMenu.Add(ToolsContextMenu.WindowInputs, "Window/Inputs", b == ExtraWindow.Inputs);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ internal enum ToolsContextMenu
OpenTerminal,
StacktraceUtility,
LayoutViewer,
Commands,
WindowMemory,
WindowInputs,
WindowHidden
Expand Down
52 changes: 52 additions & 0 deletions com.unity.mobile.android-logcat/Editor/AndroidLogcatDevice.cs
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,16 @@ internal virtual void UninstallPackage(string packageName) { }
internal virtual void KillProcess(int processId, PosixSignal signal = PosixSignal.SIGNONE) { }
internal virtual void KillProcess(string packageName, int processId, PosixSignal signal = PosixSignal.SIGNONE) { }

/// <summary>
/// Runs an arbitrary adb command against this device asynchronously.
/// The device is targeted with -s automatically unless the caller already specified one.
/// <paramref name="onComplete"/> is invoked on the main thread.
/// </summary>
internal virtual void RunAdbCommandAsync(AndroidLogcatDispatcher dispatcher, string arguments, Action<AndroidLogcatCommandResult> onComplete)
{
onComplete?.Invoke(AndroidLogcatCommandResult.CreateFailure(arguments, "No device selected."));
}

internal bool SupportsFilteringByPid
{
get { return OSVersion >= kAndroidVersion70; }
Expand Down Expand Up @@ -471,6 +481,48 @@ internal override void UninstallPackage(string packageName)
m_ADB.Run(args, $"Failed to uninstall package '{packageName}'");
}

/// <summary>
/// Runs an arbitrary adb command against this device on the dispatcher's worker thread, so a
/// slow or hanging command cannot block the Editor's main thread.
/// </summary>
internal override void RunAdbCommandAsync(AndroidLogcatDispatcher dispatcher, string arguments, Action<AndroidLogcatCommandResult> onComplete)
{
if (dispatcher == null)
{
onComplete?.Invoke(AndroidLogcatCommandResult.CreateFailure(arguments, "Dispatcher is not available."));
return;
}

// Target this device unless the caller already picked one explicitly.
var effectiveArgs = AndroidLogcatCommandParser.SpecifiesDevice(arguments)
? arguments
: $"-s {Id} {arguments}";

dispatcher.Schedule(
new AndroidLogcatTaskInput<AndroidBridge.ADB, string, string>()
{
data1 = m_ADB,
data2 = effectiveArgs,
data3 = arguments
},
(input) =>
{
var inputData = (AndroidLogcatTaskInput<AndroidBridge.ADB, string, string>)input;
AndroidLogcatInternalLog.Log($"adb {inputData.data2}");
try
{
var output = inputData.data1.Run(new[] { inputData.data2 }, $"Failed to run 'adb {inputData.data3}'");
return AndroidLogcatCommandResult.CreateSuccess(inputData.data3, output);
}
catch (Exception ex)
{
return AndroidLogcatCommandResult.CreateFailure(inputData.data3, AndroidLogcatCommandResult.Unwrap(ex).Message);
}
},
(result) => onComplete?.Invoke((AndroidLogcatCommandResult)result),
false);
}

internal override void KillProcess(int processId, PosixSignal signal = PosixSignal.SIGNONE)
{
var packageName = AndroidLogcatUtilities.GetProcessNameFromPid(m_ADB, this, processId);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,15 @@ internal class QueryLayoutSettings
internal string LastScreenshotSaveLocation;
}

[Serializable]
internal class CommandsSettingsData
{
[SerializeField]
internal List<AndroidLogcatCommandEntry> Favorites = new List<AndroidLogcatCommandEntry>();
[SerializeField]
internal List<AndroidLogcatCommandEntry> GeneralCommands = new List<AndroidLogcatCommandEntry>();
}

[SerializeField]
private string m_SelectedDeviceId;
[SerializeField]
Expand Down Expand Up @@ -116,6 +125,8 @@ internal class QueryLayoutSettings
private QueryLayoutSettings m_QueryLayoutSettings;
[SerializeField]
private InputSettings m_InputSettings;
[SerializeField]
private CommandsSettingsData m_CommandsSettings;

[SerializeField]
private AutoScroll m_AutoScroll;
Expand Down Expand Up @@ -178,6 +189,7 @@ public Priority SelectedPriority
public ScreenCaptureSettings CaptureSettings { set => m_ScreenCaptureSettings = value; get => m_ScreenCaptureSettings; }
public QueryLayoutSettings LayoutSettings { set => m_QueryLayoutSettings = value; get => m_QueryLayoutSettings; }
public InputSettings DeviceInputSettings { set => m_InputSettings = value; get => m_InputSettings; }
public CommandsSettingsData CommandsSettings { set => m_CommandsSettings = value; get => m_CommandsSettings; }

public AutoScroll AutoScroll { set => m_AutoScroll = value; get => m_AutoScroll; }

Expand Down Expand Up @@ -360,6 +372,8 @@ internal void Reset()
SendText = string.Empty,
TargetProcess = new ProcessInformation()
};

m_CommandsSettings = new CommandsSettingsData();
}

internal void ResetCaptureVideoSettings()
Expand Down
71 changes: 66 additions & 5 deletions com.unity.mobile.android-logcat/Editor/AndroidTools/Shell.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System;
using System.Diagnostics;
using System.Text;
using System.Threading;

namespace Unity.Android.Logcat
{
Expand Down Expand Up @@ -66,7 +67,21 @@ internal static ShellReturnInfo RunProcess(string fileName, string arguments, st
return RunProcess(new ShellStartInfo() { FileName = fileName, Arguments = arguments, WorkingDirectory = workingDirectory });
}

/// <summary>
/// Runs a process, killing it if it doesn't finish within <paramref name="timeoutMs"/>.
/// Use this for commands supplied by the user, which may never exit on their own.
/// </summary>
internal static ShellReturnInfo RunProcess(string fileName, string arguments, int timeoutMs, out bool timedOut)
{
return RunProcess(new ShellStartInfo() { FileName = fileName, Arguments = arguments }, timeoutMs, out timedOut);
}

internal static ShellReturnInfo RunProcess(ShellStartInfo startInfo)
{
return RunProcess(startInfo, Timeout.Infinite, out _);
}

internal static ShellReturnInfo RunProcess(ShellStartInfo startInfo, int timeoutMs, out bool timedOut)
{
Process process = new Process();
process.StartInfo.FileName = startInfo.FileName;
Expand All @@ -76,12 +91,16 @@ internal static ShellReturnInfo RunProcess(ShellStartInfo startInfo)
process.StartInfo.RedirectStandardOutput = true;
process.StartInfo.RedirectStandardError = true;
process.StartInfo.CreateNoWindow = true;
// The data received handlers are invoked on threadpool threads, so all access to these
// builders is guarded by a lock - StringBuilder is not thread safe.
var streamLock = new object();
var output = new StringBuilder();
process.OutputDataReceived += new DataReceivedEventHandler((sender, e) =>
{
if (!string.IsNullOrEmpty(e.Data))
{
output.AppendLine(e.Data);
lock (streamLock)
output.AppendLine(e.Data);
}
});

Expand All @@ -90,18 +109,60 @@ internal static ShellReturnInfo RunProcess(ShellStartInfo startInfo)
{
if (!string.IsNullOrEmpty(e.Data))
{
error.AppendLine(e.Data);
lock (streamLock)
error.AppendLine(e.Data);
}
});

process.Start();
process.BeginOutputReadLine();
process.BeginErrorReadLine();
process.WaitForExit();
var exitCode = process.ExitCode;

timedOut = false;
if (timeoutMs == Timeout.Infinite)
{
process.WaitForExit();
}
else if (process.WaitForExit(timeoutMs))
{
// WaitForExit(int) only waits for the process to exit, not for the asynchronous
// output handlers to drain. The parameterless overload does, and returns immediately
// here because the process has already exited.
process.WaitForExit();
}
else
{
timedOut = true;
try
{
process.Kill();
// Give the process a moment to actually die so we can read its exit code.
process.WaitForExit(1000);
}
catch (Exception)
{
// Kill can fail if the process exited between the timeout and this call, or if
// the OS denies access. Either way there's nothing useful to do about it.
}

// Stop the readers rather than waiting for them, so a process that could not be
// killed doesn't block this thread indefinitely.
try
{
process.CancelOutputRead();
process.CancelErrorRead();
}
catch (InvalidOperationException)
{
// Reads were already stopped.
}
}

var exitCode = process.HasExited ? process.ExitCode : -1;
process.Close();

return new ShellReturnInfo(startInfo, exitCode, output.ToString(), error.ToString());
lock (streamLock)
return new ShellReturnInfo(startInfo, exitCode, output.ToString(), error.ToString());
}
}
}
8 changes: 8 additions & 0 deletions com.unity.mobile.android-logcat/Editor/Commands.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading