@@ -203,6 +203,8 @@ struct ModelInstaller::State {
203203 std::string state = " queued" ;
204204 std::string message;
205205 std::filesystem::path log_path;
206+ std::filesystem::path cancel_path;
207+ bool cancel_requested = false ;
206208 int exit_code = -1 ;
207209 int64_t started_at_ms = 0 ;
208210 int64_t finished_at_ms = 0 ;
@@ -247,7 +249,7 @@ bool ModelInstaller::has_active_jobs() const {
247249 std::lock_guard<std::mutex> lock (state_->mutex );
248250 for (const auto & [id, job] : state_->jobs ) {
249251 (void ) id;
250- if (job.state == " queued" || job.state == " running" ) {
252+ if (job.state == " queued" || job.state == " running" || job. state == " cancelling " ) {
251253 return true ;
252254 }
253255 }
@@ -301,37 +303,46 @@ std::string ModelInstaller::start(
301303 const auto output_file_path = resolve_optional_path (output_file);
302304
303305 const auto log_path = state_->job_root / (package_id + " .log" );
306+ const auto cancel_path = state_->job_root / (package_id + " .cancel" );
307+ {
308+ std::error_code error;
309+ std::filesystem::remove (cancel_path, error);
310+ }
304311 {
305312 std::lock_guard<std::mutex> lock (state_->mutex );
306313 const auto existing = state_->jobs .find (package_id);
307314 if (existing != state_->jobs .end () &&
308- (existing->second .state == " queued" || existing->second .state == " running" )) {
315+ (existing->second .state == " queued" || existing->second .state == " running" ||
316+ existing->second .state == " cancelling" )) {
309317 throw std::runtime_error (" installation is already running for " + package_id);
310318 }
311319 State::Job job;
312320 job.package_id = package_id;
313321 job.message = " Waiting for model preparation worker" ;
314322 job.log_path = log_path;
323+ job.cancel_path = cancel_path;
315324 state_->jobs [package_id] = std::move (job);
316325 }
317326
318327 const auto shared = state_;
319328 std::thread ([shared, package_id, source_file_path, output_file_path, source_path, variant, overwrite,
320- legacy_conversion, script, log_path]() {
329+ legacy_conversion, script, log_path, cancel_path ]() {
321330 try {
322331 {
323332 std::lock_guard<std::mutex> lock (shared->mutex );
324333 auto & job = shared->jobs .at (package_id);
325- job.state = " running" ;
326- job.message = " Downloading and preparing model files" ;
334+ if (!job.cancel_requested ) {
335+ job.state = " running" ;
336+ job.message = " Downloading and preparing model files" ;
337+ }
327338 job.started_at_ms = now_ms ();
328339 }
329340
330341 std::string command = python_command () + " -u " + shell_quote (script.string ()) +
331342 " install " + shell_quote (package_id) +
332343 " --models-root " + shell_quote (shared->models_root .string ());
333344 if (!legacy_conversion) {
334- command += " --progress" ;
345+ command += " --progress --cancel-file " + shell_quote (cancel_path. string ()) ;
335346 }
336347 if (overwrite) {
337348 command += " --overwrite" ;
@@ -356,12 +367,14 @@ std::string ModelInstaller::start(
356367 auto & job = shared->jobs .at (package_id);
357368 job.exit_code = result;
358369 job.finished_at_ms = now_ms ();
359- job.state = result == 0 ? " complete" : " failed" ;
370+ const bool cancelled = job.cancel_requested || std::filesystem::is_regular_file (cancel_path);
371+ job.state = cancelled ? " cancelled" : result == 0 ? " complete" : " failed" ;
360372 job.downloaded_bytes = log.downloaded_bytes ;
361373 job.total_bytes = log.total_bytes ;
362374 job.message = !log.message .empty ()
363375 ? log.message
364- : (result == 0 ? " Model installation completed" : " Model installation failed" );
376+ : (cancelled ? " Download stopped; completed staging files were removed"
377+ : result == 0 ? " Model installation completed" : " Model installation failed" );
365378 if (result == 0 ) {
366379 ++shared->size_generation ;
367380 shared->size_state = " idle" ;
@@ -370,8 +383,10 @@ std::string ModelInstaller::start(
370383 } catch (const std::exception & error) {
371384 std::lock_guard<std::mutex> lock (shared->mutex );
372385 auto & job = shared->jobs .at (package_id);
373- job.state = " failed" ;
374- job.message = error.what ();
386+ job.state = job.cancel_requested ? " cancelled" : " failed" ;
387+ job.message = job.cancel_requested
388+ ? " Download stopped; completed staging files were removed"
389+ : error.what ();
375390 job.exit_code = -1 ;
376391 job.finished_at_ms = now_ms ();
377392 }
@@ -386,7 +401,7 @@ std::string ModelInstaller::status(const std::string & package_id) const {
386401 std::string message = job.message ;
387402 uint64_t downloaded_bytes = job.downloaded_bytes ;
388403 uint64_t total_bytes = job.total_bytes ;
389- if (job.state == " running" ) {
404+ if (job.state == " running" || job. state == " cancelling " ) {
390405 const auto log = inspect_log (job.log_path );
391406 if (!log.message .empty ()) {
392407 message = log.message ;
@@ -438,6 +453,80 @@ std::string ModelInstaller::status(const std::string & package_id) const {
438453 return result + " ]}" ;
439454}
440455
456+ std::string ModelInstaller::stop (const std::string & package_id) {
457+ if (!valid_package_id (package_id)) {
458+ throw std::runtime_error (" invalid model-manager package id" );
459+ }
460+ {
461+ std::lock_guard<std::mutex> lock (state_->mutex );
462+ const auto found = state_->jobs .find (package_id);
463+ if (found == state_->jobs .end ()) {
464+ throw std::runtime_error (" no installation job exists for " + package_id);
465+ }
466+ auto & job = found->second ;
467+ if (job.state != " queued" && job.state != " running" && job.state != " cancelling" ) {
468+ throw std::runtime_error (" installation is not running for " + package_id);
469+ }
470+ std::ofstream marker (job.cancel_path , std::ios::binary | std::ios::trunc);
471+ if (!marker) {
472+ throw std::runtime_error (" could not create download cancellation marker" );
473+ }
474+ marker << " cancel\n " ;
475+ job.cancel_requested = true ;
476+ job.state = " cancelling" ;
477+ job.message = " Stopping download and removing its staging files" ;
478+ }
479+ return status (package_id);
480+ }
481+
482+ std::string ModelInstaller::clean_partial (const std::string & package_id) {
483+ if (!valid_package_id (package_id)) {
484+ throw std::runtime_error (" invalid model-manager package id" );
485+ }
486+ const auto script = state_->repository_root / " tools" / " model_manager_v2.py" ;
487+ if (!std::filesystem::is_regular_file (script)) {
488+ throw std::runtime_error (" model cleanup helper was not found at " + script.string ());
489+ }
490+ std::filesystem::path cancel_path;
491+ {
492+ std::lock_guard<std::mutex> lock (state_->mutex );
493+ const auto found = state_->jobs .find (package_id);
494+ if (found != state_->jobs .end () &&
495+ (found->second .state == " queued" || found->second .state == " running" ||
496+ found->second .state == " cancelling" )) {
497+ throw std::runtime_error (" stop the active installation before cleaning partial files for " + package_id);
498+ }
499+ if (found != state_->jobs .end ()) {
500+ cancel_path = found->second .cancel_path ;
501+ }
502+ }
503+
504+ const auto log_path = state_->job_root / (package_id + " -clean-partial.log" );
505+ std::string command = python_command () + " -u " + shell_quote (script.string ()) +
506+ " clean-partial " + shell_quote (package_id) +
507+ " --models-root " + shell_quote (state_->models_root .string ()) +
508+ " > " + shell_quote (log_path.string ()) + " 2>&1" ;
509+ const int result = std::system (command.c_str ());
510+ std::string message = read_log_tail_text (log_path);
511+ while (!message.empty () && std::isspace (static_cast <unsigned char >(message.back ()))) {
512+ message.pop_back ();
513+ }
514+ if (result != 0 ) {
515+ throw std::runtime_error (message.empty () ? " partial download cleanup failed" : message);
516+ }
517+ if (!cancel_path.empty ()) {
518+ std::error_code error;
519+ std::filesystem::remove (cancel_path, error);
520+ }
521+ {
522+ std::lock_guard<std::mutex> lock (state_->mutex );
523+ state_->jobs .erase (package_id);
524+ }
525+ return " {\" id\" :" + json_quote (package_id) +
526+ " ,\" cleaned\" :true,\" message\" :" +
527+ json_quote (message.empty () ? " Partial download files cleaned" : message) + " }" ;
528+ }
529+
441530std::string ModelInstaller::remove (const std::string & package_id) {
442531 if (!valid_package_id (package_id)) {
443532 throw std::runtime_error (" invalid model-manager package id" );
@@ -450,7 +539,8 @@ std::string ModelInstaller::remove(const std::string & package_id) {
450539 std::lock_guard<std::mutex> lock (state_->mutex );
451540 const auto job = state_->jobs .find (package_id);
452541 if (job != state_->jobs .end () &&
453- (job->second .state == " queued" || job->second .state == " running" )) {
542+ (job->second .state == " queued" || job->second .state == " running" ||
543+ job->second .state == " cancelling" )) {
454544 throw std::runtime_error (" installation is still running for " + package_id);
455545 }
456546 }
0 commit comments