diff --git a/lib/command/backup.rb b/lib/command/backup.rb index e1c8acc5..6b9d9095 100644 --- a/lib/command/backup.rb +++ b/lib/command/backup.rb @@ -46,7 +46,12 @@ def create_backup(data) backup_dir = novel_dir.join(BACKUP_DIR_NAME) backup_dir.mkdir unless backup_dir.exist? Zip.unicode_names = true unless Helper.os_windows? - Zip::File.open(backup_dir.join(zipfilename), Zip::File::CREATE) do |zip| + + # Windowsでのスレッド内ファイル操作対策: GCを強制実行してファイルハンドルを解放 + GC.start + sleep 0.1 + + Zip::File.open(backup_dir.join(zipfilename), create: true) do |zip| paths.each do |path| relative_path = path_to_relative(novel_dir, path).to_s zipped_filename = diff --git a/lib/command/update.rb b/lib/command/update.rb index 44b53896..2d3ebdc8 100644 --- a/lib/command/update.rb +++ b/lib/command/update.rb @@ -210,7 +210,19 @@ def execute(argv) end convert_argv = [target] convert_argv << "--no-open" if @options["no-open"] + + # WebUI: 変換メッセージを変換コンソールに出力するため、一時的に$stdout2を切り替え + original_stdout2 = $stdout2 + if $stdout2.respond_to?(:push_server) && $stdout2.respond_to?(:target_console) + # WebUIの場合、変換用の別コンソールに出力 + $stdout2 = Narou::StreamingLogger.new($stdout2.push_server, $stdout2, target_console: "convert") + end + convert_status = Convert.execute!(convert_argv) + + # $stdout2を元に戻す + $stdout2 = original_stdout2 if original_stdout2 + if convert_status > 0 # 変換が失敗したか、中断された data["_convert_failure"] = true diff --git a/lib/command/web.rb b/lib/command/web.rb index 9bac54dd..42f7a785 100644 --- a/lib/command/web.rb +++ b/lib/command/web.rb @@ -180,12 +180,12 @@ def open_browser_when_server_boot(address) def send_rebooted_event_when_connection_recover(push_server) return unless @rebooted - Thread.new do |th| + Thread.new do timeout = Time.now + 20 # WebSocketのコネクションが回復するまで待つ until push_server.connections.count != 0 sleep 0.2 - th.kill if Time.now > timeout + Thread.current.kill if Time.now > timeout end puts "再起動が完了しました。".termcolor push_server.send_all(:"server.rebooted") diff --git a/lib/device/ibunko.rb b/lib/device/ibunko.rb index 2c51ba6e..ea4eee89 100644 --- a/lib/device/ibunko.rb +++ b/lib/device/ibunko.rb @@ -53,7 +53,12 @@ def create_pure_aozora_zip zipfile_path = @converted_txt_path.sub(/.txt$/, @device.ebook_file_ext) File.delete(zipfile_path) if File.exist?(zipfile_path) - Zip::File.open(zipfile_path, Zip::File::CREATE) do |zip| + + # Windowsでのスレッド内ファイル操作対策: GCを強制実行してファイルハンドルを解放 + GC.start + sleep 0.1 + + Zip::File.open(zipfile_path, create: true) do |zip| # テキスト本体(整形済み) zip.add(File.basename(@converted_txt_path), sanitized_txt_path) { true } # 挿絵(Aozora注記のまま。画像ファイルは同梱) @@ -72,7 +77,7 @@ def create_pure_aozora_zip end end FileUtils.rm_f(sanitized_txt_path) - output_io = (respond_to?(:stream_io) ? stream_io : nil) || $stdout + output_io = $stdout2 output_io.puts File.basename(zipfile_path) + " を出力しました" output_io.puts "#{@device.display_name}用ファイルを出力しました".termcolor if Narou.economy?("cleanup_temp") && @argument_target_type == :novel @@ -98,7 +103,12 @@ def hook_convert_txt_to_ebook_file(&original_func) translate_illust_chuki_to_img_tag zipfile_path = @converted_txt_path.sub(/.txt$/, @device.ebook_file_ext) File.delete(zipfile_path) if File.exist?(zipfile_path) - Zip::File.open(zipfile_path, Zip::File::CREATE) do |zip| + + # Windowsでのスレッド内ファイル操作対策: GCを強制実行してファイルハンドルを解放 + GC.start + sleep 0.1 + + Zip::File.open(zipfile_path, create: true) do |zip| # テキスト本体 zip.add(File.basename(@converted_txt_path), @converted_txt_path) { true } # 挿絵 @@ -116,7 +126,7 @@ def hook_convert_txt_to_ebook_file(&original_func) zip.add(cover_name, File.join(dirpath, cover_name)) { true } end end - output_io = (respond_to?(:stream_io) ? stream_io : nil) || $stdout + output_io = $stdout2 output_io.puts File.basename(zipfile_path) + " を出力しました" output_io.puts "#{@device.display_name}用ファイルを出力しました".termcolor if Narou.economy?("cleanup_temp") && @argument_target_type == :novel diff --git a/lib/downloader.rb b/lib/downloader.rb index 9fb28831..909dcc4b 100644 --- a/lib/downloader.rb +++ b/lib/downloader.rb @@ -755,6 +755,7 @@ def get_toc_source toc_url = @setting["toc_url"] return nil unless toc_url max_retry = 5 + retry_count = LIMIT_TO_RETRY_NETWORK toc_source = "" cookie = @setting["cookie"] || "" open_uri_options = make_open_uri_options("Cookie" => cookie, allow_redirections: :safe) @@ -787,6 +788,28 @@ def get_toc_source else raise end + rescue OpenURI::HTTPError, Errno::ECONNRESET, Errno::ECONNABORTED, Errno::ETIMEDOUT, Net::OpenTimeout, IO::TimeoutError, SocketError => e + case e.message + when /^503/ + @stream&.error "server message: #{e.message}" + display_hint if @stream + raise SuspendDownload + when /^404/ + # 404は上位のget_latest_table_of_contentsで処理させるため、そのまま再raise + raise e + else + if retry_count == 0 + @stream&.error "上限までリトライしましたが目次がダウンロード出来ませんでした" + raise SuspendDownload + end + retry_count -= 1 + @stream&.puts <<~MSG + server message: #{e.message} + リトライ待機中... + MSG + sleep(WAIT_TIME_TO_RETRY_NETWORK) + retry + end end toc_source end @@ -834,7 +857,7 @@ def get_latest_table_of_contents(old_toc, through_error: false) "subtitles" => subtitles } toc_objects - rescue OpenURI::HTTPError, Errno::ECONNRESET, Errno::ETIMEDOUT, Net::OpenTimeout, IO::TimeoutError => e + rescue OpenURI::HTTPError, Errno::ECONNRESET, Errno::ECONNABORTED, Errno::ETIMEDOUT, Net::OpenTimeout, IO::TimeoutError, SocketError => e raise if through_error # エラー処理はしなくていいからそのまま例外を受け取りたい時用 if e.message.include?("404") @stream.error "小説が削除されているか非公開な可能性があります" @@ -1235,7 +1258,7 @@ def download_raw_data(url) URI.open(url, "r:#{@setting["encoding"]}", open_uri_options) do |fp| raw = Helper.pretreatment_source(fp.read, @setting["encoding"]) end - rescue OpenURI::HTTPError, Errno::ECONNRESET, Errno::ETIMEDOUT, Net::OpenTimeout, IO::TimeoutError => e + rescue OpenURI::HTTPError, Errno::ECONNRESET, Errno::ECONNABORTED, Errno::ETIMEDOUT, Net::OpenTimeout, IO::TimeoutError, SocketError => e case e.message when /^503/ # 503 はアクセス規制やメンテ等でリトライしてもほぼ意味がないことが多いため一度で諦める diff --git a/lib/novelconverter.rb b/lib/novelconverter.rb index a120df02..3ce386f1 100644 --- a/lib/novelconverter.rb +++ b/lib/novelconverter.rb @@ -280,17 +280,21 @@ def self.add_dc_subject_to_epub(epub_path, subjects, stream_io: $stdout2) return :error end - content = opf_body.dup + content = opf_body.dup.force_encoding("UTF-8") content.gsub!(/.*?<\/dc:subject>\s*\n?\s*/m, "") dc_subject_lines = subjects.map(&:strip).reject(&:empty?).map { |s| esc = s.gsub("&","&").gsub("<","<").gsub(">",">").gsub("\"",""").gsub("'","'") - " #{esc}" + "\t\t#{esc}" } if dc_subject_lines.any? dc_subjects_xml = dc_subject_lines.join("\n") + "\n" content.sub!(/(\s*)<\/metadata>/, "\n#{dc_subjects_xml}\\1") end - entries[opf_name] = content + entries[opf_name] = content.b + + # Windowsでのスレッド内ファイル操作対策: GCを強制実行してファイルハンドルを解放 + GC.start + sleep 0.1 # 再Zip化 (mimetypeは無圧縮で先頭) File.delete(epub_path) @@ -315,7 +319,7 @@ def self.add_dc_subject_to_epub(epub_path, subjects, stream_io: $stdout2) stream_io.puts "dc:subjectを追加しました: #{subjects.join(', ')}" :success rescue => e - stream_io.error "dc:subject追加中にエラーが発生しました: #{e.message}" + stream_io.error "dc:subject追加中にエラーが発生しました: #{e.class} - #{e.message}" :error end end diff --git a/lib/web/public/resources/js/narou.library.js b/lib/web/public/resources/js/narou.library.js index e7a70e3e..2854faa5 100644 --- a/lib/web/public/resources/js/narou.library.js +++ b/lib/web/public/resources/js/narou.library.js @@ -1902,6 +1902,23 @@ var Narou = (function () { return matched; }); + // 通常単語を追加するためのイベント(テーブル tbody に直接登録) + // サーバーサイド/クライアントサイド両方で必要 + $("#novel-list tbody").on("click", "[data-add-filter]", function (e) { + e.preventDefault(); + e.stopPropagation(); + e.stopImmediatePropagation(); + self.appendWordToFilter($(this).data("addFilter")); + return false; + }); + // 追加イベントが発生した時に行選択イベントが起こらないように + $("#novel-list tbody").on("mousedown", "[data-add-filter]", function (e) { + e.preventDefault(); + e.stopPropagation(); + e.stopImmediatePropagation(); + return false; + }); + // サーバーサイド処理の場合はカスタムフィルタボックスのイベントを登録しない // (narou.ui.jsで登録されるハンドラを使用) if (this.table.settings()[0].oFeatures.bServerSide) { @@ -1931,17 +1948,6 @@ var Narou = (function () { self.search(); } }); - - this.table - // 通常単語を追加するためのイベント - .on("click", "[data-add-filter]", function (e) { - e.stopPropagation(); - self.appendWordToFilter($(this).data("addFilter")); - }) - // 追加イベントが発生した時に行選択イベントが起こらないように - .on("mousedown", "[data-add-filter]", function (e) { - e.stopPropagation(); - }); }, // フィルタにタグを追加 @@ -2107,6 +2113,7 @@ var Narou = (function () { $target .on("click", ".tag", args, function (e) { if (e.data.stop_bubbling) e.stopPropagation(); + e.preventDefault(); // テキスト選択を防ぐ var tag_name = String($(this).data("tag")); self.search.appendTagToFilter(tag_name, e.altKey); self.table.$("[data-toggle=tooltip]").tooltip("hide"); @@ -2114,6 +2121,7 @@ var Narou = (function () { .on("mousedown", ".tag", args, function (e) { // 範囲選択モードでもクリック出来るように if (e.data.stop_bubbling) e.stopPropagation(); + e.preventDefault(); // テキスト選択を防ぐ }); }, diff --git a/lib/web/public/resources/js/narou.ui.js b/lib/web/public/resources/js/narou.ui.js index 459ef75a..9c1a6e9d 100644 --- a/lib/web/public/resources/js/narou.ui.js +++ b/lib/web/public/resources/js/narou.ui.js @@ -222,7 +222,6 @@ $(function() { var isLightweightMode = false; var table, t; var action; // アクション管理インスタンス - window.action = action; var isInitialLoad = true; // 初回ロードフラグ /* @@ -1285,7 +1284,12 @@ $(function() { // ここだけはスマフォ系でも touchstart ではなく click で。 // touchstart だと画面スクロールのためのスワイプでも反応してしまう table - .on("click", "tr", function () { + .on("click", "tr", function (e) { + // data-add-filter要素がクリックされた場合は何もしない + if ($(e.target).closest('[data-add-filter]').length > 0) { + return; + } + // DOM の選択状態を変更 $(this).toggleClass("selected"); @@ -1311,6 +1315,10 @@ $(function() { // リンクをクリックした時は選択処理は行わない e.stopPropagation(); }) + .on("click", "[data-add-filter]", function(e) { + // フィルタ追加要素をクリックした時は選択処理は行わない + e.stopPropagation(); + }) $("#novel-list tbody").css("cursor", "pointer"); } diff --git a/lib/web/pushserver.rb b/lib/web/pushserver.rb index d7ed6487..26aecf50 100644 --- a/lib/web/pushserver.rb +++ b/lib/web/pushserver.rb @@ -50,9 +50,16 @@ def run end thread = Thread.new do - while true - data = que.pop - ws.send(data) + begin + while true + data = que.pop + ws.send(data) + end + rescue Errno::ECONNRESET, Errno::EPIPE, IOError => e + # 接続が切れた場合、スレッドを終了 + rescue => e + # その他のエラーもログに出力してスレッド終了 + puts "[ERROR] WebSocket send thread error: #{e.class}: #{e.message}" if $DEBUG end end