public void Start(Book.Book book, CollectionSettings collectionSettings, Color backColor) { if (_wifiAdvertiser != null) { Stop(); } // This listens for a BloomReader to request a book. // It requires a firewall hole allowing Bloom to receive messages on _portToListen. // We initialize it before starting the Advertiser to avoid any chance of a race condition // where a BloomReader manages to request an advertised book before we start the listener. _wifiListener = new BloomReaderUDPListener(); _wifiListener.NewMessageReceived += (sender, args) => { var json = Encoding.UTF8.GetString(args.Data); try { dynamic settings = JsonConvert.DeserializeObject(json); // The property names used here must match the ones in BloomReader, doInBackground method of SendMessage, // a private class of NewBookListenerService. var androidIpAddress = (string)settings.deviceAddress; var androidName = (string)settings.deviceName; // This prevents the device (or other devices) from queuing up requests while we're busy with this one. // In effect, the Android is only allowed to request a retry after we've given up this try at sending. // Of course, there are async effects from network latency. But if we do get another request while // handling this one, we will ignore it, since StartSendBook checks for a transfer in progress. _wifiAdvertiser.Paused = true; StartSendBookOverWiFi(book, androidIpAddress, androidName, backColor); // Returns immediately. But we don't resume advertisements until the async send completes. } // If there's something wrong with the JSON (maybe an obsolete or newer version of reader?) // just ignore the request. catch (Exception ex) when(ex is JsonReaderException || ex is JsonSerializationException) { _progress.Error(id: "BadBookRequest", message: "Got a book request we could not process. Possibly the device is running an incompatible version of BloomReader?"); //this is too technical/hard to translate _progress.ErrorWithoutLocalizing($" Request contains {json}; trying to interpret as JSON we got {ex.Message}"); } }; var pathHtmlFile = book.GetPathHtmlFile(); _wifiAdvertiser = new WiFiAdvertiser(_progress) { BookTitle = BookStorage.SanitizeNameForFileSystem(book.Title), // must be the exact same name as the file we will send if requested TitleLanguage = collectionSettings.Language1Iso639Code, BookVersion = Book.Book.MakeVersionCode(File.ReadAllText(pathHtmlFile), pathHtmlFile) }; AndroidView.CheckBookLayout(book, _progress); _wifiAdvertiser.Start(); _progress.Message(id: "WifiInstructions1", message: "On the Android, run Bloom Reader, open the menu and choose 'Receive Books from computer'."); _progress.Message(id: "WifiInstructions2", message: "You can do this on as many devices as you like. Make sure each device is connected to the same network as this computer."); }
private void Work() { _progress.Message(idSuffix: "beginAdvertising", message: "Advertising book to Bloom Readers on local network..."); try { while (true) { if (!Paused) { UpdateAdvertisementBasedOnCurrentIpAddress(); _client.BeginSend(_sendBytes, _sendBytes.Length, _endPoint, SendCallback, _client); } Thread.Sleep(1000); } } catch (ThreadAbortException) { _progress.Message(idSuffix: "Stopped", message: "Stopped Advertising."); _client.Close(); } catch (Exception error) { // not worth localizing _progress.MessageWithoutLocalizing($"Error in Advertiser: {error.Message}", ProgressKind.Error); } }
public static string StageBloomD(Book.Book book, BookServer bookServer, WebSocketProgress progress, Color backColor, AndroidPublishSettings settings = null) { progress.Message("PublishTab.Epub.PreparingPreview", "Preparing Preview"); // message shared with Epub publishing if (settings?.LanguagesToInclude != null) { var message = new LicenseChecker().CheckBook(book, settings.LanguagesToInclude.ToArray()); if (message != null) { progress.MessageWithoutLocalizing(message, MessageKind.Error); return(null); } } _stagingFolder?.Dispose(); if (AudioProcessor.IsAnyCompressedAudioMissing(book.FolderPath, book.RawDom)) { progress.Message("CompressingAudio", "Compressing audio files"); AudioProcessor.TryCompressingAudioAsNeeded(book.FolderPath, book.RawDom); } // We don't use the folder found here, but this method does some checks we want done. BookStorage.FindBookHtmlInFolder(book.FolderPath); _stagingFolder = new TemporaryFolder(StagingFolder); var modifiedBook = BloomReaderFileMaker.PrepareBookForBloomReader(book.FolderPath, bookServer, _stagingFolder, progress, settings: settings); progress.Message("Common.Done", "Shown in a list of messages when Bloom has completed a task.", "Done"); return(modifiedBook.FolderPath.ToLocalhost()); }
/// <summary> /// Attempt to connect to a device /// </summary> /// <param name="book"></param> public void Connect(Book.Book book, Color backColor, AndroidPublishSettings settings = null) { try { // Calls to this come from JavaScript, not sure they will always be on the UI thread. // Before I added this, I definitely saw race conditions with more than one thread trying // to figure out what was connected. lock (this) { PublishToAndroidApi.CheckBookLayout(book, _progress); if (_connectionHandler != null) { // we're in an odd state...should only be able to click the button that calls this // while stopped. // Try to really get into the right state in case the user tries again. _androidDeviceUsbConnection.StopFindingDevice(); return; } // Create this while locked...once we have it, can't enter the main logic of this method // on another thread. _connectionHandler = new BackgroundWorker(); } _progress.Message(idSuffix: "LookingForDevice", comment: "This is a progress message; MTP is an acronym for the system that allows computers to access files on devices.", message: "Looking for an Android device connected by USB cable and set up for file transfer (MTP)..."); _androidDeviceUsbConnection.OneReadyDeviceFound = HandleOneReadyDeviceFound; _androidDeviceUsbConnection.OneReadyDeviceNotFound = HandleOneReadyDeviceNotFound; // Don't suppress the first message after (re)starting. _previousDeviceNotFoundReportType = DeviceNotFoundReportType.Unknown; _connectionHandler.DoWork += (sender, args) => _androidDeviceUsbConnection.ConnectAndSendToOneDevice(book, backColor, settings); _connectionHandler.RunWorkerCompleted += (sender, args) => { if (args.Error != null) { UsbFailConnect(args.Error); } _connectionHandler = null; // now OK to try to connect again. }; _connectionHandler.RunWorkerAsync(); } catch (Exception e) { UsbFailConnect(e); } }
/// <summary> /// This is the core of sending a book to a device. We need a book and a bookServer in order to come up /// with the .bloomd file. /// We are either simply saving the .bloomd to destFileName, or else we will make a temporary .bloomd file and /// actually send it using sendAction. /// We report important progress on the progress control. This includes reporting that we are starting /// the actual transmission using startingMessageAction, which is passed the safe file name (for checking pre-existence /// in UsbPublisher) and the book title (typically inserted into the message). /// If a confirmAction is passed (currently only by UsbPublisher), we use it check for a successful transfer /// before reporting completion (except for file save, where the current message is inappropriate). /// This is an awkward case where the three ways of publishing are similar enough that /// it's annoying and dangerous to have three entirely separate methods but somewhat awkward to combine them. /// Possibly we could eventually make them more similar, e.g., it would simplify things if they all said /// "Sending X to Y", though I'm not sure that would be good i18n if Y is sometimes a device name /// and sometimes a path. /// </summary> /// <param name="book"></param> /// <param name="destFileName"></param> /// <param name="sendAction"></param> /// <param name="progress"></param> /// <param name="bookServer"></param> /// <param name="startingMessageFunction"></param> public static void SendBook(Book.Book book, BookServer bookServer, string destFileName, Action <string, string> sendAction, WebSocketProgress progress, Func <string, string, string> startingMessageFunction, Func <string, bool> confirmFunction, Color backColor, AndroidPublishSettings settings = null) { var bookTitle = book.Title; progress.MessageUsingTitle("PackagingBook", "Packaging \"{0}\" for use with Bloom Reader...", bookTitle, MessageKind.Progress); // compress audio if needed, with progress message if (AudioProcessor.IsAnyCompressedAudioMissing(book.FolderPath, book.RawDom)) { progress.Message("CompressingAudio", "Compressing audio files"); AudioProcessor.TryCompressingAudioAsNeeded(book.FolderPath, book.RawDom); } var publishedFileName = bookTitle + BookCompressor.ExtensionForDeviceBloomBook; if (startingMessageFunction != null) { progress.MessageWithoutLocalizing(startingMessageFunction(publishedFileName, bookTitle)); } if (destFileName == null) { // wifi or usb...make the .bloomd in a temp folder. using (var bloomdTempFile = TempFile.WithFilenameInTempFolder(publishedFileName)) { BloomReaderFileMaker.CreateBloomDigitalBook(bloomdTempFile.Path, book, bookServer, backColor, progress, settings); sendAction(publishedFileName, bloomdTempFile.Path); if (confirmFunction != null && !confirmFunction(publishedFileName)) { throw new ApplicationException("Book does not exist after write operation."); } progress.MessageUsingTitle("BookSent", "You can now read \"{0}\" in Bloom Reader!", bookTitle, MessageKind.Note); } } else { // save file...user has supplied name, there is no further action. Debug.Assert(sendAction == null, "further actions are not supported when passing a path name"); BloomReaderFileMaker.CreateBloomDigitalBook(destFileName, book, bookServer, backColor, progress, settings); progress.Message("PublishTab.Epub.Done", "Done", useL10nIdPrefix: false); // share message string with epub publishing } }
public static void SendBatchedWarningMessagesToProgress(ISet <string> warningMessages, WebSocketProgress progress) { if (warningMessages.Any()) { progress.Message("Common.Warning", "Warning", ProgressKind.Warning, false); } foreach (var warningMessage in warningMessages) { // Messages are already localized progress.MessageWithoutLocalizing(warningMessage, ProgressKind.Warning); } }
private string FindAceByDaisyOrTellUser(ApiRequest request) { _webSocketProgress.Message("FindingAce", "Finding Ace by DAISY on this computer..."); var whereProgram = Platform.IsWindows ? "where" : "which"; var npmFileName = Platform.IsWindows ? "npm.cmd" : "npm"; var whereResult = CommandLineRunner.Run(whereProgram, npmFileName, Encoding.ASCII, "", 2, new NullProgress()); if (!String.IsNullOrEmpty(whereResult.StandardError)) { _webSocketProgress.ErrorWithoutLocalizing(whereResult.StandardError); } if (!whereResult.StandardOutput.Contains(npmFileName)) { ReportErrorAndFailTheRequest(request, whereResult, "Could not find npm."); return(null); } var fullNpmPath = whereResult.StandardOutput.Split('\n')[0].Trim(); // note: things like nvm will mess with where the global node_modules lives. The best way seems to be // to ask npm: var result = CommandLineRunner.Run(npmFileName, "root -g", Encoding.ASCII, Path.GetDirectoryName(fullNpmPath), 10, new NullProgress()); const string kCoreError = "Could not get \"npm -g root\" to work. Is Node & npm installed and working?"; if (result == null) { // I don't think this could happen, but *something* was null for Sue. ReportErrorAndFailTheRequest(request, whereResult, $"{kCoreError} CommandLineRunner.Run() returned null."); return(null); } if (!string.IsNullOrWhiteSpace(result.StandardError)) { ReportErrorAndFailTheRequest(request, whereResult, $"{kCoreError} <br>StandardError:<br>" + result.StandardError); return(null); } if (result.StandardOutput == null) { ReportErrorAndFailTheRequest(request, whereResult, $"{kCoreError} StandardOutput was null."); return(null); } if (!result.StandardOutput.Contains("node_modules")) { ReportErrorAndFailTheRequest(request, whereResult, kCoreError); return(null); } var nodeModulesDirectory = result.StandardOutput.Trim(); if (!Directory.Exists((nodeModulesDirectory))) { ReportErrorAndFailTheRequest(request, whereResult, "Could not find global node_modules directory"); return(null); } // if they installed via npm install -g @daisy/ace var daisyDirectory = Path.Combine(nodeModulesDirectory, "@daisy/ace/bin/"); if (!Directory.Exists((daisyDirectory))) { // if they just installed via npm install -g @daisy/ace-cli daisyDirectory = Path.Combine(nodeModulesDirectory, "@daisy/ace-cli/bin/"); if (!Directory.Exists((daisyDirectory))) { ReportErrorAndFailTheRequest(request, whereResult, $"Could not find daisy-ace at {daisyDirectory}."); return(null); } } _webSocketProgress.Message("FoundAce", "Found."); return(daisyDirectory); }