ExecutionReporter.cpp (12760B)
1 // Copyright (c) Microsoft Corporation. 2 // Licensed under the MIT License. 3 #include "pch.h" 4 #include "ExecutionReporter.h" 5 #include <AppInstallerErrors.h> 6 7 8 namespace AppInstaller::CLI::Execution 9 { 10 using namespace Settings; 11 using namespace VirtualTerminal; 12 13 const Sequence& HelpCommandEmphasis = TextFormat::Foreground::Bright; 14 const Sequence& HelpArgumentEmphasis = TextFormat::Foreground::Bright; 15 const Sequence& ManifestInfoEmphasis = TextFormat::Foreground::Bright; 16 const Sequence& SourceInfoEmphasis = TextFormat::Foreground::Bright; 17 const Sequence& NameEmphasis = TextFormat::Foreground::BrightCyan; 18 const Sequence& IdEmphasis = TextFormat::Foreground::BrightCyan; 19 const Sequence& UrlEmphasis = TextFormat::Foreground::BrightBlue; 20 const Sequence& PromptEmphasis = TextFormat::Foreground::Bright; 21 const Sequence& ConvertToUpgradeFlowEmphasis = TextFormat::Foreground::BrightYellow; 22 const Sequence& ConfigurationIntentEmphasis = TextFormat::Foreground::Bright; 23 const Sequence& ConfigurationUnitEmphasis = TextFormat::Foreground::BrightCyan; 24 const Sequence& AuthenticationEmphasis = TextFormat::Foreground::BrightYellow; 25 26 namespace 27 { 28 DWORD GetStdHandleType(DWORD stdHandle) 29 { 30 DWORD result = FILE_TYPE_UNKNOWN; 31 32 HANDLE handle = GetStdHandle(stdHandle); 33 if (handle != INVALID_HANDLE_VALUE && handle != NULL) 34 { 35 result = GetFileType(handle); 36 } 37 38 return result; 39 } 40 } 41 42 Reporter::Reporter() : 43 Reporter(std::cout, std::cin) 44 { 45 m_outStreamFileType = GetStdHandleType(STD_OUTPUT_HANDLE); 46 m_inStreamFileType = GetStdHandleType(STD_INPUT_HANDLE); 47 } 48 49 Reporter::Reporter(std::ostream& outStream, std::istream& inStream) : 50 Reporter(std::make_shared<BaseStream>(outStream, true, ConsoleModeRestore::Instance().IsVTEnabled()), inStream) 51 { 52 SetProgressSink(this); 53 } 54 55 Reporter::Reporter(std::shared_ptr<BaseStream> outStream, std::istream& inStream) : 56 m_out(outStream), 57 m_in(inStream) 58 { 59 auto sixelSupported = [&]() { return SixelsSupported(); }; 60 m_spinner = IIndefiniteSpinner::CreateForStyle(*m_out, ConsoleModeRestore::Instance().IsVTEnabled(), VisualStyle::Accent, sixelSupported); 61 m_progressBar = IProgressBar::CreateForStyle(*m_out, ConsoleModeRestore::Instance().IsVTEnabled(), VisualStyle::Accent, sixelSupported); 62 63 SetProgressSink(this); 64 } 65 66 Reporter::~Reporter() 67 { 68 this->CloseOutputStream(); 69 } 70 71 Reporter::Reporter(const Reporter& other, clone_t) : 72 Reporter(other.m_out, other.m_in) 73 { 74 m_outStreamFileType = other.m_outStreamFileType; 75 m_inStreamFileType = other.m_inStreamFileType; 76 77 SetChannel(other.m_channel); 78 79 if (other.m_style.has_value()) 80 { 81 SetStyle(*other.m_style); 82 } 83 } 84 85 std::optional<PrimaryDeviceAttributes> Reporter::GetPrimaryDeviceAttributes() 86 { 87 if (ConsoleModeRestore::Instance().IsVTEnabled()) 88 { 89 return PrimaryDeviceAttributes{ m_out->Get(), m_in }; 90 } 91 else 92 { 93 return std::nullopt; 94 } 95 } 96 97 OutputStream Reporter::GetOutputStream(Level level) 98 { 99 // If the level is not enabled, return a default stream which is disabled 100 if (WI_AreAllFlagsClear(m_enabledLevels, level)) 101 { 102 return OutputStream(*m_out, false, false); 103 } 104 105 OutputStream result = GetBasicOutputStream(); 106 107 switch (level) 108 { 109 case Level::Verbose: 110 result.AddFormat(TextFormat::Default); 111 break; 112 case Level::Info: 113 result.AddFormat(TextFormat::Default); 114 break; 115 case Level::Warning: 116 result.AddFormat(TextFormat::Foreground::BrightYellow); 117 break; 118 case Level::Error: 119 result.AddFormat(TextFormat::Foreground::BrightRed); 120 break; 121 default: 122 THROW_HR(E_UNEXPECTED); 123 } 124 125 return result; 126 } 127 128 OutputStream Reporter::GetBasicOutputStream() 129 { 130 return { *m_out, m_channel == Channel::Output }; 131 } 132 133 void Reporter::SetChannel(Channel channel) 134 { 135 m_channel = channel; 136 137 if (m_channel != Channel::Output) 138 { 139 // Disable progress for non-output channels 140 m_spinner.reset(); 141 m_progressBar.reset(); 142 } 143 } 144 145 void Reporter::SetStyle(VisualStyle style) 146 { 147 m_style = style; 148 149 if (m_channel == Channel::Output) 150 { 151 auto sixelSupported = [&]() { return SixelsSupported(); }; 152 m_spinner = IIndefiniteSpinner::CreateForStyle(*m_out, ConsoleModeRestore::Instance().IsVTEnabled(), style, sixelSupported); 153 m_progressBar = IProgressBar::CreateForStyle(*m_out, ConsoleModeRestore::Instance().IsVTEnabled(), style, sixelSupported); 154 } 155 156 if (style == VisualStyle::NoVT) 157 { 158 m_out->SetVTEnabled(false); 159 } 160 } 161 162 std::istream& Reporter::RawInputStream() 163 { 164 return m_in; 165 } 166 167 bool Reporter::InputStreamIsInteractive() const 168 { 169 AICLI_LOG(CLI, Verbose, << "Reporter::m_inStreamFileType is " << m_inStreamFileType); 170 return m_inStreamFileType == FILE_TYPE_CHAR; 171 } 172 173 bool Reporter::PromptForBoolResponse(Resource::LocString message, Level level, bool resultIfDisabled) 174 { 175 auto out = GetOutputStream(level); 176 177 if (!out.IsEnabled()) 178 { 179 return resultIfDisabled; 180 } 181 182 const std::vector<BoolPromptOption> options 183 { 184 BoolPromptOption{ Resource::String::PromptOptionYes, 'Y', true }, 185 BoolPromptOption{ Resource::String::PromptOptionNo, 'N', false }, 186 }; 187 188 out << message << std::endl; 189 190 // Try prompting until we get a recognized option 191 for (;;) 192 { 193 // Output all options 194 for (size_t i = 0; i < options.size(); ++i) 195 { 196 out << PromptEmphasis << "[" + options[i].Hotkey.get() + "] " + options[i].Label.get(); 197 198 if (i + 1 == options.size()) 199 { 200 out << PromptEmphasis << ": "; 201 } 202 else 203 { 204 out << " "; 205 } 206 } 207 208 // Read the response 209 std::string response; 210 if (!std::getline(m_in, response)) 211 { 212 THROW_HR(APPINSTALLER_CLI_ERROR_PROMPT_INPUT_ERROR); 213 } 214 215 // Find the matching option ignoring whitespace 216 Utility::Trim(response); 217 for (const auto& option : options) 218 { 219 if (Utility::CaseInsensitiveEquals(response, option.Label) || 220 Utility::CaseInsensitiveEquals(response, option.Hotkey)) 221 { 222 return option.Value; 223 } 224 } 225 } 226 } 227 228 void Reporter::PromptForEnter(Level level) 229 { 230 auto out = GetOutputStream(level); 231 if (!out.IsEnabled()) 232 { 233 return; 234 } 235 236 out << std::endl << Resource::String::PressEnterToContinue << std::endl; 237 m_in.get(); 238 } 239 240 std::filesystem::path Reporter::PromptForPath(Resource::LocString message, Level level, std::filesystem::path resultIfDisabled) 241 { 242 auto out = GetOutputStream(level); 243 244 if (!out.IsEnabled()) 245 { 246 return resultIfDisabled; 247 } 248 249 // Try prompting until we get a valid answer 250 for (;;) 251 { 252 out << message << ' '; 253 254 // Read the response 255 std::string response; 256 if (!std::getline(m_in, response)) 257 { 258 THROW_HR(APPINSTALLER_CLI_ERROR_PROMPT_INPUT_ERROR); 259 } 260 261 // Validate the path 262 std::filesystem::path path{ response }; 263 if (path.is_absolute()) 264 { 265 return path; 266 } 267 } 268 269 } 270 271 void Reporter::ShowIndefiniteProgress(bool running) 272 { 273 if (m_spinner) 274 { 275 if (running) 276 { 277 m_spinner->ShowSpinner(); 278 } 279 else 280 { 281 m_spinner->StopSpinner(); 282 } 283 } 284 } 285 286 void Reporter::OnProgress(uint64_t current, uint64_t maximum, ProgressType type) 287 { 288 ShowIndefiniteProgress(false); 289 if (m_progressBar) 290 { 291 m_progressBar->ShowProgress(current, maximum, type); 292 } 293 } 294 295 void Reporter::SetProgressMessage(std::string_view message) 296 { 297 if (m_spinner) 298 { 299 m_spinner->SetMessage(message); 300 } 301 } 302 303 void Reporter::BeginProgress() 304 { 305 GetBasicOutputStream() << VirtualTerminal::Cursor::Visibility::DisableShow; 306 ShowIndefiniteProgress(true); 307 }; 308 309 void Reporter::EndProgress(bool hideProgressWhenDone) 310 { 311 ShowIndefiniteProgress(false); 312 if (m_progressBar) 313 { 314 m_progressBar->EndProgress(hideProgressWhenDone); 315 } 316 SetProgressMessage({}); 317 GetBasicOutputStream() << VirtualTerminal::Cursor::Visibility::EnableShow; 318 }; 319 320 Reporter::AsyncProgressScope::AsyncProgressScope(Reporter& reporter, IProgressSink* sink, bool hideProgressWhenDone) : 321 m_reporter(reporter), m_callback(sink) 322 { 323 reporter.SetProgressCallback(&m_callback); 324 sink->BeginProgress(); 325 m_hideProgressWhenDone = hideProgressWhenDone; 326 } 327 328 Reporter::AsyncProgressScope::~AsyncProgressScope() 329 { 330 m_reporter.get().SetProgressCallback(nullptr); 331 m_callback.GetSink()->EndProgress(m_hideProgressWhenDone); 332 } 333 334 ProgressCallback& Reporter::AsyncProgressScope::Callback() 335 { 336 return m_callback; 337 } 338 339 IProgressCallback* Reporter::AsyncProgressScope::operator->() 340 { 341 return &m_callback; 342 } 343 344 bool Reporter::AsyncProgressScope::HideProgressWhenDone() const 345 { 346 return m_hideProgressWhenDone; 347 } 348 349 void Reporter::AsyncProgressScope::HideProgressWhenDone(bool value) 350 { 351 m_hideProgressWhenDone.store(value); 352 } 353 354 std::unique_ptr<Reporter::AsyncProgressScope> Reporter::BeginAsyncProgress(bool hideProgressWhenDone) 355 { 356 return std::make_unique<AsyncProgressScope>(*this, m_progressSink.load(), hideProgressWhenDone); 357 } 358 359 void Reporter::SetProgressCallback(ProgressCallback* callback) 360 { 361 auto lock = m_progressCallbackLock.lock_exclusive(); 362 // Attempting two progress operations at the same time; not supported. 363 THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), m_progressCallback != nullptr && callback != nullptr); 364 m_progressCallback = callback; 365 } 366 367 void Reporter::CancelInProgressTask(bool force, CancelReason reason) 368 { 369 // TODO: Maybe ask the user if they really want to cancel? 370 UNREFERENCED_PARAMETER(force); 371 auto lock = m_progressCallbackLock.lock_shared(); 372 ProgressCallback* callback = m_progressCallback.load(); 373 if (callback) 374 { 375 if (!callback->IsCancelledBy(CancelReason::Any)) 376 { 377 callback->SetProgressMessage(Resource::String::CancellingOperation()); 378 callback->Cancel(reason); 379 } 380 } 381 } 382 383 void Reporter::CloseOutputStream(bool forceDisable) 384 { 385 if (forceDisable) 386 { 387 m_out->Disable(); 388 } 389 m_out->RestoreDefault(); 390 } 391 392 void Reporter::SetLevelMask(Level reporterLevel, bool setEnabled) { 393 394 if (setEnabled) 395 { 396 WI_SetAllFlags(m_enabledLevels, reporterLevel); 397 } 398 else 399 { 400 WI_ClearAllFlags(m_enabledLevels, reporterLevel); 401 } 402 } 403 404 bool Reporter::SixelsSupported() 405 { 406 auto attributes = GetPrimaryDeviceAttributes(); 407 return (attributes ? attributes->Supports(PrimaryDeviceAttributes::Extension::Sixel) : false); 408 } 409 410 bool Reporter::SixelsEnabled() 411 { 412 return Settings::User().Get<Settings::Setting::EnableSixelDisplay>() && SixelsSupported(); 413 } 414 }