WorkerDemoForm: gore dugačak posao, dole terminal. Obe strane rade istovremeno i UI ostaje živ.
powershell i kucaj git --version, dir, Get-Process.async void Click handlerawait Task.Run(...)BeginInvokee.Cancel = true, zatraži prekid, zatvori kad se posao uredno završiU Program.cs: Application.Run(new WorkerDemoForm()); (stari red sa Form1 je zakomentarisan).
WorkerDemoForm.cs
using System.Diagnostics;
namespace WinFormsApp_test
{
/// <summary>
/// Gore: dugačak posao u pozadini (progress, log, pauza, prekid).
/// Dole: živ cmd/powershell — komande idu u proces, izlaz se vraća u prozor, dok posao gore radi.
///
/// Pravilo: UI nit SAMO crta i reaguje na klik. Sve teško ide u Task.Run.
/// Pozadina → UI ide na dva načina:
/// 1) česte stvari (log, progres): pozadina upisuje u red/brojač, uiTimer (100 ms) prikazuje
/// 2) retke stvari (shell ugašen): BeginInvoke
/// </summary>
public partial class WorkerDemoForm : Form
{
private readonly UiLogSink _jobLog;
private readonly UiLogSink _termLog;
private readonly Stopwatch _elapsed = new();
private BatchJob? _job;
private CancellationTokenSource? _cts;
private bool _closeWhenJobEnds;
private ShellSession? _shell;
private readonly List<string> _history = new();
private int _historyIndex;
public WorkerDemoForm()
{
InitializeComponent();
_jobLog = new UiLogSink(txtJobLog);
_termLog = new UiLogSink(txtTermOut);
cmbShell.SelectedIndex = 0;
numParallel.Value = Math.Min(Environment.ProcessorCount, (int)numParallel.Maximum);
}
// ───────────────────────── Dugačak posao ─────────────────────────
private async void btnStart_Click(object sender, EventArgs e)
{
_job = new BatchJob((int)numCount.Value);
_job.Log += _jobLog.Write; // Log stiže sa pozadinskih niti → samo u red, bez Invoke
_cts = new CancellationTokenSource();
int parallelism = (int)numParallel.Value;
SetJobRunning(true);
_jobLog.Info($"START {_job.Total:N0} dokumenata, paralelno {parallelism}");
_elapsed.Restart();
try
{
// Task.Run = posao ide na thread pool. await = UI nit je slobodna dok se ne završi,
// a kod POSLE await-a se opet izvršava na UI niti (zato smemo da diramo kontrole u finally).
await Task.Run(() => _job.RunAsync(parallelism, _cts.Token));
_jobLog.Info($"GOTOVO ok: {_job.Done:N0}, greške: {_job.Failed:N0}, vreme: {_elapsed.Elapsed:mm\\:ss\\.f}");
}
catch (OperationCanceledException)
{
_jobLog.Info($"PREKINUTO posle {_job.Processed:N0} dokumenata");
}
catch (Exception ex)
{
_jobLog.Info("NEOČEKIVANA GREŠKA: " + ex);
}
finally
{
_elapsed.Stop();
_cts.Dispose();
_cts = null;
UpdateProgress();
SetJobRunning(false);
}
if (_closeWhenJobEnds) Close();
}
private void btnPause_Click(object sender, EventArgs e)
{
if (_job == null) return;
if (_job.IsPaused)
{
_job.Resume();
_elapsed.Start();
_jobLog.Info("NASTAVAK");
}
else
{
_job.Pause();
_elapsed.Stop();
_jobLog.Info("PAUZA (radnici završavaju dokument na kome su)");
}
btnPause.Text = _job.IsPaused ? "Nastavi" : "Pauza";
}
private void btnCancel_Click(object sender, EventArgs e)
{
_cts?.Cancel(); // GUI → posao: token javlja svim radnicima da stanu
btnCancel.Enabled = false;
}
private void SetJobRunning(bool running)
{
btnStart.Enabled = !running;
btnBlockUi.Enabled = !running;
numCount.Enabled = !running;
numParallel.Enabled = !running;
btnPause.Enabled = running;
btnCancel.Enabled = running;
btnPause.Text = "Pauza";
}
private void UpdateProgress()
{
if (_job == null) return;
int processed = _job.Processed;
progressBar.Maximum = _job.Total;
progressBar.Value = Math.Min(processed, _job.Total);
double seconds = _elapsed.Elapsed.TotalSeconds;
double rate = seconds > 0 ? processed / seconds : 0;
string eta = rate > 0 ? TimeSpan.FromSeconds((_job.Total - processed) / rate).ToString(@"hh\:mm\:ss") : "--:--:--";
lblStatus.Text = $"{processed:N0} / {_job.Total:N0} ({100.0 * processed / _job.Total:F1}%) " +
$"greške: {_job.Failed:N0} {rate:N0} dok/s proteklo {_elapsed.Elapsed:hh\\:mm\\:ss} ETA {eta}" +
(_job.IsPaused ? " [PAUZA]" : "");
}
/// <summary>
/// Ovako NE: posao direktno u Click handleru. Prozor se zamrzne ("Not responding"),
/// progress bar i log se ne osvežavaju, terminal dole ne reaguje — sve dok se ne završi.
/// </summary>
private void btnBlockUi_Click(object sender, EventArgs e)
{
progressBar.Maximum = 50;
for (int i = 1; i <= 50; i++)
{
Thread.Sleep(100); // "obrada" na UI niti
progressBar.Value = i; // ne vidi se — UI nit nema kad da crta
_jobLog.Info($"UI nit radi korak {i}/50");
}
lblStatus.Text = "UI nit je bila blokirana 5 s.";
}
// ───────────────────────── Terminal ─────────────────────────
private void btnShellStart_Click(object sender, EventArgs e)
{
_shell?.Dispose();
string shellName = (string)cmbShell.SelectedItem!;
string workDir = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "source", "repos");
if (!Directory.Exists(workDir)) workDir = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
try
{
_shell = ShellSession.Start(shellName, workDir);
}
catch (Exception ex)
{
_termLog.Write($"!! {shellName} ne može da se pokrene: {ex.Message}");
return;
}
ShellSession shell = _shell;
shell.Output += _termLog.Write; // proces → GUI: ide u red, uiTimer prikazuje
shell.Exited += code => // retko → može direktno BeginInvoke
{
if (IsDisposed || !IsHandleCreated) return;
BeginInvoke(() =>
{
_termLog.Write($"── {shell.ShellName} završen (exit {code}) ──");
if (_shell == shell) SetShellRunning(false);
});
};
_termLog.Write($"── {shellName} pokrenut u {workDir} ──");
SetShellRunning(true);
txtInput.Focus();
}
private void btnShellKill_Click(object sender, EventArgs e)
{
_shell?.Kill(); // ubija i sve što je shell pokrenuo (npr. "ping -t")
}
private void txtInput_KeyDown(object sender, KeyEventArgs e)
{
switch (e.KeyCode)
{
case Keys.Enter:
e.SuppressKeyPress = true; // bez "ding" zvuka
SendCommand(txtInput.Text);
break;
case Keys.Up when _history.Count > 0:
e.SuppressKeyPress = true;
_historyIndex = Math.Max(0, _historyIndex - 1);
ShowHistory();
break;
case Keys.Down when _history.Count > 0:
e.SuppressKeyPress = true;
_historyIndex = Math.Min(_history.Count, _historyIndex + 1);
ShowHistory();
break;
}
}
private void SendCommand(string command)
{
if (_shell is not { IsRunning: true })
{
_termLog.Write("!! shell nije pokrenut — klikni 'Pokreni'");
return;
}
_termLog.Write("> " + command);
_shell.Send(command); // GUI → proces: upis u stdin
if (command.Length > 0 && (_history.Count == 0 || _history[^1] != command))
_history.Add(command);
_historyIndex = _history.Count;
txtInput.Clear();
}
private void ShowHistory()
{
txtInput.Text = _historyIndex < _history.Count ? _history[_historyIndex] : "";
txtInput.SelectionStart = txtInput.TextLength;
}
private void SetShellRunning(bool running)
{
btnShellKill.Enabled = running;
cmbShell.Enabled = !running;
btnShellStart.Text = running ? "Restart" : "Pokreni";
lblShellState.Text = running ? $"{_shell?.ShellName} radi" : "nije pokrenut";
}
// ───────────────────────── Zajedničko ─────────────────────────
/// <summary>Jedino mesto gde se log i progres crtaju — 10 puta u sekundi, koliko god da posao radi.</summary>
private void uiTimer_Tick(object sender, EventArgs e)
{
_jobLog.Flush();
_termLog.Flush();
if (_cts != null) UpdateProgress();
}
private void WorkerDemoForm_FormClosing(object sender, FormClosingEventArgs e)
{
// Posao još radi → zatraži prekid, zatvori prozor tek kad se posao uredno završi
if (_cts != null)
{
e.Cancel = true;
_closeWhenJobEnds = true;
_job?.Resume();
_cts.Cancel();
return;
}
_shell?.Dispose();
}
}
}WorkerDemoForm.Designer.cs je standardan dizajnerski fajl (SplitContainer, dva FlowLayoutPanel-a, ProgressBar, TextBox-ovi, Timer na 100 ms) i otvara se u Visual Studio dizajneru.